Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions font-codegen/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ compiling various font tables. For an in-depth overview of what code we generate
and how it works, see the [codegen-tour][] document.

The basics:

- Inputs live in `resources/codegen_inputs`.
- To run the code generator:
```sh
Expand All @@ -27,7 +28,6 @@ The basics:
[`include!`][] macro into a corresponding module, generally in
`$crate/src/tables/$name.rs`.


## Adding a new table

- Create a new codegen input file in `resources/codegen_inputs`. The name of
Expand All @@ -51,7 +51,6 @@ The basics:
and ensure it is producing reasonable output.
- Repeat this process for the `write-fonts` crate.


## Modifying the codegen code

It is possible that in adding a table you will need to modify the codegen code
Expand Down Expand Up @@ -107,7 +106,7 @@ Offset16 langTagOffset Language-tag string offset from start of storage area (in
```

- all objects are separated by a newline, and begin with `@OBJECT_TYPE`.
- record & table are currently interchangeable, but this may change, and you
- record & table are currently interchangeable, but this may change, and you
should follow the spec.
- enum & flags require an explicit format
- this does not handle lifetimes, which will need to be added manually
Expand Down Expand Up @@ -164,18 +163,27 @@ The following annotations are supported on top-level objects:
with the signature `fn(&self, &mut ValidationCtx)`.

#### field attributes

- `#[nullable]`: only allowed on offsets or arrays of offsets, and indicates
that this field is allowed to be null. This changes the behaviour of getters,
as well as validation and compilation code.
- `#[since_version(version)]`: indicates that a field only exists in a given version
of the table. The `version` may be either a single integer literal
(`#[since_version(1)]`), or a major.minor pair (`#[since_version(1.1)]`).
- `#[before_version(version)]`: indicates that a field only exists prior to a given version
of the table. The `version` may be either a single integer literal
(`#[before_version(2)]`) or a major.minor pair (`#[before_version(1.1)]`).
- `#[if_flag($field, Flags::SOME_FLAG)]`: indicates that a given field is only
present if a particular flag is set on the named field. The field is expected
to be a bitset with a `contains` method.
- `#[if_cond($field, Flags::SOME_FLAG_A, Flags::SOME_FLAG_B, ...)]`: indicates that a
given field is only present if at least one of the listed flags is set on the named
field. The field is expected to be a bitset with a `contains` method.
- `#[if_cond($method(...))]`:
A function identifier, then one or more arguments.
- `#[if_cond(any_flag($field, Flags::SOME_FLAG_A, Flags::SOME_FLAG_B, ...))]`: indicates that a
given field is only present if at least one of the listed flags is set on the named
field. The field is expected to be a bitset with an `intersects` method.
- `#[if_cond(not_flag($field, Flags::SOME_FLAG_A, Flags::SOME_FLAG_B, ...))]`: indicates that a
given field is only present if none of the listed flags are set on the named
field. The field is expected to be a bitset with an `intersects` method.
- `#[skip_getter]`: if present, we will not generate a getter for this field.
Used on things like padding fields.
- `#[offset_getter(method name)]`: only allowed on offsets or arrays of offsets.
Expand Down Expand Up @@ -233,7 +241,6 @@ The following annotations are supported on top-level objects:
- `#[to_owned(expr)]`: uncommon/hacky: provide an expression that will be used
in `FromObjRef` to convert the parse type to the compile type.


### codegen plans

There is also the concept of a 'codegen plan', which is a simple toml file
Expand All @@ -243,6 +250,8 @@ intended to be the general mechanism by which codegen is run.
See `../resources/codegen_plan.toml` for an example.

[opentype]: https://docs.microsoft.com/en-us/typography/opentype/

[`include!`]: http://doc.rust-lang.org/1.64.0/std/macro.include.html

[codegen-tour]: ../docs/codegen-tour.md

39 changes: 35 additions & 4 deletions font-codegen/src/fields.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ impl Fields {
if matches!(fld.attrs.count.as_deref(), Some(Count::All(_)))
&& i != self.fields.len() - 1
{
return Err(logged_syn_error(
fld.name.span(),
"#[count(..)] or VarLenArray fields can only be last field in table.",
));
// TODO: This needs to take into account cfg-ed out fields
// return Err(logged_syn_error(
// fld.name.span(),
// "#[count(..)] or VarLenArray fields can only be last field in table.",
// ));
}
fld.sanity_check(phase)?;
}
Expand Down Expand Up @@ -201,6 +202,11 @@ impl Fields {
ctx.report(format!("field must be present for version {version}"));
}
},
Condition::BeforeVersion(_) => quote! {
if #condition && self.#name.is_none() {
ctx.report(format!("field must be present for version {version}"));
}
},
Condition::IfFlag { flag, .. } => {
let flag = stringify_path(flag);
let flag_missing = format!("'{name}' is present but {flag} not set",);
Expand Down Expand Up @@ -229,6 +235,21 @@ impl Fields {
}
}
}
IfTransform::NotFlag(_, _) => {
let condition_not_set_message = format!(
"if_cond is not satisfied but '{name}' is not present."
);
let condition_set_message =
format!("if_cond is satisfied by '{name}' is present.");
quote! {
if !(#condition) && self.#name.is_some() {
ctx.report(#condition_not_set_message);
}
if (#condition) && self.#name.is_none() {
ctx.report(#condition_set_message);
}
}
}
},
}
});
Expand Down Expand Up @@ -311,13 +332,21 @@ fn if_expression(xform: &IfTransform, add_self: bool) -> TokenStream {
quote!(#field.intersects(#(#flags)|*))
}
}
IfTransform::NotFlag(field, flags) => {
if add_self {
quote!(!self.#field.intersects(#(#flags)|*))
} else {
quote!(!#field.intersects(#(#flags)|*))
}
}
}
}

