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