1extern crate proc_macro;
15
16use proc_macro::TokenStream;
17use proc_macro2::TokenStream as TokenStream2;
18use quote::{format_ident, quote};
19use syn::parse::{Parse, ParseStream};
20use syn::punctuated::Punctuated;
21use syn::spanned::Spanned;
22use syn::{parse_macro_input, Expr, ItemFn, LitBool, LitStr, Meta, Token};
23
24mod sig_parser;
25
26#[proc_macro_attribute]
44pub fn harn_builtin(attr: TokenStream, item: TokenStream) -> TokenStream {
45 let attrs = parse_macro_input!(attr as BuiltinAttrs);
46 let item_fn = parse_macro_input!(item as ItemFn);
47 match expand(attrs, item_fn) {
48 Ok(ts) => ts.into(),
49 Err(e) => e.to_compile_error().into(),
50 }
51}
52
53#[derive(Debug, Default)]
54struct BuiltinAttrs {
55 sig: Option<LitStr>,
56 sig_expr: Option<Expr>,
57 aliases: Vec<LitStr>,
58 category: Option<LitStr>,
59 kind: BuiltinKind,
60 parser_only: bool,
61 runtime_only: bool,
62 doc: Option<LitStr>,
63}
64
65#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
66enum BuiltinKind {
67 #[default]
68 Sync,
69 Async,
70}
71
72impl Parse for BuiltinAttrs {
73 fn parse(input: ParseStream) -> syn::Result<Self> {
74 let mut out = BuiltinAttrs::default();
75 let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
76 for meta in metas {
77 match &meta {
78 Meta::NameValue(nv) => {
79 let key = nv
80 .path
81 .get_ident()
82 .ok_or_else(|| syn::Error::new(nv.path.span(), "expected identifier key"))?
83 .to_string();
84 match key.as_str() {
85 "sig" => out.sig = Some(parse_lit_str(&nv.value)?),
86 "sig_expr" => out.sig_expr = Some(nv.value.clone()),
87 "category" => out.category = Some(parse_lit_str(&nv.value)?),
88 "doc" => out.doc = Some(parse_lit_str(&nv.value)?),
89 "kind" => {
90 let s = parse_lit_str(&nv.value)?;
91 out.kind = match s.value().as_str() {
92 "sync" => BuiltinKind::Sync,
93 "async" => BuiltinKind::Async,
94 other => {
95 return Err(syn::Error::new(
96 s.span(),
97 format!(
98 "unknown kind {other:?}, expected \"sync\" or \"async\""
99 ),
100 ));
101 }
102 };
103 }
104 "parser_only" => out.parser_only = parse_lit_bool(&nv.value)?,
105 "runtime_only" => out.runtime_only = parse_lit_bool(&nv.value)?,
106 "aliases" => out.aliases = parse_str_array(&nv.value)?,
107 other => {
108 return Err(syn::Error::new(
109 nv.path.span(),
110 format!("unknown #[harn_builtin] key: {other}"),
111 ));
112 }
113 }
114 }
115 other => {
116 return Err(syn::Error::new(
117 other.span(),
118 "expected key = value attributes",
119 ))
120 }
121 }
122 }
123 if let (Some(sig_lit), Some(_)) = (out.sig.as_ref(), out.sig_expr.as_ref()) {
124 return Err(syn::Error::new(
125 sig_lit.span(),
126 "specify either `sig` (Harn-style string) or `sig_expr` (raw Rust expression), not both",
127 ));
128 }
129 if out.sig.is_none() && out.sig_expr.is_none() && !out.runtime_only {
130 return Err(syn::Error::new(
131 proc_macro2::Span::call_site(),
132 "#[harn_builtin] requires `sig = \"...\"`, `sig_expr = ...`, or `runtime_only = true`",
133 ));
134 }
135 Ok(out)
136 }
137}
138
139fn parse_lit_str(expr: &Expr) -> syn::Result<LitStr> {
140 match expr {
141 Expr::Lit(syn::ExprLit {
142 lit: syn::Lit::Str(s),
143 ..
144 }) => Ok(s.clone()),
145 other => Err(syn::Error::new(other.span(), "expected string literal")),
146 }
147}
148
149fn parse_lit_bool(expr: &Expr) -> syn::Result<bool> {
150 match expr {
151 Expr::Lit(syn::ExprLit {
152 lit: syn::Lit::Bool(LitBool { value, .. }),
153 ..
154 }) => Ok(*value),
155 other => Err(syn::Error::new(other.span(), "expected boolean literal")),
156 }
157}
158
159fn parse_str_array(expr: &Expr) -> syn::Result<Vec<LitStr>> {
160 match expr {
161 Expr::Array(arr) => arr.elems.iter().map(parse_lit_str).collect(),
162 Expr::Reference(r) => parse_str_array(&r.expr),
163 other => Err(syn::Error::new(
164 other.span(),
165 "expected array of string literals, e.g. [\"alias1\", \"alias2\"]",
166 )),
167 }
168}
169
170fn expand(attrs: BuiltinAttrs, item_fn: ItemFn) -> syn::Result<TokenStream2> {
171 let fn_name = &item_fn.sig.ident;
172 let def_ident = format_ident!("{}_DEF", fn_name.to_string().to_uppercase());
173 let support = quote!(crate::stdlib::macros);
174
175 let sig_expr = if let Some(expr) = &attrs.sig_expr {
177 quote!(#expr)
178 } else if let Some(sig_lit) = &attrs.sig {
179 sig_parser::parse_sig(&sig_lit.value(), sig_lit.span(), &support)?
180 } else {
181 let name_str = fn_name.to_string();
183 quote!(#support::BuiltinSignature::simple(
184 #name_str,
185 &[],
186 #support::TY_ANY,
187 ))
188 };
189
190 let signature_text_expr = match &attrs.sig {
195 Some(sig_lit) => {
196 let raw = sig_lit.value();
197 quote!(::core::option::Option::Some(#raw))
198 }
199 None => quote!(::core::option::Option::None),
200 };
201
202 let aliases = attrs.aliases.iter().map(|s| quote!(#s));
203 let aliases_arr = quote!(&[#(#aliases),*]);
204
205 let category = match &attrs.category {
206 Some(c) => {
207 let v = c.value();
208 quote!(::core::option::Option::Some(#v))
209 }
210 None => quote!(::core::option::Option::None),
211 };
212
213 let doc = if let Some(d) = &attrs.doc {
215 let v = d.value();
216 quote!(::core::option::Option::Some(#v))
217 } else {
218 let collected: String = item_fn
219 .attrs
220 .iter()
221 .filter_map(|a| {
222 if a.path().is_ident("doc") {
223 if let Meta::NameValue(nv) = &a.meta {
224 if let Expr::Lit(syn::ExprLit {
225 lit: syn::Lit::Str(s),
226 ..
227 }) = &nv.value
228 {
229 return Some(s.value().trim().to_string());
230 }
231 }
232 }
233 None
234 })
235 .collect::<Vec<_>>()
236 .join("\n");
237 if collected.is_empty() {
238 quote!(::core::option::Option::None)
239 } else {
240 quote!(::core::option::Option::Some(#collected))
241 }
242 };
243
244 let parser_only = attrs.parser_only;
245 let runtime_only = attrs.runtime_only;
246
247 let async_thunk_ident = format_ident!("__harn_async_wrap_{}", fn_name);
251 let (handler_expr, extra_items) = match (attrs.kind, attrs.parser_only) {
252 (_, true) => (quote!(#support::VmBuiltinHandler::None), quote!()),
253 (BuiltinKind::Sync, _) => (quote!(#support::VmBuiltinHandler::Sync(#fn_name)), quote!()),
254 (BuiltinKind::Async, _) => {
255 let is_async_fn = item_fn.sig.asyncness.is_some();
261 if is_async_fn {
262 let thunk = quote! {
263 #[doc(hidden)]
264 #[allow(non_snake_case)]
265 fn #async_thunk_ident(
266 ctx: crate::vm::AsyncBuiltinCtx,
267 args: ::std::vec::Vec<#support::VmValue>,
268 ) -> #support::AsyncBuiltinFuture {
269 ::std::boxed::Box::pin(#fn_name(ctx, args))
270 }
271 };
272 (
273 quote!(#support::VmBuiltinHandler::Async(#async_thunk_ident)),
274 thunk,
275 )
276 } else {
277 (
278 quote!(#support::VmBuiltinHandler::Async(#fn_name)),
279 quote!(),
280 )
281 }
282 }
283 };
284
285 let link_ident = format_ident!("__{}_LINKME", fn_name.to_string().to_uppercase());
292
293 let out = quote! {
294 #item_fn
295
296 #extra_items
297
298 #[doc(hidden)]
299 #[allow(non_upper_case_globals)]
300 pub static #def_ident: #support::VmBuiltinDef = #support::VmBuiltinDef {
301 sig: #sig_expr,
302 aliases: #aliases_arr,
303 handler: #handler_expr,
304 category: #category,
305 doc: #doc,
306 signature_text: #signature_text_expr,
307 parser_only: #parser_only,
308 runtime_only: #runtime_only,
309 };
310
311 #[doc(hidden)]
312 #[allow(non_upper_case_globals)]
313 #[#support::distributed_slice(#support::ALL_BUILTIN_DEFS)]
314 static #link_ident: &'static #support::VmBuiltinDef = &#def_ident;
315 };
316 Ok(out)
317}