impl Condition {
fn condition_tokens_for_read(&self) -> TokenStream {
match self {
Condition::SinceVersion(version) => quote!(version.compatible(#version)),
Condition::BeforeVersion(version) => quote!(!version.compatible(#version)),
Condition::IfFlag { field, flag } => quote!(#field.contains(#flag)),
Condition::IfCond { xform } => if_expression(xform, false),
}
Expand All @@ -326,6 +355,7 @@ impl Condition {
fn condition_tokens_for_write(&self) -> TokenStream {
match self {
Condition::SinceVersion(version) => quote!(version.compatible(#version)),
Condition::BeforeVersion(version) => quote!(!version.compatible(#version)),
Condition::IfFlag { field, flag } => quote!(self.#field.contains(#flag)),
Condition::IfCond { xform } => if_expression(xform, true),
}
Expand All @@ -336,6 +366,7 @@ impl Condition {
match self {
// special case, we always treat a version field as input
Condition::SinceVersion(_) => vec![],
Condition::BeforeVersion(_) => vec![],
Condition::IfFlag { field, .. } => vec![field.clone()],
Condition::IfCond { xform } => xform.input_field(),
}
Expand Down
37 changes: 37 additions & 0 deletions font-codegen/src/parsing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ pub(crate) struct FieldReadArgs {
#[derive(Clone, Debug)]
pub(crate) enum Condition {
SinceVersion(VersionSpec),
BeforeVersion(VersionSpec),
IfFlag { field: syn::Ident, flag: syn::Path },
IfCond { xform: IfTransform },
}
Expand All @@ -256,6 +257,10 @@ pub(crate) enum IfTransform {
///
/// Evaluates to true if field has at least one of the input flags set.
AnyFlag(syn::Ident, Vec<syn::Path>),
/// not_flag(field, flag_a, ...):
///
/// Evaluates to true if field does *not* have any flag set
NotFlag(syn::Ident, Vec<syn::Path>),
}

enum IfArg {
Expand Down Expand Up @@ -320,6 +325,8 @@ pub(crate) enum CountTransform {
SubAddTwo,
/// requires exactly one arg. Get the count from the $arg1.`try_into::<usize>`().
TryInto,
/// requires exactly one arg. Count the number of 1 bits in the value
CountOnes,
}

/// Attributes for specifying how to compile a field
Expand Down Expand Up @@ -1042,6 +1049,7 @@ static NULLABLE: &str = "nullable";
static SKIP_GETTER: &str = "skip_getter";
static COUNT: &str = "count";
static SINCE_VERSION: &str = "since_version";
static BEFORE_VERSION: &str = "before_version";
static IF_COND: &str = "if_cond";
static IF_FLAG: &str = "if_flag";
static FORMAT: &str = "format";
Expand Down Expand Up @@ -1103,6 +1111,9 @@ impl Parse for FieldAttrs {
} else if ident == SINCE_VERSION {
let spec = attr.parse_args()?;
this.checked_set_condition(ident, Condition::SinceVersion(spec))?;
} else if ident == BEFORE_VERSION {
let spec = attr.parse_args()?;
this.checked_set_condition(ident, Condition::BeforeVersion(spec))?;
} else if ident == IF_FLAG {
let condition = parse_if_flag(&attr)?;
this.checked_set_condition(ident, condition)?;
Expand Down Expand Up @@ -1471,6 +1482,7 @@ static TRANSFORM_IDENTS: &[(CountTransform, &str)] = &[
(CountTransform::MaxValueBitmapLen, "max_value_bitmap_len"),
(CountTransform::SubAddTwo, "subtract_add_two"),
(CountTransform::TryInto, "try_into"),
(CountTransform::CountOnes, "count_ones"),
];

impl FromStr for CountTransform {
Expand Down Expand Up @@ -1509,6 +1521,7 @@ impl CountTransform {
CountTransform::MaxValueBitmapLen => 1,
CountTransform::SubAddTwo => 2,
CountTransform::TryInto => 1,
CountTransform::CountOnes => 1,
}
}
}
Expand Down Expand Up @@ -1669,6 +1682,9 @@ impl Count {
(CountTransform::TryInto, [a]) => {
quote!(usize::try_from(#a).unwrap_or_default())
}
(CountTransform::CountOnes, [a]) => {
quote!(transforms::count_ones(#a))
}
_ => unreachable!("validated before now"),
},
}
Expand Down Expand Up @@ -1861,6 +1877,7 @@ impl IfTransform {
fn from_args(s: &str, args: Vec<IfArg>) -> Result<Self, String> {
match s {
"any_flag" => Self::any_flag(args),
"not_flag" => Self::not_flag(args),
_ => Err(format!("invalid if_cond transform function: {}", s)),
}
}
Expand All @@ -1884,9 +1901,29 @@ impl IfTransform {
Ok(IfTransform::AnyFlag(field.clone(), flags))
}

fn not_flag(args: Vec<IfArg>) -> Result<Self, String> {
let Some(IfArg::Field(field)) = args.first() else {
return Err("First argument to not_flag must be a field name.".to_string());
};

let mut flags: Vec<syn::Path> = vec![];
for arg in args.iter().skip(1) {
let IfArg::Path(flag) = arg else {
return Err(
"Arguments after the first argument to not_flag must be a flag names."
.to_string(),
);
};
flags.push(flag.clone());
}

Ok(IfTransform::NotFlag(field.clone(), flags))
}

pub(crate) fn input_field(&self) -> Vec<syn::Ident> {
match self {
IfTransform::AnyFlag(field, _) => vec![field.clone()],
IfTransform::NotFlag(field, _) => vec![field.clone()],
}
}
}
Expand Down
Loading