use super::*; /// Expand the `#[derive(Cast)]` macro. pub fn derive_cast(item: &DeriveInput) -> Result { let ty = &item.ident; let syn::Data::Enum(data) = &item.data else { bail!(item, "only enums are supported"); }; let mut variants = vec![]; for variant in &data.variants { if let Some((_, expr)) = &variant.discriminant { bail!(expr, "explicit discriminant is not allowed"); } let string = if let Some(attr) = variant.attrs.iter().find(|attr| attr.path().is_ident("string")) { attr.parse_args::()?.value() } else { kebab_case(&variant.ident) }; variants.push(Variant { ident: variant.ident.clone(), string, docs: documentation(&variant.attrs), }); } let strs_to_variants = variants.iter().map(|Variant { ident, string, docs }| { quote! { #[doc = #docs] #string => Self::#ident } }); let variants_to_strs = variants.iter().map(|Variant { ident, string, .. }| { quote! { #ty::#ident => #string } }); Ok(quote! { ::typst::eval::cast! { #ty, self => ::typst::eval::IntoValue::into_value(match self { #(#variants_to_strs),* }), #(#strs_to_variants),* } }) } /// An enum variant in a `derive(Cast)`. struct Variant { ident: Ident, string: String, docs: String, } /// Expand the `cast!` macro. pub fn cast(stream: TokenStream) -> Result { let input: CastInput = syn::parse2(stream)?; let ty = &input.ty; let eval = quote! { ::typst::eval }; let castable_body = create_castable_body(&input); let describe_body = create_describe_body(&input); let into_value_body = create_into_value_body(&input); let from_value_body = create_from_value_body(&input); let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| { quote! { impl #eval::Reflect for #ty { fn describe() -> #eval::CastInfo { #describe_body } fn castable(value: &#eval::Value) -> bool { #castable_body } } } }); let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { quote! { impl #eval::IntoValue for #ty { fn into_value(self) -> #eval::Value { #into_value_body } } } }); let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { quote! { impl #eval::FromValue for #ty { fn from_value(value: #eval::Value) -> ::typst::diag::StrResult { #from_value_body } } } }); let ty = input.name.as_ref().map(|name| { quote! { impl #eval::Type for #ty { const TYPE_NAME: &'static str = #name; } } }); Ok(quote! { #reflect #into_value #from_value #ty }) } /// The input to `cast!`. struct CastInput { ty: syn::Type, name: Option, into_value: Option, from_value: Punctuated, } impl Parse for CastInput { fn parse(input: ParseStream) -> Result { let ty; let mut name = None; if input.peek(syn::Token![type]) { let _: syn::Token![type] = input.parse()?; ty = input.parse()?; let _: syn::Token![:] = input.parse()?; name = Some(input.parse()?); } else { ty = input.parse()?; } let _: syn::Token![,] = input.parse()?; let mut into_value = None; if input.peek(syn::Token![self]) { let _: syn::Token![self] = input.parse()?; let _: syn::Token![=>] = input.parse()?; into_value = Some(input.parse()?); let _: syn::Token![,] = input.parse()?; } let from_value = Punctuated::parse_terminated(input)?; Ok(Self { ty, name, into_value, from_value }) } } impl Parse for Cast { fn parse(input: ParseStream) -> Result { let attrs = input.call(syn::Attribute::parse_outer)?; let pattern = input.parse()?; let _: syn::Token![=>] = input.parse()?; let expr = input.parse()?; Ok(Self { attrs, pattern, expr }) } } impl Parse for Pattern { fn parse(input: ParseStream) -> Result { if input.peek(syn::LitStr) { Ok(Pattern::Str(input.parse()?)) } else { let pat = syn::Pat::parse_single(input)?; let _: syn::Token![:] = input.parse()?; let ty = input.parse()?; Ok(Pattern::Ty(pat, ty)) } } } /// A single cast, e.g. `v: i64 => Self::Int(v)`. struct Cast { attrs: Vec, pattern: Pattern, expr: syn::Expr, } /// A pattern in a cast, e.g.`"ascender"` or `v: i64`. enum Pattern { Str(syn::LitStr), Ty(syn::Pat, syn::Type), } fn create_castable_body(input: &CastInput) -> TokenStream { let mut strings = vec![]; let mut casts = vec![]; for cast in &input.from_value { match &cast.pattern { Pattern::Str(lit) => { strings.push(quote! { #lit => return true }); } Pattern::Ty(_, ty) => { casts.push(quote! { if <#ty as ::typst::eval::Reflect>::castable(value) { return true; } }); } } } let dynamic_check = input.name.is_some().then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if dynamic.is::() { return true; } } } }); let str_check = (!strings.is_empty()).then(|| { quote! { if let ::typst::eval::Value::Str(string) = &value { match string.as_str() { #(#strings,)* _ => {} } } } }); quote! { #dynamic_check #str_check #(#casts)* false } } fn create_describe_body(input: &CastInput) -> TokenStream { let mut infos = vec![]; for cast in &input.from_value { let docs = documentation(&cast.attrs); infos.push(match &cast.pattern { Pattern::Str(lit) => { quote! { ::typst::eval::CastInfo::Value( ::typst::eval::IntoValue::into_value(#lit), #docs, ) } } Pattern::Ty(_, ty) => { quote! { <#ty as ::typst::eval::Reflect>::describe() } } }); } if let Some(name) = &input.name { infos.push(quote! { ::typst::eval::CastInfo::Type(#name) }); } quote! { #(#infos)+* } } fn create_into_value_body(input: &CastInput) -> TokenStream { if let Some(expr) = &input.into_value { quote! { #expr } } else { quote! { ::typst::eval::Value::dynamic(self) } } } fn create_from_value_body(input: &CastInput) -> TokenStream { let mut string_arms = vec![]; let mut cast_checks = vec![]; for cast in &input.from_value { let expr = &cast.expr; match &cast.pattern { Pattern::Str(lit) => { string_arms.push(quote! { #lit => return Ok(#expr) }); } Pattern::Ty(binding, ty) => { cast_checks.push(quote! { if <#ty as ::typst::eval::Reflect>::castable(&value) { let #binding = <#ty as ::typst::eval::FromValue>::from_value(value)?; return Ok(#expr); } }); } } } let dynamic_check = input.name.is_some().then(|| { quote! { if let ::typst::eval::Value::Dyn(dynamic) = &value { if let Some(concrete) = dynamic.downcast::() { return Ok(concrete.clone()); } } } }); let str_check = (!string_arms.is_empty()).then(|| { quote! { if let ::typst::eval::Value::Str(string) = &value { match string.as_str() { #(#string_arms,)* _ => {} } } } }); quote! { #dynamic_check #str_check #(#cast_checks)* Err(::error(&value)) } }