diff options
| author | Laurenz <laurmaedje@gmail.com> | 2023-07-02 19:59:52 +0200 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2023-07-02 20:07:43 +0200 |
| commit | ebfdb1dafa430786db10dad2ef7d5467c1bdbed1 (patch) | |
| tree | 2bbc24ddb4124c4bb14dec0e536129d4de37b056 /crates/typst-macros/src | |
| parent | 3ab19185093d7709f824b95b979060ce125389d8 (diff) | |
Move everything into `crates/` directory
Diffstat (limited to 'crates/typst-macros/src')
| -rw-r--r-- | crates/typst-macros/src/castable.rs | 331 | ||||
| -rw-r--r-- | crates/typst-macros/src/element.rs | 554 | ||||
| -rw-r--r-- | crates/typst-macros/src/func.rs | 268 | ||||
| -rw-r--r-- | crates/typst-macros/src/lib.rs | 63 | ||||
| -rw-r--r-- | crates/typst-macros/src/symbols.rs | 78 | ||||
| -rw-r--r-- | crates/typst-macros/src/util.rs | 145 |
6 files changed, 1439 insertions, 0 deletions
diff --git a/crates/typst-macros/src/castable.rs b/crates/typst-macros/src/castable.rs new file mode 100644 index 00000000..05c1b4d1 --- /dev/null +++ b/crates/typst-macros/src/castable.rs @@ -0,0 +1,331 @@ +use super::*; + +/// Expand the `#[derive(Cast)]` macro. +pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> { + 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::<syn::LitStr>()?.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<TokenStream> { + 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<Self> { + #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<syn::LitStr>, + into_value: Option<syn::Expr>, + from_value: Punctuated<Cast, Token![,]>, +} + +impl Parse for CastInput { + fn parse(input: ParseStream) -> Result<Self> { + 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<Self> { + 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<Self> { + 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<syn::Attribute>, + 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::<Self>() { + 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::<Self>() { + 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(<Self as ::typst::eval::Reflect>::error(&value)) + } +} diff --git a/crates/typst-macros/src/element.rs b/crates/typst-macros/src/element.rs new file mode 100644 index 00000000..6ce91fcb --- /dev/null +++ b/crates/typst-macros/src/element.rs @@ -0,0 +1,554 @@ +use super::*; + +/// Expand the `#[element]` macro. +pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> { + let element = prepare(stream, body)?; + Ok(create(&element)) +} + +struct Elem { + name: String, + display: String, + category: String, + keywords: Option<String>, + docs: String, + vis: syn::Visibility, + ident: Ident, + capable: Vec<Ident>, + fields: Vec<Field>, + scope: Option<BlockWithReturn>, +} + +struct Field { + name: String, + docs: String, + internal: bool, + external: bool, + positional: bool, + required: bool, + variadic: bool, + synthesized: bool, + fold: bool, + resolve: bool, + parse: Option<BlockWithReturn>, + 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<Elem> { + 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::<Ident, Token![,]>::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( + <Self as ::typst::model::Element>::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>( + <Self as ::typst::model::Element>::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( + <Self as ::typst::model::Element>::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::<Self>().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::<dyn #capability>() { + 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::Variadics>::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( + <Self as ::typst::model::Element>::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( + 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 new file mode 100644 index 00000000..4a68e846 --- /dev/null +++ b/crates/typst-macros/src/func.rs @@ -0,0 +1,268 @@ +use super::*; + +/// Expand the `#[func]` macro. +pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> { + let func = prepare(stream, item)?; + Ok(create(&func, item)) +} + +struct Func { + name: String, + display: String, + category: String, + keywords: Option<String>, + docs: String, + vis: syn::Visibility, + ident: Ident, + ident_func: Ident, + parent: Option<syn::Type>, + vm: bool, + vt: bool, + args: bool, + span: bool, + params: Vec<Param>, + returns: syn::Type, + scope: Option<BlockWithReturn>, +} + +struct Param { + name: String, + docs: String, + external: bool, + named: bool, + variadic: bool, + default: Option<syn::Expr>, + ident: Ident, + ty: syn::Type, +} + +fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> { + let sig = &item.sig; + + let Parent(parent) = syn::parse2(stream)?; + + let mut vm = false; + let mut vt = false; + let mut args = false; + let mut span = false; + let mut params = vec![]; + for input in &sig.inputs { + let syn::FnArg::Typed(typed) = input else { + 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)?; + } + } + } + + 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) +} + +fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { + let Func { + name, + display, + category, + docs, + vis, + ident, + ident_func, + returns, + .. + } = func; + + let handlers = func + .params + .iter() + .filter(|param| !param.external) + .map(create_param_parser); + + let args = func + .params + .iter() + .filter(|param| !param.external) + .map(|param| ¶m.ident); + + 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 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(); + } + 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()); + + 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 + } + + #[doc = #docs] + #item + } +} + +/// Create a parameter info for a field. +fn create_param_info(param: &Param) -> TokenStream { + let Param { name, docs, named, variadic, ty, default, .. } = param; + let positional = !named; + let required = default.is_none(); + let default = quote_option(&default.as_ref().map(|_default| { + quote! { + || { + let typed: #ty = #default; + ::typst::eval::IntoValue::into_value(typed) + } + } + })); + let ty = if *variadic { + quote! { <#ty as ::typst::eval::Variadics>::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: false, + } + } +} + +/// Create argument parsing code for a parameter. +fn create_param_parser(param: &Param) -> TokenStream { + let Param { name, ident, ty, .. } = param; + + let mut value = if param.variadic { + quote! { args.all()? } + } else if param.named { + quote! { args.named(#name)? } + } else if param.default.is_some() { + quote! { args.eat()? } + } else { + quote! { args.expect(#name)? } + }; + + if let Some(default) = ¶m.default { + value = quote! { #value.unwrap_or_else(|| #default) } + } + + quote! { let mut #ident: #ty = #value; } +} + +struct Parent(Option<syn::Type>); + +impl Parse for Parent { + fn parse(input: ParseStream) -> Result<Self> { + Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None })) + } +} diff --git a/crates/typst-macros/src/lib.rs b/crates/typst-macros/src/lib.rs new file mode 100644 index 00000000..49840ef2 --- /dev/null +++ b/crates/typst-macros/src/lib.rs @@ -0,0 +1,63 @@ +//! Procedural macros for Typst. + +extern crate proc_macro; + +#[macro_use] +mod util; +mod castable; +mod element; +mod func; +mod symbols; + +use proc_macro::TokenStream as BoundaryStream; +use proc_macro2::TokenStream; +use quote::quote; +use syn::ext::IdentExt; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::punctuated::Punctuated; +use syn::{parse_quote, DeriveInput, Ident, Result, Token}; + +use self::util::*; + +/// Turns a function into a `NativeFunc`. +#[proc_macro_attribute] +pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { + let item = syn::parse_macro_input!(item as syn::ItemFn); + func::func(stream.into(), &item) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Turns a type into an `Element`. +#[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) + .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) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. +#[proc_macro] +pub fn cast(stream: BoundaryStream) -> BoundaryStream { + castable::cast(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Defines a list of `Symbol`s. +#[proc_macro] +pub fn symbols(stream: BoundaryStream) -> BoundaryStream { + symbols::symbols(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} diff --git a/crates/typst-macros/src/symbols.rs b/crates/typst-macros/src/symbols.rs new file mode 100644 index 00000000..cdb7f5d7 --- /dev/null +++ b/crates/typst-macros/src/symbols.rs @@ -0,0 +1,78 @@ +use super::*; + +/// Expand the `symbols!` macro. +pub fn symbols(stream: TokenStream) -> Result<TokenStream> { + let list: Punctuated<Symbol, Token![,]> = + Punctuated::parse_terminated.parse2(stream)?; + 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::Multiple(variants) => { + let variants = variants.iter().map(|variant| { + let name = &variant.name; + let c = &variant.c; + quote! { (#name, #c) } + }); + quote! { + typst::eval::Symbol::list(&[#(#variants),*]) + } + } + }; + quote! { (#name, #kind) } + }); + Ok(quote! { &[#(#pairs),*] }) +} + +struct Symbol { + name: syn::Ident, + kind: Kind, +} + +enum Kind { + Single(syn::LitChar), + Multiple(Punctuated<Variant, Token![,]>), +} + +struct Variant { + name: String, + c: syn::LitChar, +} + +impl Parse for Symbol { + fn parse(input: ParseStream) -> Result<Self> { + let name = input.call(Ident::parse_any)?; + input.parse::<Token![:]>()?; + let kind = input.parse()?; + Ok(Self { name, kind }) + } +} + +impl Parse for Kind { + fn parse(input: ParseStream) -> Result<Self> { + if input.peek(syn::LitChar) { + Ok(Self::Single(input.parse()?)) + } else { + let content; + syn::bracketed!(content in input); + Ok(Self::Multiple(Punctuated::parse_terminated(&content)?)) + } + } +} + +impl Parse for Variant { + fn parse(input: ParseStream) -> Result<Self> { + let mut name = String::new(); + if input.peek(syn::Ident::peek_any) { + name.push_str(&input.call(Ident::parse_any)?.to_string()); + while input.peek(Token![.]) { + input.parse::<Token![.]>()?; + name.push('.'); + name.push_str(&input.call(Ident::parse_any)?.to_string()); + } + input.parse::<Token![:]>()?; + } + let c = input.parse()?; + Ok(Self { name, c }) + } +} diff --git a/crates/typst-macros/src/util.rs b/crates/typst-macros/src/util.rs new file mode 100644 index 00000000..389fed06 --- /dev/null +++ b/crates/typst-macros/src/util.rs @@ -0,0 +1,145 @@ +use heck::ToKebabCase; +use quote::ToTokens; + +use super::*; + +/// Return an error at the given item. +macro_rules! bail { + (callsite, $($tts:tt)*) => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!("typst: {}", format!($($tts)*)) + )) + }; + ($item:expr, $($tts:tt)*) => { + return Err(syn::Error::new_spanned( + &$item, + format!("typst: {}", format!($($tts)*)) + )) + }; +} + +/// For parsing attributes of the form: +/// #[attr( +/// statement; +/// statement; +/// returned_expression +/// )] +pub struct BlockWithReturn { + pub prefix: Vec<syn::Stmt>, + pub expr: syn::Stmt, +} + +impl Parse for BlockWithReturn { + fn parse(input: ParseStream) -> Result<Self> { + 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 }) + } +} + +/// Whether an attribute list has a specified attribute. +pub fn has_attr(attrs: &mut Vec<syn::Attribute>, target: &str) -> bool { + take_attr(attrs, target).is_some() +} + +/// Whether an attribute list has a specified attribute. +pub fn parse_attr<T: Parse>( + attrs: &mut Vec<syn::Attribute>, + target: &str, +) -> Result<Option<Option<T>>> { + take_attr(attrs, target) + .map(|attr| { + Ok(match attr.meta { + syn::Meta::Path(_) => None, + syn::Meta::List(list) => Some(list.parse_args()?), + syn::Meta::NameValue(meta) => bail!(meta, "not valid here"), + }) + }) + .transpose() +} + +/// Whether an attribute list has a specified attribute. +pub fn take_attr( + attrs: &mut Vec<syn::Attribute>, + target: &str, +) -> Option<syn::Attribute> { + attrs + .iter() + .position(|attr| attr.path().is_ident(target)) + .map(|i| attrs.remove(i)) +} + +/// Ensure that no unrecognized attributes remain. +pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> { + for attr in attrs { + if !attr.path().is_ident("doc") && !attr.path().is_ident("derive") { + let ident = attr.path().get_ident().unwrap(); + bail!(ident, "unrecognized attribute: {ident}"); + } + } + 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<T: ToTokens>(option: &Option<T>) -> TokenStream { + if let Some(value) = option { + quote! { Some(#value) } + } else { + quote! { None } + } +} |
