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::*;
/// 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 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()
} else {
kebab_case(&variant.ident)
variant.ident.to_string().to_kebab_case()
};
variants.push(Variant {
@ -62,20 +64,25 @@ struct Variant {
/// Expand the `cast!` macro.
pub fn cast(stream: TokenStream) -> Result<TokenStream> {
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
let eval = quote! { ::typst::eval };
let input: CastInput = syn::parse2(stream)?;
let ty = &input.ty;
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 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! {
impl #eval::Reflect for #ty {
fn describe() -> #eval::CastInfo {
#describe_body
fn input() -> #eval::CastInfo {
#input_body
}
fn output() -> #eval::CastInfo {
#output_body
}
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! {
impl #eval::IntoValue for #ty {
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! {
impl #eval::FromValue for #ty {
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! {
#reflect
#into_value
#from_value
#ty
})
}
/// The input to `cast!`.
struct CastInput {
ty: syn::Type,
name: Option<syn::LitStr>,
dynamic: bool,
into_value: Option<syn::Expr>,
from_value: Punctuated<Cast, Token![,]>,
}
impl Parse for CastInput {
fn parse(input: ParseStream) -> Result<Self> {
let ty;
let mut name = None;
let mut dynamic = false;
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()?;
dynamic = true;
}
let ty = input.parse()?;
let _: syn::Token![,] = input.parse()?;
let mut into_value = None;
let mut to_value = None;
if input.peek(syn::Token![self]) {
let _: syn::Token![self] = input.parse()?;
let _: syn::Token![=>] = input.parse()?;
into_value = Some(input.parse()?);
to_value = Some(input.parse()?);
let _: syn::Token![,] = input.parse()?;
}
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! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
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![];
for cast in &input.from_value {
@ -256,14 +250,14 @@ fn create_describe_body(input: &CastInput) -> TokenStream {
}
}
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! {
::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 {
if let Some(expr) = &input.into_value {
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! {
if let ::typst::eval::Value::Dyn(dynamic) = &value {
if let Some(concrete) = dynamic.downcast::<Self>() {

View file

@ -1,152 +1,175 @@
use heck::ToKebabCase;
use super::*;
/// Expand the `#[element]` macro.
pub fn element(stream: TokenStream, body: &syn::ItemStruct) -> Result<TokenStream> {
let element = prepare(stream, body)?;
/// Expand the `#[elem]` macro.
pub fn elem(stream: TokenStream, body: syn::ItemStruct) -> Result<TokenStream> {
let element = parse(stream, &body)?;
Ok(create(&element))
}
/// Details about an element.
struct Elem {
name: String,
display: String,
category: String,
keywords: Option<String>,
title: String,
scope: bool,
keywords: Vec<String>,
docs: String,
vis: syn::Visibility,
ident: Ident,
capable: Vec<Ident>,
capabilities: Vec<Ident>,
fields: Vec<Field>,
scope: Option<BlockWithReturn>,
}
/// Details about an element 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_in: Ident,
with_ident: Ident,
push_ident: Ident,
set_ident: Ident,
vis: syn::Visibility,
ty: 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 {
/// Whether the field is present on every instance of the element.
fn inherent(&self) -> bool {
self.required || self.variadic
}
/// Whether the field can be used with set rules.
fn settable(&self) -> bool {
!self.inherent()
}
}
/// Preprocess the element's definition.
fn prepare(stream: TokenStream, body: &syn::ItemStruct) -> Result<Elem> {
/// The `..` in `#[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 {
bail!(body, "expected named fields");
};
let fields = named.named.iter().map(parse_field).collect::<Result<_>>()?;
let mut fields = vec![];
for field in &named.named {
let Some(ident) = field.ident.clone() else {
bail!(field, "expected named field");
};
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,
Ok(Elem {
name,
title,
scope: meta.scope,
keywords: meta.keywords,
docs,
vis: body.vis.clone(),
ident: body.ident.clone(),
capable,
capabilities: meta.capabilities,
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)?;
Ok(element)
Ok(field)
}
/// Produce the element's definition.
@ -166,13 +189,13 @@ fn create(element: &Elem) -> TokenStream {
// Trait implementations.
let element_impl = create_pack_impl(element);
let construct_impl = element
.capable
.capabilities
.iter()
.all(|capability| capability != "Construct")
.then(|| create_construct_impl(element));
let set_impl = create_set_impl(element);
let locatable_impl = element
.capable
.capabilities
.iter()
.any(|capability| capability == "Locatable")
.then(|| quote! { impl ::typst::model::Locatable for #ident {} });
@ -231,7 +254,7 @@ fn create_new_func(element: &Elem) -> TokenStream {
/// Create a new element.
pub fn new(#(#params),*) -> Self {
Self(::typst::model::Content::new(
<Self as ::typst::model::Element>::func()
<Self as ::typst::model::NativeElement>::elem()
))
#(#builder_calls)*
}
@ -285,7 +308,7 @@ fn create_style_chain_access(field: &Field, inherent: TokenStream) -> TokenStrea
quote! {
styles.#getter::<#ty>(
<Self as ::typst::model::Element>::func(),
<Self as ::typst::model::NativeElement>::elem(),
#name,
#inherent,
|| #default,
@ -325,7 +348,7 @@ fn create_set_field_method(field: &Field) -> TokenStream {
#[doc = #doc]
#vis fn #set_ident(#ident: #ty) -> ::typst::model::Style {
::typst::model::Style::Property(::typst::model::Property::new(
<Self as ::typst::model::Element>::func(),
<Self as ::typst::model::NativeElement>::elem(),
#name,
#ident,
))
@ -335,49 +358,54 @@ fn create_set_field_method(field: &Field) -> TokenStream {
/// Create the element's `Pack` implementation.
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 infos = element
let params = element
.fields
.iter()
.filter(|field| !field.internal && !field.synthesized)
.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! {
impl ::typst::model::Element for #ident {
fn pack(self) -> ::typst::model::Content {
impl #model::NativeElement for #ident {
fn data() -> &'static #model::NativeElementData {
static DATA: #model::NativeElementData = #data;
&DATA
}
fn pack(self) -> #model::Content {
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)].
content.is::<Self>().then(|| unsafe {
::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.
fn create_vtable_func(element: &Elem) -> TokenStream {
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| {
quote! {
if id == ::std::any::TypeId::of::<dyn #capability>() {
@ -399,7 +427,7 @@ fn create_vtable_func(element: &Elem) -> TokenStream {
quote! {
|id| {
let null = Self(::typst::model::Content::new(
<#ident as ::typst::model::Element>::func()
<#ident as ::typst::model::NativeElement>::elem()
));
#(#checks)*
None
@ -441,7 +469,7 @@ fn create_param_info(field: &Field) -> TokenStream {
::typst::eval::ParamInfo {
name: #name,
docs: #docs,
cast: <#ty as ::typst::eval::Reflect>::describe(),
input: <#ty as ::typst::eval::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
@ -488,7 +516,7 @@ fn create_construct_impl(element: &Elem) -> TokenStream {
args: &mut ::typst::eval::Args,
) -> ::typst::diag::SourceResult<::typst::model::Content> {
let mut element = Self(::typst::model::Content::new(
<Self as ::typst::model::Element>::func()
<Self as ::typst::model::NativeElement>::elem()
));
#(#handlers)*
Ok(element.0)

View file

@ -1,206 +1,340 @@
use super::*;
use heck::ToKebabCase;
/// Expand the `#[func]` macro.
pub fn func(stream: TokenStream, item: &syn::ItemFn) -> Result<TokenStream> {
let func = prepare(stream, item)?;
let func = parse(stream, item)?;
Ok(create(&func, item))
}
/// Details about a function.
struct Func {
name: String,
display: String,
category: String,
keywords: Option<String>,
title: String,
scope: bool,
constructor: bool,
keywords: Vec<String>,
parent: Option<syn::Type>,
docs: String,
vis: syn::Visibility,
ident: Ident,
ident_func: Ident,
parent: Option<syn::Type>,
special: SpecialParams,
params: Vec<Param>,
returns: syn::Type,
}
/// Special parameters provided by the runtime.
#[derive(Default)]
struct SpecialParams {
self_: Option<Param>,
vm: bool,
vt: bool,
args: bool,
span: bool,
params: Vec<Param>,
returns: syn::Type,
scope: Option<BlockWithReturn>,
}
/// Details about a function parameter.
struct Param {
name: String,
docs: String,
external: bool,
named: bool,
variadic: bool,
default: Option<syn::Expr>,
binding: Binding,
ident: Ident,
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> {
let sig = &item.sig;
/// How a parameter is bound.
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;
let mut vt = false;
let mut args = false;
let mut span = false;
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)?,
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![];
for input in &sig.inputs {
let syn::FnArg::Typed(typed) = input else {
println!("option a");
bail!(input, "self is not allowed here");
};
for input in &item.sig.inputs {
parse_param(&mut special, &mut params, meta.parent.as_ref(), input)?;
}
let syn::Pat::Ident(syn::PatIdent {
by_ref: None, mutability: None, ident, ..
}) = &*typed.pat
else {
bail!(typed.pat, "expected identifier");
};
let returns = match &item.sig.output {
syn::ReturnType::Default => parse_quote! { () },
syn::ReturnType::Type(_, ty) => ty.as_ref().clone(),
};
match ident.to_string().as_str() {
"vm" => vm = true,
"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(),
});
if meta.parent.is_some() && meta.scope {
bail!(item, "scoped function cannot have a scope");
}
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();
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)
Ok(())
}
/// Produce the function's definition.
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 {
name,
display,
category,
docs,
vis,
ident,
ident_func,
name,
title,
docs,
keywords,
returns,
scope,
parent,
constructor,
..
} = func;
let handlers = func
.params
.iter()
.filter(|param| !param.external)
.map(create_param_parser);
let scope = if *scope {
quote! { <#ident as #eval::NativeScope>::scope() }
} else {
quote! { #eval::Scope::new() }
};
let args = func
.params
.iter()
.filter(|param| !param.external)
.map(|param| &param.ident);
let closure = create_wrapper_closure(func);
let params = func.special.self_.iter().chain(&func.params).map(create_param_info);
let parent = func.parent.as_ref().map(|ty| quote! { #ty:: });
let vm_ = func.vm.then(|| quote! { vm, });
let vt_ = func.vt.then(|| quote! { &mut vm.vt, });
let args_ = func.args.then(|| quote! { args.take(), });
let span_ = func.span.then(|| quote! { args.span, });
let wrapper = quote! {
|vm, args| {
let __typst_func = #parent #ident;
#(#handlers)*
let output = __typst_func(#(#args,)* #vm_ #vt_ #args_ #span_);
::typst::eval::IntoResult::into_result(output, args.span)
let name = if *constructor {
quote! { <#parent as #eval::NativeType>::NAME }
} else {
quote! { #name }
};
quote! {
#eval::NativeFuncData {
function: #closure,
name: #name,
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();
item.attrs.clear();
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();
// This is the actual function call.
let call = {
let self_ = func
.special
.self_
.as_ref()
.map(bind)
.map(|tokens| quote! { #tokens, });
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! {
#[doc(hidden)]
#vis fn #ident_func() -> &'static ::typst::eval::NativeFunc {
static FUNC: ::typst::eval::NativeFunc = ::typst::eval::NativeFunc {
func: #wrapper,
info: ::typst::eval::Lazy::new(|| typst::eval::FuncInfo {
name: #name,
display: #display,
keywords: #keywords,
category: #category,
docs: #docs,
params: ::std::vec![#(#params),*],
returns: <#returns as ::typst::eval::Reflect>::describe(),
scope: #scope,
}),
};
&FUNC
|vm, args| {
let __typst_func = #parent #ident;
#handlers
let output = #call;
::typst::eval::IntoResult::into_result(output, args.span)
}
#[doc = #docs]
#item
}
}
@ -226,7 +360,7 @@ fn create_param_info(param: &Param) -> TokenStream {
::typst::eval::ParamInfo {
name: #name,
docs: #docs,
cast: <#ty as ::typst::eval::Reflect>::describe(),
input: <#ty as ::typst::eval::Reflect>::input(),
default: #default,
positional: #positional,
named: #named,
@ -258,10 +392,29 @@ fn create_param_parser(param: &Param) -> TokenStream {
quote! { let mut #ident: #ty = #value; }
}
struct Parent(Option<syn::Type>);
impl Parse for Parent {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self(if !input.is_empty() { Some(input.parse()?) } else { None }))
/// Apply the binding to a parameter.
fn bind(param: &Param) -> TokenStream {
let ident = &param.ident;
match param.binding {
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]
mod util;
mod castable;
mod element;
mod cast;
mod elem;
mod func;
mod scope;
mod symbols;
mod ty;
use proc_macro::TokenStream as BoundaryStream;
use proc_macro2::TokenStream;
@ -19,7 +21,79 @@ use syn::{parse_quote, DeriveInput, Ident, Result, Token};
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]
pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemFn);
@ -28,33 +102,230 @@ pub fn func(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
.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]
pub fn element(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::ItemStruct);
element::element(stream.into(), &item)
pub fn ty(stream: BoundaryStream, item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as syn::Item);
ty::ty(stream.into(), item)
.unwrap_or_else(|err| err.to_compile_error())
.into()
}
/// Implements `Reflect`, `FromValue`, and `IntoValue` for an enum.
#[proc_macro_derive(Cast, attributes(string))]
pub fn derive_cast(item: BoundaryStream) -> BoundaryStream {
let item = syn::parse_macro_input!(item as DeriveInput);
castable::derive_cast(&item)
/// Makes a native Rust type usable as a Typst element.
///
/// This implements `NativeElement` for the given type.
///
/// ```
/// /// 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())
.into()
}
/// 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]
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())
.into()
}
/// 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]
pub fn symbols(stream: BoundaryStream) -> BoundaryStream {
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 name = symbol.name.to_string();
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) => {
let variants = variants.iter().map(|variant| {
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 syn::token::Token;
use syn::Attribute;
use super::*;
@ -19,25 +21,27 @@ macro_rules! bail {
};
}
/// For parsing attributes of the form:
/// #[attr(
/// statement;
/// statement;
/// returned_expression
/// )]
pub struct BlockWithReturn {
pub prefix: Vec<syn::Stmt>,
pub expr: syn::Stmt,
}
/// Extract documentation comments from an attribute list.
pub fn documentation(attrs: &[syn::Attribute]) -> String {
let mut doc = String::new();
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 })
// 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()
}
/// Whether an attribute list has a specified attribute.
@ -83,58 +87,6 @@ pub fn validate_attrs(attrs: &[syn::Attribute]) -> Result<()> {
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.
pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
if let Some(value) = option {
@ -143,3 +95,156 @@ pub fn quote_option<T: ToTokens>(option: &Option<T>) -> TokenStream {
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()?,
})
}
}