diff --git a/font-codegen/src/fields.rs b/font-codegen/src/fields.rs index 03221d4c1..fe1a1f1ca 100644 --- a/font-codegen/src/fields.rs +++ b/font-codegen/src/fields.rs @@ -1,6 +1,6 @@ //! methods on fields -use std::{backtrace::Backtrace, ops::Deref}; +use std::ops::Deref; use proc_macro2::TokenStream; use quote::{quote, ToTokens}; @@ -211,6 +211,13 @@ impl Fields { } } +fn big_endian(typ: &syn::Ident) -> TokenStream { + if typ == "u8" { + return quote!(#typ); + } + quote!(BigEndian<#typ>) +} + fn traversal_arm_for_field( fld: &Field, in_record: bool, @@ -227,7 +234,7 @@ fn traversal_arm_for_field( FieldType::Offset { target: Some(OffsetTarget::Array(inner)), .. - } if matches!(inner.deref(), FieldType::PendingResolution { .. }) => { + } if matches!(inner.deref(), FieldType::Struct { .. }) => { let typ = inner.cooked_type_tokens(); let getter = fld.offset_getter_name(); let offset_data = fld.offset_getter_data_src(); @@ -256,15 +263,14 @@ fn traversal_arm_for_field( quote!(Field::new(#name_str, FieldType::unknown_offset(self.#name()#maybe_unwrap))) } FieldType::Scalar { .. } => quote!(Field::new(#name_str, self.#name()#maybe_unwrap)), - FieldType::Struct { .. } => quote!(Field::new(#name_str, self.#name()#maybe_unwrap)), FieldType::Array { inner_typ } => match inner_typ.as_ref() { FieldType::Scalar { .. } => quote!(Field::new(#name_str, self.#name()#maybe_unwrap)), //HACK: glyf has fields that are [u8] - FieldType::PendingResolution { typ } if typ == "u8" => { + FieldType::Struct { typ } if typ == "u8" => { quote!(Field::new( #name_str, self.#name()#maybe_unwrap)) } - FieldType::PendingResolution { typ } if !in_record => { + FieldType::Struct { typ } if !in_record => { let offset_data = fld.offset_getter_data_src(); quote!(Field::new( #name_str, @@ -307,11 +313,12 @@ fn traversal_arm_for_field( FieldType::Offset { target: Some(OffsetTarget::Array(_)), .. - } => quote!(compile_error!( - "achievement unlocked: 'added arrays of offsets to arrays to OpenType spec'" - )), + } => panic!( + "achievement unlocked: 'added arrays of offsets to arrays to OpenType spec' {:#?}", + fld + ), FieldType::Offset { .. } => quote!(Field::new(#name_str, self.#name()#maybe_unwrap)), - _ => quote!(compile_error!("unhandled traversal case")), + _ => panic!("unhandled traversal case {:#?}", fld), }, FieldType::ComputedArray(arr) => { // if we are in a record, we pass in empty data. This lets us @@ -335,13 +342,15 @@ fn traversal_arm_for_field( FieldType::VarLenArray(_) => { quote!(Field::new(#name_str, traversal::FieldType::var_array(self.#name()#maybe_unwrap))) } - FieldType::PendingResolution { typ } if typ == "ValueRecord" => { + // HACK: who wouldn't want to hard-code ValueRecord handling + FieldType::Struct { typ } if typ == "ValueRecord" => { let clone = in_record.then(|| quote!(.clone())); quote!(Field::new(#name_str, self.#name() #clone #maybe_unwrap)) } - FieldType::PendingResolution { .. } => { + FieldType::Struct { .. } => { quote!(compile_error!(concat!("another weird type: ", #name_str))) } + FieldType::PendingResolution { .. } => panic!("Should have resolved {:#?}", fld), } } @@ -350,7 +359,6 @@ fn check_resolution(phase: Phase, field_type: &FieldType) -> syn::Result<()> { return Ok(()); } if let FieldType::PendingResolution { typ } = field_type { - eprintln!("{}", Backtrace::capture()); // very helpful when troubleshooting return Err(syn::Error::new( typ.span(), format!( @@ -368,7 +376,7 @@ impl Field { FieldType::Offset { typ, .. } if self.is_nullable() => { quote!(BigEndian>) } - FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => quote!(BigEndian<#typ>), + FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => big_endian(typ), FieldType::Struct { typ } => typ.to_token_stream(), FieldType::ComputedArray(array) => { let inner = array.type_with_lifetime(); @@ -380,7 +388,8 @@ impl Field { quote!(&'a [BigEndian>]) } FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { - quote!(&'a [BigEndian<#typ>]) + let be = big_endian(typ); + quote!(&'a [#be]) } FieldType::Struct { typ } => quote!( &[#typ] ), FieldType::PendingResolution { typ } => { @@ -496,12 +505,10 @@ impl Field { // is this a scalar/offset? then it's just 'RAW_BYTE_LEN' // is this computed? then it is stored match &self.typ { - FieldType::Offset { typ, .. } - | FieldType::Scalar { typ } - | FieldType::Struct { typ } => { + FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { quote!(#typ::RAW_BYTE_LEN) } - FieldType::PendingResolution { .. } + FieldType::Struct { .. } | FieldType::Array { .. } | FieldType::ComputedArray { .. } | FieldType::VarLenArray(_) => { @@ -509,6 +516,7 @@ impl Field { let try_op = self.is_version_dependent().then(|| quote!(?)); quote!(self.#len_field #try_op) } + FieldType::PendingResolution { .. } => panic!("Should have resolved {:?}", self), } } @@ -553,13 +561,13 @@ impl Field { FieldType::Offset { typ, .. } | FieldType::Scalar { typ } | FieldType::Struct { typ } => typ.to_token_stream(), - FieldType::PendingResolution { typ } => typ.to_token_stream(), FieldType::Array { inner_typ } => match inner_typ.as_ref() { FieldType::Offset { typ, .. } if self.is_nullable() => { quote!(&'a [BigEndian>]) } FieldType::Offset { typ, .. } | FieldType::Scalar { typ } => { - quote!(&'a [BigEndian<#typ>]) + let be = big_endian(typ); + quote!(&'a [#be]) } FieldType::Struct { typ } => quote!(&'a [#typ]), FieldType::PendingResolution { typ } => quote!( &'a [#typ] ), @@ -573,6 +581,7 @@ impl Field { let inner = array.type_with_lifetime(); quote!(VarLenArray<'a, #inner>) } + FieldType::PendingResolution { .. } => panic!("Should have resolved {:?}", self), } } @@ -640,18 +649,24 @@ impl Field { // their fields on access. let add_borrow_just_for_record = matches!( self.typ, - FieldType::PendingResolution { .. } | FieldType::ComputedArray { .. } + FieldType::Struct { .. } | FieldType::ComputedArray { .. } ) .then(|| quote!(&)); let getter_expr = match &self.typ { - FieldType::Scalar { .. } | FieldType::Offset { .. } | FieldType::Struct { .. } => { - quote!(self.#name.get()) + FieldType::Scalar { typ } | FieldType::Offset { typ, .. } => { + if typ == "u8" { + quote!(self.#name) + } else { + quote!(self.#name.get()) + } } - FieldType::ComputedArray { .. } | FieldType::VarLenArray(_) => quote!(&self.#name), + FieldType::Struct { .. } + | FieldType::ComputedArray { .. } + | FieldType::VarLenArray(_) => quote!(&self.#name), FieldType::Array { .. } => quote!(self.#name), - FieldType::PendingResolution { typ } => { - panic!("Should have resolved {}", quote! { #typ }); + FieldType::PendingResolution { .. } => { + panic!("Should have resolved {:?}", self) } }; @@ -880,10 +895,12 @@ impl Field { .as_deref() .map(FieldReadArgs::to_tokens_for_validation); - if let FieldType::PendingResolution { typ } = &self.typ { + if let FieldType::Struct { typ } = &self.typ { return Some(quote!( <#typ as ComputeSize>::compute_size(&#read_args))); } - + if let FieldType::PendingResolution { .. } = &self.typ { + panic!("Should have resolved {:?}", self) + } let len_expr = match self.attrs.count.as_deref() { Some(Count::All) => quote!(cursor.remaining_bytes()), Some(other) => { @@ -1031,7 +1048,7 @@ impl Field { | FieldType::VarLenArray(_) => true, FieldType::Array { inner_typ } => matches!( inner_typ.as_ref(), - FieldType::Offset { .. } | FieldType::PendingResolution { .. } + FieldType::Offset { .. } | FieldType::Struct { .. } ), FieldType::PendingResolution { typ } => { panic!("Should have resolved {}", quote! { #typ }); @@ -1045,10 +1062,10 @@ impl Field { _ if self.attrs.to_owned.is_some() => false, FieldType::Offset { .. } => in_record, FieldType::ComputedArray(_) | FieldType::VarLenArray(_) => true, - FieldType::PendingResolution { .. } => true, + FieldType::Struct { .. } => true, FieldType::Array { inner_typ } => match inner_typ.as_ref() { FieldType::Offset { .. } => in_record, - FieldType::PendingResolution { .. } => true, + FieldType::Struct { .. } => true, _ => false, }, _ => false, @@ -1068,7 +1085,7 @@ impl Field { self.attrs.to_owned.as_ref().unwrap().expr.to_token_stream() } FieldType::Scalar { .. } => quote!(obj.#name()), - FieldType::PendingResolution { .. } => quote!(obj.#name().to_owned_obj(offset_data)), + FieldType::Struct { .. } => quote!(obj.#name().to_owned_obj(offset_data)), FieldType::Offset { target: Some(_), .. } => { @@ -1082,7 +1099,7 @@ impl Field { FieldType::Array { inner_typ } => { // we write different code based on whether or not this is a versioned field let (getter, converter) = match inner_typ.as_ref() { - FieldType::Scalar { .. } | FieldType::Struct { .. } => ( + FieldType::Scalar { .. } => ( quote!(obj.#name()), quote!(.iter().map(|x| x.get()).collect()), ), @@ -1093,7 +1110,7 @@ impl Field { quote!(.map(|x| x.into()).collect()), ) } - FieldType::PendingResolution { .. } => ( + FieldType::Struct { .. } => ( quote!(obj.#name()), quote!(.iter().map(|x| FromObjRef::from_obj_ref(x, offset_data)).collect()), ), @@ -1131,11 +1148,8 @@ impl FieldType { FieldType::Offset { typ, .. } | FieldType::Scalar { typ } | FieldType::Struct { typ } => typ, - FieldType::PendingResolution { typ } => { - panic!( - "Should never cook a type pending resolution {}", - quote! { #typ } - ); + FieldType::PendingResolution { .. } => { + panic!("Should never cook a type pending resolution {:#?}", self); } FieldType::Array { .. } | FieldType::ComputedArray { .. } @@ -1149,7 +1163,6 @@ impl FieldType { let raw_type = match self { FieldType::Scalar { typ } => typ.into_token_stream(), FieldType::Struct { typ } => typ.into_token_stream(), - FieldType::PendingResolution { typ } => typ.into_token_stream(), FieldType::Offset { typ, target } => { let target = target .as_ref() @@ -1173,6 +1186,7 @@ impl FieldType { quote!( Vec<#inner_tokens> ) } FieldType::ComputedArray(array) | FieldType::VarLenArray(array) => array.compile_type(), + FieldType::PendingResolution { .. } => panic!("Should have resolved {:?}", self), }; if version_dependent { quote!( Option<#raw_type> ) diff --git a/font-codegen/src/lib.rs b/font-codegen/src/lib.rs index f90cdb617..641571c96 100644 --- a/font-codegen/src/lib.rs +++ b/font-codegen/src/lib.rs @@ -27,11 +27,7 @@ pub enum Mode { Compile, } -pub fn generate_code(code_str: &str, mode: Mode) -> Result { - let tables = match mode { - Mode::Parse => generate_parse_module(code_str), - Mode::Compile => generate_compile_module(code_str), - }?; +fn touchup_code(tables: proc_macro2::TokenStream) -> Result { // if this is not valid code just pass it through directly, and then we // can see the compiler errors let source_str = match rustfmt_wrapper::rustfmt(&tables) { @@ -47,31 +43,42 @@ pub fn generate_code(code_str: &str, mode: Mode) -> Result { // add newlines after top-level items let re2 = regex::Regex::new(r"\n\}").unwrap(); let source_str = re2.replace_all(&source_str, "\n}\n\n"); - let source_str = rustfmt_wrapper::rustfmt(source_str).unwrap(); - - Ok(format!( - "\ - // THIS FILE IS AUTOGENERATED.\n\ - // Any changes to this file will be overwritten.\n\ - // For more information about how codegen works, see font-codegen/README.md\n\n{}", - source_str, - )) + Ok(rustfmt_wrapper::rustfmt(source_str).unwrap()) } -pub fn generate_parse_module(code: &str) -> Result { - // Generation is done in 3 phases (https://github.com/googlefonts/fontations/issues/71): +pub fn generate_code(code_str: &str, mode: Mode) -> Result { + // Generation is done in phases (https://github.com/googlefonts/fontations/issues/71): // 1. Parse - eprintln!("Parse"); - let mut items: Items = syn::parse_str(code)?; + eprintln!("Parse {:?}", mode); + let mut items: Items = syn::parse_str(code_str)?; items.sanity_check(Phase::Parse)?; // 2. Contemplate (semantic analysis) - eprintln!("Analyze"); + eprintln!("Analyze {:?}", mode); resolve_pending(&mut items)?; items.sanity_check(Phase::Analysis)?; // 3. Generate - eprintln!("Generate"); + eprintln!("Generate {:?}", mode); + let tables = match &mode { + Mode::Parse => generate_parse_module(&items), + Mode::Compile => generate_compile_module(&items), + }?; + + // 4. Touchup + eprintln!("Touchup {:?}", mode); + let source_str = touchup_code(tables)?; + + Ok(format!( + "\ + // THIS FILE IS AUTOGENERATED.\n\ + // Any changes to this file will be overwritten.\n\ + // For more information about how codegen works, see font-codegen/README.md\n\n{}", + source_str, + )) +} + +pub(crate) fn generate_parse_module(items: &Items) -> Result { let mut code = Vec::new(); for item in &items.items { let item_code = match item { @@ -96,41 +103,41 @@ fn map_the_known_world(items: &Items) -> HashMap { return items .items .iter() - .filter(|item| { - if let Item::Format(..) = item { - false - } else { - true - } + .filter(|item| match item { + Item::Format(..) | Item::GenericGroup(..) => false, + _ => true, }) // clone like crazy to avoid holding an immutable borrow of the items .map(|item| match item { + // We're "forgetting" this is a record, could keep if we find a need Item::Record(data) => ( data.name.to_string(), FieldType::Struct { typ: data.name.clone(), }, ), + // We're "forgetting" this is a table, could keep if we find a need Item::Table(data) => ( data.raw_name().to_string(), FieldType::Struct { typ: data.raw_name().clone(), }, ), - Item::GenericGroup(..) => panic!("What do we do with generic groups?!"), + // We're "forgetting" the underlying type, could keep if we find a need Item::RawEnum(data) => ( data.name.to_string(), FieldType::Scalar { - typ: data.typ.clone(), + typ: data.name.clone(), }, ), + // We're "forgetting" the underlying type, could keep if we find a need Item::Flags(data) => ( data.name.to_string(), FieldType::Scalar { - typ: data.typ.clone(), + typ: data.name.clone(), }, ), - Item::Format(..) => unreachable!("We filtered you out!!"), + Item::Format(..) | Item::GenericGroup(..) => unreachable!("We filtered you out!!"), }) .collect(); } @@ -139,7 +146,7 @@ fn resolve_ident<'a>( known: &'a HashMap, field_name: &syn::Ident, ident: &syn::Ident, -) -> &'a FieldType { +) -> Result<&'a FieldType, syn::Error> { if let Some(item) = known.get(&ident.to_string()) { eprintln!( "Resolve {}: {} to {:?}", @@ -147,19 +154,22 @@ fn resolve_ident<'a>( ident.to_string(), item ); - return item; + return Ok(item); } else { - panic!( - "I don't know what this is, do you? {}: {}", - field_name.to_string().as_str(), - ident.to_string(), - ); + return Err(syn::Error::new( + field_name.span(), + format!( + "I don't know what this is, do you? {}: {}", + field_name.to_string().as_str(), + ident.to_string() + ), + )); } } -fn resolve_field(known: &HashMap, field: &mut Field) { +fn resolve_field(known: &HashMap, field: &mut Field) -> Result<(), syn::Error> { if let FieldType::PendingResolution { typ } = &field.typ { - let resolved_typ = resolve_ident(known, &field.name, typ); + let resolved_typ = resolve_ident(known, &field.name, typ)?; *field = Field { typ: resolved_typ.clone(), ..field.clone() @@ -169,7 +179,7 @@ fn resolve_field(known: &HashMap, field: &mut Field) { // Array and offsets can nest FieldType, pursue the rabbit if let FieldType::Array { inner_typ } = &field.typ { if let FieldType::PendingResolution { typ } = inner_typ.as_ref() { - let resolved_typ = resolve_ident(known, &field.name, typ); + let resolved_typ = resolve_ident(known, &field.name, typ)?; *field = Field { typ: FieldType::Array { inner_typ: Box::new(resolved_typ.clone()), @@ -183,7 +193,7 @@ fn resolve_field(known: &HashMap, field: &mut Field) { let offset_typ = typ; if let Some(OffsetTarget::Array(array_of)) = target { if let FieldType::PendingResolution { typ } = array_of.as_ref() { - let resolved_typ = resolve_ident(known, &field.name, typ); + let resolved_typ = resolve_ident(known, &field.name, typ)?; *field = Field { typ: FieldType::Offset { typ: offset_typ.clone(), @@ -194,9 +204,10 @@ fn resolve_field(known: &HashMap, field: &mut Field) { } } } + Ok(()) } -fn resolve_pending(items: &mut Items) -> syn::Result<()> { +fn resolve_pending(items: &mut Items) -> Result<(), syn::Error> { // We should know what some stuff is now // In theory we could repeat resolution until we succeed or stop learning // but I don't think ever need that currently @@ -209,21 +220,21 @@ fn resolve_pending(items: &mut Items) -> syn::Result<()> { // Try to resolve everything pending against the known world for item in &mut items.items { - match item { + let fields = match item { Item::Record(item) => &mut item.fields.fields, Item::Table(item) => &mut item.fields.fields, _ => continue, + }; + for field in fields { + resolve_field(&known, field)?; } - .iter_mut() - .for_each(|field| resolve_field(&known, field)); } Ok(()) } -pub fn generate_compile_module(code: &str) -> Result { - let items: Items = syn::parse_str(code)?; - items.sanity_check(Phase::Parse)?; - +pub(crate) fn generate_compile_module( + items: &Items, +) -> Result { let code = items .items .iter() diff --git a/font-codegen/src/parsing.rs b/font-codegen/src/parsing.rs index 13f144027..109b44087 100644 --- a/font-codegen/src/parsing.rs +++ b/font-codegen/src/parsing.rs @@ -614,10 +614,21 @@ fn is_wellknown_scalar(path: &syn::PathSegment) -> bool { return false; } // TODO use spec types not Rust types - // TODO feels like this should be an enum + // TODO feels like this should be an enum, know it's size, etc match path.ident.to_string().as_str() { "u8" | "u16" | "Uint24" | "u32" | "i16" | "GlyphId" | "Fixed" | "FWord" | "UfWord" - | "F2Dot14" => true, + | "F2Dot14" | "Tag" | "MajorMinor" | "LongDateTime" | "Version16Dot16" => true, + _ => false, + } +} + +// TODO use an explicit declaration of user defined types and delete this +fn is_wellknown_struct(path: &syn::PathSegment) -> bool { + if !path.arguments.is_empty() { + return false; + } + match path.ident.to_string().as_str() { + "ValueRecord" => true, _ => false, } } @@ -668,6 +679,12 @@ impl FieldType { }); } + if is_wellknown_struct(last) { + return Ok(FieldType::Struct { + typ: last.ident.clone(), + }); + } + if ["Offset16", "Offset24", "Offset32"].contains(&last.ident.to_string().as_str()) { let target = get_offset_target(&last.arguments)?; return Ok(FieldType::Offset { @@ -1222,7 +1239,7 @@ mod tests { #[test] fn offset_target() { - let array_target = make_path_seg("Offset16<[BigEndian]>"); + let array_target = make_path_seg("Offset16<[u16]>"); assert!(get_offset_target(&array_target.arguments) .unwrap() .is_some()); diff --git a/resources/codegen_inputs/cpal.rs b/resources/codegen_inputs/cpal.rs index 53d776175..e56c14ab8 100644 --- a/resources/codegen_inputs/cpal.rs +++ b/resources/codegen_inputs/cpal.rs @@ -29,7 +29,7 @@ table Cpal { #[available(1)] #[nullable] #[read_offset_with($num_palettes)] - palette_types_array_offset: Offset32<[u32>], + palette_types_array_offset: Offset32<[u32]>, /// Offset from the beginning of CPAL table to the [Palette Labels Array][]. /// /// This is an array of 'name' table IDs (typically in the font-specific name @@ -40,7 +40,7 @@ table Cpal { #[available(1)] #[nullable] #[read_offset_with($num_palettes)] - palette_labels_array_offset: Offset32<[u16>], + palette_labels_array_offset: Offset32<[u16]>, /// Offset from the beginning of CPAL table to the [Palette Entry Labels Array][]. /// /// This is an array of 'name' table IDs (typically in the font-specific name @@ -53,7 +53,7 @@ table Cpal { #[available(1)] #[nullable] #[read_offset_with($num_palette_entries)] - palette_entry_labels_array_offset: Offset32<[u16>], + palette_entry_labels_array_offset: Offset32<[u16]>, } /// [CPAL (Color Record)](https://learn.microsoft.com/en-us/typography/opentype/spec/cpal#palette-entries-and-color-records) record diff --git a/resources/codegen_inputs/test.rs b/resources/codegen_inputs/test.rs index 5c4001125..e5085761e 100644 --- a/resources/codegen_inputs/test.rs +++ b/resources/codegen_inputs/test.rs @@ -19,7 +19,7 @@ table KindsOfOffsets { array_offset_count: u16, /// An offset to an array: #[read_offset_with($array_offset_count)] - array_offset: Offset16<[u16>], + array_offset: Offset16<[u16]>, /// A normal offset that is versioned #[available(MajorMinor::VERSION_1_1)] versioned_nonnullable_offset: Offset16,