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, ExprLit, Ident, ItemFn, Lit, LitBool, LitStr, Meta, Token};
23
24mod sig_parser;
25
26#[proc_macro_attribute]
49pub fn harn_builtin(attr: TokenStream, item: TokenStream) -> TokenStream {
50 let attrs = parse_macro_input!(attr as BuiltinAttrs);
51 let item_fn = parse_macro_input!(item as ItemFn);
52 match expand(attrs, item_fn) {
53 Ok(ts) => ts.into(),
54 Err(e) => e.to_compile_error().into(),
55 }
56}
57
58#[proc_macro]
62pub fn harn_capability_method(input: TokenStream) -> TokenStream {
63 let input = parse_macro_input!(input as CapabilityMethodInput);
64 match expand_capability_method(input) {
65 Ok(tokens) => tokens.into(),
66 Err(error) => error.to_compile_error().into(),
67 }
68}
69
70#[proc_macro]
72pub fn harn_capability_contract(input: TokenStream) -> TokenStream {
73 let input = parse_macro_input!(input as CapabilityMethodInput);
74 match expand_leaf_capability_contract(input) {
75 Ok(tokens) => tokens.into(),
76 Err(error) => error.to_compile_error().into(),
77 }
78}
79
80struct CapabilityMethodInput {
81 rust_name: Ident,
82 exposure: LitStr,
83 effects: Vec<LitStr>,
84 signature: Expr,
85 doc: LitStr,
86}
87
88impl Parse for CapabilityMethodInput {
89 fn parse(input: ParseStream) -> syn::Result<Self> {
90 let rust_name = input.parse()?;
91 input.parse::<Token![,]>()?;
92 let exposure = input.parse()?;
93 input.parse::<Token![,]>()?;
94 let effects_expr: Expr = input.parse()?;
95 let effects = parse_str_array(&effects_expr)?;
96 input.parse::<Token![,]>()?;
97 let signature = input.parse()?;
98 input.parse::<Token![,]>()?;
99 let doc = input.parse()?;
100 if !input.is_empty() {
101 return Err(input.error("unexpected capability method tokens"));
102 }
103 Ok(Self {
104 rust_name,
105 exposure,
106 effects,
107 signature,
108 doc,
109 })
110 }
111}
112
113fn expand_capability_method(input: CapabilityMethodInput) -> syn::Result<TokenStream2> {
114 let support = quote!(crate::stdlib::macros);
115 let (sig_expr, signature_text, signature_attr) = match &input.signature {
116 Expr::Lit(ExprLit {
117 lit: Lit::Str(signature),
118 ..
119 }) => (
120 sig_parser::parse_sig(&signature.value(), signature.span(), &support)?,
121 Some(signature.value()),
122 Some(signature.clone()),
123 ),
124 expression => (quote!(#expression), None, None),
125 };
126 let attrs = BuiltinAttrs {
127 sig: signature_attr,
128 exposure: Some(input.exposure),
129 effects: input.effects,
130 effects_declared: true,
131 parser_only: true,
132 ..BuiltinAttrs::default()
133 };
134 let contract = contract_expr(&attrs, &support)?;
135 let upper = input.rust_name.to_string().to_uppercase();
136 let def_ident = format_ident!("{upper}_DEF");
137 let link_ident = format_ident!("__{upper}_LINKME");
138 let doc = input.doc.value();
139 let signature_text_expr = match signature_text {
140 Some(signature) => quote!(::core::option::Option::Some(#signature)),
141 None => quote!(::core::option::Option::None),
142 };
143 Ok(quote! {
144 #[doc(hidden)]
145 #[allow(non_upper_case_globals)]
146 pub static #def_ident: #support::VmBuiltinDef = #support::VmBuiltinDef {
147 sig: #sig_expr,
148 contract: #contract,
149 aliases: &[],
150 handler: #support::VmBuiltinHandler::None,
151 category: ::core::option::Option::Some("capability"),
152 doc: ::core::option::Option::Some(#doc),
153 signature_text: #signature_text_expr,
154 parser_only: true,
155 runtime_only: false,
156 };
157
158 #[doc(hidden)]
159 #[allow(non_upper_case_globals)]
160 #[#support::distributed_slice(#support::ALL_BUILTIN_DEFS)]
161 static #link_ident: &'static #support::VmBuiltinDef = &#def_ident;
162 })
163}
164
165fn expand_leaf_capability_contract(input: CapabilityMethodInput) -> syn::Result<TokenStream2> {
166 let support = quote!(crate::support);
167 let (sig_expr, signature_text, signature_attr) = match &input.signature {
168 Expr::Lit(ExprLit {
169 lit: Lit::Str(signature),
170 ..
171 }) => (
172 sig_parser::parse_sig(&signature.value(), signature.span(), &support)?,
173 Some(signature.value()),
174 Some(signature.clone()),
175 ),
176 expression => (quote!(#expression), None, None),
177 };
178 let attrs = BuiltinAttrs {
179 sig: signature_attr,
180 exposure: Some(input.exposure),
181 effects: input.effects,
182 effects_declared: true,
183 parser_only: true,
184 ..BuiltinAttrs::default()
185 };
186 let contract = contract_expr(&attrs, &support)?;
187 let upper = input.rust_name.to_string().to_uppercase();
188 let def_ident = format_ident!("{upper}_DEF");
189 let link_ident = format_ident!("__{upper}_LINKME");
190 let doc = input.doc.value();
191 let signature_text_expr = match signature_text {
192 Some(signature) => quote!(::core::option::Option::Some(#signature)),
193 None => quote!(::core::option::Option::None),
194 };
195 Ok(quote! {
196 #[doc(hidden)]
197 #[allow(non_upper_case_globals)]
198 pub static #def_ident: #support::CapabilityMethodDef = #support::CapabilityMethodDef {
199 signature: #sig_expr,
200 contract: #contract,
201 doc: #doc,
202 signature_text: #signature_text_expr,
203 };
204
205 #[doc(hidden)]
206 #[allow(non_upper_case_globals)]
207 #[#support::distributed_slice(#support::ALL_CAPABILITY_METHOD_DEFS)]
208 static #link_ident: &'static #support::CapabilityMethodDef = &#def_ident;
209 })
210}
211
212#[derive(Debug, Default)]
213struct BuiltinAttrs {
214 sig: Option<LitStr>,
215 sig_expr: Option<Expr>,
216 aliases: Vec<LitStr>,
217 exposure: Option<LitStr>,
218 effects: Vec<LitStr>,
219 effects_declared: bool,
220 category: Option<LitStr>,
221 kind: BuiltinKind,
222 parser_only: bool,
223 runtime_only: bool,
224 doc: Option<LitStr>,
225}
226
227#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
228enum BuiltinKind {
229 #[default]
230 Sync,
231 Async,
232}
233
234impl Parse for BuiltinAttrs {
235 fn parse(input: ParseStream) -> syn::Result<Self> {
236 let mut out = BuiltinAttrs::default();
237 let metas = Punctuated::<Meta, Token![,]>::parse_terminated(input)?;
238 for meta in metas {
239 match &meta {
240 Meta::NameValue(nv) => {
241 let key = nv
242 .path
243 .get_ident()
244 .ok_or_else(|| syn::Error::new(nv.path.span(), "expected identifier key"))?
245 .to_string();
246 match key.as_str() {
247 "sig" => out.sig = Some(parse_lit_str(&nv.value)?),
248 "sig_expr" => out.sig_expr = Some(nv.value.clone()),
249 "category" => out.category = Some(parse_lit_str(&nv.value)?),
250 "doc" => out.doc = Some(parse_lit_str(&nv.value)?),
251 "kind" => {
252 let s = parse_lit_str(&nv.value)?;
253 out.kind = match s.value().as_str() {
254 "sync" => BuiltinKind::Sync,
255 "async" => BuiltinKind::Async,
256 other => {
257 return Err(syn::Error::new(
258 s.span(),
259 format!(
260 "unknown kind {other:?}, expected \"sync\" or \"async\""
261 ),
262 ));
263 }
264 };
265 }
266 "parser_only" => out.parser_only = parse_lit_bool(&nv.value)?,
267 "runtime_only" => out.runtime_only = parse_lit_bool(&nv.value)?,
268 "aliases" => out.aliases = parse_str_array(&nv.value)?,
269 "exposure" => out.exposure = Some(parse_lit_str(&nv.value)?),
270 "effects" => {
271 out.effects = parse_str_array(&nv.value)?;
272 out.effects_declared = true;
273 }
274 other => {
275 return Err(syn::Error::new(
276 nv.path.span(),
277 format!("unknown #[harn_builtin] key: {other}"),
278 ));
279 }
280 }
281 }
282 other => {
283 return Err(syn::Error::new(
284 other.span(),
285 "expected key = value attributes",
286 ))
287 }
288 }
289 }
290 if let (Some(sig_lit), Some(_)) = (out.sig.as_ref(), out.sig_expr.as_ref()) {
291 return Err(syn::Error::new(
292 sig_lit.span(),
293 "specify either `sig` (Harn-style string) or `sig_expr` (raw Rust expression), not both",
294 ));
295 }
296 if out.sig.is_none() && out.sig_expr.is_none() && !out.runtime_only {
297 return Err(syn::Error::new(
298 proc_macro2::Span::call_site(),
299 "#[harn_builtin] requires `sig = \"...\"`, `sig_expr = ...`, or `runtime_only = true`",
300 ));
301 }
302 if out.exposure.is_some() != out.effects_declared {
303 return Err(syn::Error::new(
304 proc_macro2::Span::call_site(),
305 "`exposure` and `effects` must be declared together",
306 ));
307 }
308 Ok(out)
309 }
310}
311
312fn parse_lit_str(expr: &Expr) -> syn::Result<LitStr> {
313 match expr {
314 Expr::Lit(syn::ExprLit {
315 lit: syn::Lit::Str(s),
316 ..
317 }) => Ok(s.clone()),
318 other => Err(syn::Error::new(other.span(), "expected string literal")),
319 }
320}
321
322fn parse_lit_bool(expr: &Expr) -> syn::Result<bool> {
323 match expr {
324 Expr::Lit(syn::ExprLit {
325 lit: syn::Lit::Bool(LitBool { value, .. }),
326 ..
327 }) => Ok(*value),
328 other => Err(syn::Error::new(other.span(), "expected boolean literal")),
329 }
330}
331
332fn parse_str_array(expr: &Expr) -> syn::Result<Vec<LitStr>> {
333 match expr {
334 Expr::Array(arr) => arr.elems.iter().map(parse_lit_str).collect(),
335 Expr::Reference(r) => parse_str_array(&r.expr),
336 other => Err(syn::Error::new(
337 other.span(),
338 "expected array of string literals, e.g. [\"alias1\", \"alias2\"]",
339 )),
340 }
341}
342
343fn expand(attrs: BuiltinAttrs, item_fn: ItemFn) -> syn::Result<TokenStream2> {
344 let fn_name = &item_fn.sig.ident;
345 let def_ident = format_ident!("{}_DEF", fn_name.to_string().to_uppercase());
346 let support = quote!(crate::stdlib::macros);
347
348 let sig_expr = if let Some(expr) = &attrs.sig_expr {
350 quote!(#expr)
351 } else if let Some(sig_lit) = &attrs.sig {
352 sig_parser::parse_sig(&sig_lit.value(), sig_lit.span(), &support)?
353 } else {
354 let name_str = fn_name.to_string();
356 quote!(#support::BuiltinSignature::simple(
357 #name_str,
358 &[],
359 #support::TY_ANY,
360 ))
361 };
362
363 let signature_text_expr = match &attrs.sig {
368 Some(sig_lit) => {
369 let raw = sig_lit.value();
370 quote!(::core::option::Option::Some(#raw))
371 }
372 None => quote!(::core::option::Option::None),
373 };
374
375 let aliases = attrs.aliases.iter().map(|s| quote!(#s));
376 let aliases_arr = quote!(&[#(#aliases),*]);
377 let contract_expr = contract_expr(&attrs, &support)?;
378
379 let category = match &attrs.category {
380 Some(c) => {
381 let v = c.value();
382 quote!(::core::option::Option::Some(#v))
383 }
384 None => quote!(::core::option::Option::None),
385 };
386
387 let doc = if let Some(d) = &attrs.doc {
389 let v = d.value();
390 quote!(::core::option::Option::Some(#v))
391 } else {
392 let collected: String = item_fn
393 .attrs
394 .iter()
395 .filter_map(|a| {
396 if a.path().is_ident("doc") {
397 if let Meta::NameValue(nv) = &a.meta {
398 if let Expr::Lit(syn::ExprLit {
399 lit: syn::Lit::Str(s),
400 ..
401 }) = &nv.value
402 {
403 return Some(s.value().trim().to_string());
404 }
405 }
406 }
407 None
408 })
409 .collect::<Vec<_>>()
410 .join("\n");
411 if collected.is_empty() {
412 quote!(::core::option::Option::None)
413 } else {
414 quote!(::core::option::Option::Some(#collected))
415 }
416 };
417
418 let parser_only = attrs.parser_only;
419 let runtime_only = attrs.runtime_only;
420
421 let async_thunk_ident = format_ident!("__harn_async_wrap_{}", fn_name);
425 let (handler_expr, extra_items) = match (attrs.kind, attrs.parser_only) {
426 (_, true) => (quote!(#support::VmBuiltinHandler::None), quote!()),
427 (BuiltinKind::Sync, _) => (quote!(#support::VmBuiltinHandler::Sync(#fn_name)), quote!()),
428 (BuiltinKind::Async, _) => {
429 let is_async_fn = item_fn.sig.asyncness.is_some();
435 if is_async_fn {
436 let thunk = quote! {
437 #[doc(hidden)]
438 #[allow(non_snake_case)]
439 fn #async_thunk_ident(
440 ctx: crate::vm::AsyncBuiltinCtx,
441 args: ::std::vec::Vec<#support::VmValue>,
442 ) -> #support::AsyncBuiltinFuture {
443 ::std::boxed::Box::pin(#fn_name(ctx, args))
444 }
445 };
446 (
447 quote!(#support::VmBuiltinHandler::Async(#async_thunk_ident)),
448 thunk,
449 )
450 } else {
451 (
452 quote!(#support::VmBuiltinHandler::Async(#fn_name)),
453 quote!(),
454 )
455 }
456 }
457 };
458
459 let link_ident = format_ident!("__{}_LINKME", fn_name.to_string().to_uppercase());
466
467 let out = quote! {
468 #item_fn
469
470 #extra_items
471
472 #[doc(hidden)]
473 #[allow(non_upper_case_globals)]
474 pub static #def_ident: #support::VmBuiltinDef = #support::VmBuiltinDef {
475 sig: #sig_expr,
476 contract: #contract_expr,
477 aliases: #aliases_arr,
478 handler: #handler_expr,
479 category: #category,
480 doc: #doc,
481 signature_text: #signature_text_expr,
482 parser_only: #parser_only,
483 runtime_only: #runtime_only,
484 };
485
486 #[doc(hidden)]
487 #[allow(non_upper_case_globals)]
488 #[#support::distributed_slice(#support::ALL_BUILTIN_DEFS)]
489 static #link_ident: &'static #support::VmBuiltinDef = &#def_ident;
490 };
491 Ok(out)
492}
493
494fn contract_expr(attrs: &BuiltinAttrs, support: &TokenStream2) -> syn::Result<TokenStream2> {
495 let Some(exposure) = attrs.exposure.as_ref() else {
496 return Ok(quote!(#support::BuiltinContract::UNDECLARED));
497 };
498
499 let effects = attrs
500 .effects
501 .iter()
502 .map(|effect| parse_effect_spec(&effect.value(), effect.span(), support))
503 .collect::<syn::Result<Vec<_>>>()?;
504 let effects = quote!(&[#(#effects),*]);
505 let raw = exposure.value();
506 match raw.as_str() {
507 "pure" => {
508 if !attrs.effects.is_empty() {
509 return Err(syn::Error::new(
510 exposure.span(),
511 "pure builtins must declare `effects = []`",
512 ));
513 }
514 Ok(quote!(#support::BuiltinContract::PURE))
515 }
516 "runtime_internal" => {
517 if !attrs.effects.is_empty() {
518 return Err(syn::Error::new(
519 exposure.span(),
520 "runtime-internal builtins cannot declare script effects",
521 ));
522 }
523 Ok(quote!(#support::BuiltinContract::RUNTIME_INTERNAL))
524 }
525 "privileged_wire" => Ok(quote!(#support::BuiltinContract::privileged_wire(#effects))),
526 _ => {
527 if let Some(index) = raw.strip_prefix("capability_arg:") {
528 let authority_argument = index.parse::<u16>().map_err(|_| {
529 syn::Error::new(
530 exposure.span(),
531 "capability argument exposure must be `capability_arg:<index>`",
532 )
533 })?;
534 if attrs.effects.is_empty() {
535 return Err(syn::Error::new(
536 exposure.span(),
537 "capability argument builtins must declare at least one effect",
538 ));
539 }
540 return Ok(quote!(
541 #support::BuiltinContract::capability_function(
542 #authority_argument,
543 #effects,
544 )
545 ));
546 }
547 let Some(rest) = raw.strip_prefix("harness.") else {
548 return Err(syn::Error::new(
549 exposure.span(),
550 "unknown exposure; expected `pure`, `runtime_internal`, \
551 `privileged_wire`, `capability_arg:<index>`, or \
552 `harness.<capability>.<method>`",
553 ));
554 };
555 let Some((capability, method)) = rest.split_once('.') else {
556 return Err(syn::Error::new(
557 exposure.span(),
558 "harness exposure must be `harness.<capability>.<method>`",
559 ));
560 };
561 if method.is_empty() || method.contains('.') {
562 return Err(syn::Error::new(
563 exposure.span(),
564 "harness method must be one non-empty identifier",
565 ));
566 }
567 let capability = capability_expr(capability, exposure.span(), support)?;
568 Ok(quote!(#support::BuiltinContract::harness(
569 #capability,
570 #method,
571 #effects,
572 )))
573 }
574 }
575}
576
577fn capability_expr(
578 name: &str,
579 span: proc_macro2::Span,
580 support: &TokenStream2,
581) -> syn::Result<TokenStream2> {
582 let variant = harn_builtin_meta::CapabilityId::from_field_name(name)
583 .map(harn_builtin_meta::CapabilityId::variant_name)
584 .ok_or_else(|| syn::Error::new(span, format!("unknown harness capability `{name}`")))?;
585 let ident = format_ident!("{variant}");
586 Ok(quote!(#support::CapabilityId::#ident))
587}
588
589fn parse_effect_spec(
590 raw: &str,
591 span: proc_macro2::Span,
592 support: &TokenStream2,
593) -> syn::Result<TokenStream2> {
594 let (head, selectors) = raw
595 .split_once('@')
596 .map_or((raw, None), |(head, selectors)| (head, Some(selectors)));
597 let Some((kind, access)) = head.split_once('.') else {
598 return Err(syn::Error::new(
599 span,
600 "effect must be `<kind>.<access>` with optional `@selectors`",
601 ));
602 };
603 let kind = match kind {
604 "stdio" => quote!(#support::EffectKind::Stdio),
605 "fs" => quote!(#support::EffectKind::Fs),
606 "env" => quote!(#support::EffectKind::Env),
607 "clock" => quote!(#support::EffectKind::Clock),
608 "random" => quote!(#support::EffectKind::Random),
609 "network" => quote!(#support::EffectKind::Network),
610 "process" => quote!(#support::EffectKind::Process),
611 "llm" => quote!(#support::EffectKind::Llm),
612 "tool" => quote!(#support::EffectKind::Tool),
613 "mcp" => quote!(#support::EffectKind::Mcp),
614 "host" => quote!(#support::EffectKind::Host),
615 "worker" => quote!(#support::EffectKind::Worker),
616 "secret" => quote!(#support::EffectKind::Secret),
617 "observability" => quote!(#support::EffectKind::Observability),
618 "channel" => quote!(#support::EffectKind::Channel),
619 "state" => quote!(#support::EffectKind::State),
620 _ => {
621 return Err(syn::Error::new(
622 span,
623 format!("unknown effect kind `{kind}`"),
624 ))
625 }
626 };
627 let access = match access {
628 "read" => quote!(#support::EffectAccess::Read),
629 "write" => quote!(#support::EffectAccess::Write),
630 "mutate" => quote!(#support::EffectAccess::Mutate),
631 "observe" => quote!(#support::EffectAccess::Observe),
632 _ => {
633 return Err(syn::Error::new(
634 span,
635 format!("unknown effect access `{access}`"),
636 ))
637 }
638 };
639 let selectors = selectors
640 .filter(|selectors| !selectors.is_empty())
641 .map(|selectors| {
642 selectors
643 .split('+')
644 .map(|selector| parse_resource_selector(selector, span, support))
645 .collect::<syn::Result<Vec<_>>>()
646 })
647 .transpose()?
648 .unwrap_or_default();
649 Ok(quote!(#support::EffectSpec::new(
650 #kind,
651 #access,
652 &[#(#selectors),*],
653 )))
654}
655
656fn parse_resource_selector(
657 raw: &str,
658 span: proc_macro2::Span,
659 support: &TokenStream2,
660) -> syn::Result<TokenStream2> {
661 if raw == "dynamic" {
662 return Ok(quote!(#support::ResourceSelector::Dynamic));
663 }
664 if let Some(value) = raw.strip_prefix("const=") {
665 if value.is_empty() {
666 return Err(syn::Error::new(span, "constant selector cannot be empty"));
667 }
668 return Ok(quote!(#support::ResourceSelector::Constant(#value)));
669 }
670 if let Some(index) = raw.strip_prefix("each") {
671 let index = parse_selector_index(index, span)?;
672 return Ok(quote!(#support::ResourceSelector::EachArgument(#index)));
673 }
674 let Some(rest) = raw.strip_prefix("arg") else {
675 return Err(syn::Error::new(
676 span,
677 format!("unknown resource selector `{raw}`"),
678 ));
679 };
680 let mut parts = rest.split('.');
681 let index = parse_selector_index(parts.next().unwrap_or_default(), span)?;
682 let path = parts.collect::<Vec<_>>();
683 if path.is_empty() {
684 Ok(quote!(#support::ResourceSelector::Argument(#index)))
685 } else if path.iter().any(|part| part.is_empty()) {
686 Err(syn::Error::new(span, "resource field path cannot be empty"))
687 } else {
688 Ok(quote!(#support::ResourceSelector::Field {
689 argument: #index,
690 path: &[#(#path),*],
691 }))
692 }
693}
694
695fn parse_selector_index(raw: &str, span: proc_macro2::Span) -> syn::Result<u16> {
696 raw.parse::<u16>()
697 .map_err(|_| syn::Error::new(span, format!("invalid argument index `{raw}`")))
698}