summaryrefslogtreecommitdiff
path: root/macros/src
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
parentf3980c704544a464f9729cc8bc9f97d3a7454769 (diff)
Reflection for castables
Diffstat (limited to 'macros/src')
-rw-r--r--macros/src/capable.rs (renamed from macros/src/capability.rs)4
-rw-r--r--macros/src/castable.rs229
-rw-r--r--macros/src/func.rs63
-rw-r--r--macros/src/lib.rs34
-rw-r--r--macros/src/node.rs46
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(|| {