Better proc macros

This commit is contained in:
Laurenz 2023-09-11 14:38:54 +02:00
parent 921b40cf9c
commit 8f36fca684
8 changed files with 1247 additions and 422 deletions

View File

@ -1,7 +1,9 @@
use heck::ToKebabCase;
use super::*; use super::*;
/// Expand the `#[derive(Cast)]` macro. /// Expand the `#[derive(Cast)]` macro.
pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> { pub fn derive_cast(item: DeriveInput) -> Result<TokenStream> {
let ty = &item.ident; let ty = &item.ident;
let syn::Data::Enum(data) = &item.data else { let syn::Data::Enum(data) = &item.data else {
@ -19,7 +21,7 @@ pub fn derive_cast(item: &DeriveInput) -> Result<TokenStream> {
{ {
attr.parse_args::<syn::LitStr>()?.value() attr.parse_args::<syn::LitStr>()?.value()
} else { } else {
kebab_case(&variant.ident) variant.ident.to_string().to_kebab_case()
}; };
variants.push(Variant { variants.push(Variant {
@ -62,20 +64,25 @@ struct Variant {
/// Expand the `cast!` macro. /// Expand the `cast!` macro.
pub fn cast(stream: TokenStream) -> Result<TokenStream> { pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
let eval = quote! { ::typst::eval }; let eval = quote! { ::typst::eval };
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
let castable_body = create_castable_body(&input); let castable_body = create_castable_body(&input);
let describe_body = create_describe_body(&input); let input_body = create_input_body(&input);
let output_body = create_output_body(&input);
let into_value_body = create_into_value_body(&input); let into_value_body = create_into_value_body(&input);
let from_value_body = create_from_value_body(&input); let from_value_body = create_from_value_body(&input);
let reflect = (!input.from_value.is_empty() || input.name.is_some()).then(|| { let reflect = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! { quote! {
impl #eval::Reflect for #ty { impl #eval::Reflect for #ty {
fn describe() -> #eval::CastInfo { fn input() -> #eval::CastInfo {
#describe_body #input_body
}
fn output() -> #eval::CastInfo {
#output_body
} }
fn castable(value: &#eval::Value) -> bool { fn castable(value: &#eval::Value) -> bool {
@ -85,7 +92,7 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
} }
}); });
let into_value = (input.into_value.is_some() || input.name.is_some()).then(|| { let into_value = (input.into_value.is_some() || input.dynamic).then(|| {
quote! { quote! {
impl #eval::IntoValue for #ty { impl #eval::IntoValue for #ty {
fn into_value(self) -> #eval::Value { fn into_value(self) -> #eval::Value {
@ -95,7 +102,7 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
} }
}); });
let from_value = (!input.from_value.is_empty() || input.name.is_some()).then(|| { let from_value = (!input.from_value.is_empty() || input.dynamic).then(|| {
quote! { quote! {
impl #eval::FromValue for #ty { impl #eval::FromValue for #ty {
fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> { fn from_value(value: #eval::Value) -> ::typst::diag::StrResult<Self> {
@ -105,55 +112,42 @@ pub fn cast(stream: TokenStream) -> Result<TokenStream> {
} }
}); });
let ty = input.name.as_ref().map(|name| {
quote! {
impl #eval::Type for #ty {
const TYPE_NAME: &'static str = #name;
}
}
});
Ok(quote! { Ok(quote! {
#reflect #reflect
#into_value #into_value
#from_value #from_value
#ty
}) })
} }
/// The input to `cast!`. /// The input to `cast!`.
struct CastInput { struct CastInput {
ty: syn::Type, ty: syn::Type,
name: Option<syn::LitStr>, dynamic: bool,
into_value: Option<syn::Expr>, into_value: Option<syn::Expr>,
from_value: Punctuated<Cast, Token![,]>, from_value: Punctuated<Cast, Token![,]>,
} }
impl Parse for CastInput { impl Parse for CastInput {
fn parse(input: ParseStream) -> Result<Self> { fn parse(input: ParseStream) -> Result<Self> {
let ty; let mut dynamic = false;
let mut name = None;
if input.peek(syn::Token![type]) { if input.peek(syn::Token![type]) {
let _: syn::Token![type] = input.parse()?; let _: syn::Token![type] = input.parse()?;
ty = input.parse()?; dynamic = true;
let _: syn::Token![:] = input.parse()?;
name = Some(input.parse()?);
} else {
ty = input.parse()?;
} }
let ty = input.parse()?;
let _: syn::Token![,] = input.parse()?; let _: syn::Token![,] = input.parse()?;
let mut into_value = None; let mut to_value = None;
if input.peek(syn::Token![self]) { if input.peek(syn::Token![self]) {
let _: syn::Token![self] = input.parse()?; let _: syn::Token![self] = input.parse()?;
let _: syn::Token![=>] = input.parse()?; let _: syn::Token![=>] = input.parse()?;
into_value = Some(input.parse()?); to_value = Some(input.parse()?);
let _: syn::Token![,] = input.parse()?; let _: syn::Token![,] = input.parse()?;
} }
let from_value = Punctuated::parse_terminated(input)?; let from_value = Punctuated::parse_terminated(input)?;
Ok(Self { ty, name, into_value, from_value }) Ok(Self { ty, dynamic, into_value: to_value, from_value })
} }
} }
@ -212,7 +206,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
} }
} }
let dynamic_check = input.name.is_some().then(|| { let dynamic_check = input.dynamic.then(|| {
quote! { quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value { if let ::typst::eval::Value::Dyn(dynamic) = &value {
if dynamic.is::<Self>() { if dynamic.is::<Self>() {
@ -241,7 +235,7 @@ fn create_castable_body(input: &CastInput) -> TokenStream {
} }
} }
fn create_describe_body(input: &CastInput) -> TokenStream { fn create_input_body(input: &CastInput) -> TokenStream {
let mut infos = vec![]; let mut infos = vec![];
for cast in &input.from_value { for cast in &input.from_value {
@ -256,14 +250,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream {
} }
} }
Pattern::Ty(_, ty) => { Pattern::Ty(_, ty) => {
quote! { <#ty as ::typst::eval::Reflect>::describe() } quote! { <#ty as ::typst::eval::Reflect>::input() }
} }
}); });
} }
if let Some(name) = &input.name { if input.dynamic {
infos.push(quote! { infos.push(quote! {
::typst::eval::CastInfo::Type(#name) ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>())
}); });
} }
@ -272,6 +266,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream {
} }
} }
fn create_output_body(input: &CastInput) -> TokenStream {
if input.dynamic {
quote! { ::typst::eval::CastInfo::Type(::typst::eval::Type::of::<Self>()) }
} else {
quote! { Self::input() }
}
}
fn create_into_value_body(input: &CastInput) -> TokenStream { fn create_into_value_body(input: &CastInput) -> TokenStream {
if let Some(expr) = &input.into_value { if let Some(expr) = &input.into_value {
quote! { #expr } quote! { #expr }
@ -301,7 +303,7 @@ fn create_from_value_body(input: &CastInput) -> TokenStream {
} }
} }
let dynamic_check = input.name.is_some().then(|| { let dynamic_check = input.dynamic.then(|| {
quote! { quote! {
if let ::typst::eval::Value::Dyn(dynamic) = &value { if let ::typst::eval::Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() { if let Some(concrete) = dynamic.downcast::<Self>() {

View File

@ -1,152 +1,175 @@
use heck::ToKebabCase;
use super::*; use super::*;
/// Expand the `#[element]` macro. /// Expand the `#[elem]` macro.
pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> { pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
let element = prepare(stream, body)?; let element = parse(stream, &body)?;
Ok(create(&element)) Ok(create(&element))
} }
/// Details about an element.
struct Elem { struct Elem {
name: String, name: String,
display: String, title: String,
category: String, scope: bool,
keywords: Option<String>, keywords: Vec<String>,
docs: String, docs: String,
vis: syn::Visibility, vis: syn::Visibility,
ident: Ident, ident: Ident,
capable: Vec<Ident>, capabilities: Vec<Ident>,
fields: Vec<Field>, fields: Vec<Field>,
scope: Option<BlockWithReturn>,
} }
/// Details about an element field.
struct Field { 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: Ident,
ident_in: Ident, ident_in: Ident,
with_ident: Ident, with_ident: Ident,
push_ident: Ident, push_ident: Ident,
set_ident: Ident, set_ident: Ident,
vis: syn::Visibility,
ty: syn::Type, ty: syn::Type,
output: syn::Type, output: syn::Type,
name: String,
docs: String,
positional: bool,
required: bool,
variadic: bool,
resolve: bool,
fold: bool,
internal: bool,
external: bool,
synthesized: bool,
parse: Option<BlockWithReturn>,
default: syn::Expr,
} }
impl Field { impl Field {
/// Whether the field is present on every instance of the element.
fn inherent(&self) -> bool { fn inherent(&self) -> bool {
self.required || self.variadic self.required || self.variadic
} }
/// Whether the field can be used with set rules.
fn settable(&self) -> bool { fn settable(&self) -> bool {
!self.inherent() !self.inherent()
} }
} }
/// Preprocess the element's definition. /// The `..` in `#[elem(..)]`.
fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> { struct Meta {
scope: bool,
name: Option<String>,
title: Option<String>,
keywords: Vec<String>,
capabilities: Vec<Ident>,
}
impl Parse for Meta {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
scope: parse_flag::<kw::scope>(input)?,
name: parse_string::<kw::name>(input)?,
title: parse_string::<kw::title>(input)?,
keywords: parse_string_array::<kw::keywords>(input)?,
capabilities: Punctuated::<Ident, Token![,]>::parse_terminated(input)?
.into_iter()
.collect(),
})
}
}
/// Parse details about the element from its struct definition.
fn parse(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> {
let meta: Meta = syn::parse2(stream)?;
let (name, title) = determine_name_and_title(
meta.name,
meta.title,
&body.ident,
Some(|base| base.trim_end_matches("Elem")),
)?;
let docs = documentation(&body.attrs);
let syn::Fields::Named(named) = &body.fields else { let syn::Fields::Named(named) = &body.fields else {
bail!(body, "expected named fields"); bail!(body, "expected named fields");
}; };
let fields = named.named.iter().map(parse_field).collect::<Result<_>>()?;
let mut fields = vec![]; Ok(Elem {
for field in &named.named { name,
let Some(ident) = field.ident.clone() else { title,
bail!(field, "expected named field"); scope: meta.scope,
}; keywords: meta.keywords,
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, docs,
vis: body.vis.clone(), vis: body.vis.clone(),
ident: body.ident.clone(), ident: body.ident.clone(),
capable, capabilities: meta.capabilities,
fields, fields,
scope: parse_attr(&mut attrs, "scope")?.flatten(), })
}
fn parse_field(field: &syn::Field) -> Result<Field> {
let Some(ident) = field.ident.clone() else {
bail!(field, "expected named field");
}; };
if ident == "label" {
bail!(ident, "invalid field name");
}
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;
let mut field = Field {
name: ident.to_string().to_kebab_case(),
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)?; validate_attrs(&attrs)?;
Ok(element)
Ok(field)
} }
/// Produce the element's definition. /// Produce the element's definition.
@ -166,13 +189,13 @@ fn create(element: &Elem) -> TokenStream {
// Trait implementations. // Trait implementations.
let element_impl = create_pack_impl(element); let element_impl = create_pack_impl(element);
let construct_impl = element let construct_impl = element
.capable .capabilities
.iter() .iter()
.all(|capability| capability != "Construct") .all(|capability| capability != "Construct")
.then(|| create_construct_impl(element)); .then(|| create_construct_impl(element));
let set_impl = create_set_impl(element); let set_impl = create_set_impl(element);
let locatable_impl = element let locatable_impl = element
.capable .capabilities
.iter() .iter()
.any(|capability| capability == "Locatable") .any(|capability| capability == "Locatable")
.then(|| quote! { impl ::typst::model::Locatable for #ident {} }); .then(|| quote! { impl ::typst::model::Locatable for #ident {} });
@ -231,7 +254,7 @@ fn create_new_func(element: &Elem) -> TokenStream {
/// Create a new element. /// Create a new element.
pub fn new(#(#params),*) -> Self { pub fn new(#(#params),*) -> Self {
Self(::typst::model::Content::new( Self(::typst::model::Content::new(
<Self as ::typst::model::Element>::func() <Self as ::typst::model::NativeElement>::elem()
)) ))
#(#builder_calls)* #(#builder_calls)*
} }
@ -285,7 +308,7 @@ fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStrea
quote! { quote! {
styles.#getter::<#ty>( styles.#getter::<#ty>(
<Self as ::typst::model::Element>::func(), <Self as ::typst::model::NativeElement>::elem(),
#name, #name,
#inherent, #inherent,
|| #default, || #default,
@ -325,7 +348,7 @@ fn create_set_field_method(field: &Field) -> TokenStream {
#[doc = #doc] #[doc = #doc]
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Style { #vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
::typst::model::Style::Property(::typst::model::Property::new( ::typst::model::Style::Property(::typst::model::Property::new(
<Self as ::typst::model::Element>::func(), <Self as ::typst::model::NativeElement>::elem(),
#name, #name,
#ident, #ident,
)) ))
@ -335,49 +358,54 @@ fn create_set_field_method(field: &Field) -> TokenStream {
/// Create the element's `Pack` implementation. /// Create the element's `Pack` implementation.
fn create_pack_impl(element: &Elem) -> TokenStream { fn create_pack_impl(element: &Elem) -> TokenStream {
let Elem { ident, name, display, keywords, category, docs, .. } = element; let eval = quote! { ::typst::eval };
let model = quote! { ::typst::model };
let Elem { name, ident, title, scope, keywords, docs, .. } = element;
let vtable_func = create_vtable_func(element); let vtable_func = create_vtable_func(element);
let infos = element let params = element
.fields .fields
.iter() .iter()
.filter(|field| !field.internal && !field.synthesized) .filter(|field| !field.internal && !field.synthesized)
.map(create_param_info); .map(create_param_info);
let scope = create_scope_builder(element.scope.as_ref());
let keywords = quote_option(keywords); let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
};
let data = quote! {
#model::NativeElementData {
name: #name,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
construct: <#ident as #model::Construct>::construct,
set: <#ident as #model::Set>::set,
vtable: #vtable_func,
scope: #eval::Lazy::new(|| #scope),
params: #eval::Lazy::new(|| ::std::vec![#(#params),*])
}
};
quote! { quote! {
impl ::typst::model::Element for #ident { impl #model::NativeElement for #ident {
fn pack(self) -> ::typst::model::Content { fn data() -> &'static #model::NativeElementData {
static DATA: #model::NativeElementData = #data;
&DATA
}
fn pack(self) -> #model::Content {
self.0 self.0
} }
fn unpack(content: &::typst::model::Content) -> ::std::option::Option<&Self> { fn unpack(content: &#model::Content) -> ::std::option::Option<&Self> {
// Safety: Elements are #[repr(transparent)]. // Safety: Elements are #[repr(transparent)].
content.is::<Self>().then(|| unsafe { content.is::<Self>().then(|| unsafe {
::std::mem::transmute(content) ::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()
}
} }
} }
} }
@ -385,7 +413,7 @@ fn create_pack_impl(element: &Elem) -> TokenStream {
/// Create the element's casting vtable. /// Create the element's casting vtable.
fn create_vtable_func(element: &Elem) -> TokenStream { fn create_vtable_func(element: &Elem) -> TokenStream {
let ident = &element.ident; let ident = &element.ident;
let relevant = element.capable.iter().filter(|&ident| ident != "Construct"); let relevant = element.capabilities.iter().filter(|&ident| ident != "Construct");
let checks = relevant.map(|capability| { let checks = relevant.map(|capability| {
quote! { quote! {
if id == ::std::any::TypeId::of::<dyn #capability>() { if id == ::std::any::TypeId::of::<dyn #capability>() {
@ -399,7 +427,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream {
quote! { quote! {
|id| { |id| {
let null = Self(::typst::model::Content::new( let null = Self(::typst::model::Content::new(
<#ident as ::typst::model::Element>::func() <#ident as ::typst::model::NativeElement>::elem()
)); ));
#(#checks)* #(#checks)*
None None
@ -441,7 +469,7 @@ fn create_param_info(field: &Field) -> TokenStream {
::typst::eval::ParamInfo { ::typst::eval::ParamInfo {
name: #name, name: #name,
docs: #docs, docs: #docs,
cast: <#ty as ::typst::eval::Reflect>::describe(), input: <#ty as ::typst::eval::Reflect>::input(),
default: #default, default: #default,
positional: #positional, positional: #positional,
named: #named, named: #named,
@ -488,7 +516,7 @@ fn create_construct_impl(element: &Elem) -> TokenStream {
args: &mut ::typst::eval::Args, args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Content> { ) -> ::typst::diag::SourceResult<::typst::model::Content> {
let mut element = Self(::typst::model::Content::new( let mut element = Self(::typst::model::Content::new(
<Self as ::typst::model::Element>::func() <Self as ::typst::model::NativeElement>::elem()
)); ));
#(#handlers)* #(#handlers)*
Ok(element.0) Ok(element.0)

View File

@ -1,206 +1,340 @@
use super::*; use super::*;
use heck::ToKebabCase;
/// Expand the `#[func]` macro. /// Expand the `#[func]` macro.
pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> { pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
let func = prepare(stream, item)?; let func = parse(stream, item)?;
Ok(create(&func, item)) Ok(create(&func, item))
} }
/// Details about a function.
struct Func { struct Func {
name: String, name: String,
display: String, title: String,
category: String, scope: bool,
keywords: Option<String>, constructor: bool,
keywords: Vec<String>,
parent: Option<syn::Type>,
docs: String, docs: String,
vis: syn::Visibility, vis: syn::Visibility,
ident: Ident, ident: Ident,
ident_func: Ident, special: SpecialParams,
parent: Option<syn::Type>, params: Vec<Param>,
returns: syn::Type,
}
/// Special parameters provided by the runtime.
#[derive(Default)]
struct SpecialParams {
self_: Option<Param>,
vm: bool, vm: bool,
vt: bool, vt: bool,
args: bool, args: bool,
span: bool, span: bool,
params: Vec<Param>,
returns: syn::Type,
scope: Option<BlockWithReturn>,
} }
/// Details about a function parameter.
struct Param { struct Param {
name: String, binding: Binding,
docs: String,
external: bool,
named: bool,
variadic: bool,
default: Option<syn::Expr>,
ident: Ident, ident: Ident,
ty: syn::Type, ty: syn::Type,
name: String,
docs: String,
named: bool,
variadic: bool,
external: bool,
default: Option<syn::Expr>,
} }
fn prepare(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> { /// How a parameter is bound.
let sig = &item.sig; enum Binding {
/// Normal parameter.
Owned,
/// `&self`.
Ref,
/// `&mut self`.
RefMut,
}
let Parent(parent) = syn::parse2(stream)?; /// The `..` in `#[func(..)]`.
pub struct Meta {
pub scope: bool,
pub name: Option<String>,
pub title: Option<String>,
pub constructor: bool,
pub keywords: Vec<String>,
pub parent: Option<syn::Type>,
}
let mut vm = false; impl Parse for Meta {
let mut vt = false; fn parse(input: ParseStream) -> Result<Self> {
let mut args = false; Ok(Self {
let mut span = false; scope: parse_flag::<kw::scope>(input)?,
name: parse_string::<kw::name>(input)?,
title: parse_string::<kw::title>(input)?,
constructor: parse_flag::<kw::constructor>(input)?,
keywords: parse_string_array::<kw::keywords>(input)?,
parent: parse_key_value::<kw::parent, _>(input)?,
})
}
}
/// Parse details about the function from the fn item.
fn parse(stream: TokenStream, item: &syn::ItemFn) -> Result<Func> {
let meta: Meta = syn::parse2(stream)?;
let (name, title) =
determine_name_and_title(meta.name, meta.title, &item.sig.ident, None)?;
let docs = documentation(&item.attrs);
let mut special = SpecialParams::default();
let mut params = vec![]; let mut params = vec![];
for input in &sig.inputs { for input in &item.sig.inputs {
let syn::FnArg::Typed(typed) = input else { parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?;
println!("option a"); }
bail!(input, "self is not allowed here");
};
let syn::Pat::Ident(syn::PatIdent { let returns = match &item.sig.output {
by_ref: None, mutability: None, ident, .. syn::ReturnType::Default => parse_quote! { () },
}) = &*typed.pat syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
else { };
bail!(typed.pat, "expected identifier");
};
match ident.to_string().as_str() { if meta.parent.is_some() && meta.scope {
"vm" => vm = true, bail!(item, "scoped function cannot have a scope");
"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)?; Ok(Func {
} name,
title,
scope: meta.scope,
constructor: meta.constructor,
keywords: meta.keywords,
parent: meta.parent,
docs,
vis: item.vis.clone(),
ident: item.sig.ident.clone(),
special,
params,
returns,
})
}
/// Parse details about a functino parameter.
fn parse_param(
special: &mut SpecialParams,
params: &mut Vec<Param>,
parent: Option<&syn::Type>,
input: &syn::FnArg,
) -> Result<()> {
let typed = match input {
syn::FnArg::Receiver(recv) => {
let mut binding = Binding::Owned;
if recv.reference.is_some() {
if recv.mutability.is_some() {
binding = Binding::RefMut
} else {
binding = Binding::Ref
}
};
special.self_ = Some(Param {
binding,
ident: syn::Ident::new("self_", recv.self_token.span),
ty: match parent {
Some(ty) => ty.clone(),
None => bail!(recv, "explicit parent type required"),
},
name: "self".into(),
docs: documentation(&recv.attrs),
named: false,
variadic: false,
external: false,
default: None,
});
return Ok(());
}
syn::FnArg::Typed(typed) => typed,
};
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" => special.vm = true,
"vt" => special.vt = true,
"args" => special.args = true,
"span" => special.span = true,
_ => {
let mut attrs = typed.attrs.clone();
params.push(Param {
binding: Binding::Owned,
ident: ident.clone(),
ty: (*typed.ty).clone(),
name: ident.to_string().to_kebab_case(),
docs: documentation(&attrs),
named: has_attr(&mut attrs, "named"),
variadic: has_attr(&mut attrs, "variadic"),
external: has_attr(&mut attrs, "external"),
default: parse_attr(&mut attrs, "default")?.map(|expr| {
expr.unwrap_or_else(
|| parse_quote! { ::std::default::Default::default() },
)
}),
});
validate_attrs(&attrs)?;
} }
} }
let mut attrs = item.attrs.clone(); Ok(())
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)
} }
/// Produce the function's definition.
fn create(func: &Func, item: &syn::ItemFn) -> TokenStream { fn create(func: &Func, item: &syn::ItemFn) -> TokenStream {
let eval = quote! { ::typst::eval };
let Func { docs, vis, ident, .. } = func;
let item = rewrite_fn_item(item);
let ty = create_func_ty(func);
let data = create_func_data(func);
let creator = if ty.is_some() {
quote! {
impl #eval::NativeFunc for #ident {
fn data() -> &'static #eval::NativeFuncData {
static DATA: #eval::NativeFuncData = #data;
&DATA
}
}
}
} else {
let ident_data = quote::format_ident!("{ident}_data");
quote! {
#[doc(hidden)]
#vis fn #ident_data() -> &'static #eval::NativeFuncData {
static DATA: #eval::NativeFuncData = #data;
&DATA
}
}
};
quote! {
#[doc = #docs]
#[allow(dead_code)]
#item
#[doc(hidden)]
#ty
#creator
}
}
/// Create native function data for the function.
fn create_func_data(func: &Func) -> TokenStream {
let eval = quote! { ::typst::eval };
let Func { let Func {
name,
display,
category,
docs,
vis,
ident, ident,
ident_func, name,
title,
docs,
keywords,
returns, returns,
scope,
parent,
constructor,
.. ..
} = func; } = func;
let handlers = func let scope = if *scope {
.params quote! { <#ident as #eval::NativeScope>::scope() }
.iter() } else {
.filter(|param| !param.external) quote! { #eval::Scope::new() }
.map(create_param_parser); };
let args = func let closure = create_wrapper_closure(func);
.params let params = func.special.self_.iter().chain(&func.params).map(create_param_info);
.iter()
.filter(|param| !param.external)
.map(|param| &param.ident);
let parent = func.parent.as_ref().map(|ty| quote! { #ty:: }); let name = if *constructor {
let vm_ = func.vm.then(|| quote! { vm, }); quote! { <#parent as #eval::NativeType>::NAME }
let vt_ = func.vt.then(|| quote! { &mut vm.vt, }); } else {
let args_ = func.args.then(|| quote! { args.take(), }); quote! { #name }
let span_ = func.span.then(|| quote! { args.span, }); };
let wrapper = quote! {
|vm, args| { quote! {
let __typst_func = #parent #ident; #eval::NativeFuncData {
#(#handlers)* function: #closure,
let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_); name: #name,
::typst::eval::IntoResult::into_result(output, args.span) title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
scope: #eval::Lazy::new(|| #scope),
params: #eval::Lazy::new(|| ::std::vec![#(#params),*]),
returns: #eval::Lazy::new(|| <#returns as #eval::Reflect>::output()),
}
}
}
/// Create a type that shadows the function.
fn create_func_ty(func: &Func) -> Option<TokenStream> {
if func.parent.is_some() {
return None;
}
let Func { vis, ident, .. } = func;
Some(quote! {
#[doc(hidden)]
#[allow(non_camel_case_types)]
#vis enum #ident {}
})
}
/// Create the runtime-compatible wrapper closure that parses arguments.
fn create_wrapper_closure(func: &Func) -> TokenStream {
// These handlers parse the arguments.
let handlers = {
let func_handlers = func
.params
.iter()
.filter(|param| !param.external)
.map(create_param_parser);
let self_handler = func.special.self_.as_ref().map(create_param_parser);
quote! {
#self_handler
#(#func_handlers)*
} }
}; };
let mut item = item.clone(); // This is the actual function call.
item.attrs.clear(); let call = {
let self_ = func
let inputs = item.sig.inputs.iter().cloned().filter_map(|mut input| { .special
if let syn::FnArg::Typed(typed) = &mut input { .self_
if typed.attrs.iter().any(|attr| attr.path().is_ident("external")) { .as_ref()
return None; .map(bind)
} .map(|tokens| quote! { #tokens, });
typed.attrs.clear(); let vm_ = func.special.vm.then(|| quote! { vm, });
let vt_ = func.special.vt.then(|| quote! { &mut vm.vt, });
let args_ = func.special.args.then(|| quote! { args.take(), });
let span_ = func.special.span.then(|| quote! { args.span, });
let forwarded = func.params.iter().filter(|param| !param.external).map(bind);
quote! {
__typst_func(#self_ #vm_ #vt_ #args_ #span_ #(#forwarded,)*)
} }
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());
// This is the whole wrapped closure.
let ident = &func.ident;
let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
quote! { quote! {
#[doc(hidden)] |vm, args| {
#vis fn #ident_func() -> &'static ::typst::eval::NativeFunc { let __typst_func = #parent #ident;
static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc { #handlers
func: #wrapper, let output = #call;
info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo { ::typst::eval::IntoResult::into_result(output, args.span)
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
} }
} }
@ -226,7 +360,7 @@ fn create_param_info(param: &Param) -> TokenStream {
::typst::eval::ParamInfo { ::typst::eval::ParamInfo {
name: #name, name: #name,
docs: #docs, docs: #docs,
cast: <#ty as ::typst::eval::Reflect>::describe(), input: <#ty as ::typst::eval::Reflect>::input(),
default: #default, default: #default,
positional: #positional, positional: #positional,
named: #named, named: #named,
@ -258,10 +392,29 @@ fn create_param_parser(param: &Param) -> TokenStream {
quote! { let mut #ident: #ty = #value; } quote! { let mut #ident: #ty = #value; }
} }
struct Parent(Option<syn::Type>); /// Apply the binding to a parameter.
fn bind(param: &Param) -> TokenStream {
impl Parse for Parent { let ident = &param.ident;
fn parse(input: ParseStream) -> Result<Self> { match param.binding {
Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None })) Binding::Owned => quote! { #ident },
Binding::Ref => quote! { &#ident },
Binding::RefMut => quote! { &mut #ident },
} }
} }
/// Removes attributes and so on from the native function.
fn rewrite_fn_item(item: &syn::ItemFn) -> syn::ItemFn {
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)
});
let mut item = item.clone();
item.attrs.clear();
item.sig.inputs = parse_quote! { #(#inputs),* };
item
}

View File

@ -4,10 +4,12 @@ extern crate proc_macro;
#[macro_use] #[macro_use]
mod util; mod util;
mod castable; mod cast;
mod element; mod elem;
mod func; mod func;
mod scope;
mod symbols; mod symbols;
mod ty;
use proc_macro::TokenStream as BoundaryStream; use proc_macro::TokenStream as BoundaryStream;
use proc_macro2::TokenStream; use proc_macro2::TokenStream;
@ -19,7 +21,79 @@ use syn::{parse_quote, DeriveInput, Ident, Result, Token};
use self::util::*; use self::util::*;
/// Turns a function into a `NativeFunc`. /// Makes a native Rust function usable as a Typst function.
///
/// This implements `NativeFunction` for a freshly generated type with the same
/// name as a function. (In Rust, functions and types live in separate
/// namespace, so both can coexist.)
///
/// If the function is in an impl block annotated with `#[scope]`, things work a
/// bit differently because the no type can be generated within the impl block.
/// In that case, a function named `{name}_data` that returns `&'static
/// NativeFuncData` is generated. You typically don't need to interact with this
/// function though because the `#[scope]` macro hooks everything up for you.
///
/// ```ignore
/// /// Doubles an integer.
/// #[func]
/// fn double(x: i64) -> i64 {
/// 2 * x
/// }
/// ```
///
/// # Properties
/// You can customize some properties of the resulting function:
/// - `scope`: Indicates that the function has an associated scope defined by
/// the `#[scope]` macro.
/// - `name`: The functions's normal name (e.g. `min`). Defaults to the Rust
/// name in kebab-case.
/// - `title`: The functions's title case name (e.g. `Minimum`). Defaults to the
/// normal name in title case.
///
/// # Arguments
/// By default, function arguments are positional and required. You can use
/// various attributes to configure their parsing behaviour:
///
/// - `#[named]`: Makes the argument named and optional. The argument type must
/// either be `Option<_>` _or_ the `#[default]` attribute must be used. (If
/// it's both `Option<_>` and `#[default]`, then the argument can be specified
/// as `none` in Typst).
/// - `#[default]`: Specifies the default value of the argument as
/// `Default::default()`.
/// - `#[default(..)]`: Specifies the default value of the argument as `..`.
/// - `#[variadic]`: Parses a variable number of arguments. The argument type
/// must be `Vec<_>`.
/// - `#[external]`: The argument appears in documentation, but is otherwise
/// ignored. Can be useful if you want to do something manually for more
/// flexibility.
///
/// Defaults can be specified for positional and named arguments. This is in
/// contrast to user-defined functions which currently cannot have optional
/// positional arguments (except through argument sinks).
///
/// In the example below, we define a `min` function that could be called as
/// `min(1, 2, 3, default: 0)` in Typst.
///
/// ```ignore
/// /// Determines the minimum of a sequence of values.
/// #[func(title = "Minimum")]
/// fn min(
/// /// The values to extract the minimum from.
/// #[variadic]
/// values: Vec<i64>,
/// /// A default value to return if there are no values.
/// #[named]
/// #[default(0)]
/// default: i64,
/// ) -> i64 {
/// self.values.iter().min().unwrap_or(default)
/// }
/// ```
///
/// As you can see, arguments can also have doc-comments, which will be rendered
/// in the documentation. The first line of documentation should be concise and
/// self-contained as it is the designated short description, which is used in
/// overviews in the documentation (and for autocompletion).
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemFn); let item = syn::parse_macro_input!(item as syn::ItemFn);
@ -28,33 +102,230 @@ pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
.into() .into()
} }
/// Turns a type into an `Element`. /// Makes a native Rust type usable as a Typst type.
///
/// This implements `NativeType` for the given type.
///
/// ```ignore
/// /// A sequence of codepoints.
/// #[ty(scope, title = "String")]
/// struct Str(EcoString);
///
/// #[scope]
/// impl Str {
/// ...
/// }
/// ```
///
/// # Properties
/// You can customize some properties of the resulting type:
/// - `scope`: Indicates that the type has an associated scope defined by the
/// `#[scope]` macro
/// - `name`: The type's normal name (e.g. `str`). Defaults to the Rust name in
/// kebab-case.
/// - `title`: The type's title case name (e.g. `String`). Defaults to the
/// normal name in title case.
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream { pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemStruct); let item = syn::parse_macro_input!(item as syn::Item);
element::element(stream.into(), &item) ty::ty(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum. /// Makes a native Rust type usable as a Typst element.
#[proc_macro_derive(Cast, attributes(string))] ///
pub fn derive_cast(item: BoundaryStream) -> BoundaryStream { /// This implements `NativeElement` for the given type.
let item = syn::parse_macro_input!(item as DeriveInput); ///
castable::derive_cast(&item) /// ```
/// /// A section heading.
/// #[elem(Show, Count)]
/// struct HeadingElem {
/// /// The logical nesting depth of the heading, starting from one.
/// #[default(NonZeroUsize::ONE)]
/// level: NonZeroUsize,
///
/// /// The heading's title.
/// #[required]
/// body: Content,
/// }
/// ```
///
/// # Properties
/// You can customize some properties of the resulting type:
/// - `scope`: Indicates that the type has an associated scope defined by the
/// `#[scope]` macro
/// - `name`: The element's normal name (e.g. `str`). Defaults to the Rust name
/// in kebab-case.
/// - `title`: The type's title case name (e.g. `String`). Defaults to the long
/// name in title case.
/// - The remaining entries in the `elem` macros list are traits the element
/// is capable of. These can be dynamically accessed.
///
/// # Fields
/// By default, element fields are named and optional (and thus settable). You
/// can use various attributes to configure their parsing behaviour:
///
/// - `#[positional]`: Makes the argument positional (but still optional).
/// - `#[required]`: Makes the argument positional and required.
/// - `#[default(..)]`: Specifies the default value of the argument as `..`.
/// - `#[variadic]`: Parses a variable number of arguments. The field type must
/// be `Vec<_>`. The field will be exposed as an array.
/// - `#[parse({ .. })]`: A block of code that parses the field manually.
///
/// In addition that there are a number of attributes that configure other
/// aspects of the field than the parsing behaviour.
/// - `#[resolve]`: When accessing the field, it will be automatically
/// resolved through the `Resolve` trait. This, for instance, turns `Length`
/// into `Abs`. It's just convenient.
/// - `#[fold]`: When there are multiple set rules for the field, all values
/// are folded together into one. E.g. `set rect(stroke: 2pt)` and
/// `set rect(stroke: red)` are combined into the equivalent of
/// `set rect(stroke: 2pt + red)` instead of having `red` override `2pt`.
/// - `#[internal]`: The field does not appear in the documentation.
/// - `#[external]`: The field appears in the documentation, but is otherwise
/// ignored. Can be useful if you want to do something manually for more
/// flexibility.
/// - `#[synthesized]`: The field cannot be specified in a constructor or set
/// rule. Instead, it is added to an element before its show rule runs
/// through the `Synthesize` trait.
#[proc_macro_attribute]
pub fn elem(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemStruct);
elem::elem(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Provides an associated scope to a native function, type, or element.
///
/// This implements `NativeScope` for the function's shadow type, the type, or
/// the element.
///
/// The implementation block can contain four kinds of items:
/// - constants, which will be defined through `scope.define`
/// - functions, which will be defined through `scope.define_func`
/// - types, which will be defined through `scope.define_type`
/// - elements, which will be defined through `scope.define_elem`
///
/// ```ignore
/// #[func(scope)]
/// fn name() { .. }
///
/// #[scope]
/// impl name {
/// /// A simple constant.
/// const VAL: u32 = 0;
///
/// /// A function.
/// #[func]
/// fn foo() -> EcoString {
/// "foo!".into()
/// }
///
/// /// A type.
/// type Brr;
///
/// /// An element.
/// #[elem]
/// type NiceElem;
/// }
///
/// #[ty]
/// struct Brr;
///
/// #[elem]
/// struct NiceElem {}
/// ```
#[proc_macro_attribute]
pub fn scope(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::Item);
scope::scope(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
/// Implements `Reflect`, `FromValue`, and `IntoValue` for a type. /// Implements `Reflect`, `FromValue`, and `IntoValue` for a type.
///
/// - `Reflect` makes Typst's runtime aware of the type's characteristics.
/// It's important for autocompletion, error messages, etc.
/// - `FromValue` defines how to cast from a value into this type.
/// - `IntoValue` defines how to cast fromthis type into a value.
///
/// ```ignore
/// /// An integer between 0 and 13.
/// struct CoolInt(u8);
///
/// cast! {
/// CoolInt,
///
/// // Defines how to turn a `CoolInt` into a value.
/// self => self.0.into_value(),
///
/// // Defines "match arms" of types that can be cast into a `CoolInt`.
/// // These types needn't be value primitives, they can themselves use
/// // `cast!`.
/// v: bool => Self(v as u8),
/// v: i64 => if matches!(v, 0..=13) {
/// Self(v as u8)
/// } else {
/// bail!("integer is not nice :/")
/// },
/// }
/// ```
#[proc_macro] #[proc_macro]
pub fn cast(stream: BoundaryStream) -> BoundaryStream { pub fn cast(stream: BoundaryStream) -> BoundaryStream {
castable::cast(stream.into()) cast::cast(stream.into())
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum.
///
/// The enum will become castable from kebab-case strings. The doc-comments will
/// become user-facing documentation for each variant. The `#[string]` attribute
/// can be used to override the string corresponding to a variant.
///
/// ```ignore
/// /// A stringy enum of options.
/// #[derive(Cast)]
/// enum Niceness {
/// /// Clearly nice (parses from `"nice"`).
/// Nice,
/// /// Not so nice (parses from `"not-nice"`).
/// NotNice,
/// /// Very much not nice (parses from `"❌"`).
/// #[string("❌")]
/// Unnice,
/// }
/// ```
#[proc_macro_derive(Cast, attributes(string))]
pub fn derive_cast(item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as DeriveInput);
cast::derive_cast(item)
.unwrap_or_else(|err| err.to_compile_error()) .unwrap_or_else(|err| err.to_compile_error())
.into() .into()
} }
/// Defines a list of `Symbol`s. /// Defines a list of `Symbol`s.
///
/// ```ignore
/// const EMOJI: &[(&str, Symbol)] = symbols! {
/// // A plain symbol without modifiers.
/// abacus: '🧮',
///
/// // A symbol with a modifierless default and one modifier.
/// alien: ['👽', monster: '👾'],
///
/// // A symbol where each variant has a modifier. The first one will be
/// // the default.
/// clock: [one: '🕐', two: '🕑', ...],
/// }
/// ```
///
/// _Note:_ While this could use `macro_rules!` instead of a proc-macro, it was
/// horribly slow in rust-analyzer. The underlying cause might be
/// [this issue](https://github.com/rust-lang/rust-analyzer/issues/11108).
#[proc_macro] #[proc_macro]
pub fn symbols(stream: BoundaryStream) -> BoundaryStream { pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
symbols::symbols(stream.into()) symbols::symbols(stream.into())

View File

@ -0,0 +1,153 @@
use heck::ToKebabCase;
use super::*;
/// Expand the `#[scope]` macro.
pub fn scope(_: TokenStream, item: syn::Item) -> Result<TokenStream> {
let syn::Item::Impl(mut item) = item else {
bail!(item, "expected module or impl item");
};
let eval = quote! { ::typst::eval };
let self_ty = &item.self_ty;
let mut definitions = vec![];
let mut constructor = quote! { None };
for child in &mut item.items {
let def = match child {
syn::ImplItem::Const(item) => handle_const(self_ty, item)?,
syn::ImplItem::Fn(item) => match handle_fn(self_ty, item)? {
FnKind::Member(tokens) => tokens,
FnKind::Constructor(tokens) => {
constructor = tokens;
continue;
}
},
syn::ImplItem::Verbatim(item) => handle_type_or_elem(item)?,
_ => bail!(child, "unexpected item in scope"),
};
definitions.push(def);
}
item.items.retain(|item| !matches!(item, syn::ImplItem::Verbatim(_)));
let mut base = quote! { #item };
if let syn::Type::Path(syn::TypePath { path, .. }) = self_ty.as_ref() {
if let Some(ident) = path.get_ident() {
if is_primitive(ident) {
base = rewrite_primitive_base(&item, ident);
}
}
}
Ok(quote! {
#base
impl #eval::NativeScope for #self_ty {
fn constructor() -> ::std::option::Option<&'static #eval::NativeFuncData> {
#constructor
}
fn scope() -> #eval::Scope {
let mut scope = #eval::Scope::deduplicating();
#(#definitions;)*
scope
}
}
})
}
/// Process a const item and returns its definition.
fn handle_const(self_ty: &syn::Type, item: &syn::ImplItemConst) -> Result<TokenStream> {
let ident = &item.ident;
let name = ident.to_string().to_kebab_case();
Ok(quote! { scope.define(#name, #self_ty::#ident) })
}
/// Process a type item.
fn handle_type_or_elem(item: &TokenStream) -> Result<TokenStream> {
let item: BareType = syn::parse2(item.clone())?;
let ident = &item.ident;
let define = if item.attrs.iter().any(|attr| attr.path().is_ident("elem")) {
quote! { define_elem }
} else {
quote! { define_type }
};
Ok(quote! { scope.#define::<#ident>() })
}
/// Process a function, return its definition, and register it as a constructor
/// if applicable.
fn handle_fn(self_ty: &syn::Type, item: &mut syn::ImplItemFn) -> Result<FnKind> {
let Some(attr) = item.attrs.iter_mut().find(|attr| attr.meta.path().is_ident("func"))
else {
bail!(item, "scope function is missing #[func] attribute");
};
let ident_data = quote::format_ident!("{}_data", item.sig.ident);
match &mut attr.meta {
syn::Meta::Path(_) => {
*attr = parse_quote! { #[func(parent = #self_ty)] };
}
syn::Meta::List(list) => {
let tokens = &list.tokens;
let meta: super::func::Meta = syn::parse2(tokens.clone())?;
list.tokens = quote! { #tokens, parent = #self_ty };
if meta.constructor {
return Ok(FnKind::Constructor(quote! { Some(#self_ty::#ident_data()) }));
}
}
syn::Meta::NameValue(_) => bail!(attr.meta, "invalid func attribute"),
}
Ok(FnKind::Member(quote! { scope.define_func_with_data(#self_ty::#ident_data()) }))
}
enum FnKind {
Constructor(TokenStream),
Member(TokenStream),
}
/// Whether the identifier describes a primitive type.
fn is_primitive(ident: &syn::Ident) -> bool {
ident == "bool" || ident == "i64" || ident == "f64"
}
/// Rewrite an impl block for a primitive into a trait + trait impl.
fn rewrite_primitive_base(item: &syn::ItemImpl, ident: &syn::Ident) -> TokenStream {
let mut sigs = vec![];
let mut items = vec![];
for sub in &item.items {
let syn::ImplItem::Fn(mut func) = sub.clone() else { continue };
func.vis = syn::Visibility::Inherited;
items.push(func.clone());
let mut sig = func.sig;
let inputs = sig.inputs.iter().cloned().map(|mut input| {
if let syn::FnArg::Typed(typed) = &mut input {
typed.attrs.clear();
}
input
});
sig.inputs = parse_quote! { #(#inputs),* };
let ident_data = quote::format_ident!("{}_data", sig.ident);
sigs.push(quote! { #sig; });
sigs.push(quote! {
fn #ident_data() -> &'static ::typst::eval::NativeFuncData;
});
}
let ident_ext = quote::format_ident!("{ident}Ext");
let self_ty = &item.self_ty;
quote! {
trait #ident_ext {
#(#sigs)*
}
impl #ident_ext for #self_ty {
#(#items)*
}
}
}

View File

@ -7,7 +7,7 @@ pub fn symbols(stream: TokenStream) -> Result<TokenStream> {
let pairs = list.iter().map(|symbol| { let pairs = list.iter().map(|symbol| {
let name = symbol.name.to_string(); let name = symbol.name.to_string();
let kind = match &symbol.kind { let kind = match &symbol.kind {
Kind::Single(c) => quote! { typst::eval::Symbol::new(#c), }, Kind::Single(c) => quote! { typst::eval::Symbol::single(#c), },
Kind::Multiple(variants) => { Kind::Multiple(variants) => {
let variants = variants.iter().map(|variant| { let variants = variants.iter().map(|variant| {
let name = &variant.name; let name = &variant.name;

View File

@ -0,0 +1,113 @@
use syn::Attribute;
use super::*;
/// Expand the `#[ty]` macro.
pub fn ty(stream: TokenStream, item: syn::Item) -> Result<TokenStream> {
let meta: Meta = syn::parse2(stream)?;
let bare: BareType;
let (ident, attrs, keep) = match &item {
syn::Item::Struct(item) => (&item.ident, &item.attrs, true),
syn::Item::Type(item) => (&item.ident, &item.attrs, true),
syn::Item::Enum(item) => (&item.ident, &item.attrs, true),
syn::Item::Verbatim(item) => {
bare = syn::parse2(item.clone())?;
(&bare.ident, &bare.attrs, false)
}
_ => bail!(item, "invalid type item"),
};
let ty = parse(meta, ident.clone(), attrs)?;
Ok(create(&ty, keep.then_some(&item)))
}
/// Holds all relevant parsed data about a type.
struct Type {
ident: Ident,
name: String,
long: String,
scope: bool,
title: String,
docs: String,
keywords: Vec<String>,
}
/// The `..` in `#[ty(..)]`.
struct Meta {
scope: bool,
name: Option<String>,
title: Option<String>,
keywords: Vec<String>,
}
impl Parse for Meta {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
scope: parse_flag::<kw::scope>(input)?,
name: parse_string::<kw::name>(input)?,
title: parse_string::<kw::title>(input)?,
keywords: parse_string_array::<kw::keywords>(input)?,
})
}
}
/// Parse details about the type from its definition.
fn parse(meta: Meta, ident: Ident, attrs: &[Attribute]) -> Result<Type> {
let docs = documentation(attrs);
let (name, title) = determine_name_and_title(meta.name, meta.title, &ident, None)?;
let long = title.to_lowercase();
Ok(Type {
ident,
name,
long,
scope: meta.scope,
keywords: meta.keywords,
title,
docs,
})
}
/// Produce the output of the macro.
fn create(ty: &Type, item: Option<&syn::Item>) -> TokenStream {
let eval = quote! { ::typst::eval };
let Type {
ident, name, long, title, docs, keywords, scope, ..
} = ty;
let constructor = if *scope {
quote! { <#ident as #eval::NativeScope>::constructor() }
} else {
quote! { None }
};
let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
};
let data = quote! {
#eval::NativeTypeData {
name: #name,
long_name: #long,
title: #title,
docs: #docs,
keywords: &[#(#keywords),*],
constructor: #eval::Lazy::new(|| #constructor),
scope: #eval::Lazy::new(|| #scope),
}
};
quote! {
#item
impl #eval::NativeType for #ident {
const NAME: &'static str = #name;
fn data() -> &'static #eval::NativeTypeData {
static DATA: #eval::NativeTypeData = #data;
&DATA
}
}
}
}

View File

@ -1,5 +1,7 @@
use heck::ToKebabCase; use heck::{ToKebabCase, ToTitleCase};
use quote::ToTokens; use quote::ToTokens;
use syn::token::Token;
use syn::Attribute;
use super::*; use super::*;
@ -19,25 +21,27 @@ macro_rules! bail {
}; };
} }
/// For parsing attributes of the form: /// Extract documentation comments from an attribute list.
/// #[attr( pub fn documentation(attrs: &[syn::Attribute]) -> String {
/// statement; let mut doc = String::new();
/// statement;
/// returned_expression
/// )]
pub struct BlockWithReturn {
pub prefix: Vec<syn::Stmt>,
pub expr: syn::Stmt,
}
impl Parse for BlockWithReturn { // Parse doc comments.
fn parse(input: ParseStream) -> Result<Self> { for attr in attrs {
let mut stmts = syn::Block::parse_within(input)?; if let syn::Meta::NameValue(meta) = &attr.meta {
let Some(expr) = stmts.pop() else { if meta.path.is_ident("doc") {
return Err(input.error("expected at least one expression")); if let syn::Expr::Lit(lit) = &meta.value {
}; if let syn::Lit::Str(string) = &lit.lit {
Ok(Self { prefix: stmts, expr }) let full = string.value();
let line = full.strip_prefix(' ').unwrap_or(&full);
doc.push_str(line);
doc.push('\n');
}
}
}
}
} }
doc.trim().into()
} }
/// Whether an attribute list has a specified attribute. /// Whether an attribute list has a specified attribute.
@ -83,58 +87,6 @@ pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
Ok(()) 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. /// Quotes an option literally.
pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream { pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
if let Some(value) = option { if let Some(value) = option {
@ -143,3 +95,156 @@ pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
quote! { None } quote! { None }
} }
} }
/// Parse a metadata key-value pair, separated by `=`.
pub fn parse_key_value<K: Token + Default + Parse, V: Parse>(
input: ParseStream,
) -> Result<Option<V>> {
if !input.peek(|_| K::default()) {
return Ok(None);
}
let _: K = input.parse()?;
let _: Token![=] = input.parse()?;
let value: V = input.parse::<V>()?;
eat_comma(input);
Ok(Some(value))
}
/// Parse a metadata key-array pair, separated by `=`.
pub fn parse_key_value_array<K: Token + Default + Parse, V: Parse>(
input: ParseStream,
) -> Result<Vec<V>> {
Ok(parse_key_value::<K, Array<V>>(input)?.map_or(vec![], |array| array.0))
}
/// Parse a metadata key-string pair, separated by `=`.
pub fn parse_string<K: Token + Default + Parse>(
input: ParseStream,
) -> Result<Option<String>> {
Ok(parse_key_value::<K, syn::LitStr>(input)?.map(|s| s.value()))
}
/// Parse a metadata key-string pair, separated by `=`.
pub fn parse_string_array<K: Token + Default + Parse>(
input: ParseStream,
) -> Result<Vec<String>> {
Ok(parse_key_value_array::<K, syn::LitStr>(input)?
.into_iter()
.map(|lit| lit.value())
.collect())
}
/// Parse a metadata flag that can be present or not.
pub fn parse_flag<K: Token + Default + Parse>(input: ParseStream) -> Result<bool> {
if input.peek(|_| K::default()) {
let _: K = input.parse()?;
eat_comma(input);
return Ok(true);
}
Ok(false)
}
/// Parse a comma if there is one.
pub fn eat_comma(input: ParseStream) {
if input.peek(Token![,]) {
let _: Token![,] = input.parse().unwrap();
}
}
/// Determine the normal and title case name of a function, type, or element.
pub fn determine_name_and_title(
specified_name: Option<String>,
specified_title: Option<String>,
ident: &syn::Ident,
trim: Option<fn(&str) -> &str>,
) -> Result<(String, String)> {
let name = {
let trim = trim.unwrap_or(|s| s);
let default = trim(&ident.to_string()).to_kebab_case();
if specified_name.as_ref() == Some(&default) {
bail!(ident, "name was specified unncessarily");
}
specified_name.unwrap_or(default)
};
let title = {
let default = name.to_title_case();
if specified_title.as_ref() == Some(&default) {
bail!(ident, "title was specified unncessarily");
}
specified_title.unwrap_or(default)
};
Ok((name, title))
}
/// A generic parseable array.
struct Array<T>(Vec<T>);
impl<T: Parse> Parse for Array<T> {
fn parse(input: ParseStream) -> Result<Self> {
let content;
syn::bracketed!(content in input);
let mut elems = Vec::new();
while !content.is_empty() {
let first: T = content.parse()?;
elems.push(first);
if !content.is_empty() {
let _: Token![,] = content.parse()?;
}
}
Ok(Self(elems))
}
}
/// 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 })
}
}
pub mod kw {
syn::custom_keyword!(name);
syn::custom_keyword!(title);
syn::custom_keyword!(scope);
syn::custom_keyword!(constructor);
syn::custom_keyword!(keywords);
syn::custom_keyword!(parent);
}
/// Parse a bare `type Name;` item.
pub struct BareType {
pub attrs: Vec<Attribute>,
pub type_token: Token![type],
pub ident: Ident,
pub semi_token: Token![;],
}
impl Parse for BareType {
fn parse(input: ParseStream) -> Result<Self> {
Ok(BareType {
attrs: input.call(Attribute::parse_outer)?,
type_token: input.parse()?,
ident: input.parse()?,
semi_token: input.parse()?,
})
}
}