From 8f36fca68447a5d42a3d54b5fac7e5546ee244be Mon Sep 17 00:00:00 2001 From: Laurenz Date: Mon, 11 Sep 2023 14:38:54 +0200 Subject: Better proc macros --- crates/typst-macros/src/cast.rs | 333 ++++++++++++++++++++ crates/typst-macros/src/castable.rs | 331 -------------------- crates/typst-macros/src/elem.rs | 583 ++++++++++++++++++++++++++++++++++++ crates/typst-macros/src/element.rs | 555 ---------------------------------- crates/typst-macros/src/func.rs | 459 ++++++++++++++++++---------- crates/typst-macros/src/lib.rs | 295 +++++++++++++++++- crates/typst-macros/src/scope.rs | 153 ++++++++++ crates/typst-macros/src/symbols.rs | 2 +- crates/typst-macros/src/ty.rs | 113 +++++++ crates/typst-macros/src/util.rs | 229 ++++++++++---- 10 files changed, 1939 insertions(+), 1114 deletions(-) create mode 100644 crates/typst-macros/src/cast.rs delete mode 100644 crates/typst-macros/src/castable.rs create mode 100644 crates/typst-macros/src/elem.rs delete mode 100644 crates/typst-macros/src/element.rs create mode 100644 crates/typst-macros/src/scope.rs create mode 100644 crates/typst-macros/src/ty.rs diff --git a/crates/typst-macros/src/cast.rs b/crates/typst-macros/src/cast.rs new file mode 100644 index 00000000..74bdc590 --- /dev/null +++ b/crates/typst-macros/src/cast.rs @@ -0,0 +1,333 @@ +use heck::ToKebabCase; + +use super::*; + +/// Expand the `#[derive(Cast)]` macro. +pub fn derive_cast(item: DeriveInput) -> Result { + let ty = &item.ident; + + let syn::Data::Enum(data) = &item.data else { + bail!(item, "only enums are supported"); + }; + + let mut variants = vec![]; + for variant in &data.variants { + if let Some((_, expr)) = &variant.discriminant { + bail!(expr, "explicit discriminant is not allowed"); + } + + let string = if let Some(attr) = + variant.attrs.iter().find(|attr| attr.path().is_ident("string")) + { + attr.parse_args::()?.value() + } else { + variant.ident.to_string().to_kebab_case() + }; + + variants.push(Variant { + ident: variant.ident.clone(), + string, + docs: documentation(&variant.attrs), + }); + } + + let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| { + quote! { + #[doc = #docs] + #string => Self::#ident + } + }); + + let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| { + quote! { + #ty::#ident => #string + } + }); + + Ok(quote! { + ::typst::eval::cast! { + #ty, + self => ::typst::eval::IntoValue::into_value(match self { + #(#variants_to_strs),* + }), + #(#strs_to_variants),* + } + }) +} + +/// An enum variant in a `derive(Cast)`. +struct Variant { + ident: Ident, + string: String, + docs: String, +} + +/// Expand the `cast!` macro. +pub fn cast(stream: TokenStream) -> Result { + let eval = quote! { ::typst::eval }; + + let input: CastInput = syn::parse2(stream)?; + let ty = &input.ty; + let castable_body = create_castable_body(&input); + let input_body = create_input_body(&input); + let output_body = create_output_body(&input); + let into_value_body = create_into_value_body(&input); + let from_value_body = create_from_value_body(&input); + + let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| { + quote! { + impl #eval::Reflect for #ty { + fn input() -> #eval::CastInfo { + #input_body + } + + fn output() -> #eval::CastInfo { + #output_body + } + + fn castable(value: &#eval::Value) -> bool { + #castable_body + } + } + } + }); + + let into_value = (input.into_value.is_some() || input.dynamic).then(|| { + quote! { + impl #eval::IntoValue for #ty { + fn into_value(self) -> #eval::Value { + #into_value_body + } + } + } + }); + + let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| { + quote! { + impl #eval::FromValue for #ty { + fn from_value(value: #eval::Value) -> ::typst::diag::StrResult { + #from_value_body + } + } + } + }); + + Ok(quote! { + #reflect + #into_value + #from_value + }) +} + +/// The input to `cast!`. +struct CastInput { + ty: syn::Type, + dynamic: bool, + into_value: Option, + from_value: Punctuated, +} + +impl Parse for CastInput { + fn parse(input: ParseStream) -> Result { + let mut dynamic = false; + if input.peek(syn::Token![type]) { + let _: syn::Token![type] = input.parse()?; + dynamic = true; + } + + let ty = input.parse()?; + let _: syn::Token![,] = input.parse()?; + + let mut to_value = None; + if input.peek(syn::Token![self]) { + let _: syn::Token![self] = input.parse()?; + let _: syn::Token![=>] = input.parse()?; + to_value = Some(input.parse()?); + let _: syn::Token![,] = input.parse()?; + } + + let from_value = Punctuated::parse_terminated(input)?; + Ok(Self { ty, dynamic, into_value: to_value, from_value }) + } +} + +impl Parse for Cast { + fn parse(input: ParseStream) -> Result { + let attrs = input.call(syn::Attribute::parse_outer)?; + let pattern = input.parse()?; + let _: syn::Token![=>] = input.parse()?; + let expr = input.parse()?; + Ok(Self { attrs, pattern, expr }) + } +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> Result { + if input.peek(syn::LitStr) { + Ok(Pattern::Str(input.parse()?)) + } else { + let pat = syn::Pat::parse_single(input)?; + let _: syn::Token![:] = input.parse()?; + let ty = input.parse()?; + Ok(Pattern::Ty(pat, ty)) + } + } +} + +/// A single cast, e.g. `v: i64 => Self::Int(v)`. +struct Cast { + attrs: Vec, + pattern: Pattern, + expr: syn::Expr, +} + +/// A pattern in a cast, e.g.`"ascender"` or `v: i64`. +enum Pattern { + Str(syn::LitStr), + Ty(syn::Pat, syn::Type), +} + +fn create_castable_body(input: &CastInput) -> TokenStream { + let mut strings = vec![]; + let mut casts = vec![]; + + for cast in &input.from_value { + match &cast.pattern { + Pattern::Str(lit) => { + strings.push(quote! { #lit => return true }); + } + Pattern::Ty(_, ty) => { + casts.push(quote! { + if <#ty as ::typst::eval::Reflect>::castable(value) { + return true; + } + }); + } + } + } + + let dynamic_check = input.dynamic.then(|| { + quote! { + if let ::typst::eval::Value::Dyn(dynamic) = &value { + if dynamic.is::() { + return true; + } + } + } + }); + + let str_check = (!strings.is_empty()).then(|| { + quote! { + if let ::typst::eval::Value::Str(string) = &value { + match string.as_str() { + #(#strings,)* + _ => {} + } + } + } + }); + + quote! { + #dynamic_check + #str_check + #(#casts)* + false + } +} + +fn create_input_body(input: &CastInput) -> TokenStream { + let mut infos = vec![]; + + for cast in &input.from_value { + let docs = documentation(&cast.attrs); + infos.push(match &cast.pattern { + Pattern::Str(lit) => { + quote! { + ::typst::eval::CastInfo::Value( + ::typst::eval::IntoValue::into_value(#lit), + #docs, + ) + } + } + Pattern::Ty(_, ty) => { + quote! { <#ty as ::typst::eval::Reflect>::input() } + } + }); + } + + if input.dynamic { + infos.push(quote! { + ::typst::eval::CastInfo::Type(::typst::eval::Type::of::()) + }); + } + + quote! { + #(#infos)+* + } +} + +fn create_output_body(input: &CastInput) -> TokenStream { + if input.dynamic { + quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::()) } + } else { + quote! { Self::input() } + } +} + +fn create_into_value_body(input: &CastInput) -> TokenStream { + if let Some(expr) = &input.into_value { + quote! { #expr } + } else { + quote! { ::typst::eval::Value::dynamic(self) } + } +} + +fn create_from_value_body(input: &CastInput) -> TokenStream { + let mut string_arms = vec![]; + let mut cast_checks = vec![]; + + for cast in &input.from_value { + let expr = &cast.expr; + match &cast.pattern { + Pattern::Str(lit) => { + string_arms.push(quote! { #lit => return Ok(#expr) }); + } + Pattern::Ty(binding, ty) => { + cast_checks.push(quote! { + if <#ty as ::typst::eval::Reflect>::castable(&value) { + let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; + return Ok(#expr); + } + }); + } + } + } + + let dynamic_check = input.dynamic.then(|| { + quote! { + if let ::typst::eval::Value::Dyn(dynamic) = &value { + if let Some(concrete) = dynamic.downcast::() { + return Ok(concrete.clone()); + } + } + } + }); + + let str_check = (!string_arms.is_empty()).then(|| { + quote! { + if let ::typst::eval::Value::Str(string) = &value { + match string.as_str() { + #(#string_arms,)* + _ => {} + } + } + } + }); + + quote! { + #dynamic_check + #str_check + #(#cast_checks)* + Err(::error(&value)) + } +} diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/castable.rs deleted file mode 100644 index 05c1b4d1..00000000 --- a/crates/typst-macros/src/castable.rs +++ /dev/null @@ -1,331 +0,0 @@ -use super::*; - -/// Expand the `#[derive(Cast)]` macro. -pub fn derive_cast(item: &DeriveInput) -> Result { - let ty = &item.ident; - - let syn::Data::Enum(data) = &item.data else { - bail!(item, "only enums are supported"); - }; - - let mut variants = vec![]; - for variant in &data.variants { - if let Some((_, expr)) = &variant.discriminant { - bail!(expr, "explicit discriminant is not allowed"); - } - - let string = if let Some(attr) = - variant.attrs.iter().find(|attr| attr.path().is_ident("string")) - { - attr.parse_args::()?.value() - } else { - kebab_case(&variant.ident) - }; - - variants.push(Variant { - ident: variant.ident.clone(), - string, - docs: documentation(&variant.attrs), - }); - } - - let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| { - quote! { - #[doc = #docs] - #string => Self::#ident - } - }); - - let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| { - quote! { - #ty::#ident => #string - } - }); - - Ok(quote! { - ::typst::eval::cast! { - #ty, - self => ::typst::eval::IntoValue::into_value(match self { - #(#variants_to_strs),* - }), - #(#strs_to_variants),* - } - }) -} - -/// An enum variant in a `derive(Cast)`. -struct Variant { - ident: Ident, - string: String, - docs: String, -} - -/// Expand the `cast!` macro. -pub fn cast(stream: TokenStream) -> Result { - let input: CastInput = syn::parse2(stream)?; - let ty = &input.ty; - let eval = quote! { ::typst::eval }; - - let castable_body = create_castable_body(&input); - let describe_body = create_describe_body(&input); - let into_value_body = create_into_value_body(&input); - let from_value_body = create_from_value_body(&input); - - let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| { - quote! { - impl #eval::Reflect for #ty { - fn describe() -> #eval::CastInfo { - #describe_body - } - - fn castable(value: &#eval::Value) -> bool { - #castable_body - } - } - } - }); - - let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { - quote! { - impl #eval::IntoValue for #ty { - fn into_value(self) -> #eval::Value { - #into_value_body - } - } - } - }); - - let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { - quote! { - impl #eval::FromValue for #ty { - fn from_value(value: #eval::Value) -> ::typst::diag::StrResult { - #from_value_body - } - } - } - }); - - let ty = input.name.as_ref().map(|name| { - quote! { - impl #eval::Type for #ty { - const TYPE_NAME: &'static str = #name; - } - } - }); - - Ok(quote! { - #reflect - #into_value - #from_value - #ty - }) -} - -/// The input to `cast!`. -struct CastInput { - ty: syn::Type, - name: Option, - into_value: Option, - from_value: Punctuated, -} - -impl Parse for CastInput { - fn parse(input: ParseStream) -> Result { - let ty; - let mut name = None; - if input.peek(syn::Token![type]) { - let _: syn::Token![type] = input.parse()?; - ty = input.parse()?; - let _: syn::Token![:] = input.parse()?; - name = Some(input.parse()?); - } else { - ty = input.parse()?; - } - - let _: syn::Token![,] = input.parse()?; - - let mut into_value = None; - if input.peek(syn::Token![self]) { - let _: syn::Token![self] = input.parse()?; - let _: syn::Token![=>] = input.parse()?; - into_value = Some(input.parse()?); - let _: syn::Token![,] = input.parse()?; - } - - let from_value = Punctuated::parse_terminated(input)?; - Ok(Self { ty, name, into_value, from_value }) - } -} - -impl Parse for Cast { - fn parse(input: ParseStream) -> Result { - let attrs = input.call(syn::Attribute::parse_outer)?; - let pattern = input.parse()?; - let _: syn::Token![=>] = input.parse()?; - let expr = input.parse()?; - Ok(Self { attrs, pattern, expr }) - } -} - -impl Parse for Pattern { - fn parse(input: ParseStream) -> Result { - if input.peek(syn::LitStr) { - Ok(Pattern::Str(input.parse()?)) - } else { - let pat = syn::Pat::parse_single(input)?; - let _: syn::Token![:] = input.parse()?; - let ty = input.parse()?; - Ok(Pattern::Ty(pat, ty)) - } - } -} - -/// A single cast, e.g. `v: i64 => Self::Int(v)`. -struct Cast { - attrs: Vec, - pattern: Pattern, - expr: syn::Expr, -} - -/// A pattern in a cast, e.g.`"ascender"` or `v: i64`. -enum Pattern { - Str(syn::LitStr), - Ty(syn::Pat, syn::Type), -} - -fn create_castable_body(input: &CastInput) -> TokenStream { - let mut strings = vec![]; - let mut casts = vec![]; - - for cast in &input.from_value { - match &cast.pattern { - Pattern::Str(lit) => { - strings.push(quote! { #lit => return true }); - } - Pattern::Ty(_, ty) => { - casts.push(quote! { - if <#ty as ::typst::eval::Reflect>::castable(value) { - return true; - } - }); - } - } - } - - let dynamic_check = input.name.is_some().then(|| { - quote! { - if let ::typst::eval::Value::Dyn(dynamic) = &value { - if dynamic.is::() { - return true; - } - } - } - }); - - let str_check = (!strings.is_empty()).then(|| { - quote! { - if let ::typst::eval::Value::Str(string) = &value { - match string.as_str() { - #(#strings,)* - _ => {} - } - } - } - }); - - quote! { - #dynamic_check - #str_check - #(#casts)* - false - } -} - -fn create_describe_body(input: &CastInput) -> TokenStream { - let mut infos = vec![]; - - for cast in &input.from_value { - let docs = documentation(&cast.attrs); - infos.push(match &cast.pattern { - Pattern::Str(lit) => { - quote! { - ::typst::eval::CastInfo::Value( - ::typst::eval::IntoValue::into_value(#lit), - #docs, - ) - } - } - Pattern::Ty(_, ty) => { - quote! { <#ty as ::typst::eval::Reflect>::describe() } - } - }); - } - - if let Some(name) = &input.name { - infos.push(quote! { - ::typst::eval::CastInfo::Type(#name) - }); - } - - quote! { - #(#infos)+* - } -} - -fn create_into_value_body(input: &CastInput) -> TokenStream { - if let Some(expr) = &input.into_value { - quote! { #expr } - } else { - quote! { ::typst::eval::Value::dynamic(self) } - } -} - -fn create_from_value_body(input: &CastInput) -> TokenStream { - let mut string_arms = vec![]; - let mut cast_checks = vec![]; - - for cast in &input.from_value { - let expr = &cast.expr; - match &cast.pattern { - Pattern::Str(lit) => { - string_arms.push(quote! { #lit => return Ok(#expr) }); - } - Pattern::Ty(binding, ty) => { - cast_checks.push(quote! { - if <#ty as ::typst::eval::Reflect>::castable(&value) { - let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; - return Ok(#expr); - } - }); - } - } - } - - let dynamic_check = input.name.is_some().then(|| { - quote! { - if let ::typst::eval::Value::Dyn(dynamic) = &value { - if let Some(concrete) = dynamic.downcast::() { - return Ok(concrete.clone()); - } - } - } - }); - - let str_check = (!string_arms.is_empty()).then(|| { - quote! { - if let ::typst::eval::Value::Str(string) = &value { - match string.as_str() { - #(#string_arms,)* - _ => {} - } - } - } - }); - - quote! { - #dynamic_check - #str_check - #(#cast_checks)* - Err(::error(&value)) - } -} diff --git a/crates/typst-macros/src/elem.rs b/crates/typst-macros/src/elem.rs new file mode 100644 index 00000000..5584bdb6 --- /dev/null +++ b/crates/typst-macros/src/elem.rs @@ -0,0 +1,583 @@ +use heck::ToKebabCase; + +use super::*; + +/// Expand the `#[elem]` macro. +pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result { + let element = parse(stream, &body)?; + Ok(create(&element)) +} + +/// Details about an element. +struct Elem { + name: String, + title: String, + scope: bool, + keywords: Vec, + docs: String, + vis: syn::Visibility, + ident: Ident, + capabilities: Vec, + fields: Vec, +} + +/// Details about an element field. +struct Field { + ident: Ident, + ident_in: Ident, + with_ident: Ident, + push_ident: Ident, + set_ident: Ident, + vis: syn::Visibility, + ty: syn::Type, + output: syn::Type, + name: String, + docs: String, + positional: bool, + required: bool, + variadic: bool, + resolve: bool, + fold: bool, + internal: bool, + external: bool, + synthesized: bool, + parse: Option, + default: syn::Expr, +} + +impl Field { + /// Whether the field is present on every instance of the element. + fn inherent(&self) -> bool { + self.required || self.variadic + } + + /// Whether the field can be used with set rules. + fn settable(&self) -> bool { + !self.inherent() + } +} + +/// The `..` in `#[elem(..)]`. +struct Meta { + scope: bool, + name: Option, + title: Option, + keywords: Vec, + capabilities: Vec, +} + +impl Parse for Meta { + fn parse(input: ParseStream) -> Result { + Ok(Self { + scope: parse_flag::(input)?, + name: parse_string::(input)?, + title: parse_string::(input)?, + keywords: parse_string_array::(input)?, + capabilities: Punctuated::::parse_terminated(input)? + .into_iter() + .collect(), + }) + } +} + +/// Parse details about the element from its struct definition. +fn parse(stream: TokenStream, body: &syn::ItemStruct) -> Result { + let meta: Meta = syn::parse2(stream)?; + let (name, title) = determine_name_and_title( + meta.name, + meta.title, + &body.ident, + Some(|base| base.trim_end_matches("Elem")), + )?; + + let docs = documentation(&body.attrs); + + let syn::Fields::Named(named) = &body.fields else { + bail!(body, "expected named fields"); + }; + let fields = named.named.iter().map(parse_field).collect::>()?; + + Ok(Elem { + name, + title, + scope: meta.scope, + keywords: meta.keywords, + docs, + vis: body.vis.clone(), + ident: body.ident.clone(), + capabilities: meta.capabilities, + fields, + }) +} + +fn parse_field(field: &syn::Field) -> Result { + let Some(ident) = field.ident.clone() else { + bail!(field, "expected named field"); + }; + + if ident == "label" { + bail!(ident, "invalid field name"); + } + + let mut attrs = field.attrs.clone(); + let variadic = has_attr(&mut attrs, "variadic"); + let required = has_attr(&mut attrs, "required") || variadic; + let positional = has_attr(&mut attrs, "positional") || required; + + let mut field = Field { + name: ident.to_string().to_kebab_case(), + docs: documentation(&attrs), + internal: has_attr(&mut attrs, "internal"), + external: has_attr(&mut attrs, "external"), + positional, + required, + variadic, + synthesized: has_attr(&mut attrs, "synthesized"), + fold: has_attr(&mut attrs, "fold"), + resolve: has_attr(&mut attrs, "resolve"), + parse: parse_attr(&mut attrs, "parse")?.flatten(), + default: parse_attr(&mut attrs, "default")? + .flatten() + .unwrap_or_else(|| parse_quote! { ::std::default::Default::default() }), + vis: field.vis.clone(), + ident: ident.clone(), + ident_in: Ident::new(&format!("{}_in", ident), ident.span()), + with_ident: Ident::new(&format!("with_{}", ident), ident.span()), + push_ident: Ident::new(&format!("push_{}", ident), ident.span()), + set_ident: Ident::new(&format!("set_{}", ident), ident.span()), + ty: field.ty.clone(), + output: field.ty.clone(), + }; + + if field.required && (field.fold || field.resolve) { + bail!(ident, "required fields cannot be folded or resolved"); + } + + if field.required && !field.positional { + bail!(ident, "only positional fields can be required"); + } + + if field.resolve { + let output = &field.output; + field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output }; + } + + if field.fold { + let output = &field.output; + field.output = parse_quote! { <#output as ::typst::model::Fold>::Output }; + } + + validate_attrs(&attrs)?; + + Ok(field) +} + +/// Produce the element's definition. +fn create(element: &Elem) -> TokenStream { + let Elem { vis, ident, docs, .. } = element; + let all = element.fields.iter().filter(|field| !field.external); + let settable = all.clone().filter(|field| !field.synthesized && field.settable()); + + // Inherent methods and functions. + let new = create_new_func(element); + let field_methods = all.clone().map(create_field_method); + let field_in_methods = settable.clone().map(create_field_in_method); + let with_field_methods = all.clone().map(create_with_field_method); + let push_field_methods = all.map(create_push_field_method); + let field_style_methods = settable.map(create_set_field_method); + + // Trait implementations. + let element_impl = create_pack_impl(element); + let construct_impl = element + .capabilities + .iter() + .all(|capability| capability != "Construct") + .then(|| create_construct_impl(element)); + let set_impl = create_set_impl(element); + let locatable_impl = element + .capabilities + .iter() + .any(|capability| capability == "Locatable") + .then(|| quote! { impl ::typst::model::Locatable for #ident {} }); + + quote! { + #[doc = #docs] + #[derive(Debug, Clone, Hash)] + #[repr(transparent)] + #vis struct #ident(pub ::typst::model::Content); + + impl #ident { + #new + #(#field_methods)* + #(#field_in_methods)* + #(#with_field_methods)* + #(#push_field_methods)* + #(#field_style_methods)* + + /// The element's span. + pub fn span(&self) -> ::typst::syntax::Span { + self.0.span() + } + + /// Set the element's span. + pub fn spanned(self, span: ::typst::syntax::Span) -> Self { + Self(self.0.spanned(span)) + } + } + + #element_impl + #construct_impl + #set_impl + #locatable_impl + + impl ::typst::eval::IntoValue for #ident { + fn into_value(self) -> ::typst::eval::Value { + ::typst::eval::Value::Content(self.0) + } + } + } +} + +/// Create the `new` function for the element. +fn create_new_func(element: &Elem) -> TokenStream { + let relevant = element + .fields + .iter() + .filter(|field| !field.external && !field.synthesized && field.inherent()); + let params = relevant.clone().map(|Field { ident, ty, .. }| { + quote! { #ident: #ty } + }); + let builder_calls = relevant.map(|Field { ident, with_ident, .. }| { + quote! { .#with_ident(#ident) } + }); + quote! { + /// Create a new element. + pub fn new(#(#params),*) -> Self { + Self(::typst::model::Content::new( + ::elem() + )) + #(#builder_calls)* + } + } +} + +/// Create an accessor methods for a field. +fn create_field_method(field: &Field) -> TokenStream { + let Field { vis, docs, ident, name, output, .. } = field; + if field.inherent() || field.synthesized { + quote! { + #[doc = #docs] + #[track_caller] + #vis fn #ident(&self) -> #output { + self.0.expect_field(#name) + } + } + } else { + let access = create_style_chain_access(field, quote! { self.0.field(#name) }); + quote! { + #[doc = #docs] + #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output { + #access + } + } + } +} + +/// Create a style chain access method for a field. +fn create_field_in_method(field: &Field) -> TokenStream { + let Field { vis, ident_in, name, output, .. } = field; + let doc = format!("Access the `{}` field in the given style chain.", name); + let access = create_style_chain_access(field, quote! { None }); + quote! { + #[doc = #doc] + #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output { + #access + } + } +} + +/// Create a style chain access method for a field. +fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream { + let Field { name, ty, default, .. } = field; + let getter = match (field.fold, field.resolve) { + (false, false) => quote! { get }, + (false, true) => quote! { get_resolve }, + (true, false) => quote! { get_fold }, + (true, true) => quote! { get_resolve_fold }, + }; + + quote! { + styles.#getter::<#ty>( + ::elem(), + #name, + #inherent, + || #default, + ) + } +} + +/// Create a builder pattern method for a field. +fn create_with_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, with_ident, name, ty, .. } = field; + let doc = format!("Set the [`{}`](Self::{}) field.", name, ident); + quote! { + #[doc = #doc] + #vis fn #with_ident(mut self, #ident: #ty) -> Self { + Self(self.0.with_field(#name, #ident)) + } + } +} + +/// Create a set-style method for a field. +fn create_push_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, push_ident, name, ty, .. } = field; + let doc = format!("Push the [`{}`](Self::{}) field.", name, ident); + quote! { + #[doc = #doc] + #vis fn #push_ident(&mut self, #ident: #ty) { + self.0.push_field(#name, #ident); + } + } +} + +/// Create a setter method for a field. +fn create_set_field_method(field: &Field) -> TokenStream { + let Field { vis, ident, set_ident, name, ty, .. } = field; + let doc = format!("Create a style property for the `{}` field.", name); + quote! { + #[doc = #doc] + #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { + ::typst::model::Style::Property(::typst::model::Property::new( + ::elem(), + #name, + #ident, + )) + } + } +} + +/// Create the element's `Pack` implementation. +fn create_pack_impl(element: &Elem) -> TokenStream { + let eval = quote! { ::typst::eval }; + let model = quote! { ::typst::model }; + + let Elem { name, ident, title, scope, keywords, docs, .. } = element; + let vtable_func = create_vtable_func(element); + let params = element + .fields + .iter() + .filter(|field| !field.internal && !field.synthesized) + .map(create_param_info); + + let scope = if *scope { + quote! { <#ident as #eval::NativeScope>::scope() } + } else { + quote! { #eval::Scope::new() } + }; + + let data = quote! { + #model::NativeElementData { + name: #name, + title: #title, + docs: #docs, + keywords: &[#(#keywords),*], + construct: <#ident as #model::Construct>::construct, + set: <#ident as #model::Set>::set, + vtable: #vtable_func, + scope: #eval::Lazy::new(|| #scope), + params: #eval::Lazy::new(|| ::std::vec![#(#params),*]) + } + }; + + quote! { + impl #model::NativeElement for #ident { + fn data() -> &'static #model::NativeElementData { + static DATA: #model::NativeElementData = #data; + &DATA + } + + fn pack(self) -> #model::Content { + self.0 + } + + fn unpack(content: &#model::Content) -> ::std::option::Option<&Self> { + // Safety: Elements are #[repr(transparent)]. + content.is::().then(|| unsafe { + ::std::mem::transmute(content) + }) + } + } + } +} + +/// Create the element's casting vtable. +fn create_vtable_func(element: &Elem) -> TokenStream { + let ident = &element.ident; + let relevant = element.capabilities.iter().filter(|&ident| ident != "Construct"); + let checks = relevant.map(|capability| { + quote! { + if id == ::std::any::TypeId::of::() { + return Some(unsafe { + ::typst::util::fat::vtable(&null as &dyn #capability) + }); + } + } + }); + + quote! { + |id| { + let null = Self(::typst::model::Content::new( + <#ident as ::typst::model::NativeElement>::elem() + )); + #(#checks)* + None + } + } +} + +/// Create a parameter info for a field. +fn create_param_info(field: &Field) -> TokenStream { + let Field { + name, + docs, + positional, + variadic, + required, + default, + fold, + ty, + output, + .. + } = field; + let named = !positional; + let settable = field.settable(); + let default_ty = if *fold { &output } else { &ty }; + let default = quote_option(&settable.then(|| { + quote! { + || { + let typed: #default_ty = #default; + ::typst::eval::IntoValue::into_value(typed) + } + } + })); + let ty = if *variadic { + quote! { <#ty as ::typst::eval::Container>::Inner } + } else { + quote! { #ty } + }; + quote! { + ::typst::eval::ParamInfo { + name: #name, + docs: #docs, + input: <#ty as ::typst::eval::Reflect>::input(), + default: #default, + positional: #positional, + named: #named, + variadic: #variadic, + required: #required, + settable: #settable, + } + } +} + +/// Create the element's `Construct` implementation. +fn create_construct_impl(element: &Elem) -> TokenStream { + let ident = &element.ident; + let handlers = element + .fields + .iter() + .filter(|field| { + !field.external + && !field.synthesized + && (!field.internal || field.parse.is_some()) + }) + .map(|field| { + let push_ident = &field.push_ident; + let (prefix, value) = create_field_parser(field); + if field.settable() { + quote! { + #prefix + if let Some(value) = #value { + element.#push_ident(value); + } + } + } else { + quote! { + #prefix + element.#push_ident(#value); + } + } + }); + + quote! { + impl ::typst::model::Construct for #ident { + fn construct( + vm: &mut ::typst::eval::Vm, + args: &mut ::typst::eval::Args, + ) -> ::typst::diag::SourceResult<::typst::model::Content> { + let mut element = Self(::typst::model::Content::new( + ::elem() + )); + #(#handlers)* + Ok(element.0) + } + } + } +} + +/// Create the element's `Set` implementation. +fn create_set_impl(element: &Elem) -> TokenStream { + let ident = &element.ident; + let handlers = element + .fields + .iter() + .filter(|field| { + !field.external + && !field.synthesized + && field.settable() + && (!field.internal || field.parse.is_some()) + }) + .map(|field| { + let set_ident = &field.set_ident; + let (prefix, value) = create_field_parser(field); + quote! { + #prefix + if let Some(value) = #value { + styles.set(Self::#set_ident(value)); + } + } + }); + + quote! { + impl ::typst::model::Set for #ident { + fn set( + vm: &mut Vm, + args: &mut ::typst::eval::Args, + ) -> ::typst::diag::SourceResult<::typst::model::Styles> { + let mut styles = ::typst::model::Styles::new(); + #(#handlers)* + Ok(styles) + } + } + } +} + +/// Create argument parsing code for a field. +fn create_field_parser(field: &Field) -> (TokenStream, TokenStream) { + if let Some(BlockWithReturn { prefix, expr }) = &field.parse { + return (quote! { #(#prefix);* }, quote! { #expr }); + } + + let name = &field.name; + let value = if field.variadic { + quote! { args.all()? } + } else if field.required { + quote! { args.expect(#name)? } + } else if field.positional { + quote! { args.find()? } + } else { + quote! { args.named(#name)? } + }; + + (quote! {}, value) +} diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/element.rs deleted file mode 100644 index e047e606..00000000 --- a/crates/typst-macros/src/element.rs +++ /dev/null @@ -1,555 +0,0 @@ -use super::*; - -/// Expand the `#[element]` macro. -pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result { - let element = prepare(stream, body)?; - Ok(create(&element)) -} - -struct Elem { - name: String, - display: String, - category: String, - keywords: Option, - docs: String, - vis: syn::Visibility, - ident: Ident, - capable: Vec, - fields: Vec, - scope: Option, -} - -struct Field { - name: String, - docs: String, - internal: bool, - external: bool, - positional: bool, - required: bool, - variadic: bool, - synthesized: bool, - fold: bool, - resolve: bool, - parse: Option, - default: syn::Expr, - vis: syn::Visibility, - ident: Ident, - ident_in: Ident, - with_ident: Ident, - push_ident: Ident, - set_ident: Ident, - ty: syn::Type, - output: syn::Type, -} - -impl Field { - fn inherent(&self) -> bool { - self.required || self.variadic - } - - fn settable(&self) -> bool { - !self.inherent() - } -} - -/// Preprocess the element's definition. -fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { - let syn::Fields::Named(named) = &body.fields else { - bail!(body, "expected named fields"); - }; - - let mut fields = vec![]; - for field in &named.named { - let Some(ident) = field.ident.clone() else { - bail!(field, "expected named field"); - }; - - let mut attrs = field.attrs.clone(); - let variadic = has_attr(&mut attrs, "variadic"); - let required = has_attr(&mut attrs, "required") || variadic; - let positional = has_attr(&mut attrs, "positional") || required; - - if ident == "label" { - bail!(ident, "invalid field name"); - } - - let mut field = Field { - name: kebab_case(&ident), - docs: documentation(&attrs), - internal: has_attr(&mut attrs, "internal"), - external: has_attr(&mut attrs, "external"), - positional, - required, - variadic, - synthesized: has_attr(&mut attrs, "synthesized"), - fold: has_attr(&mut attrs, "fold"), - resolve: has_attr(&mut attrs, "resolve"), - parse: parse_attr(&mut attrs, "parse")?.flatten(), - default: parse_attr(&mut attrs, "default")? - .flatten() - .unwrap_or_else(|| parse_quote! { ::std::default::Default::default() }), - vis: field.vis.clone(), - ident: ident.clone(), - ident_in: Ident::new(&format!("{}_in", ident), ident.span()), - with_ident: Ident::new(&format!("with_{}", ident), ident.span()), - push_ident: Ident::new(&format!("push_{}", ident), ident.span()), - set_ident: Ident::new(&format!("set_{}", ident), ident.span()), - ty: field.ty.clone(), - output: field.ty.clone(), - }; - - if field.required && (field.fold || field.resolve) { - bail!(ident, "required fields cannot be folded or resolved"); - } - - if field.required && !field.positional { - bail!(ident, "only positional fields can be required"); - } - - if field.resolve { - let output = &field.output; - field.output = parse_quote! { <#output as ::typst::model::Resolve>::Output }; - } - if field.fold { - let output = &field.output; - field.output = parse_quote! { <#output as ::typst::model::Fold>::Output }; - } - - validate_attrs(&attrs)?; - fields.push(field); - } - - let capable = Punctuated::::parse_terminated - .parse2(stream)? - .into_iter() - .collect(); - - let mut attrs = body.attrs.clone(); - let docs = documentation(&attrs); - let mut lines = docs.split('\n').collect(); - let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); - let category = meta_line(&mut lines, "Category")?.into(); - let display = meta_line(&mut lines, "Display")?.into(); - let docs = lines.join("\n").trim().into(); - - let element = Elem { - name: body.ident.to_string().trim_end_matches("Elem").to_lowercase(), - display, - category, - keywords, - docs, - vis: body.vis.clone(), - ident: body.ident.clone(), - capable, - fields, - scope: parse_attr(&mut attrs, "scope")?.flatten(), - }; - - validate_attrs(&attrs)?; - Ok(element) -} - -/// Produce the element's definition. -fn create(element: &Elem) -> TokenStream { - let Elem { vis, ident, docs, .. } = element; - let all = element.fields.iter().filter(|field| !field.external); - let settable = all.clone().filter(|field| !field.synthesized && field.settable()); - - // Inherent methods and functions. - let new = create_new_func(element); - let field_methods = all.clone().map(create_field_method); - let field_in_methods = settable.clone().map(create_field_in_method); - let with_field_methods = all.clone().map(create_with_field_method); - let push_field_methods = all.map(create_push_field_method); - let field_style_methods = settable.map(create_set_field_method); - - // Trait implementations. - let element_impl = create_pack_impl(element); - let construct_impl = element - .capable - .iter() - .all(|capability| capability != "Construct") - .then(|| create_construct_impl(element)); - let set_impl = create_set_impl(element); - let locatable_impl = element - .capable - .iter() - .any(|capability| capability == "Locatable") - .then(|| quote! { impl ::typst::model::Locatable for #ident {} }); - - quote! { - #[doc = #docs] - #[derive(Debug, Clone, Hash)] - #[repr(transparent)] - #vis struct #ident(pub ::typst::model::Content); - - impl #ident { - #new - #(#field_methods)* - #(#field_in_methods)* - #(#with_field_methods)* - #(#push_field_methods)* - #(#field_style_methods)* - - /// The element's span. - pub fn span(&self) -> ::typst::syntax::Span { - self.0.span() - } - - /// Set the element's span. - pub fn spanned(self, span: ::typst::syntax::Span) -> Self { - Self(self.0.spanned(span)) - } - } - - #element_impl - #construct_impl - #set_impl - #locatable_impl - - impl ::typst::eval::IntoValue for #ident { - fn into_value(self) -> ::typst::eval::Value { - ::typst::eval::Value::Content(self.0) - } - } - } -} - -/// Create the `new` function for the element. -fn create_new_func(element: &Elem) -> TokenStream { - let relevant = element - .fields - .iter() - .filter(|field| !field.external && !field.synthesized && field.inherent()); - let params = relevant.clone().map(|Field { ident, ty, .. }| { - quote! { #ident: #ty } - }); - let builder_calls = relevant.map(|Field { ident, with_ident, .. }| { - quote! { .#with_ident(#ident) } - }); - quote! { - /// Create a new element. - pub fn new(#(#params),*) -> Self { - Self(::typst::model::Content::new( - ::func() - )) - #(#builder_calls)* - } - } -} - -/// Create an accessor methods for a field. -fn create_field_method(field: &Field) -> TokenStream { - let Field { vis, docs, ident, name, output, .. } = field; - if field.inherent() || field.synthesized { - quote! { - #[doc = #docs] - #[track_caller] - #vis fn #ident(&self) -> #output { - self.0.expect_field(#name) - } - } - } else { - let access = create_style_chain_access(field, quote! { self.0.field(#name) }); - quote! { - #[doc = #docs] - #vis fn #ident(&self, styles: ::typst::model::StyleChain) -> #output { - #access - } - } - } -} - -/// Create a style chain access method for a field. -fn create_field_in_method(field: &Field) -> TokenStream { - let Field { vis, ident_in, name, output, .. } = field; - let doc = format!("Access the `{}` field in the given style chain.", name); - let access = create_style_chain_access(field, quote! { None }); - quote! { - #[doc = #doc] - #vis fn #ident_in(styles: ::typst::model::StyleChain) -> #output { - #access - } - } -} - -/// Create a style chain access method for a field. -fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStream { - let Field { name, ty, default, .. } = field; - let getter = match (field.fold, field.resolve) { - (false, false) => quote! { get }, - (false, true) => quote! { get_resolve }, - (true, false) => quote! { get_fold }, - (true, true) => quote! { get_resolve_fold }, - }; - - quote! { - styles.#getter::<#ty>( - ::func(), - #name, - #inherent, - || #default, - ) - } -} - -/// Create a builder pattern method for a field. -fn create_with_field_method(field: &Field) -> TokenStream { - let Field { vis, ident, with_ident, name, ty, .. } = field; - let doc = format!("Set the [`{}`](Self::{}) field.", name, ident); - quote! { - #[doc = #doc] - #vis fn #with_ident(mut self, #ident: #ty) -> Self { - Self(self.0.with_field(#name, #ident)) - } - } -} - -/// Create a set-style method for a field. -fn create_push_field_method(field: &Field) -> TokenStream { - let Field { vis, ident, push_ident, name, ty, .. } = field; - let doc = format!("Push the [`{}`](Self::{}) field.", name, ident); - quote! { - #[doc = #doc] - #vis fn #push_ident(&mut self, #ident: #ty) { - self.0.push_field(#name, #ident); - } - } -} - -/// Create a setter method for a field. -fn create_set_field_method(field: &Field) -> TokenStream { - let Field { vis, ident, set_ident, name, ty, .. } = field; - let doc = format!("Create a style property for the `{}` field.", name); - quote! { - #[doc = #doc] - #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { - ::typst::model::Style::Property(::typst::model::Property::new( - ::func(), - #name, - #ident, - )) - } - } -} - -/// Create the element's `Pack` implementation. -fn create_pack_impl(element: &Elem) -> TokenStream { - let Elem { ident, name, display, keywords, category, docs, .. } = element; - let vtable_func = create_vtable_func(element); - let infos = element - .fields - .iter() - .filter(|field| !field.internal && !field.synthesized) - .map(create_param_info); - let scope = create_scope_builder(element.scope.as_ref()); - let keywords = quote_option(keywords); - quote! { - impl ::typst::model::Element for #ident { - fn pack(self) -> ::typst::model::Content { - self.0 - } - - fn unpack(content: &::typst::model::Content) -> ::std::option::Option<&Self> { - // Safety: Elements are #[repr(transparent)]. - content.is::().then(|| unsafe { - ::std::mem::transmute(content) - }) - } - - fn func() -> ::typst::model::ElemFunc { - static NATIVE: ::typst::model::NativeElemFunc = ::typst::model::NativeElemFunc { - name: #name, - vtable: #vtable_func, - construct: <#ident as ::typst::model::Construct>::construct, - set: <#ident as ::typst::model::Set>::set, - info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { - name: #name, - display: #display, - keywords: #keywords, - docs: #docs, - params: ::std::vec![#(#infos),*], - returns: ::typst::eval::CastInfo::Union(::std::vec![ - ::typst::eval::CastInfo::Type("content") - ]), - category: #category, - scope: #scope, - }), - }; - (&NATIVE).into() - } - } - } -} - -/// Create the element's casting vtable. -fn create_vtable_func(element: &Elem) -> TokenStream { - let ident = &element.ident; - let relevant = element.capable.iter().filter(|&ident| ident != "Construct"); - let checks = relevant.map(|capability| { - quote! { - if id == ::std::any::TypeId::of::() { - return Some(unsafe { - ::typst::util::fat::vtable(&null as &dyn #capability) - }); - } - } - }); - - quote! { - |id| { - let null = Self(::typst::model::Content::new( - <#ident as ::typst::model::Element>::func() - )); - #(#checks)* - None - } - } -} - -/// Create a parameter info for a field. -fn create_param_info(field: &Field) -> TokenStream { - let Field { - name, - docs, - positional, - variadic, - required, - default, - fold, - ty, - output, - .. - } = field; - let named = !positional; - let settable = field.settable(); - let default_ty = if *fold { &output } else { &ty }; - let default = quote_option(&settable.then(|| { - quote! { - || { - let typed: #default_ty = #default; - ::typst::eval::IntoValue::into_value(typed) - } - } - })); - let ty = if *variadic { - quote! { <#ty as ::typst::eval::Container>::Inner } - } else { - quote! { #ty } - }; - quote! { - ::typst::eval::ParamInfo { - name: #name, - docs: #docs, - cast: <#ty as ::typst::eval::Reflect>::describe(), - default: #default, - positional: #positional, - named: #named, - variadic: #variadic, - required: #required, - settable: #settable, - } - } -} - -/// Create the element's `Construct` implementation. -fn create_construct_impl(element: &Elem) -> TokenStream { - let ident = &element.ident; - let handlers = element - .fields - .iter() - .filter(|field| { - !field.external - && !field.synthesized - && (!field.internal || field.parse.is_some()) - }) - .map(|field| { - let push_ident = &field.push_ident; - let (prefix, value) = create_field_parser(field); - if field.settable() { - quote! { - #prefix - if let Some(value) = #value { - element.#push_ident(value); - } - } - } else { - quote! { - #prefix - element.#push_ident(#value); - } - } - }); - - quote! { - impl ::typst::model::Construct for #ident { - fn construct( - vm: &mut ::typst::eval::Vm, - args: &mut ::typst::eval::Args, - ) -> ::typst::diag::SourceResult<::typst::model::Content> { - let mut element = Self(::typst::model::Content::new( - ::func() - )); - #(#handlers)* - Ok(element.0) - } - } - } -} - -/// Create the element's `Set` implementation. -fn create_set_impl(element: &Elem) -> TokenStream { - let ident = &element.ident; - let handlers = element - .fields - .iter() - .filter(|field| { - !field.external - && !field.synthesized - && field.settable() - && (!field.internal || field.parse.is_some()) - }) - .map(|field| { - let set_ident = &field.set_ident; - let (prefix, value) = create_field_parser(field); - quote! { - #prefix - if let Some(value) = #value { - styles.set(Self::#set_ident(value)); - } - } - }); - - quote! { - impl ::typst::model::Set for #ident { - fn set( - vm: &mut Vm, - args: &mut ::typst::eval::Args, - ) -> ::typst::diag::SourceResult<::typst::model::Styles> { - let mut styles = ::typst::model::Styles::new(); - #(#handlers)* - Ok(styles) - } - } - } -} - -/// Create argument parsing code for a field. -fn create_field_parser(field: &Field) -> (TokenStream, TokenStream) { - if let Some(BlockWithReturn { prefix, expr }) = &field.parse { - return (quote! { #(#prefix);* }, quote! { #expr }); - } - - let name = &field.name; - let value = if field.variadic { - quote! { args.all()? } - } else if field.required { - quote! { args.expect(#name)? } - } else if field.positional { - quote! { args.find()? } - } else { - quote! { args.named(#name)? } - }; - - (quote! {}, value) -} diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs index 13a6eb2d..87d57f19 100644 --- a/crates/typst-macros/src/func.rs +++ b/crates/typst-macros/src/func.rs @@ -1,206 +1,340 @@ use super::*; +use heck::ToKebabCase; + /// Expand the `#[func]` macro. pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result { - let func = prepare(stream, item)?; + let func = parse(stream, item)?; Ok(create(&func, item)) } +/// Details about a function. struct Func { name: String, - display: String, - category: String, - keywords: Option, + title: String, + scope: bool, + constructor: bool, + keywords: Vec, + parent: Option, docs: String, vis: syn::Visibility, ident: Ident, - ident_func: Ident, - parent: Option, + special: SpecialParams, + params: Vec, + returns: syn::Type, +} + +/// Special parameters provided by the runtime. +#[derive(Default)] +struct SpecialParams { + self_: Option, vm: bool, vt: bool, args: bool, span: bool, - params: Vec, - returns: syn::Type, - scope: Option, } +/// Details about a function parameter. struct Param { + binding: Binding, + ident: Ident, + ty: syn::Type, name: String, docs: String, - external: bool, named: bool, variadic: bool, + external: bool, default: Option, - ident: Ident, - ty: syn::Type, } -fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result { - let sig = &item.sig; +/// How a parameter is bound. +enum Binding { + /// Normal parameter. + Owned, + /// `&self`. + Ref, + /// `&mut self`. + RefMut, +} - let Parent(parent) = syn::parse2(stream)?; +/// The `..` in `#[func(..)]`. +pub struct Meta { + pub scope: bool, + pub name: Option, + pub title: Option, + pub constructor: bool, + pub keywords: Vec, + pub parent: Option, +} - let mut vm = false; - let mut vt = false; - let mut args = false; - let mut span = false; +impl Parse for Meta { + fn parse(input: ParseStream) -> Result { + Ok(Self { + scope: parse_flag::(input)?, + name: parse_string::(input)?, + title: parse_string::(input)?, + constructor: parse_flag::(input)?, + keywords: parse_string_array::(input)?, + parent: parse_key_value::(input)?, + }) + } +} + +/// Parse details about the function from the fn item. +fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result { + let meta: Meta = syn::parse2(stream)?; + let (name, title) = + determine_name_and_title(meta.name, meta.title, &item.sig.ident, None)?; + + let docs = documentation(&item.attrs); + + let mut special = SpecialParams::default(); let mut params = vec![]; - for input in &sig.inputs { - let syn::FnArg::Typed(typed) = input else { - println!("option a"); - bail!(input, "self is not allowed here"); - }; - - let syn::Pat::Ident(syn::PatIdent { - by_ref: None, mutability: None, ident, .. - }) = &*typed.pat - else { - bail!(typed.pat, "expected identifier"); - }; - - match ident.to_string().as_str() { - "vm" => vm = true, - "vt" => vt = true, - "args" => args = true, - "span" => span = true, - _ => { - let mut attrs = typed.attrs.clone(); - params.push(Param { - name: kebab_case(ident), - docs: documentation(&attrs), - external: has_attr(&mut attrs, "external"), - named: has_attr(&mut attrs, "named"), - variadic: has_attr(&mut attrs, "variadic"), - default: parse_attr(&mut attrs, "default")?.map(|expr| { - expr.unwrap_or_else( - || parse_quote! { ::std::default::Default::default() }, - ) - }), - ident: ident.clone(), - ty: (*typed.ty).clone(), - }); - - validate_attrs(&attrs)?; - } - } + for input in &item.sig.inputs { + parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?; } - let mut attrs = item.attrs.clone(); - let docs = documentation(&attrs); - let mut lines = docs.split('\n').collect(); - let keywords = meta_line(&mut lines, "Keywords").ok().map(Into::into); - let category = meta_line(&mut lines, "Category")?.into(); - let display = meta_line(&mut lines, "Display")?.into(); - let docs = lines.join("\n").trim().into(); - - let func = Func { - name: sig.ident.to_string().trim_end_matches('_').replace('_', "-"), - display, - category, - keywords, + let returns = match &item.sig.output { + syn::ReturnType::Default => parse_quote! { () }, + syn::ReturnType::Type(_, ty) => ty.as_ref().clone(), + }; + + if meta.parent.is_some() && meta.scope { + bail!(item, "scoped function cannot have a scope"); + } + + Ok(Func { + name, + title, + scope: meta.scope, + constructor: meta.constructor, + keywords: meta.keywords, + parent: meta.parent, docs, vis: item.vis.clone(), - ident: sig.ident.clone(), - ident_func: Ident::new( - &format!("{}_func", sig.ident.to_string().trim_end_matches('_')), - sig.ident.span(), - ), - parent, + ident: item.sig.ident.clone(), + special, params, - returns: match &sig.output { - syn::ReturnType::Default => parse_quote! { () }, - syn::ReturnType::Type(_, ty) => ty.as_ref().clone(), - }, - scope: parse_attr(&mut attrs, "scope")?.flatten(), - vm, - vt, - args, - span, + returns, + }) +} + +/// Parse details about a functino parameter. +fn parse_param( + special: &mut SpecialParams, + params: &mut Vec, + parent: Option<&syn::Type>, + input: &syn::FnArg, +) -> Result<()> { + let typed = match input { + syn::FnArg::Receiver(recv) => { + let mut binding = Binding::Owned; + if recv.reference.is_some() { + if recv.mutability.is_some() { + binding = Binding::RefMut + } else { + binding = Binding::Ref + } + }; + + special.self_ = Some(Param { + binding, + ident: syn::Ident::new("self_", recv.self_token.span), + ty: match parent { + Some(ty) => ty.clone(), + None => bail!(recv, "explicit parent type required"), + }, + name: "self".into(), + docs: documentation(&recv.attrs), + named: false, + variadic: false, + external: false, + default: None, + }); + return Ok(()); + } + syn::FnArg::Typed(typed) => typed, + }; + + let syn::Pat::Ident(syn::PatIdent { by_ref: None, mutability: None, ident, .. }) = + &*typed.pat + else { + bail!(typed.pat, "expected identifier"); }; - Ok(func) + match ident.to_string().as_str() { + "vm" => special.vm = true, + "vt" => special.vt = true, + "args" => special.args = true, + "span" => special.span = true, + _ => { + let mut attrs = typed.attrs.clone(); + params.push(Param { + binding: Binding::Owned, + ident: ident.clone(), + ty: (*typed.ty).clone(), + name: ident.to_string().to_kebab_case(), + docs: documentation(&attrs), + named: has_attr(&mut attrs, "named"), + variadic: has_attr(&mut attrs, "variadic"), + external: has_attr(&mut attrs, "external"), + default: parse_attr(&mut attrs, "default")?.map(|expr| { + expr.unwrap_or_else( + || parse_quote! { ::std::default::Default::default() }, + ) + }), + }); + validate_attrs(&attrs)?; + } + } + + Ok(()) } +/// Produce the function's definition. fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { + let eval = quote! { ::typst::eval }; + + let Func { docs, vis, ident, .. } = func; + let item = rewrite_fn_item(item); + let ty = create_func_ty(func); + let data = create_func_data(func); + + let creator = if ty.is_some() { + quote! { + impl #eval::NativeFunc for #ident { + fn data() -> &'static #eval::NativeFuncData { + static DATA: #eval::NativeFuncData = #data; + &DATA + } + } + } + } else { + let ident_data = quote::format_ident!("{ident}_data"); + quote! { + #[doc(hidden)] + #vis fn #ident_data() -> &'static #eval::NativeFuncData { + static DATA: #eval::NativeFuncData = #data; + &DATA + } + } + }; + + quote! { + #[doc = #docs] + #[allow(dead_code)] + #item + + #[doc(hidden)] + #ty + #creator + } +} + +/// Create native function data for the function. +fn create_func_data(func: &Func) -> TokenStream { + let eval = quote! { ::typst::eval }; + let Func { + ident, name, - display, - category, + title, docs, - vis, - ident, - ident_func, + keywords, returns, + scope, + parent, + constructor, .. } = func; - let handlers = func - .params - .iter() - .filter(|param| !param.external) - .map(create_param_parser); + let scope = if *scope { + quote! { <#ident as #eval::NativeScope>::scope() } + } else { + quote! { #eval::Scope::new() } + }; - let args = func - .params - .iter() - .filter(|param| !param.external) - .map(|param| ¶m.ident); + let closure = create_wrapper_closure(func); + let params = func.special.self_.iter().chain(&func.params).map(create_param_info); - let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); - let vm_ = func.vm.then(|| quote! { vm, }); - let vt_ = func.vt.then(|| quote! { &mut vm.vt, }); - let args_ = func.args.then(|| quote! { args.take(), }); - let span_ = func.span.then(|| quote! { args.span, }); - let wrapper = quote! { - |vm, args| { - let __typst_func = #parent #ident; - #(#handlers)* - let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_); - ::typst::eval::IntoResult::into_result(output, args.span) - } + let name = if *constructor { + quote! { <#parent as #eval::NativeType>::NAME } + } else { + quote! { #name } }; - let mut item = item.clone(); - item.attrs.clear(); - - let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| { - if let syn::FnArg::Typed(typed) = &mut input { - if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) { - return None; - } - typed.attrs.clear(); + quote! { + #eval::NativeFuncData { + function: #closure, + name: #name, + title: #title, + docs: #docs, + keywords: &[#(#keywords),*], + scope: #eval::Lazy::new(|| #scope), + params: #eval::Lazy::new(|| ::std::vec![#(#params),*]), + returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()), } - Some(input) - }); - - item.sig.inputs = parse_quote! { #(#inputs),* }; + } +} - let keywords = quote_option(&func.keywords); - let params = func.params.iter().map(create_param_info); - let scope = create_scope_builder(func.scope.as_ref()); +/// Create a type that shadows the function. +fn create_func_ty(func: &Func) -> Option { + if func.parent.is_some() { + return None; + } - quote! { + let Func { vis, ident, .. } = func; + Some(quote! { #[doc(hidden)] - #vis fn #ident_func() -> &'static ::typst::eval::NativeFunc { - static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc { - func: #wrapper, - info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { - name: #name, - display: #display, - keywords: #keywords, - category: #category, - docs: #docs, - params: ::std::vec![#(#params),*], - returns: <#returns as ::typst::eval::Reflect>::describe(), - scope: #scope, - }), - }; - &FUNC + #[allow(non_camel_case_types)] + #vis enum #ident {} + }) +} + +/// Create the runtime-compatible wrapper closure that parses arguments. +fn create_wrapper_closure(func: &Func) -> TokenStream { + // These handlers parse the arguments. + let handlers = { + let func_handlers = func + .params + .iter() + .filter(|param| !param.external) + .map(create_param_parser); + let self_handler = func.special.self_.as_ref().map(create_param_parser); + quote! { + #self_handler + #(#func_handlers)* } + }; - #[doc = #docs] - #item + // This is the actual function call. + let call = { + let self_ = func + .special + .self_ + .as_ref() + .map(bind) + .map(|tokens| quote! { #tokens, }); + let vm_ = func.special.vm.then(|| quote! { vm, }); + let vt_ = func.special.vt.then(|| quote! { &mut vm.vt, }); + let args_ = func.special.args.then(|| quote! { args.take(), }); + let span_ = func.special.span.then(|| quote! { args.span, }); + let forwarded = func.params.iter().filter(|param| !param.external).map(bind); + quote! { + __typst_func(#self_ #vm_ #vt_ #args_ #span_ #(#forwarded,)*) + } + }; + + // This is the whole wrapped closure. + let ident = &func.ident; + let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); + quote! { + |vm, args| { + let __typst_func = #parent #ident; + #handlers + let output = #call; + ::typst::eval::IntoResult::into_result(output, args.span) + } } } @@ -226,7 +360,7 @@ fn create_param_info(param: &Param) -> TokenStream { ::typst::eval::ParamInfo { name: #name, docs: #docs, - cast: <#ty as ::typst::eval::Reflect>::describe(), + input: <#ty as ::typst::eval::Reflect>::input(), default: #default, positional: #positional, named: #named, @@ -258,10 +392,29 @@ fn create_param_parser(param: &Param) -> TokenStream { quote! { let mut #ident: #ty = #value; } } -struct Parent(Option); - -impl Parse for Parent { - fn parse(input: ParseStream) -> Result { - Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None })) +/// Apply the binding to a parameter. +fn bind(param: &Param) -> TokenStream { + let ident = ¶m.ident; + match param.binding { + Binding::Owned => quote! { #ident }, + Binding::Ref => quote! { &#ident }, + Binding::RefMut => quote! { &mut #ident }, } } + +/// Removes attributes and so on from the native function. +fn rewrite_fn_item(item: &syn::ItemFn) -> syn::ItemFn { + let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| { + if let syn::FnArg::Typed(typed) = &mut input { + if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) { + return None; + } + typed.attrs.clear(); + } + Some(input) + }); + let mut item = item.clone(); + item.attrs.clear(); + item.sig.inputs = parse_quote! { #(#inputs),* }; + item +} diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs index 49840ef2..52f3e237 100644 --- a/crates/typst-macros/src/lib.rs +++ b/crates/typst-macros/src/lib.rs @@ -4,10 +4,12 @@ extern crate proc_macro; #[macro_use] mod util; -mod castable; -mod element; +mod cast; +mod elem; mod func; +mod scope; mod symbols; +mod ty; use proc_macro::TokenStream as BoundaryStream; use proc_macro2::TokenStream; @@ -19,7 +21,79 @@ use syn::{parse_quote, DeriveInput, Ident, Result, Token}; use self::util::*; -/// Turns a function into a `NativeFunc`. +/// Makes a native Rust function usable as a Typst function. +/// +/// This implements `NativeFunction` for a freshly generated type with the same +/// name as a function. (In Rust, functions and types live in separate +/// namespace, so both can coexist.) +/// +/// If the function is in an impl block annotated with `#[scope]`, things work a +/// bit differently because the no type can be generated within the impl block. +/// In that case, a function named `{name}_data` that returns `&'static +/// NativeFuncData` is generated. You typically don't need to interact with this +/// function though because the `#[scope]` macro hooks everything up for you. +/// +/// ```ignore +/// /// Doubles an integer. +/// #[func] +/// fn double(x: i64) -> i64 { +/// 2 * x +/// } +/// ``` +/// +/// # Properties +/// You can customize some properties of the resulting function: +/// - `scope`: Indicates that the function has an associated scope defined by +/// the `#[scope]` macro. +/// - `name`: The functions's normal name (e.g. `min`). Defaults to the Rust +/// name in kebab-case. +/// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the +/// normal name in title case. +/// +/// # Arguments +/// By default, function arguments are positional and required. You can use +/// various attributes to configure their parsing behaviour: +/// +/// - `#[named]`: Makes the argument named and optional. The argument type must +/// either be `Option<_>` _or_ the `#[default]` attribute must be used. (If +/// it's both `Option<_>` and `#[default]`, then the argument can be specified +/// as `none` in Typst). +/// - `#[default]`: Specifies the default value of the argument as +/// `Default::default()`. +/// - `#[default(..)]`: Specifies the default value of the argument as `..`. +/// - `#[variadic]`: Parses a variable number of arguments. The argument type +/// must be `Vec<_>`. +/// - `#[external]`: The argument appears in documentation, but is otherwise +/// ignored. Can be useful if you want to do something manually for more +/// flexibility. +/// +/// Defaults can be specified for positional and named arguments. This is in +/// contrast to user-defined functions which currently cannot have optional +/// positional arguments (except through argument sinks). +/// +/// In the example below, we define a `min` function that could be called as +/// `min(1, 2, 3, default: 0)` in Typst. +/// +/// ```ignore +/// /// Determines the minimum of a sequence of values. +/// #[func(title = "Minimum")] +/// fn min( +/// /// The values to extract the minimum from. +/// #[variadic] +/// values: Vec, +/// /// A default value to return if there are no values. +/// #[named] +/// #[default(0)] +/// default: i64, +/// ) -> i64 { +/// self.values.iter().min().unwrap_or(default) +/// } +/// ``` +/// +/// As you can see, arguments can also have doc-comments, which will be rendered +/// in the documentation. The first line of documentation should be concise and +/// self-contained as it is the designated short description, which is used in +/// overviews in the documentation (and for autocompletion). #[proc_macro_attribute] pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::ItemFn); @@ -28,33 +102,230 @@ pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { .into() } -/// Turns a type into an `Element`. +/// Makes a native Rust type usable as a Typst type. +/// +/// This implements `NativeType` for the given type. +/// +/// ```ignore +/// /// A sequence of codepoints. +/// #[ty(scope, title = "String")] +/// struct Str(EcoString); +/// +/// #[scope] +/// impl Str { +/// ... +/// } +/// ``` +/// +/// # Properties +/// You can customize some properties of the resulting type: +/// - `scope`: Indicates that the type has an associated scope defined by the +/// `#[scope]` macro +/// - `name`: The type's normal name (e.g. `str`). Defaults to the Rust name in +/// kebab-case. +/// - `title`: The type's title case name (e.g. `String`). Defaults to the +/// normal name in title case. #[proc_macro_attribute] -pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { +pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::Item); + ty::ty(stream.into(), item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Makes a native Rust type usable as a Typst element. +/// +/// This implements `NativeElement` for the given type. +/// +/// ``` +/// /// A section heading. +/// #[elem(Show, Count)] +/// struct HeadingElem { +/// /// The logical nesting depth of the heading, starting from one. +/// #[default(NonZeroUsize::ONE)] +/// level: NonZeroUsize, +/// +/// /// The heading's title. +/// #[required] +/// body: Content, +/// } +/// ``` +/// +/// # Properties +/// You can customize some properties of the resulting type: +/// - `scope`: Indicates that the type has an associated scope defined by the +/// `#[scope]` macro +/// - `name`: The element's normal name (e.g. `str`). Defaults to the Rust name +/// in kebab-case. +/// - `title`: The type's title case name (e.g. `String`). Defaults to the long +/// name in title case. +/// - The remaining entries in the `elem` macros list are traits the element +/// is capable of. These can be dynamically accessed. +/// +/// # Fields +/// By default, element fields are named and optional (and thus settable). You +/// can use various attributes to configure their parsing behaviour: +/// +/// - `#[positional]`: Makes the argument positional (but still optional). +/// - `#[required]`: Makes the argument positional and required. +/// - `#[default(..)]`: Specifies the default value of the argument as `..`. +/// - `#[variadic]`: Parses a variable number of arguments. The field type must +/// be `Vec<_>`. The field will be exposed as an array. +/// - `#[parse({ .. })]`: A block of code that parses the field manually. +/// +/// In addition that there are a number of attributes that configure other +/// aspects of the field than the parsing behaviour. +/// - `#[resolve]`: When accessing the field, it will be automatically +/// resolved through the `Resolve` trait. This, for instance, turns `Length` +/// into `Abs`. It's just convenient. +/// - `#[fold]`: When there are multiple set rules for the field, all values +/// are folded together into one. E.g. `set rect(stroke: 2pt)` and +/// `set rect(stroke: red)` are combined into the equivalent of +/// `set rect(stroke: 2pt + red)` instead of having `red` override `2pt`. +/// - `#[internal]`: The field does not appear in the documentation. +/// - `#[external]`: The field appears in the documentation, but is otherwise +/// ignored. Can be useful if you want to do something manually for more +/// flexibility. +/// - `#[synthesized]`: The field cannot be specified in a constructor or set +/// rule. Instead, it is added to an element before its show rule runs +/// through the `Synthesize` trait. +#[proc_macro_attribute] +pub fn elem(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::ItemStruct); - element::element(stream.into(), &item) + elem::elem(stream.into(), item) .unwrap_or_else(|err| err.to_compile_error()) .into() } -/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum. -#[proc_macro_derive(Cast, attributes(string))] -pub fn derive_cast(item: BoundaryStream) -> BoundaryStream { - let item = syn::parse_macro_input!(item as DeriveInput); - castable::derive_cast(&item) +/// Provides an associated scope to a native function, type, or element. +/// +/// This implements `NativeScope` for the function's shadow type, the type, or +/// the element. +/// +/// The implementation block can contain four kinds of items: +/// - constants, which will be defined through `scope.define` +/// - functions, which will be defined through `scope.define_func` +/// - types, which will be defined through `scope.define_type` +/// - elements, which will be defined through `scope.define_elem` +/// +/// ```ignore +/// #[func(scope)] +/// fn name() { .. } +/// +/// #[scope] +/// impl name { +/// /// A simple constant. +/// const VAL: u32 = 0; +/// +/// /// A function. +/// #[func] +/// fn foo() -> EcoString { +/// "foo!".into() +/// } +/// +/// /// A type. +/// type Brr; +/// +/// /// An element. +/// #[elem] +/// type NiceElem; +/// } +/// +/// #[ty] +/// struct Brr; +/// +/// #[elem] +/// struct NiceElem {} +/// ``` +#[proc_macro_attribute] +pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::Item); + scope::scope(stream.into(), item) .unwrap_or_else(|err| err.to_compile_error()) .into() } /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. +/// +/// - `Reflect` makes Typst's runtime aware of the type's characteristics. +/// It's important for autocompletion, error messages, etc. +/// - `FromValue` defines how to cast from a value into this type. +/// - `IntoValue` defines how to cast fromthis type into a value. +/// +/// ```ignore +/// /// An integer between 0 and 13. +/// struct CoolInt(u8); +/// +/// cast! { +/// CoolInt, +/// +/// // Defines how to turn a `CoolInt` into a value. +/// self => self.0.into_value(), +/// +/// // Defines "match arms" of types that can be cast into a `CoolInt`. +/// // These types needn't be value primitives, they can themselves use +/// // `cast!`. +/// v: bool => Self(v as u8), +/// v: i64 => if matches!(v, 0..=13) { +/// Self(v as u8) +/// } else { +/// bail!("integer is not nice :/") +/// }, +/// } +/// ``` #[proc_macro] pub fn cast(stream: BoundaryStream) -> BoundaryStream { - castable::cast(stream.into()) + cast::cast(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum. +/// +/// The enum will become castable from kebab-case strings. The doc-comments will +/// become user-facing documentation for each variant. The `#[string]` attribute +/// can be used to override the string corresponding to a variant. +/// +/// ```ignore +/// /// A stringy enum of options. +/// #[derive(Cast)] +/// enum Niceness { +/// /// Clearly nice (parses from `"nice"`). +/// Nice, +/// /// Not so nice (parses from `"not-nice"`). +/// NotNice, +/// /// Very much not nice (parses from `"❌"`). +/// #[string("❌")] +/// Unnice, +/// } +/// ``` +#[proc_macro_derive(Cast, attributes(string))] +pub fn derive_cast(item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as DeriveInput); + cast::derive_cast(item) .unwrap_or_else(|err| err.to_compile_error()) .into() } /// Defines a list of `Symbol`s. +/// +/// ```ignore +/// const EMOJI: &[(&str, Symbol)] = symbols! { +/// // A plain symbol without modifiers. +/// abacus: '🧮', +/// +/// // A symbol with a modifierless default and one modifier. +/// alien: ['👽', monster: '👾'], +/// +/// // A symbol where each variant has a modifier. The first one will be +/// // the default. +/// clock: [one: '🕐', two: '🕑', ...], +/// } +/// ``` +/// +/// _Note:_ While this could use `macro_rules!` instead of a proc-macro, it was +/// horribly slow in rust-analyzer. The underlying cause might be +/// [this issue](https://github.com/rust-lang/rust-analyzer/issues/11108). #[proc_macro] pub fn symbols(stream: BoundaryStream) -> BoundaryStream { symbols::symbols(stream.into()) diff --git a/crates/typst-macros/src/scope.rs b/crates/typst-macros/src/scope.rs new file mode 100644 index 00000000..4aa17c6c --- /dev/null +++ b/crates/typst-macros/src/scope.rs @@ -0,0 +1,153 @@ +use heck::ToKebabCase; + +use super::*; + +/// Expand the `#[scope]` macro. +pub fn scope(_: TokenStream, item: syn::Item) -> Result { + let syn::Item::Impl(mut item) = item else { + bail!(item, "expected module or impl item"); + }; + + let eval = quote! { ::typst::eval }; + let self_ty = &item.self_ty; + + let mut definitions = vec![]; + let mut constructor = quote! { None }; + for child in &mut item.items { + let def = match child { + syn::ImplItem::Const(item) => handle_const(self_ty, item)?, + syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? { + FnKind::Member(tokens) => tokens, + FnKind::Constructor(tokens) => { + constructor = tokens; + continue; + } + }, + syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?, + _ => bail!(child, "unexpected item in scope"), + }; + definitions.push(def); + } + + item.items.retain(|item| !matches!(item, syn::ImplItem::Verbatim(_))); + + let mut base = quote! { #item }; + if let syn::Type::Path(syn::TypePath { path, .. }) = self_ty.as_ref() { + if let Some(ident) = path.get_ident() { + if is_primitive(ident) { + base = rewrite_primitive_base(&item, ident); + } + } + } + + Ok(quote! { + #base + + impl #eval::NativeScope for #self_ty { + fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> { + #constructor + } + + fn scope() -> #eval::Scope { + let mut scope = #eval::Scope::deduplicating(); + #(#definitions;)* + scope + } + } + }) +} + +/// Process a const item and returns its definition. +fn handle_const(self_ty: &syn::Type, item: &syn::ImplItemConst) -> Result { + let ident = &item.ident; + let name = ident.to_string().to_kebab_case(); + Ok(quote! { scope.define(#name, #self_ty::#ident) }) +} + +/// Process a type item. +fn handle_type_or_elem(item: &TokenStream) -> Result { + let item: BareType = syn::parse2(item.clone())?; + let ident = &item.ident; + let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) { + quote! { define_elem } + } else { + quote! { define_type } + }; + Ok(quote! { scope.#define::<#ident>() }) +} + +/// Process a function, return its definition, and register it as a constructor +/// if applicable. +fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result { + let Some(attr) = item.attrs.iter_mut().find(|attr| attr.meta.path().is_ident("func")) + else { + bail!(item, "scope function is missing #[func] attribute"); + }; + + let ident_data = quote::format_ident!("{}_data", item.sig.ident); + + match &mut attr.meta { + syn::Meta::Path(_) => { + *attr = parse_quote! { #[func(parent = #self_ty)] }; + } + syn::Meta::List(list) => { + let tokens = &list.tokens; + let meta: super::func::Meta = syn::parse2(tokens.clone())?; + list.tokens = quote! { #tokens, parent = #self_ty }; + if meta.constructor { + return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) })); + } + } + syn::Meta::NameValue(_) => bail!(attr.meta, "invalid func attribute"), + } + + Ok(FnKind::Member(quote! { scope.define_func_with_data(#self_ty::#ident_data()) })) +} + +enum FnKind { + Constructor(TokenStream), + Member(TokenStream), +} + +/// Whether the identifier describes a primitive type. +fn is_primitive(ident: &syn::Ident) -> bool { + ident == "bool" || ident == "i64" || ident == "f64" +} + +/// Rewrite an impl block for a primitive into a trait + trait impl. +fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStream { + let mut sigs = vec![]; + let mut items = vec![]; + for sub in &item.items { + let syn::ImplItem::Fn(mut func) = sub.clone() else { continue }; + func.vis = syn::Visibility::Inherited; + items.push(func.clone()); + + let mut sig = func.sig; + let inputs = sig.inputs.iter().cloned().map(|mut input| { + if let syn::FnArg::Typed(typed) = &mut input { + typed.attrs.clear(); + } + input + }); + sig.inputs = parse_quote! { #(#inputs),* }; + + let ident_data = quote::format_ident!("{}_data", sig.ident); + sigs.push(quote! { #sig; }); + sigs.push(quote! { + fn #ident_data() -> &'static ::typst::eval::NativeFuncData; + }); + } + + let ident_ext = quote::format_ident!("{ident}Ext"); + let self_ty = &item.self_ty; + quote! { + trait #ident_ext { + #(#sigs)* + } + + impl #ident_ext for #self_ty { + #(#items)* + } + } +} diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs index cdb7f5d7..8ab47f08 100644 --- a/crates/typst-macros/src/symbols.rs +++ b/crates/typst-macros/src/symbols.rs @@ -7,7 +7,7 @@ pub fn symbols(stream: TokenStream) -> Result { let pairs = list.iter().map(|symbol| { let name = symbol.name.to_string(); let kind = match &symbol.kind { - Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), }, + Kind::Single(c) => quote! { typst::eval::Symbol::single(#c), }, Kind::Multiple(variants) => { let variants = variants.iter().map(|variant| { let name = &variant.name; diff --git a/crates/typst-macros/src/ty.rs b/crates/typst-macros/src/ty.rs new file mode 100644 index 00000000..df60e7bb --- /dev/null +++ b/crates/typst-macros/src/ty.rs @@ -0,0 +1,113 @@ +use syn::Attribute; + +use super::*; + +/// Expand the `#[ty]` macro. +pub fn ty(stream: TokenStream, item: syn::Item) -> Result { + let meta: Meta = syn::parse2(stream)?; + let bare: BareType; + let (ident, attrs, keep) = match &item { + syn::Item::Struct(item) => (&item.ident, &item.attrs, true), + syn::Item::Type(item) => (&item.ident, &item.attrs, true), + syn::Item::Enum(item) => (&item.ident, &item.attrs, true), + syn::Item::Verbatim(item) => { + bare = syn::parse2(item.clone())?; + (&bare.ident, &bare.attrs, false) + } + _ => bail!(item, "invalid type item"), + }; + let ty = parse(meta, ident.clone(), attrs)?; + Ok(create(&ty, keep.then_some(&item))) +} + +/// Holds all relevant parsed data about a type. +struct Type { + ident: Ident, + name: String, + long: String, + scope: bool, + title: String, + docs: String, + keywords: Vec, +} + +/// The `..` in `#[ty(..)]`. +struct Meta { + scope: bool, + name: Option, + title: Option, + keywords: Vec, +} + +impl Parse for Meta { + fn parse(input: ParseStream) -> Result { + Ok(Self { + scope: parse_flag::(input)?, + name: parse_string::(input)?, + title: parse_string::(input)?, + keywords: parse_string_array::(input)?, + }) + } +} + +/// Parse details about the type from its definition. +fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result { + let docs = documentation(attrs); + let (name, title) = determine_name_and_title(meta.name, meta.title, &ident, None)?; + let long = title.to_lowercase(); + Ok(Type { + ident, + name, + long, + scope: meta.scope, + keywords: meta.keywords, + title, + docs, + }) +} + +/// Produce the output of the macro. +fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream { + let eval = quote! { ::typst::eval }; + + let Type { + ident, name, long, title, docs, keywords, scope, .. + } = ty; + + let constructor = if *scope { + quote! { <#ident as #eval::NativeScope>::constructor() } + } else { + quote! { None } + }; + + let scope = if *scope { + quote! { <#ident as #eval::NativeScope>::scope() } + } else { + quote! { #eval::Scope::new() } + }; + + let data = quote! { + #eval::NativeTypeData { + name: #name, + long_name: #long, + title: #title, + docs: #docs, + keywords: &[#(#keywords),*], + constructor: #eval::Lazy::new(|| #constructor), + scope: #eval::Lazy::new(|| #scope), + } + }; + + quote! { + #item + + impl #eval::NativeType for #ident { + const NAME: &'static str = #name; + + fn data() -> &'static #eval::NativeTypeData { + static DATA: #eval::NativeTypeData = #data; + &DATA + } + } + } +} diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs index 389fed06..890db779 100644 --- a/crates/typst-macros/src/util.rs +++ b/crates/typst-macros/src/util.rs @@ -1,5 +1,7 @@ -use heck::ToKebabCase; +use heck::{ToKebabCase, ToTitleCase}; use quote::ToTokens; +use syn::token::Token; +use syn::Attribute; use super::*; @@ -19,25 +21,27 @@ macro_rules! bail { }; } -/// For parsing attributes of the form: -/// #[attr( -/// statement; -/// statement; -/// returned_expression -/// )] -pub struct BlockWithReturn { - pub prefix: Vec, - pub expr: syn::Stmt, -} +/// Extract documentation comments from an attribute list. +pub fn documentation(attrs: &[syn::Attribute]) -> String { + let mut doc = String::new(); -impl Parse for BlockWithReturn { - fn parse(input: ParseStream) -> Result { - let mut stmts = syn::Block::parse_within(input)?; - let Some(expr) = stmts.pop() else { - return Err(input.error("expected at least one expression")); - }; - Ok(Self { prefix: stmts, expr }) + // Parse doc comments. + for attr in attrs { + if let syn::Meta::NameValue(meta) = &attr.meta { + if meta.path.is_ident("doc") { + if let syn::Expr::Lit(lit) = &meta.value { + if let syn::Lit::Str(string) = &lit.lit { + let full = string.value(); + let line = full.strip_prefix(' ').unwrap_or(&full); + doc.push_str(line); + doc.push('\n'); + } + } + } + } } + + doc.trim().into() } /// Whether an attribute list has a specified attribute. @@ -83,63 +87,164 @@ pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { Ok(()) } -/// Convert an identifier to a kebab-case string. -pub fn kebab_case(name: &Ident) -> String { - name.to_string().to_kebab_case() +/// Quotes an option literally. +pub fn quote_option(option: &Option) -> TokenStream { + if let Some(value) = option { + quote! { Some(#value) } + } else { + quote! { None } + } } -/// Extract documentation comments from an attribute list. -pub fn documentation(attrs: &[syn::Attribute]) -> String { - let mut doc = String::new(); +/// Parse a metadata key-value pair, separated by `=`. +pub fn parse_key_value( + input: ParseStream, +) -> Result> { + if !input.peek(|_| K::default()) { + return Ok(None); + } - // Parse doc comments. - for attr in attrs { - if let syn::Meta::NameValue(meta) = &attr.meta { - if meta.path.is_ident("doc") { - if let syn::Expr::Lit(lit) = &meta.value { - if let syn::Lit::Str(string) = &lit.lit { - let full = string.value(); - let line = full.strip_prefix(' ').unwrap_or(&full); - doc.push_str(line); - doc.push('\n'); - } - } - } - } + let _: K = input.parse()?; + let _: Token![=] = input.parse()?; + let value: V = input.parse::()?; + eat_comma(input); + Ok(Some(value)) +} + +/// Parse a metadata key-array pair, separated by `=`. +pub fn parse_key_value_array( + input: ParseStream, +) -> Result> { + Ok(parse_key_value::>(input)?.map_or(vec![], |array| array.0)) +} + +/// Parse a metadata key-string pair, separated by `=`. +pub fn parse_string( + input: ParseStream, +) -> Result> { + Ok(parse_key_value::(input)?.map(|s| s.value())) +} + +/// Parse a metadata key-string pair, separated by `=`. +pub fn parse_string_array( + input: ParseStream, +) -> Result> { + Ok(parse_key_value_array::(input)? + .into_iter() + .map(|lit| lit.value()) + .collect()) +} + +/// Parse a metadata flag that can be present or not. +pub fn parse_flag(input: ParseStream) -> Result { + if input.peek(|_| K::default()) { + let _: K = input.parse()?; + eat_comma(input); + return Ok(true); } + Ok(false) +} - doc.trim().into() +/// Parse a comma if there is one. +pub fn eat_comma(input: ParseStream) { + if input.peek(Token![,]) { + let _: Token![,] = input.parse().unwrap(); + } } -/// Extract a line of metadata from documentation. -pub fn meta_line<'a>(lines: &mut Vec<&'a str>, key: &str) -> Result<&'a str> { - match lines.last().and_then(|line| line.strip_prefix(&format!("{key}:"))) { - Some(value) => { - lines.pop(); - Ok(value.trim()) +/// Determine the normal and title case name of a function, type, or element. +pub fn determine_name_and_title( + specified_name: Option, + specified_title: Option, + ident: &syn::Ident, + trim: Option &str>, +) -> Result<(String, String)> { + let name = { + let trim = trim.unwrap_or(|s| s); + let default = trim(&ident.to_string()).to_kebab_case(); + if specified_name.as_ref() == Some(&default) { + bail!(ident, "name was specified unncessarily"); } - None => bail!(callsite, "missing metadata key: {key}"), + specified_name.unwrap_or(default) + }; + + let title = { + let default = name.to_title_case(); + if specified_title.as_ref() == Some(&default) { + bail!(ident, "title was specified unncessarily"); + } + specified_title.unwrap_or(default) + }; + + Ok((name, title)) +} + +/// A generic parseable array. +struct Array(Vec); + +impl Parse for Array { + fn parse(input: ParseStream) -> Result { + let content; + syn::bracketed!(content in input); + + let mut elems = Vec::new(); + while !content.is_empty() { + let first: T = content.parse()?; + elems.push(first); + if !content.is_empty() { + let _: Token![,] = content.parse()?; + } + } + + Ok(Self(elems)) } } -/// Creates a block responsible for building a `Scope`. -pub fn create_scope_builder(scope_block: Option<&BlockWithReturn>) -> TokenStream { - if let Some(BlockWithReturn { prefix, expr }) = scope_block { - quote! { { - let mut scope = ::typst::eval::Scope::deduplicating(); - #(#prefix);* - #expr - } } - } else { - quote! { ::typst::eval::Scope::new() } +/// For parsing attributes of the form: +/// #[attr( +/// statement; +/// statement; +/// returned_expression +/// )] +pub struct BlockWithReturn { + pub prefix: Vec, + pub expr: syn::Stmt, +} + +impl Parse for BlockWithReturn { + fn parse(input: ParseStream) -> Result { + let mut stmts = syn::Block::parse_within(input)?; + let Some(expr) = stmts.pop() else { + return Err(input.error("expected at least one expression")); + }; + Ok(Self { prefix: stmts, expr }) } } -/// Quotes an option literally. -pub fn quote_option(option: &Option) -> TokenStream { - if let Some(value) = option { - quote! { Some(#value) } - } else { - quote! { None } +pub mod kw { + syn::custom_keyword!(name); + syn::custom_keyword!(title); + syn::custom_keyword!(scope); + syn::custom_keyword!(constructor); + syn::custom_keyword!(keywords); + syn::custom_keyword!(parent); +} + +/// Parse a bare `type Name;` item. +pub struct BareType { + pub attrs: Vec, + pub type_token: Token![type], + pub ident: Ident, + pub semi_token: Token![;], +} + +impl Parse for BareType { + fn parse(input: ParseStream) -> Result { + Ok(BareType { + attrs: input.call(Attribute::parse_outer)?, + type_token: input.parse()?, + ident: input.parse()?, + semi_token: input.parse()?, + }) } } -- cgit v1.2.3