Skip to main content

harn_builtin_macros/
lib.rs

1//! `#[harn_builtin]` proc-macro.
2//!
3//! Annotates a Rust function that implements one builtin and emits both a
4//! runtime registration entry and a parser `BuiltinSignature` from a single
5//! declaration. This is the only supported way to register stdlib builtins —
6//! see `CONTRIBUTING.md` ("Adding a stdlib builtin") for the wire-up
7//! checklist and `crates/harn-vm/src/stdlib/bytes.rs`, `runtime_scope.rs`,
8//! and `strings.rs` for sync, async, and `aliases = [...]` examples
9//! respectively. The macro contributes each emitted `VmBuiltinDef` to the
10//! workspace-global `ALL_BUILTIN_DEFS` linkme distributed slice, so simply
11//! annotating a fn (in a module already pulled into `harn-vm`) is enough to
12//! make it land in the registry — no per-module aggregation edits required.
13
14extern 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/// Marks a Rust function as the runtime handler for a Harn builtin. Emits a
27/// sibling `static <NAME>_DEF: harn_vm::stdlib::macros::VmBuiltinDef = ...`
28/// containing the signature, aliases, handler pointer, and metadata.
29///
30/// # Attribute keys
31///
32/// - `sig = "name(a: dict, b: dict) -> dict"` — Harn-style signature parsed
33///   into a `BuiltinSignature`. Mutually exclusive with `sig_expr`.
34/// - `sig_expr = <Rust expr returning BuiltinSignature>` — full struct
35///   literal used verbatim. Escape hatch for shapes, complex generics, etc.
36/// - `aliases = ["__foo"]` — additional names sharing this impl + signature.
37/// - `category = "collections"` — observability label (optional).
38/// - `kind = "sync" | "async"` — defaults to `sync`. `async` wraps the user
39///   fn into `Pin<Box<dyn Future<...>>>`.
40/// - `parser_only = true` — emit only the signature; no runtime registration.
41/// - `runtime_only = true` — emit only the runtime entry; signature suppressed.
42/// - `doc = "..."` — override doc string (defaults to the fn's `///` block).
43#[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    // Build the BuiltinSignature expression.
176    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        // runtime_only — emit a placeholder signature with the fn name.
182        let name_str = fn_name.to_string();
183        quote!(#support::BuiltinSignature::simple(
184            #name_str,
185            &[],
186            #support::TY_ANY,
187        ))
188    };
189
190    // Surface the human-readable sig text (e.g. `"foo(a: dict) -> dict"`)
191    // through to the runtime metadata layer so `harn explain` /
192    // `harn-vm-tools` / the alignment-test metadata check keep parity
193    // with the pre-migration DSL builder.
194    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    // Doc: explicit override, else extract from /// comments on the fn.
214    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    // Handler wiring depends on sync vs async. For `async fn` user
248    // functions we emit a sibling thunk that boxes the future to match the
249    // `AsyncHandler` signature.
250    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            // Async builtins receive an explicit `AsyncBuiltinCtx` handle as
256            // their first parameter (harn#2668). The macro threads it from the
257            // dispatch loop into the user fn so handler bodies mint child VMs /
258            // forward output through the ctx they were given, never an ambient
259            // task-local.
260            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    // Sibling linkme entry that registers `#def_ident` into the
286    // workspace-global `ALL_BUILTIN_DEFS` distributed slice — eliminates
287    // the need for per-module `MODULE_BUILTINS` arrays + a hand-maintained
288    // aggregator in `stdlib.rs`. The entry name is derived from the def
289    // identifier so two builtins in different modules never collide on
290    // the static name.
291    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}