summaryrefslogtreecommitdiff
path: root/macros/src/castable.rs
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2022-12-15 22:51:55 +0100
committerLaurenz <laurmaedje@gmail.com>2022-12-15 23:11:20 +0100
commitb6202b646a0d5ecced301d9bac8bfcaf977d7ee4 (patch)
tree7d42cb50f9e66153e7e8b2217009684e25f54f42 /macros/src/castable.rs
parentf3980c704544a464f9729cc8bc9f97d3a7454769 (diff)
Reflection for castables
Diffstat (limited to 'macros/src/castable.rs')
-rw-r--r--macros/src/castable.rs229
1 files changed, 229 insertions, 0 deletions
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))
+ }
+ }
+}