diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/cast.rs similarity index 83% rename from crates/typst-macros/src/castable.rs rename to crates/typst-macros/src/cast.rs index 05c1b4d1d..74bdc5902 100644 --- a/crates/typst-macros/src/castable.rs +++ b/crates/typst-macros/src/cast.rs @@ -1,7 +1,9 @@ +use heck::ToKebabCase; + use super::*; /// Expand the `#[derive(Cast)]` macro. -pub fn derive_cast(item: &DeriveInput) -> Result { +pub fn derive_cast(item: DeriveInput) -> Result { let ty = &item.ident; let syn::Data::Enum(data) = &item.data else { @@ -19,7 +21,7 @@ pub fn derive_cast(item: &DeriveInput) -> Result { { attr.parse_args::()?.value() } else { - kebab_case(&variant.ident) + variant.ident.to_string().to_kebab_case() }; variants.push(Variant { @@ -62,20 +64,25 @@ struct Variant { /// 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 input: CastInput = syn::parse2(stream)?; + let ty = &input.ty; let castable_body = create_castable_body(&input); - let describe_body = create_describe_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.name.is_some()).then(|| { + let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| { quote! { impl #eval::Reflect for #ty { - fn describe() -> #eval::CastInfo { - #describe_body + fn input() -> #eval::CastInfo { + #input_body + } + + fn output() -> #eval::CastInfo { + #output_body } fn castable(value: &#eval::Value) -> bool { @@ -85,7 +92,7 @@ pub fn cast(stream: TokenStream) -> Result { } }); - let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { + let into_value = (input.into_value.is_some() || input.dynamic).then(|| { quote! { impl #eval::IntoValue for #ty { fn into_value(self) -> #eval::Value { @@ -95,7 +102,7 @@ pub fn cast(stream: TokenStream) -> Result { } }); - let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { + 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 { @@ -105,55 +112,42 @@ pub fn cast(stream: TokenStream) -> Result { } }); - 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, + dynamic: bool, into_value: Option, from_value: Punctuated, } impl Parse for CastInput { fn parse(input: ParseStream) -> Result { - let ty; - let mut name = None; + let mut dynamic = false; 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()?; + dynamic = true; } + let ty = input.parse()?; let _: syn::Token![,] = input.parse()?; - let mut into_value = None; + let mut to_value = None; if input.peek(syn::Token![self]) { let _: syn::Token![self] = input.parse()?; let _: syn::Token![=>] = input.parse()?; - into_value = Some(input.parse()?); + to_value = Some(input.parse()?); let _: syn::Token![,] = input.parse()?; } let from_value = Punctuated::parse_terminated(input)?; - Ok(Self { ty, name, into_value, from_value }) + Ok(Self { ty, dynamic, into_value: to_value, from_value }) } } @@ -212,7 +206,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream { } } - let dynamic_check = input.name.is_some().then(|| { + let dynamic_check = input.dynamic.then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if dynamic.is::() { @@ -241,7 +235,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream { } } -fn create_describe_body(input: &CastInput) -> TokenStream { +fn create_input_body(input: &CastInput) -> TokenStream { let mut infos = vec![]; for cast in &input.from_value { @@ -256,14 +250,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream { } } Pattern::Ty(_, ty) => { - quote! { <#ty as ::typst::eval::Reflect>::describe() } + quote! { <#ty as ::typst::eval::Reflect>::input() } } }); } - if let Some(name) = &input.name { + if input.dynamic { infos.push(quote! { - ::typst::eval::CastInfo::Type(#name) + ::typst::eval::CastInfo::Type(::typst::eval::Type::of::()) }); } @@ -272,6 +266,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream { } } +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 } @@ -301,7 +303,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream { } } - let dynamic_check = input.name.is_some().then(|| { + let dynamic_check = input.dynamic.then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if let Some(concrete) = dynamic.downcast::() { diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/elem.rs similarity index 69% rename from crates/typst-macros/src/element.rs rename to crates/typst-macros/src/elem.rs index e047e6065..5584bdb68 100644 --- a/crates/typst-macros/src/element.rs +++ b/crates/typst-macros/src/elem.rs @@ -1,152 +1,175 @@ +use heck::ToKebabCase; + use super::*; -/// Expand the `#[element]` macro. -pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result { - let element = prepare(stream, body)?; +/// 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, - display: String, - category: String, - keywords: Option, + title: String, + scope: bool, + keywords: Vec, docs: String, vis: syn::Visibility, ident: Ident, - capable: Vec, + capabilities: Vec, fields: Vec, - scope: Option, } +/// Details about an element field. 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, + 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() } } -/// Preprocess the element's definition. -fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result { +/// 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::>()?; - 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, + Ok(Elem { + name, + title, + scope: meta.scope, + keywords: meta.keywords, docs, vis: body.vis.clone(), ident: body.ident.clone(), - capable, + capabilities: meta.capabilities, fields, - scope: parse_attr(&mut attrs, "scope")?.flatten(), + }) +} + +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(element) + + Ok(field) } /// Produce the element's definition. @@ -166,13 +189,13 @@ fn create(element: &Elem) -> TokenStream { // Trait implementations. let element_impl = create_pack_impl(element); let construct_impl = element - .capable + .capabilities .iter() .all(|capability| capability != "Construct") .then(|| create_construct_impl(element)); let set_impl = create_set_impl(element); let locatable_impl = element - .capable + .capabilities .iter() .any(|capability| capability == "Locatable") .then(|| quote! { impl ::typst::model::Locatable for #ident {} }); @@ -231,7 +254,7 @@ fn create_new_func(element: &Elem) -> TokenStream { /// Create a new element. pub fn new(#(#params),*) -> Self { Self(::typst::model::Content::new( - ::func() + ::elem() )) #(#builder_calls)* } @@ -285,7 +308,7 @@ fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStrea quote! { styles.#getter::<#ty>( - ::func(), + ::elem(), #name, #inherent, || #default, @@ -325,7 +348,7 @@ fn create_set_field_method(field: &Field) -> TokenStream { #[doc = #doc] #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { ::typst::model::Style::Property(::typst::model::Property::new( - ::func(), + ::elem(), #name, #ident, )) @@ -335,49 +358,54 @@ fn create_set_field_method(field: &Field) -> TokenStream { /// Create the element's `Pack` implementation. fn create_pack_impl(element: &Elem) -> TokenStream { - let Elem { ident, name, display, keywords, category, docs, .. } = element; + 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 infos = element + let params = 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); + + 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 ::typst::model::Element for #ident { - fn pack(self) -> ::typst::model::Content { + 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: &::typst::model::Content) -> ::std::option::Option<&Self> { + fn unpack(content: &#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() - } } } } @@ -385,7 +413,7 @@ fn create_pack_impl(element: &Elem) -> TokenStream { /// 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 relevant = element.capabilities.iter().filter(|&ident| ident != "Construct"); let checks = relevant.map(|capability| { quote! { if id == ::std::any::TypeId::of::() { @@ -399,7 +427,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream { quote! { |id| { let null = Self(::typst::model::Content::new( - <#ident as ::typst::model::Element>::func() + <#ident as ::typst::model::NativeElement>::elem() )); #(#checks)* None @@ -441,7 +469,7 @@ fn create_param_info(field: &Field) -> 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, @@ -488,7 +516,7 @@ fn create_construct_impl(element: &Elem) -> TokenStream { args: &mut ::typst::eval::Args, ) -> ::typst::diag::SourceResult<::typst::model::Content> { let mut element = Self(::typst::model::Content::new( - ::func() + ::elem() )); #(#handlers)* Ok(element.0) diff --git a/crates/typst-macros/src/func.rs b/crates/typst-macros/src/func.rs index 13a6eb2d4..87d57f19d 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 { - name: String, - docs: String, - external: bool, - named: bool, - variadic: bool, - default: Option, + binding: Binding, ident: Ident, ty: syn::Type, + name: String, + docs: String, + named: bool, + variadic: bool, + external: bool, + default: Option, } -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"); - }; + for input in &item.sig.inputs { + parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?; + } - let syn::Pat::Ident(syn::PatIdent { - by_ref: None, mutability: None, ident, .. - }) = &*typed.pat - else { - bail!(typed.pat, "expected identifier"); - }; + let returns = match &item.sig.output { + syn::ReturnType::Default => parse_quote! { () }, + syn::ReturnType::Type(_, ty) => ty.as_ref().clone(), + }; - 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(), - }); + if meta.parent.is_some() && meta.scope { + bail!(item, "scoped function cannot have a scope"); + } - validate_attrs(&attrs)?; - } + Ok(Func { + name, + title, + scope: meta.scope, + constructor: meta.constructor, + keywords: meta.keywords, + parent: meta.parent, + docs, + vis: item.vis.clone(), + ident: item.sig.ident.clone(), + special, + params, + 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"); + }; + + 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)?; } } - 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, - 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, - 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, - }; - - Ok(func) + 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 { - name, - display, - category, - docs, - vis, ident, - ident_func, + name, + title, + docs, + 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 } + }; + + 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()), + } + } +} + +/// Create a type that shadows the function. +fn create_func_ty(func: &Func) -> Option { + if func.parent.is_some() { + return None; + } + + let Func { vis, ident, .. } = func; + Some(quote! { + #[doc(hidden)] + #[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)* } }; - 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(); + // 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,)*) } - 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()); + }; + // This is the whole wrapped closure. + let ident = &func.ident; + let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); 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 + |vm, args| { + let __typst_func = #parent #ident; + #handlers + let output = #call; + ::typst::eval::IntoResult::into_result(output, args.span) } - - #[doc = #docs] - #item } } @@ -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 49840ef2f..52f3e237e 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 { - let item = syn::parse_macro_input!(item as syn::ItemStruct); - element::element(stream.into(), &item) +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() } -/// 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) +/// 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); + elem::elem(stream.into(), item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// 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 000000000..4aa17c6cf --- /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 cdb7f5d74..8ab47f08c 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 000000000..df60e7bbc --- /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 389fed061..890db7791 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,58 +87,6 @@ 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() -} - -/// Extract documentation comments from an attribute list. -pub fn documentation(attrs: &[syn::Attribute]) -> String { - let mut doc = String::new(); - - // 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() -} - -/// 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()) - } - None => bail!(callsite, "missing metadata key: {key}"), - } -} - -/// 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() } - } -} - /// Quotes an option literally. pub fn quote_option(option: &Option) -> TokenStream { if let Some(value) = option { @@ -143,3 +95,156 @@ pub fn quote_option(option: &Option) -> TokenStream { quote! { None } } } + +/// Parse a metadata key-value pair, separated by `=`. +pub fn parse_key_value( + input: ParseStream, +) -> Result> { + if !input.peek(|_| K::default()) { + return Ok(None); + } + + 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) +} + +/// Parse a comma if there is one. +pub fn eat_comma(input: ParseStream) { + if input.peek(Token![,]) { + let _: Token![,] = input.parse().unwrap(); + } +} + +/// 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"); + } + 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)) + } +} + +/// 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 }) + } +} + +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()?, + }) + } +}