diff options
| author | Laurenz <laurmaedje@gmail.com> | 2022-12-15 22:51:55 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2022-12-15 23:11:20 +0100 |
| commit | b6202b646a0d5ecced301d9bac8bfcaf977d7ee4 (patch) | |
| tree | 7d42cb50f9e66153e7e8b2217009684e25f54f42 /macros | |
| parent | f3980c704544a464f9729cc8bc9f97d3a7454769 (diff) | |
Reflection for castables
Diffstat (limited to 'macros')
| -rw-r--r-- | macros/src/capable.rs (renamed from macros/src/capability.rs) | 4 | ||||
| -rw-r--r-- | macros/src/castable.rs | 229 | ||||
| -rw-r--r-- | macros/src/func.rs | 63 | ||||
| -rw-r--r-- | macros/src/lib.rs | 34 | ||||
| -rw-r--r-- | macros/src/node.rs | 46 |
5 files changed, 336 insertions, 40 deletions
diff --git a/macros/src/capability.rs b/macros/src/capable.rs index aa98f584..dcfdfc82 100644 --- a/macros/src/capability.rs +++ b/macros/src/capable.rs @@ -1,9 +1,9 @@ -use super::*; - use syn::parse::Parser; use syn::punctuated::Punctuated; use syn::Token; +use super::*; + /// Expand the `#[capability]` macro. pub fn capability(item: syn::ItemTrait) -> Result<TokenStream> { let ident = &item.ident; diff --git a/macros/src/castable.rs b/macros/src/castable.rs new file mode 100644 index 00000000..48cdf9e1 --- /dev/null +++ b/macros/src/castable.rs @@ -0,0 +1,229 @@ +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::Token; + +use super::*; + +/// Expand the `castable!` macro. +pub fn castable(stream: TokenStream) -> Result<TokenStream> { + let castable: Castable = syn::parse2(stream)?; + let ty = &castable.ty; + + if castable.casts.is_empty() && castable.name.is_none() { + bail!(castable.ty, "expected at least one pattern"); + } + + let is_func = create_is_func(&castable); + let cast_func = create_cast_func(&castable); + let describe_func = create_describe_func(&castable); + let dynamic_impls = castable.name.as_ref().map(|name| { + quote! { + impl ::typst::model::Type for #ty { + const TYPE_NAME: &'static str = #name; + } + + impl From<#ty> for ::typst::model::Value { + fn from(v: #ty) -> Self { + ::typst::model::Value::Dyn(::typst::model::Dynamic::new(v)) + } + } + } + }); + + Ok(quote! { + impl ::typst::model::Cast for #ty { + #is_func + #cast_func + #describe_func + } + + #dynamic_impls + }) +} + +/// Create the castable's `is` function. +fn create_is_func(castable: &Castable) -> TokenStream { + let mut string_arms = vec![]; + let mut cast_checks = vec![]; + + for cast in &castable.casts { + match &cast.pattern { + Pattern::Str(lit) => { + string_arms.push(quote! { #lit => return true }); + } + Pattern::Ty(_, ty) => { + cast_checks.push(quote! { + if <#ty as ::typst::model::Cast>::is(value) { + return true; + } + }); + } + } + } + + let dynamic_check = castable.name.is_some().then(|| { + quote! { + if let ::typst::model::Value::Dyn(dynamic) = &value { + if dynamic.is::<Self>() { + return true; + } + } + } + }); + + let str_check = (!string_arms.is_empty()).then(|| { + quote! { + if let ::typst::model::Value::Str(string) = &value { + match string.as_str() { + #(#string_arms,)* + _ => {} + } + } + } + }); + + quote! { + fn is(value: &typst::model::Value) -> bool { + #dynamic_check + #str_check + #(#cast_checks)* + false + } + } +} + +/// Create the castable's `cast` function. +fn create_cast_func(castable: &Castable) -> TokenStream { + let mut string_arms = vec![]; + let mut cast_checks = vec![]; + + for cast in &castable.casts { + 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::model::Cast>::is(&value) { + let #binding = <#ty as ::typst::model::Cast>::cast(value)?; + return Ok(#expr); + } + }); + } + } + } + + let dynamic_check = castable.name.is_some().then(|| { + quote! { + if let ::typst::model::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::model::Value::Str(string) = &value { + match string.as_str() { + #(#string_arms,)* + _ => {} + } + } + } + }); + + quote! { + fn cast(value: ::typst::model::Value) -> ::typst::diag::StrResult<Self> { + #dynamic_check + #str_check + #(#cast_checks)* + <Self as ::typst::model::Cast>::error(value) + } + } +} + +/// Create the castable's `describe` function. +fn create_describe_func(castable: &Castable) -> TokenStream { + let mut infos = vec![]; + + for cast in &castable.casts { + let docs = doc_comment(&cast.attrs); + infos.push(match &cast.pattern { + Pattern::Str(lit) => { + quote! { ::typst::model::CastInfo::Value(#lit.into(), #docs) } + } + Pattern::Ty(_, ty) => { + quote! { <#ty as ::typst::model::Cast>::describe() } + } + }); + } + + if let Some(name) = &castable.name { + infos.push(quote! { + CastInfo::Type(#name) + }); + } + + quote! { + fn describe() -> ::typst::model::CastInfo { + #(#infos)+* + } + } +} + +struct Castable { + ty: syn::Type, + name: Option<syn::LitStr>, + casts: Punctuated<Cast, Token![,]>, +} + +impl Parse for Castable { + fn parse(input: ParseStream) -> Result<Self> { + let ty = input.parse()?; + let mut name = None; + if input.peek(Token![:]) { + let _: syn::Token![:] = input.parse()?; + name = Some(input.parse()?); + } + let _: syn::Token![,] = input.parse()?; + let casts = Punctuated::parse_terminated(input)?; + Ok(Self { ty, name, casts }) + } +} + +struct Cast { + attrs: Vec<syn::Attribute>, + pattern: Pattern, + expr: syn::Expr, +} + +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 }) + } +} + +enum Pattern { + Str(syn::LitStr), + Ty(syn::Pat, syn::Type), +} + +impl Parse for Pattern { + fn parse(input: ParseStream) -> Result<Self> { + if input.peek(syn::LitStr) { + Ok(Pattern::Str(input.parse()?)) + } else { + let pat = input.parse()?; + let _: syn::Token![:] = input.parse()?; + let ty = input.parse()?; + Ok(Pattern::Ty(pat, ty)) + } + } +} diff --git a/macros/src/func.rs b/macros/src/func.rs index af558f96..4523d48a 100644 --- a/macros/src/func.rs +++ b/macros/src/func.rs @@ -2,7 +2,37 @@ use super::*; /// Expand the `#[func]` macro. pub fn func(item: syn::Item) -> Result<TokenStream> { - let doc = documentation(&item)?; + let doc_comment = match &item { + syn::Item::Struct(item) => doc_comment(&item.attrs), + syn::Item::Enum(item) => doc_comment(&item.attrs), + syn::Item::Fn(item) => doc_comment(&item.attrs), + _ => String::new(), + }; + + let mut tags = vec![]; + let mut kept = vec![]; + for line in doc_comment.lines() { + let line = line.trim(); + if let Some(suffix) = line.trim_end_matches(".").strip_prefix("Tags: ") { + tags.extend(suffix.split(", ")); + } else { + kept.push(line); + } + } + + while kept.last().map_or(false, |line| line.is_empty()) { + kept.pop(); + } + + let docs = kept.join("\n"); + let info = quote! { + ::typst::model::FuncInfo { + name, + docs: #docs, + tags: &[#(#tags),*], + params: ::std::vec![], + } + }; if let syn::Item::Fn(item) = &item { let vis = &item.vis; @@ -29,7 +59,7 @@ pub fn func(item: syn::Item) -> Result<TokenStream> { impl::typst::model::FuncType for #ty { fn create_func(name: &'static str) -> ::typst::model::Func { - ::typst::model::Func::from_fn(name, #full, #doc) + ::typst::model::Func::from_fn(name, #full, #info) } } }) @@ -47,36 +77,9 @@ pub fn func(item: syn::Item) -> Result<TokenStream> { impl #params ::typst::model::FuncType for #ident #args #clause { fn create_func(name: &'static str) -> ::typst::model::Func { - ::typst::model::Func::from_node::<Self>(name, #doc) + ::typst::model::Func::from_node::<Self>(name, #info) } } }) } } - -/// Extract the item's documentation. -fn documentation(item: &syn::Item) -> Result<String> { - let mut doc = String::new(); - - // Extract attributes. - let attrs = match item { - syn::Item::Struct(item) => &item.attrs, - syn::Item::Enum(item) => &item.attrs, - syn::Item::Fn(item) => &item.attrs, - _ => return Ok(doc), - }; - - // Parse doc comments. - for attr in attrs { - if let syn::Meta::NameValue(meta) = attr.parse_meta()? { - if meta.path.is_ident("doc") { - if let syn::Lit::Str(string) = &meta.lit { - doc.push_str(&string.value()); - doc.push('\n'); - } - } - } - } - - Ok(doc.trim().into()) -} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 7f6a4a6c..15dc3ee7 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -12,7 +12,8 @@ macro_rules! bail { } } -mod capability; +mod capable; +mod castable; mod func; mod node; @@ -40,7 +41,7 @@ pub fn node(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { #[proc_macro_attribute] pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::ItemTrait); - capability::capability(item) + capable::capability(item) .unwrap_or_else(|err| err.to_compile_error()) .into() } @@ -49,7 +50,34 @@ pub fn capability(_: BoundaryStream, item: BoundaryStream) -> BoundaryStream { #[proc_macro_attribute] pub fn capable(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { let item = syn::parse_macro_input!(item as syn::Item); - capability::capable(stream.into(), item) + capable::capable(stream.into(), item) .unwrap_or_else(|err| err.to_compile_error()) .into() } + +/// Implement `Cast` and optionally `Type` for a type. +#[proc_macro] +pub fn castable(stream: BoundaryStream) -> BoundaryStream { + castable::castable(stream.into()) + .unwrap_or_else(|err| err.to_compile_error()) + .into() +} + +/// Extract documentation comments from an attribute list. +fn doc_comment(attrs: &[syn::Attribute]) -> String { + let mut doc = String::new(); + + // Parse doc comments. + for attr in attrs { + if let Ok(syn::Meta::NameValue(meta)) = attr.parse_meta() { + if meta.path.is_ident("doc") { + if let syn::Lit::Str(string) = &meta.lit { + doc.push_str(&string.value()); + doc.push('\n'); + } + } + } + } + + doc.trim().into() +} diff --git a/macros/src/node.rs b/macros/src/node.rs index 192ca667..ad079c0e 100644 --- a/macros/src/node.rs +++ b/macros/src/node.rs @@ -36,6 +36,7 @@ struct Property { shorthand: Option<Shorthand>, resolve: bool, fold: bool, + reflect: bool, } /// The shorthand form of a style property. @@ -117,6 +118,7 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> { let mut referenced = false; let mut resolve = false; let mut fold = false; + let mut reflect = false; // Parse the `#[property(..)]` attribute. let mut stream = tokens.into_iter().peekable(); @@ -150,14 +152,11 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> { "referenced" => referenced = true, "resolve" => resolve = true, "fold" => fold = true, + "reflect" => reflect = true, _ => bail!(ident, "invalid attribute"), } } - if skip && shorthand.is_some() { - bail!(item.ident, "skip and shorthand are mutually exclusive"); - } - if referenced && (fold || resolve) { bail!(item.ident, "referenced is mutually exclusive with fold and resolve"); } @@ -193,6 +192,7 @@ fn prepare_property(item: &syn::ImplItemConst) -> Result<Property> { referenced, resolve, fold, + reflect, }) } @@ -205,6 +205,7 @@ fn create(node: &Node) -> Result<TokenStream> { let name_method = create_node_name_method(node); let construct_func = create_node_construct_func(node); let set_func = create_node_set_func(node); + let properties_func = create_node_properties_func(node); let field_method = create_node_field_method(node); let node_impl = quote! { @@ -213,6 +214,7 @@ fn create(node: &Node) -> Result<TokenStream> { #name_method #construct_func #set_func + #properties_func #field_method } }; @@ -221,7 +223,7 @@ fn create(node: &Node) -> Result<TokenStream> { let mut items: Vec<syn::ImplItem> = vec![]; let scope = quote::format_ident!("__{}_keys", node.self_name); - for property in &node.properties { + for property in node.properties.iter() { let (key, module) = create_property_module(node, &property); modules.push(module); @@ -331,6 +333,40 @@ fn create_node_set_func(node: &Node) -> syn::ImplItemMethod { } } +/// Create the node's `properties` function. +fn create_node_properties_func(node: &Node) -> syn::ImplItemMethod { + let infos = node + .properties + .iter() + .filter(|p| !p.skip || p.reflect) + .map(|property| { + let name = property.name.to_string().replace('_', "-").to_lowercase(); + let docs = doc_comment(&property.attrs); + let value_ty = &property.value_ty; + let shorthand = matches!(property.shorthand, Some(Shorthand::Positional)); + quote! { + ::typst::model::ParamInfo { + name: #name, + docs: #docs, + settable: true, + shorthand: #shorthand, + cast: <#value_ty as ::typst::model::Cast< + ::typst::syntax::Spanned<::typst::model::Value> + >>::describe(), + } + } + }); + + parse_quote! { + fn properties() -> ::std::vec::Vec<::typst::model::ParamInfo> + where + Self: Sized + { + ::std::vec![#(#infos),*] + } + } +} + /// Create the node's `field` method. fn create_node_field_method(node: &Node) -> syn::ImplItemMethod { node.field.clone().unwrap_or_else(|| { |
