polydat_derive/lib.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! `polydat-derive` — proc-macro implementation of
5//! [`#[polydat_node]`](polydat_node).
6//!
7//! See [`docs/SRD/80_node_function_macro_collapse.md`](https://github.com/jshook/nb-rs/blob/main/docs/SRD/80_node_function_macro_collapse.md)
8//! for the design, the 8 open design questions this proc-macro
9//! is closing one-at-a-time, and the migration plan against
10//! existing polydat library nodes.
11//!
12//! ## Current scope (PR B.1)
13//!
14//! This is the SCAFFOLDING pass. The macro recognizes the
15//! simplest case only:
16//!
17//! - A standalone `fn` (no `impl` block, no struct).
18//! - All wire input arguments are PRIMITIVES with implementations
19//! of polydat's `FromValue` trait — concretely: `u64`, `f64`,
20//! `bool`, `&str` (or owned `String`).
21//! - Return type is a PRIMITIVE with a `IntoValue` implementation —
22//! same set.
23//! - No state, no const args, no JIT hooks, no variadic shapes,
24//! no polymorphism.
25//!
26//! Out of scope (deferred to later PR B.* batches):
27//!
28//! - State-bearing nodes (probability PRNG, vectors readers).
29//! - JIT-eligible nodes (the `compiled_u64` hooks).
30//! - Const-arg parameters with `ConstConstraint`.
31//! - Variadic shapes (`Variadic<T>`, `&[T]`).
32//! - Polymorphic outputs (`SameAsInput`).
33//! - Ext-typed args / returns (adapter-contributed types).
34//!
35//! ## Generated output (for the simple case)
36//!
37//! Input:
38//!
39//! ```ignore
40//! #[polydat_node]
41//! fn str_eq(a: &str, b: &str) -> u64 {
42//! if a == b { 1 } else { 0 }
43//! }
44//! ```
45//!
46//! Generated:
47//!
48//! ```ignore
49//! pub struct StrEq { meta: polydat::ast::NodeMeta }
50//! impl Default for StrEq { fn default() -> Self { Self::new() } }
51//! impl StrEq {
52//! pub fn new() -> Self {
53//! Self {
54//! meta: polydat::ast::NodeMeta {
55//! name: "str_eq".into(),
56//! ins: vec![
57//! polydat::ast::Slot::Wire(polydat::ast::Port::new(
58//! "a", polydat::ast::PortType::Str)),
59//! polydat::ast::Slot::Wire(polydat::ast::Port::new(
60//! "b", polydat::ast::PortType::Str)),
61//! ],
62//! outs: vec![polydat::ast::Port::new(
63//! "output", polydat::ast::PortType::U64)],
64//! },
65//! }
66//! }
67//! }
68//! impl polydat::ast::PolydatNode for StrEq {
69//! fn meta(&self) -> &polydat::ast::NodeMeta { &self.meta }
70//! fn eval(
71//! &self,
72//! inputs: &[polydat::ast::Value],
73//! outputs: &mut [polydat::ast::Value],
74//! ) {
75//! let a = <&str as polydat::derive_support::FromValue>::from_value(&inputs[0]);
76//! let b = <&str as polydat::derive_support::FromValue>::from_value(&inputs[1]);
77//! let result: u64 = if a == b { 1 } else { 0 };
78//! outputs[0] = <u64 as polydat::derive_support::IntoValue>::into_value(result);
79//! }
80//! }
81//! ```
82//!
83//! The original `fn str_eq` is consumed by the macro — only the
84//! struct + impl is emitted. The body of `str_eq` becomes the
85//! body of the `eval` method (with parameter rebinding via
86//! `FromValue::from_value`).
87//!
88//! FuncSig registration via inventory or similar is deferred to
89//! PR B.2 — for now the macro just generates the struct + impl
90//! so we can validate the boxing/unboxing path with a pilot
91//! node.
92
93use proc_macro::TokenStream;
94use proc_macro2::TokenStream as TokenStream2;
95use quote::{quote, format_ident};
96use syn::{
97 parse_macro_input, FnArg, Ident, ItemFn, Meta, Pat, ReturnType, Token, Type,
98 parse::Parser, punctuated::Punctuated,
99};
100
101/// `#[polydat_node]` — derive a polydat node from a typed Rust
102/// function signature.
103///
104/// See the crate docs for the supported surface and what's
105/// out of scope for this scaffolding pass.
106///
107/// ## Attribute parameters (PR B.2)
108///
109/// - `category = <ident>` — the polydat `FuncCategory` variant
110/// the node belongs to (`Comparison`, `Math`, `String`, etc.).
111/// Defaults to `Misc` when unspecified.
112#[proc_macro_attribute]
113pub fn polydat_node(attr: TokenStream, item: TokenStream) -> TokenStream {
114 let func = parse_macro_input!(item as ItemFn);
115
116 let attrs = match parse_attrs(attr.into()) {
117 Ok(a) => a,
118 Err(e) => return e.to_compile_error().into(),
119 };
120
121 // Adapter namespacing: when `adapter = "<name>"` is declared,
122 // the node's function name MUST start with `<name>_` so every
123 // adapter-provided node stays namespaced under the adapter's
124 // canonical registered name. Enforced here where the fn ident
125 // is in hand; validation-only (not threaded into codegen).
126 if let Some(adapter) = &attrs.adapter {
127 let fn_ident = &func.sig.ident;
128 let prefix = format!("{adapter}_");
129 if !fn_ident.to_string().starts_with(&prefix) {
130 let msg = format!(
131 "#[polydat_node(adapter = \"{adapter}\")] requires the node \
132 name to start with \"{prefix}\" (found \"{fn_ident}\")",
133 );
134 return syn::Error::new_spanned(fn_ident, msg)
135 .to_compile_error()
136 .into();
137 }
138 }
139
140 // SRD-80b Phase D1 — generic-over-Wire fanout. When the
141 // operator declares `instantiate(T1, T2, ...)`, the macro
142 // emits one full registration per type (per-instantiation
143 // struct + impl + NodeRegistration). The DSL function name
144 // stays shared; the Rust struct names get type-derived
145 // suffixes (`PassthroughU64`, `PassthroughF64`, ...).
146 if attrs.instantiate.is_empty() {
147 return match generate(func, attrs, None) {
148 Ok(ts) => ts.into(),
149 Err(e) => e.to_compile_error().into(),
150 };
151 }
152 match instantiate_and_generate(func, attrs) {
153 Ok(ts) => ts.into(),
154 Err(e) => e.to_compile_error().into(),
155 }
156}
157
158/// SRD-80b Phase D1 — fan out a generic-over-Wire function into
159/// one full instantiation per concrete type listed in
160/// `instantiate(...)`. Requires exactly one type parameter on
161/// the function; substitutes that parameter throughout args /
162/// return / body and emits a generate() call per instantiation
163/// with a type-derived struct-name suffix.
164fn instantiate_and_generate(
165 func: ItemFn,
166 attrs: NodeAttrs,
167) -> syn::Result<TokenStream2> {
168 let generics = &func.sig.generics;
169 // Exactly one type parameter is required. (Lifetimes and
170 // const params are not supported for instantiation.)
171 let type_params: Vec<&syn::TypeParam> = generics.type_params().collect();
172 if type_params.len() != 1 {
173 return Err(syn::Error::new_spanned(
174 &func.sig,
175 format!(
176 "#[polydat_node(instantiate(...))] requires exactly one type \
177 parameter on the function (got {}). Declare the function as \
178 `fn name<T: Wire>(...) -> ...` and list concrete `Wire`-impl \
179 types in the `instantiate(...)` clause.",
180 type_params.len(),
181 ),
182 ));
183 }
184 let type_param_ident = type_params[0].ident.clone();
185 let dsl_name = func.sig.ident.to_string();
186
187 let mut out = TokenStream2::new();
188 // Clone attrs minus the `instantiate` clause so the
189 // downstream generate() doesn't try to fan out again.
190 let mut shared_attrs = attrs.clone();
191 let instantiations = std::mem::take(&mut shared_attrs.instantiate);
192
193 for concrete in instantiations {
194 let mut inst_func = func.clone();
195 // Strip the generic parameter — the substituted form is
196 // no longer generic.
197 inst_func.sig.generics.params.clear();
198 inst_func.sig.generics.where_clause = None;
199 // Substitute T -> concrete throughout the function.
200 let mut subst = TypeSubst {
201 type_param: type_param_ident.clone(),
202 concrete: concrete.clone(),
203 };
204 syn::visit_mut::VisitMut::visit_item_fn_mut(&mut subst, &mut inst_func);
205 // Rename to a per-instantiation Rust identifier so the
206 // generated struct name carries the type suffix. The
207 // operator-facing DSL name stays `dsl_name`, passed
208 // through generate()'s name_override.
209 let suffix = type_suffix(&concrete);
210 let new_ident = syn::Ident::new(
211 &format!("{dsl_name}_{}", suffix.to_lowercase()),
212 inst_func.sig.ident.span(),
213 );
214 inst_func.sig.ident = new_ident;
215 let emit = generate(inst_func, shared_attrs.clone(), Some(dsl_name.clone()))?;
216 out.extend(emit);
217 }
218 Ok(out)
219}
220
221/// Derive a struct-name suffix from a Rust type. Used by Phase
222/// D1 instantiate to disambiguate the per-instantiation struct
223/// names. `u64` → "U64"; `String` → "String"; `Arc<[u8]>` →
224/// "ArcU8"; `SliceArc<f32>` → "SliceArcF32". The strategy
225/// strips angle brackets / refs / punctuation and uppercases
226/// each segment's first character.
227fn type_suffix(ty: &Type) -> String {
228 let raw = type_to_string(ty);
229 let mut out = String::new();
230 let mut capitalize_next = true;
231 for c in raw.chars() {
232 if c.is_alphanumeric() {
233 if capitalize_next {
234 out.extend(c.to_uppercase());
235 capitalize_next = false;
236 } else {
237 out.push(c);
238 }
239 } else {
240 capitalize_next = true;
241 }
242 }
243 if out.is_empty() { "Inst".to_string() } else { out }
244}
245
246/// syn visitor that substitutes a single named type parameter
247/// with a concrete type throughout an item function. Used by
248/// the SRD-80b Phase D1 fanout to produce per-instantiation
249/// copies of a generic-over-Wire function.
250struct TypeSubst {
251 type_param: syn::Ident,
252 concrete: Type,
253}
254
255impl syn::visit_mut::VisitMut for TypeSubst {
256 fn visit_type_mut(&mut self, ty: &mut Type) {
257 if let Type::Path(p) = ty
258 && p.qself.is_none() && p.path.is_ident(&self.type_param) {
259 *ty = self.concrete.clone();
260 return;
261 }
262 syn::visit_mut::visit_type_mut(self, ty);
263 }
264}
265
266// Make NodeAttrs cloneable for the Phase D1 fanout (we need a
267// copy per instantiation; the original parsed-once Attrs is the
268// shared template).
269
270/// Parsed `#[polydat_node(...)]` attribute parameters.
271#[derive(Clone)]
272struct NodeAttrs {
273 /// `FuncCategory` variant name — required (no default).
274 /// Forcing the operator to declare the category keeps the
275 /// `describe` / help / categorization surface coherent.
276 category: Ident,
277 /// SRD-80 PR B.7 — opt out of JIT (Phase-2/Phase-3) emission
278 /// even when the type signature qualifies. Use when body
279 /// has side effects the operator doesn't want JIT-dispatched
280 /// or when hand-written hooks override the macro version.
281 no_jit: bool,
282 /// SRD-80 PR B.7 — override path for `compiled_u64()`. When
283 /// set, the macro emits `compiled_u64(&self) -> Some(Box::new(<path>))`
284 /// instead of building the closure from the body. Free-fn
285 /// signature: `fn(&[u64], &mut [u64])`. Escape hatch for
286 /// hand-tuned SIMD / FFI / unusual carriers.
287 compiled_u64_override: Option<syn::ExprPath>,
288 /// SRD-80 PR B.7 — override path for `jit_constants()`.
289 /// Free-fn signature: `fn(&Node) -> Vec<u64>`. Macro emits
290 /// `jit_constants(&self) -> <path>(self)`.
291 jit_constants_override: Option<syn::ExprPath>,
292 /// SRD-80b Phase F (S18) — `decompose = path`. When set, the
293 /// macro emits `impl FusedNode for <Struct>` whose
294 /// `decomposed(&self)` delegates to the named free function.
295 /// Free-fn signature: `fn(&Self) -> DecomposedGraph`. The
296 /// fusion compiler reaches the equivalent unfused subgraph
297 /// through this path. Operators with bespoke fusion logic
298 /// can still `impl FusedNode` by hand alongside the macro
299 /// emission — the attribute is the canonical sugar for the
300 /// "decompose by calling one free fn" case.
301 decompose: Option<syn::ExprPath>,
302 /// SRD-80 PR B.7 — declared `Purity` (Pure / SideChannel /
303 /// Nondeterministic). Defaults to `Pure` (the trait
304 /// default). Macro emits `fn purity(&self) -> Purity::<expr>`
305 /// when present.
306 ///
307 /// Two attribute forms recognized:
308 ///
309 /// - `purity = Nondeterministic` (path) — emits `Purity::Nondeterministic`.
310 /// - `purity = SideChannel(LogBuffer)` (call) — emits the
311 /// struct-variant form `Purity::SideChannel { sink:
312 /// SideChannelSink::LogBuffer }`. The call-form variant
313 /// makes the struct-variant inline attribute parse-able
314 /// (Rust attribute grammar doesn't accept inline `{ ... }`
315 /// struct literals as attribute values).
316 purity: Option<syn::Expr>,
317 /// SRD-80 PR B.9 — variadic node identity value (the result
318 /// when called with zero inputs). Emitted into
319 /// `FuncSig.identity: Option<u64>`. Required for variadic
320 /// numeric reductions whose group has an identity (sum=0,
321 /// product=1, min=u64::MAX, max=0). Skip for variadics with
322 /// no meaningful identity (str_concat — empty list yields "").
323 identity: Option<syn::Expr>,
324 /// SRD-80 PR B.9 — `Commutativity` variant. Defaults to
325 /// `Positional`. Variadic reductions typically pass
326 /// `AllCommutative` (sum/product/min/max all hold regardless
327 /// of input order).
328 commutativity: Option<Ident>,
329 /// SRD-80 PR B.9 — minimum required wire count for variadic
330 /// nodes. Defaults to 0 (callable with zero inputs).
331 variadic_min: Option<syn::LitInt>,
332 /// SRD-80 PR B.10 — names for the elements of a tuple
333 /// return type, paired positionally with the tuple
334 /// elements. Defaults to `out_0`, `out_1`, ... when
335 /// absent. Length must match tuple arity — operator gets a
336 /// compile error otherwise.
337 output_names: Option<Vec<Ident>>,
338 /// SRD-80b Phase D1 — generic-over-Wire instantiation policy
339 /// (SRD-80b §"Open questions" item 1). For a function
340 /// declared `fn pp<T: Wire>(input: T) -> T`, the macro emits
341 /// one full instantiation per type listed here (per-instance
342 /// struct + impl + NodeRegistration). The DSL function name
343 /// is shared across instantiations; per-instantiation build
344 /// closures guard on `<T as Wire>::PORT` so only the matching
345 /// one claims the call.
346 instantiate: Vec<Type>,
347 /// Adapter canonical-name prefix enforcement. When present
348 /// (`adapter = "cql"`), the macro validates at expansion time
349 /// that the annotated function's name starts with `"<name>_"`,
350 /// keeping adapter-provided nodes namespaced under the
351 /// adapter's registered name. Validation-only for now — not
352 /// threaded into codegen. Absent → core polydat nodes stay
353 /// unprefixed.
354 adapter: Option<String>,
355}
356
357fn parse_attrs(attr: TokenStream2) -> syn::Result<NodeAttrs> {
358 if attr.is_empty() {
359 return Err(syn::Error::new(
360 proc_macro2::Span::call_site(),
361 "#[polydat_node] requires `category = <FuncCategory variant>`. \
362 Example: #[polydat_node(category = Comparison)]",
363 ));
364 }
365
366 let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
367 let items = parser.parse2(attr)?;
368
369 let mut category: Option<Ident> = None;
370 let mut no_jit = false;
371 let mut compiled_u64_override: Option<syn::ExprPath> = None;
372 let mut jit_constants_override: Option<syn::ExprPath> = None;
373 let mut decompose: Option<syn::ExprPath> = None;
374 let mut purity: Option<syn::Expr> = None;
375 let mut identity: Option<syn::Expr> = None;
376 let mut commutativity: Option<Ident> = None;
377 let mut variadic_min: Option<syn::LitInt> = None;
378 let mut output_names: Option<Vec<Ident>> = None;
379 let mut instantiate: Vec<Type> = Vec::new();
380 let mut adapter: Option<String> = None;
381
382 for item in items {
383 match item {
384 Meta::Path(p) => {
385 let key = p.get_ident()
386 .ok_or_else(|| syn::Error::new_spanned(
387 &p,
388 "#[polydat_node] flag keys must be bare identifiers",
389 ))?
390 .clone();
391 match key.to_string().as_str() {
392 "no_jit" => { no_jit = true; }
393 other => {
394 return Err(syn::Error::new_spanned(
395 &key,
396 format!(
397 "#[polydat_node] does not recognize flag `{other}`. \
398 PR B.7 flags: `no_jit`.",
399 ),
400 ));
401 }
402 }
403 }
404 Meta::NameValue(nv) => {
405 let key = nv.path.get_ident()
406 .ok_or_else(|| syn::Error::new_spanned(
407 &nv.path,
408 "#[polydat_node] parameter keys must be bare identifiers",
409 ))?
410 .clone();
411 match key.to_string().as_str() {
412 "category" => {
413 let syn::Expr::Path(p) = &nv.value else {
414 return Err(syn::Error::new_spanned(
415 &nv.value,
416 "`category` value must be a bare identifier \
417 (a polydat `FuncCategory` variant name).",
418 ));
419 };
420 category = Some(p.path.get_ident()
421 .ok_or_else(|| syn::Error::new_spanned(
422 &nv.value,
423 "`category` value must be a single identifier.",
424 ))?
425 .clone());
426 }
427 "compiled_u64" => {
428 let syn::Expr::Path(p) = &nv.value else {
429 return Err(syn::Error::new_spanned(
430 &nv.value,
431 "`compiled_u64` value must be a path to a free \
432 function with signature `fn(&[u64], &mut [u64])`.",
433 ));
434 };
435 compiled_u64_override = Some(p.clone());
436 }
437 "jit_constants" => {
438 let syn::Expr::Path(p) = &nv.value else {
439 return Err(syn::Error::new_spanned(
440 &nv.value,
441 "`jit_constants` value must be a path to a free \
442 function with signature `fn(&Node) -> Vec<u64>`.",
443 ));
444 };
445 jit_constants_override = Some(p.clone());
446 }
447 "decompose" => {
448 let syn::Expr::Path(p) = &nv.value else {
449 return Err(syn::Error::new_spanned(
450 &nv.value,
451 "`decompose` value must be a path to a free \
452 function with signature \
453 `fn(&Self) -> DecomposedGraph`.",
454 ));
455 };
456 decompose = Some(p.clone());
457 }
458 "purity" => {
459 // Accept either:
460 // purity = Nondeterministic (path)
461 // purity = SideChannel(LogBuffer) (call)
462 // The codegen dispatches on the shape.
463 match &nv.value {
464 syn::Expr::Path(_) | syn::Expr::Call(_) => {
465 purity = Some(nv.value.clone());
466 }
467 _ => {
468 return Err(syn::Error::new_spanned(
469 &nv.value,
470 "`purity` value must be a Purity variant: \
471 `Pure`, `Nondeterministic`, or \
472 `SideChannel(<sink>)` where `<sink>` is a \
473 `SideChannelSink` variant ident.",
474 ));
475 }
476 }
477 }
478 "identity" => {
479 // SRD-80 PR B.9 — variadic identity element.
480 // Any constant-evaluable expression is fine.
481 identity = Some(nv.value.clone());
482 }
483 "commutativity" => {
484 let syn::Expr::Path(p) = &nv.value else {
485 return Err(syn::Error::new_spanned(
486 &nv.value,
487 "`commutativity` value must be a `Commutativity` \
488 variant ident (Positional / AllCommutative / ...).",
489 ));
490 };
491 commutativity = Some(p.path.get_ident()
492 .ok_or_else(|| syn::Error::new_spanned(
493 &nv.value,
494 "`commutativity` value must be a single identifier.",
495 ))?
496 .clone());
497 }
498 "variadic_min" => {
499 let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Int(n), .. }) = &nv.value else {
500 return Err(syn::Error::new_spanned(
501 &nv.value,
502 "`variadic_min` value must be an integer literal.",
503 ));
504 };
505 variadic_min = Some(n.clone());
506 }
507 "adapter" => {
508 // Canonical-name prefix enforcement. The value is
509 // the adapter's registered name; the node function
510 // name must start with `<name>_` (validated in the
511 // macro entry point where the fn ident is in hand).
512 let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) = &nv.value else {
513 return Err(syn::Error::new_spanned(
514 &nv.value,
515 "`adapter` value must be a string literal \
516 (the adapter's canonical registered name, \
517 e.g. `adapter = \"cql\"`).",
518 ));
519 };
520 adapter = Some(s.value());
521 }
522 other => {
523 return Err(syn::Error::new_spanned(
524 &key,
525 format!(
526 "#[polydat_node] does not recognize parameter `{other}`. \
527 PR B.2 keys: `category = ...`. PR B.7 keys: \
528 `no_jit`, `compiled_u64 = ...`, \
529 `jit_constants = ...`, `purity = ...`. \
530 PR B.9 keys: `identity = ...`, \
531 `commutativity = ...`, `variadic_min = ...`. \
532 Namespacing: `adapter = \"...\"`.",
533 ),
534 ));
535 }
536 }
537 }
538 Meta::List(list) => {
539 let key = list.path.get_ident()
540 .ok_or_else(|| syn::Error::new_spanned(
541 &list.path,
542 "#[polydat_node] list-form keys must be bare identifiers",
543 ))?
544 .clone();
545 match key.to_string().as_str() {
546 "output_names" => {
547 let names: Punctuated<Ident, Token![,]> =
548 list.parse_args_with(Punctuated::parse_terminated)?;
549 if names.is_empty() {
550 return Err(syn::Error::new_spanned(
551 &list,
552 "`output_names(...)` requires at least one name.",
553 ));
554 }
555 output_names = Some(names.into_iter().collect());
556 }
557 "instantiate" => {
558 let types: Punctuated<Type, Token![,]> =
559 list.parse_args_with(Punctuated::parse_terminated)?;
560 if types.is_empty() {
561 return Err(syn::Error::new_spanned(
562 &list,
563 "`instantiate(...)` requires at least one type. \
564 List the concrete `Wire`-impl types that should \
565 get their own per-instantiation registrations.",
566 ));
567 }
568 instantiate = types.into_iter().collect();
569 }
570 other => {
571 return Err(syn::Error::new_spanned(
572 &key,
573 format!(
574 "#[polydat_node] does not recognize list-form key `{other}`. \
575 Recognised: `output_names(...)`, `instantiate(...)`.",
576 ),
577 ));
578 }
579 }
580 }
581 }
582 }
583
584 let category = category.ok_or_else(|| syn::Error::new(
585 proc_macro2::Span::call_site(),
586 "#[polydat_node] requires `category = <FuncCategory variant>`.",
587 ))?;
588
589 Ok(NodeAttrs {
590 category,
591 no_jit,
592 compiled_u64_override,
593 jit_constants_override,
594 decompose,
595 purity,
596 identity,
597 commutativity,
598 variadic_min,
599 output_names,
600 instantiate,
601 adapter,
602 })
603}
604
605/// One classified function argument. Drives every downstream
606/// piece of the generated output: NodeMeta slot, FuncSig
607/// param, struct field (for consts), build closure const
608/// extraction, eval-time wrapper construction.
609struct ClassifiedArg {
610 name: syn::Ident,
611 /// Original Rust type from the function signature.
612 declared_ty: Type,
613 /// Whether the arg was declared as `Const<T>`.
614 kind: ArgKind,
615 /// For const args: optional default value expression parsed
616 /// from `#[poly_default(VAL)]`. Present → the const is
617 /// optional in FuncSig and the build closure falls back to
618 /// the default when the consts slice doesn't supply one.
619 default_value: Option<syn::Expr>,
620 /// SRD-80 PR B.14 — `#[constraint(<Variant>)]` on a wire
621 /// arg. The variant name maps to `ConstConstraint::*`; the
622 /// emitted `Port` carries the constraint so strict-wire
623 /// mode can auto-insert upstream assertion nodes.
624 wire_constraint: Option<Ident>,
625}
626
627#[derive(Clone)]
628enum ArgKind {
629 Wire,
630 Const(ConstShape),
631 /// SRD-80b Phase C — `Const<Vec<C>>` workload-list const.
632 /// Inner ConstShape gives the element type (u64/f64/bool/Str).
633 /// The macro emits ONE ParamSpec in the FuncSig with the
634 /// inner element's slot type, sets `Arity::VariadicConsts`,
635 /// and at build time collects every matching ConstArg from
636 /// the tail of `consts[..]` into a `Vec<inner>` field.
637 /// Eval hands the body a `Const(self.field.clone())`.
638 ConstVec(ConstShape),
639 /// `&T` argument with `#[poly_setup(<fn_path>, from = <arg>)]`.
640 /// Generates a struct field of type `T`, computed once in
641 /// `new()` by calling `<fn_path>(<source>)` where `<source>`
642 /// is the field-access expression for the named `from` arg.
643 /// Boxed: `SetupSpec` is ~424 bytes, dwarfing the other
644 /// variants — indirection keeps `ArgKind` small.
645 Setup(Box<SetupSpec>),
646 /// SRD-80 PR B.8 — `Value` argument. Polymorphic wire whose
647 /// port type is resolved at construction (`new()` takes a
648 /// runtime `PortType`). Body sees a cloned `Value`; eval
649 /// box/unboxes via the trivial `FromValue<Value>` impl.
650 /// Triggers `OutputType::SameAsInput(<this idx>)` when the
651 /// return type is also `Value`.
652 PolyWire,
653 /// SRD-80 PR B.9 — `&[T]` argument (variadic wire). Construction
654 /// is runtime-arity (`new(n_wires)`); the macro emits N wire
655 /// slots, an `Arity::VariadicWires { min_wires }` FuncSig
656 /// entry, and a `variadic_ctor` thunk that builds with `n`
657 /// at compile time.
658 Variadic(VariadicElement),
659}
660
661/// Element type of a `&[T]` variadic arg. Determines the
662/// per-element port type, whether the node stays JIT-eligible,
663/// and how `eval()` materialises the slice for the body call.
664#[derive(Clone, Copy, PartialEq, Eq)]
665enum VariadicElement {
666 U64,
667 /// Reserved: `&[f64]` now classifies as the `VecF64` vector
668 /// wire (see `classify_variadic`), so this variant is no longer
669 /// constructed — kept for the port-type / extract match arms and
670 /// a future explicit `Variadic<f64>` spelling.
671 #[allow(dead_code)]
672 F64,
673 Bool,
674 BorrowedStr,
675 OwnedString,
676 /// `&[Value]` — polymorphic per-element type. The body sees
677 /// each element as the polydat runtime carrier; type
678 /// inspection / coercion is the body's responsibility.
679 Value,
680}
681
682impl VariadicElement {
683 fn port_type_tokens(self) -> TokenStream2 {
684 // For Value variadics we declare the per-slot port type
685 // as Str (the most common stringy use case — printf,
686 // str_concat). The body deals with type coercion via
687 // its own dispatch on the Value variant.
688 match self {
689 VariadicElement::U64 => quote!(polydat::ast::PortType::U64),
690 VariadicElement::F64 => quote!(polydat::ast::PortType::F64),
691 VariadicElement::Bool => quote!(polydat::ast::PortType::Bool),
692 VariadicElement::BorrowedStr => quote!(polydat::ast::PortType::Str),
693 VariadicElement::OwnedString => quote!(polydat::ast::PortType::Str),
694 VariadicElement::Value => quote!(polydat::ast::PortType::Str),
695 }
696 }
697
698 /// Expression that converts a single `&Value` to the body's
699 /// element type. Used to build the per-call slice in eval().
700 fn extract_from_value(self) -> TokenStream2 {
701 match self {
702 VariadicElement::U64 => quote!(|v: &polydat::ast::Value| v.as_u64()),
703 VariadicElement::F64 => quote!(|v: &polydat::ast::Value| v.as_f64()),
704 VariadicElement::Bool => quote!(|v: &polydat::ast::Value| v.as_bool()),
705 VariadicElement::BorrowedStr => quote!(|v: &polydat::ast::Value| v.as_str()),
706 VariadicElement::OwnedString => quote!(|v: &polydat::ast::Value| v.as_str().to_string()),
707 VariadicElement::Value => quote!(|v: &polydat::ast::Value| v.clone()),
708 }
709 }
710}
711
712#[derive(Clone)]
713struct SetupSpec {
714 /// `T` — the type the field stores (inner type of `&T`).
715 inner_ty: Type,
716 /// Operator-provided constructor path, e.g.
717 /// `ParsedPattern::from_pattern`.
718 setup_fn: syn::Expr,
719 /// Names of the const args whose field-values are passed to
720 /// `setup_fn`. Empty when declared as `from = ()` — the
721 /// setup fn takes no arguments and captures session-static
722 /// state (env, system clock, etc.). Length 1 for the common
723 /// single-source case (`from = ident`); length N for
724 /// multi-source `from = (a, b, c)` per SRD-80b amendment.
725 source_args: Vec<syn::Ident>,
726}
727
728#[derive(Clone, Copy, PartialEq, Eq)]
729enum ConstShape {
730 U64,
731 F64,
732 Bool,
733 Str,
734}
735
736impl ConstShape {
737 /// Token stream for the `SlotType::Const*` variant.
738 fn slot_type_tokens(self) -> TokenStream2 {
739 match self {
740 ConstShape::U64 => quote!(polydat::ast::SlotType::ConstU64),
741 ConstShape::F64 => quote!(polydat::ast::SlotType::ConstF64),
742 ConstShape::Bool => quote!(polydat::ast::SlotType::ConstU64),
743 ConstShape::Str => quote!(polydat::ast::SlotType::ConstStr),
744 }
745 }
746
747 /// Token stream for the struct field type that stores the
748 /// captured const value. `Const<&str>` → `String` (owned
749 /// backing store). Other shapes are Copy and stored
750 /// directly.
751 fn field_type_tokens(self) -> TokenStream2 {
752 match self {
753 ConstShape::U64 => quote!(u64),
754 ConstShape::F64 => quote!(f64),
755 ConstShape::Bool => quote!(bool),
756 ConstShape::Str => quote!(String),
757 }
758 }
759
760 /// Token stream that extracts a value from a `ConstArg`.
761 /// `c` is the `ConstArg` binding in scope at the call site.
762 fn extract_from_const_arg(self, c: TokenStream2) -> TokenStream2 {
763 match self {
764 ConstShape::U64 => quote!(#c.as_u64()),
765 ConstShape::F64 => quote!(#c.as_f64()),
766 ConstShape::Bool => quote!(#c.as_u64() != 0),
767 ConstShape::Str => quote!(#c.as_str().to_string()),
768 }
769 }
770
771 /// Token stream that wraps a struct-field expression as
772 /// `Const<T>` for handoff into the user's function body.
773 /// `field_ref` is the borrow / value expression for the
774 /// stored field (e.g. `&self.pattern` or `self.seed`).
775 fn wrap_as_const(self, field_ref: TokenStream2) -> TokenStream2 {
776 match self {
777 ConstShape::U64 => quote!(polydat::derive_support::Const(#field_ref)),
778 ConstShape::F64 => quote!(polydat::derive_support::Const(#field_ref)),
779 ConstShape::Bool => quote!(polydat::derive_support::Const(#field_ref)),
780 ConstShape::Str => quote!(polydat::derive_support::Const(#field_ref.as_str())),
781 }
782 }
783}
784
785/// SRD-80 PR B.7 — primitive types that fit the JIT u64 buffer.
786/// A node is Phase-2 eligible iff every wire arg / const arg /
787/// return type maps to a `JitType` and no `Setup<T>` arg is
788/// declared (Setup carries non-primitive derived state).
789#[derive(Clone, Copy, PartialEq, Eq)]
790enum JitType {
791 U64,
792 I64,
793 F64,
794 Bool,
795 // Narrow widths (alignment §8.1): each rides the u64 slot per
796 // its Wire storage convention — unsigned zero-extended, signed
797 // sign-extended (through the i64 carrier), floats bit-stuffed.
798 // The variant carries enough width information for the buffer
799 // read/write tokens to emit the exact narrowing/widening casts.
800 U8,
801 U16,
802 U32,
803 I8,
804 I16,
805 I32,
806 F32,
807 F16,
808 Str,
809 Bytes,
810 // Two-slot values (alignment §8.4 layer 1): 128-bit integers
811 // and register words ride two consecutive u64 slots in
812 // little-endian limb order, reconstructed through
813 // `polydat::ast::Bits128`.
814 U128,
815 I128,
816 RegRaw,
817 RegI8x16,
818 RegI16x8,
819 RegI32x4,
820 RegI64x2,
821 RegF16x8,
822 RegF32x4,
823 RegF64x2,
824}
825
826impl JitType {
827 /// Buffer slots this carrier occupies (alignment §8.4 layer
828 /// 1): 1 for everything riding a single u64; 2 for 128-bit
829 /// values (limb pairs).
830 fn width(self) -> usize {
831 match self {
832 JitType::U128 | JitType::I128 | JitType::RegRaw
833 | JitType::RegI8x16 | JitType::RegI16x8 | JitType::RegI32x4
834 | JitType::RegI64x2 | JitType::RegF16x8 | JitType::RegF32x4
835 | JitType::RegF64x2 => 2,
836 _ => 1,
837 }
838 }
839
840 /// Tokens reading a typed value from the Phase-2 u64 buffer
841 /// at slot offset `idx` (the prefix sum of the widths of all
842 /// preceding wire args). f64/bool are bit-reinterpreted from
843 /// the u64 carrier (the buffer-level convention shared with
844 /// every existing hand-written `compiled_u64`); two-slot
845 /// values reassemble through `Bits128`.
846 fn read_from_u64_buffer(self, idx: usize) -> TokenStream2 {
847 let i = syn::Index::from(idx);
848 let i1 = syn::Index::from(idx + 1);
849 let limbs = quote!(polydat::ast::Bits128([inputs[#i], inputs[#i1]]));
850 match self {
851 JitType::U64 => quote!(inputs[#i]),
852 JitType::I64 => quote!(inputs[#i] as i64),
853 JitType::F64 => quote!(f64::from_bits(inputs[#i])),
854 JitType::Bool => quote!(inputs[#i] != 0),
855 JitType::U8 => quote!(inputs[#i] as u8),
856 JitType::U16 => quote!(inputs[#i] as u16),
857 JitType::U32 => quote!(inputs[#i] as u32),
858 JitType::I8 => quote!((inputs[#i] as i64) as i8),
859 JitType::I16 => quote!((inputs[#i] as i64) as i16),
860 JitType::I32 => quote!((inputs[#i] as i64) as i32),
861 JitType::F32 => quote!(f32::from_bits(inputs[#i] as u32)),
862 JitType::F16 => quote!(polydat::half::f16::from_bits(inputs[#i] as u16)),
863 JitType::Str => quote!(polydat::kernel::resolve_thread_str(inputs[#i]).into()),
864 JitType::Bytes => quote!(polydat::kernel::resolve_thread_bytes(inputs[#i]).into()),
865 JitType::U128 => quote!((#limbs).as_u128()),
866 JitType::I128 => quote!((#limbs).as_i128()),
867 JitType::RegRaw => limbs,
868 JitType::RegI8x16 => quote!((#limbs).lanes_i8()),
869 JitType::RegI16x8 => quote!((#limbs).lanes_i16()),
870 JitType::RegI32x4 => quote!((#limbs).lanes_i32()),
871 JitType::RegI64x2 => quote!((#limbs).lanes_i64()),
872 JitType::RegF16x8 => quote!((#limbs).lanes_f16()),
873 JitType::RegF32x4 => quote!((#limbs).lanes_f32()),
874 JitType::RegF64x2 => quote!((#limbs).lanes_f64()),
875 }
876 }
877
878 /// Tokens writing a typed value into the Phase-2 u64 output
879 /// buffer at slot offset `base`. Inverse of the read.
880 fn write_to_u64_buffer_at(self, base: usize, result: TokenStream2) -> TokenStream2 {
881 let o = syn::Index::from(base);
882 let o1 = syn::Index::from(base + 1);
883 let write_limbs = |from: TokenStream2| {
884 quote! {{
885 let __limbs = #from;
886 outputs[#o] = __limbs.0[0];
887 outputs[#o1] = __limbs.0[1];
888 }}
889 };
890 match self {
891 JitType::U64 => quote!(outputs[#o] = #result;),
892 JitType::I64 => quote!(outputs[#o] = (#result) as u64;),
893 JitType::F64 => quote!(outputs[#o] = (#result).to_bits();),
894 JitType::Bool => quote!(outputs[#o] = if #result { 1 } else { 0 };),
895 JitType::U8 | JitType::U16 | JitType::U32
896 => quote!(outputs[#o] = (#result) as u64;),
897 JitType::I8 | JitType::I16 | JitType::I32
898 => quote!(outputs[#o] = ((#result) as i64) as u64;),
899 JitType::F32 => quote!(outputs[#o] = (#result).to_bits() as u64;),
900 JitType::F16 => quote!(outputs[#o] = (#result).to_bits() as u64;),
901 JitType::Str => quote!(outputs[#o] = polydat::kernel::put_thread_str((#result).as_ref());),
902 JitType::Bytes => quote!(outputs[#o] = polydat::kernel::put_thread_bytes((#result).as_ref());),
903 JitType::U128 => write_limbs(quote!(polydat::ast::Bits128::from_u128(#result))),
904 JitType::I128 => write_limbs(quote!(polydat::ast::Bits128::from_i128(#result))),
905 JitType::RegRaw => write_limbs(quote!(#result)),
906 JitType::RegI8x16 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_i8(#result))),
907 JitType::RegI16x8 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_i16(#result))),
908 JitType::RegI32x4 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_i32(#result))),
909 JitType::RegI64x2 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_i64(#result))),
910 JitType::RegF16x8 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_f16(#result))),
911 JitType::RegF32x4 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_f32(#result))),
912 JitType::RegF64x2 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_f64(#result))),
913 }
914 }
915
916 /// Single-return write at offset 0.
917 fn write_to_u64_buffer(self, result: TokenStream2) -> TokenStream2 {
918 self.write_to_u64_buffer_at(0, result)
919 }
920
921 /// Tokens encoding the captured Copy value of a const field
922 /// as a `u64` for `jit_constants()` (Phase-3 classifier).
923 fn const_field_as_u64(self, field_ref: TokenStream2) -> TokenStream2 {
924 match self {
925 JitType::U64 => quote!(#field_ref),
926 JitType::I64 => quote!((#field_ref) as u64),
927 JitType::F64 => quote!((#field_ref).to_bits()),
928 JitType::Bool => quote!(if #field_ref { 1 } else { 0 }),
929 JitType::U8 | JitType::U16 | JitType::U32
930 => quote!((#field_ref) as u64),
931 JitType::I8 | JitType::I16 | JitType::I32
932 => quote!(((#field_ref) as i64) as u64),
933 JitType::F32 | JitType::F16
934 => quote!((#field_ref).to_bits() as u64),
935 JitType::Str | JitType::Bytes
936 => quote!(polydat::kernel::StaticInterner::intern((#field_ref).as_ref())),
937 // ConstShape has no 128-bit / register forms, so these
938 // never appear in const position.
939 JitType::U128 | JitType::I128 | JitType::RegRaw
940 | JitType::RegI8x16 | JitType::RegI16x8 | JitType::RegI32x4
941 | JitType::RegI64x2 | JitType::RegF16x8 | JitType::RegF32x4
942 | JitType::RegF64x2 => {
943 unreachable!("128-bit/register types have no const shape")
944 }
945 }
946 }
947}
948
949/// Map a `ConstShape` to its JIT-compatible primitive carrier,
950/// or `None` if the shape can't live in the u64 buffer.
951fn const_shape_to_jit_type(s: ConstShape) -> Option<JitType> {
952 match s {
953 ConstShape::U64 => Some(JitType::U64),
954 ConstShape::F64 => Some(JitType::F64),
955 ConstShape::Bool => Some(JitType::Bool),
956 ConstShape::Str => Some(JitType::Str),
957 }
958}
959
960/// Map a wire arg's declared Rust type to its JIT carrier, or
961/// `None` for types that can't fit in the buffer.
962fn wire_type_to_jit_type(ty: &Type) -> Option<JitType> {
963 let s = type_to_string(ty);
964 match s.as_str() {
965 "u64" => Some(JitType::U64),
966 "i64" => Some(JitType::I64),
967 "f64" => Some(JitType::F64),
968 "bool" => Some(JitType::Bool),
969 "u8" => Some(JitType::U8),
970 "u16" => Some(JitType::U16),
971 "u32" => Some(JitType::U32),
972 "i8" => Some(JitType::I8),
973 "i16" => Some(JitType::I16),
974 "i32" => Some(JitType::I32),
975 "f32" => Some(JitType::F32),
976 "u128" => Some(JitType::U128),
977 "i128" => Some(JitType::I128),
978 "String" | "& str" | "&str" => Some(JitType::Str),
979 "Vec < u8 >" | "Vec<u8>" | "& [ u8 ]" | "&[u8]" => Some(JitType::Bytes),
980 // type_to_string joins every token with a space
981 // (`half : : f16`, `[ f32 ; 4 ]`), so the path/array
982 // forms compare whitespace-stripped. The two-slot types
983 // ride limb pairs per alignment §8.4 layer 1.
984 _ => {
985 let flat: String = s.split_whitespace().collect();
986 match flat.as_str() {
987 "Arc<str>" | "std::sync::Arc<str>" => Some(JitType::Str),
988 "Arc<[u8]>" | "std::sync::Arc<[u8]>" => Some(JitType::Bytes),
989 "half::f16" | "f16" => Some(JitType::F16),
990 "Bits128" | "crate::ast::Bits128" | "polydat::ast::Bits128"
991 | "ast::Bits128" => Some(JitType::RegRaw),
992 "[i8;16]" => Some(JitType::RegI8x16),
993 "[i16;8]" => Some(JitType::RegI16x8),
994 "[i32;4]" => Some(JitType::RegI32x4),
995 "[i64;2]" => Some(JitType::RegI64x2),
996 "[half::f16;8]" | "[f16;8]" => Some(JitType::RegF16x8),
997 "[f32;4]" => Some(JitType::RegF32x4),
998 "[f64;2]" => Some(JitType::RegF64x2),
999 _ => None,
1000 }
1001 }
1002 }
1003}
1004
1005/// Detect `Const<T>` in arg-type position. Returns `Some(shape)`
1006/// for recognized inner types; `None` for bare types (wire) or
1007/// unrecognized shapes. The recognition is structural — matches
1008/// the last segment of the path as `Const` with a single
1009/// generic argument resolving to a primitive type the macro
1010/// supports.
1011fn classify_type(ty: &Type) -> Option<ConstShape> {
1012 let syn::Type::Path(p) = ty else { return None; };
1013 let last = p.path.segments.last()?;
1014 if last.ident != "Const" { return None; }
1015 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1016 let inner = args.args.iter().find_map(|a| {
1017 if let syn::GenericArgument::Type(t) = a { Some(t) } else { None }
1018 })?;
1019 let s = type_to_string(inner);
1020 match s.as_str() {
1021 "u64" => Some(ConstShape::U64),
1022 "f64" => Some(ConstShape::F64),
1023 "bool" => Some(ConstShape::Bool),
1024 "& str" | "&str" => Some(ConstShape::Str),
1025 _ => None,
1026 }
1027}
1028
1029/// SRD-80b Phase C — detect `Const<Vec<T>>` in arg position.
1030/// Returns the inner element shape on match. Distinct path
1031/// from [`classify_type`]: the macro recognises the variadic-
1032/// const shape before the scalar `Const<T>` shape, so a
1033/// signature using `Const<Vec<u64>>` doesn't get misclassified.
1034fn classify_const_vec(ty: &Type) -> Option<ConstShape> {
1035 // Outer must be Const<...>.
1036 let syn::Type::Path(p) = ty else { return None; };
1037 let last = p.path.segments.last()?;
1038 if last.ident != "Const" { return None; }
1039 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1040 let inner = args.args.iter().find_map(|a| {
1041 if let syn::GenericArgument::Type(t) = a { Some(t) } else { None }
1042 })?;
1043 // Inner must be Vec<X>.
1044 let syn::Type::Path(vp) = inner else { return None; };
1045 let vlast = vp.path.segments.last()?;
1046 if vlast.ident != "Vec" { return None; }
1047 let syn::PathArguments::AngleBracketed(vargs) = &vlast.arguments else { return None; };
1048 let velem = vargs.args.iter().find_map(|a| {
1049 if let syn::GenericArgument::Type(t) = a { Some(t) } else { None }
1050 })?;
1051 let s = type_to_string(velem);
1052 match s.as_str() {
1053 "u64" => Some(ConstShape::U64),
1054 "f64" => Some(ConstShape::F64),
1055 "bool" => Some(ConstShape::Bool),
1056 "String" => Some(ConstShape::Str),
1057 "& str" | "&str" => Some(ConstShape::Str),
1058 _ => None,
1059 }
1060}
1061
1062/// SRD-80b dynamic-output shape — detect
1063/// `DynamicOutputs<T>` in return position. Returns the inner
1064/// element type `T` on match. The macro pairs this with the
1065/// function's `Const<Vec<C>>` arg to compute the output port
1066/// count at construction time.
1067fn classify_dynamic_outputs(ty: &Type) -> Option<Type> {
1068 let syn::Type::Path(p) = ty else { return None; };
1069 let last = p.path.segments.last()?;
1070 if last.ident != "DynamicOutputs" { return None; }
1071 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1072 args.args.iter().find_map(|a| {
1073 if let syn::GenericArgument::Type(t) = a { Some(t.clone()) } else { None }
1074 })
1075}
1076
1077/// Extract a `#[poly_default(EXPR)]` attribute from an arg's
1078/// outer attributes, if present. Returns the inner expression
1079/// token stream so the build closure can use it as the
1080/// fallback when the runtime `consts` slice is shorter than
1081/// the declared param list.
1082fn parse_poly_default(attrs: &[syn::Attribute]) -> syn::Result<Option<syn::Expr>> {
1083 for attr in attrs {
1084 if !attr.path().is_ident("poly_default") { continue; }
1085 let expr: syn::Expr = attr.parse_args()?;
1086 return Ok(Some(expr));
1087 }
1088 Ok(None)
1089}
1090
1091/// Extract a `#[constraint(<Variant>)]` attribute. SRD-80 PR
1092/// B.14 — wire-arg constraint metadata. The variant name
1093/// matches `ConstConstraint::*` (e.g. `NonZeroU64`,
1094/// `PositiveFiniteF64`). Strict-wire mode reads this metadata
1095/// to auto-insert assertion nodes upstream.
1096fn parse_wire_constraint(attrs: &[syn::Attribute]) -> syn::Result<Option<Ident>> {
1097 for attr in attrs {
1098 if !attr.path().is_ident("constraint") { continue; }
1099 let variant: Ident = attr.parse_args()?;
1100 return Ok(Some(variant));
1101 }
1102 Ok(None)
1103}
1104
1105/// Extract a `#[poly_const(<fn_expr>, from = <source>)]` attribute
1106/// from an arg's outer attributes, if present. Returns the
1107/// constructor expression and the source identifiers.
1108///
1109/// SRD-80b — `from` accepts three shapes:
1110/// - `from = ()` — empty source. Setup fn takes no args;
1111/// captures session-static state (env, system clock).
1112/// - `from = ident` — single source. Setup fn called as
1113/// `setup_fn(ident_value)`.
1114/// - `from = (a, b, c)` — multi-source (SRD-80b amendment).
1115/// Setup fn called as `setup_fn(a_value, b_value, c_value)`.
1116/// Order matches the tuple. Each name must reference a
1117/// `Const<T>` arg declared in the same function signature.
1118fn parse_poly_const(attrs: &[syn::Attribute]) -> syn::Result<Option<(syn::Expr, Vec<syn::Ident>)>> {
1119 for attr in attrs {
1120 if !attr.path().is_ident("poly_const") { continue; }
1121 let parser = |input: syn::parse::ParseStream| -> syn::Result<(syn::Expr, Vec<syn::Ident>)> {
1122 let fn_expr: syn::Expr = input.parse()?;
1123 let _comma: Token![,] = input.parse()?;
1124 let from_kw: syn::Ident = input.parse()?;
1125 if from_kw != "from" {
1126 return Err(syn::Error::new_spanned(
1127 from_kw,
1128 "#[poly_const(...)] requires a `from = <source>` clause. \
1129 Supported shapes: `from = ()` (empty), `from = ident` \
1130 (single), `from = (a, b, c)` (multi-source).",
1131 ));
1132 }
1133 let _eq: Token![=] = input.parse()?;
1134 // Parenthesised forms: `from = ()` or `from = (a, b, c)`.
1135 if input.peek(syn::token::Paren) {
1136 let inner;
1137 let _paren = syn::parenthesized!(inner in input);
1138 if inner.is_empty() {
1139 return Ok((fn_expr, Vec::new()));
1140 }
1141 let parsed: Punctuated<syn::Ident, Token![,]> =
1142 Punctuated::parse_terminated(&inner)?;
1143 if parsed.is_empty() {
1144 return Err(syn::Error::new_spanned(
1145 from_kw,
1146 "#[poly_const(..., from = (...))] — the parenthesised \
1147 form expects a comma-separated list of source-arg \
1148 identifiers, or an empty `()` for session-static \
1149 setup.",
1150 ));
1151 }
1152 return Ok((fn_expr, parsed.into_iter().collect()));
1153 }
1154 // Bare `from = ident` — single source.
1155 let source: syn::Ident = input.parse()?;
1156 Ok((fn_expr, vec![source]))
1157 };
1158 let parsed = attr.parse_args_with(parser)?;
1159 return Ok(Some(parsed));
1160 }
1161 Ok(None)
1162}
1163
1164/// Detect `&T` for some `T` in arg-type position. Returns
1165/// `Some(inner_t)` on match, `None` otherwise. Used for the
1166/// PR B.6 setup-arg dispatch.
1167fn classify_borrowed(ty: &Type) -> Option<Type> {
1168 let syn::Type::Reference(r) = ty else { return None; };
1169 if r.mutability.is_some() { return None; }
1170 Some((*r.elem).clone())
1171}
1172
1173/// Detect `Value` in arg-type position. SRD-80 PR B.8 —
1174/// polymorphic wire dispatch. Matches the last path segment
1175/// being `Value`, so both `Value` and `polydat::ast::Value`
1176/// (and any other fully-qualified path ending in `Value`) work.
1177fn classify_polywire(ty: &Type) -> bool {
1178 let syn::Type::Path(p) = ty else { return false; };
1179 p.path.segments.last().map(|s| s.ident == "Value").unwrap_or(false)
1180}
1181
1182/// SRD-80 PR B.11/B.13 — structural classifier for the
1183/// wrapper-typed wire arg shapes. Returns the matching wire
1184/// kind, or `None` if the type isn't one of the recognised
1185/// wrapper shapes.
1186#[derive(Clone, Copy, PartialEq, Eq)]
1187enum WrapperWire {
1188 Bytes,
1189 Json,
1190 /// `Arc<T>` for some T that isn't `[u8]` or `serde_json::Value`.
1191 /// Inline-downcast in arg_bindings; inline-upcast in
1192 /// result_to_outputs. Handle dispatch.
1193 Handle,
1194 /// One of the seven typed vector variants: `VecF32` / `VecI32`
1195 /// / `VecF64` / `VecI64` / `VecF16` / `VecI16` / `VecI8`. The
1196 /// macro emits the matching `PortType::Vec*`; the Wire impls in
1197 /// derive_support are autogenerated from a macro_rules!
1198 /// expansion per element type.
1199 VecF32, VecI32, VecF64, VecI64, VecF16, VecI16, VecI8,
1200}
1201
1202fn classify_wrapper_wire(ty: &Type) -> Option<WrapperWire> {
1203 // SRD-80 PR B.13 — typed vectors. Check first to catch
1204 // `Vec<f32>` etc. before they fall into Handle territory
1205 // (which is the catch-all for Arc<T>).
1206 if let Some(kind) = classify_vec_wire(ty) {
1207 return Some(kind);
1208 }
1209
1210 // `Arc<[u8]>` — Arc with [u8] generic.
1211 if let Some(inner) = strip_arc(ty)
1212 && let syn::Type::Slice(slc) = inner
1213 && let syn::Type::Path(p) = &*slc.elem
1214 && p.path.is_ident("u8")
1215 {
1216 return Some(WrapperWire::Bytes);
1217 }
1218 // `Arc<serde_json::Value>` / `Arc<Value>` (last segment).
1219 if let Some(inner) = strip_arc(ty)
1220 && let syn::Type::Path(p) = inner
1221 && last_segment_is(p, "Value")
1222 && path_contains_segment(p, "serde_json")
1223 {
1224 return Some(WrapperWire::Json);
1225 }
1226 // `Arc<str>` — Str port via the dedicated Wire impl. Don't
1227 // route through Handle (str isn't Sized so the Handle's
1228 // `Value::handle<T: Sized>` constructor would reject it).
1229 if let Some(inner) = strip_arc(ty)
1230 && let syn::Type::Path(p) = inner
1231 && p.path.is_ident("str")
1232 {
1233 return None;
1234 }
1235 // `Arc<dyn Any + Send + Sync>` — Handle via the dedicated
1236 // Wire impl. Fall through to trait dispatch rather than
1237 // the structural Handle path (which expects a concrete
1238 // Arc<ConcreteT> for the downcast).
1239 if let Some(inner) = strip_arc(ty)
1240 && matches!(inner, syn::Type::TraitObject(_))
1241 {
1242 return None;
1243 }
1244 // Any other `Arc<T>` is a Handle.
1245 if strip_arc(ty).is_some() {
1246 return Some(WrapperWire::Handle);
1247 }
1248 // `Vec<u8>`.
1249 if let syn::Type::Path(p) = ty
1250 && let Some(last) = p.path.segments.last()
1251 && last.ident == "Vec"
1252 && let syn::PathArguments::AngleBracketed(args) = &last.arguments
1253 && let Some(syn::GenericArgument::Type(syn::Type::Path(elem))) = args.args.first()
1254 && elem.path.is_ident("u8")
1255 {
1256 return Some(WrapperWire::Bytes);
1257 }
1258 // `&[u8]` — borrowed bytes.
1259 if let syn::Type::Reference(r) = ty
1260 && r.mutability.is_none()
1261 && let syn::Type::Slice(slc) = &*r.elem
1262 && let syn::Type::Path(p) = &*slc.elem
1263 && p.path.is_ident("u8")
1264 {
1265 return Some(WrapperWire::Bytes);
1266 }
1267 // `&serde_json::Value`.
1268 if let syn::Type::Reference(r) = ty
1269 && r.mutability.is_none()
1270 && let syn::Type::Path(p) = &*r.elem
1271 && last_segment_is(p, "Value")
1272 && path_contains_segment(p, "serde_json")
1273 {
1274 return Some(WrapperWire::Json);
1275 }
1276 None
1277}
1278
1279fn strip_arc(ty: &Type) -> Option<&Type> {
1280 let syn::Type::Path(p) = ty else { return None; };
1281 let last = p.path.segments.last()?;
1282 if last.ident != "Arc" { return None; }
1283 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1284 args.args.iter().find_map(|a| match a {
1285 syn::GenericArgument::Type(t) => Some(t),
1286 _ => None,
1287 })
1288}
1289
1290fn last_segment_is(p: &syn::TypePath, name: &str) -> bool {
1291 p.path.segments.last().map(|s| s.ident == name).unwrap_or(false)
1292}
1293
1294fn path_contains_segment(p: &syn::TypePath, name: &str) -> bool {
1295 p.path.segments.iter().any(|s| s.ident == name)
1296}
1297
1298/// For a `Handle` arg, extract the inner T (the downcast target).
1299fn extract_handle_inner(ty: &Type) -> Option<Type> {
1300 strip_arc(ty).cloned()
1301}
1302
1303/// `Option<T>` recognition. Returns `true` if the type's last
1304/// path segment is `Option` with a single generic argument. Used
1305/// to decide whether to auto-emit `accepts_none_inputs() -> true`
1306/// — the runtime kernel's SRD-74 Rule 1 short-circuits `Value::None`
1307/// inputs on opt-in nodes; `Option<T>` wires are the canonical
1308/// opt-in shape.
1309fn is_option_arg(ty: &Type) -> bool {
1310 let syn::Type::Path(p) = ty else { return false; };
1311 let Some(last) = p.path.segments.last() else { return false; };
1312 if last.ident != "Option" { return false; }
1313 matches!(&last.arguments,
1314 syn::PathArguments::AngleBracketed(args)
1315 if args.args.iter().any(|a| matches!(a, syn::GenericArgument::Type(_))))
1316}
1317
1318/// Borrow-shape detection for SRD-80b Wire cutover. The macro
1319/// dispatches owned types through `<T as Wire>::extract` / `::inject`;
1320/// borrow shapes are recognised syntactically and emitted as
1321/// direct `match`-on-`Value` extraction at the eval call site.
1322/// This keeps the [`Wire`] trait bound at `Sized + 'static` without
1323/// needing lifetime parameters.
1324///
1325/// Returns the matched `Value::<Variant>(inner)` pattern and the
1326/// accessor expression that yields the body's expected borrow.
1327#[derive(Clone)]
1328enum BorrowWire {
1329 /// `&str` → `Value::Str(arc)` → `arc.as_ref()` (`&str`).
1330 Str,
1331 /// `&[u8]` → `Value::Bytes(arc)` → `arc.as_ref()` (`&[u8]`).
1332 Bytes,
1333 /// `&serde_json::Value` → `Value::Json(j)` → `j.as_ref()`.
1334 Json,
1335 /// `&[T]` for T in {f32, i32, f64, i64, f16, i16} — typed
1336 /// vector borrow. Variant tracked separately so we can emit
1337 /// the right `Value::Vec*` arm; element type is recovered
1338 /// from the syntactic recognition.
1339 Vec(&'static str /* variant name */, TokenStream2 /* PortType expr */),
1340}
1341
1342fn is_borrow_wire_shape(ty: &Type) -> Option<BorrowWire> {
1343 let syn::Type::Reference(r) = ty else { return None; };
1344 if r.mutability.is_some() { return None; }
1345 match &*r.elem {
1346 // `&str`
1347 syn::Type::Path(p) if p.path.is_ident("str") => Some(BorrowWire::Str),
1348 // `&[T]` — bytes (T=u8) and typed vectors.
1349 syn::Type::Slice(slc) => {
1350 if let syn::Type::Path(p) = &*slc.elem {
1351 if p.path.is_ident("u8") {
1352 return Some(BorrowWire::Bytes);
1353 }
1354 let elem_name = p.path.segments.last()?.ident.to_string();
1355 let (variant, port_expr) = match elem_name.as_str() {
1356 "f32" => ("VecF32", quote!(polydat::ast::PortType::VecF32)),
1357 "i32" => ("VecI32", quote!(polydat::ast::PortType::VecI32)),
1358 "f64" => ("VecF64", quote!(polydat::ast::PortType::VecF64)),
1359 "i64" => ("VecI64", quote!(polydat::ast::PortType::VecI64)),
1360 "f16" => ("VecF16", quote!(polydat::ast::PortType::VecF16)),
1361 "i16" => ("VecI16", quote!(polydat::ast::PortType::VecI16)),
1362 "i8" => ("VecI8", quote!(polydat::ast::PortType::VecI8)),
1363 _ => return None,
1364 };
1365 return Some(BorrowWire::Vec(variant, port_expr));
1366 }
1367 None
1368 }
1369 // `&serde_json::Value` — recognise by last segment `Value`
1370 // alongside `serde_json` somewhere in the path.
1371 syn::Type::Path(p) if last_segment_is(p, "Value")
1372 && path_contains_segment(p, "serde_json") => Some(BorrowWire::Json),
1373 _ => None,
1374 }
1375}
1376
1377/// Token stream for extracting a borrow-shape wire from
1378/// `&inputs[idx]`. The macro emits this directly (no trait
1379/// dispatch) so the borrow's lifetime is bound to the eval
1380/// call's `&inputs` borrow naturally — no `unsafe transmute`.
1381fn borrow_extract_tokens(shape: BorrowWire, input_expr: TokenStream2) -> TokenStream2 {
1382 match shape {
1383 BorrowWire::Str => quote! {
1384 match #input_expr {
1385 polydat::ast::Value::Str(__arc) => __arc.as_ref(),
1386 __other => panic!("expected Str wire, got {__other:?}"),
1387 }
1388 },
1389 BorrowWire::Bytes => quote! {
1390 match #input_expr {
1391 polydat::ast::Value::Bytes(__arc) => __arc.as_ref(),
1392 __other => panic!("expected Bytes wire, got {__other:?}"),
1393 }
1394 },
1395 BorrowWire::Json => quote! {
1396 match #input_expr {
1397 polydat::ast::Value::Json(__arc) => __arc.as_ref(),
1398 __other => panic!("expected Json wire, got {__other:?}"),
1399 }
1400 },
1401 BorrowWire::Vec(variant, _port) => {
1402 let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
1403 quote! {
1404 match #input_expr {
1405 polydat::ast::Value::#v(__arc) => __arc.as_slice(),
1406 __other => panic!(
1407 concat!("expected ", stringify!(#v), " wire, got {:?}"),
1408 __other),
1409 }
1410 }
1411 }
1412 }
1413}
1414
1415/// Token stream for the static `PortType` of a borrow-shape wire.
1416fn borrow_port_type(shape: &BorrowWire) -> TokenStream2 {
1417 match shape {
1418 BorrowWire::Str => quote!(polydat::ast::PortType::Str),
1419 BorrowWire::Bytes => quote!(polydat::ast::PortType::Bytes),
1420 BorrowWire::Json => quote!(polydat::ast::PortType::Json),
1421 BorrowWire::Vec(_, port_expr) => port_expr.clone(),
1422 }
1423}
1424
1425/// SRD-80 PR B.13 — typed-vector classifier. Recognises three
1426/// input shapes per element type: `SliceArc<T>`, `Vec<T>`,
1427/// `&[T]`. The element type's last path segment selects the
1428/// `WrapperWire::Vec*` variant.
1429fn classify_vec_wire(ty: &Type) -> Option<WrapperWire> {
1430 // Try to extract the element type from any of the three shapes:
1431 let elem: Type = if let Some(elem) = strip_vec(ty) {
1432 elem.clone()
1433 } else if let Some(elem) = strip_slice_arc(ty) {
1434 elem.clone()
1435 } else if let Some(elem) = strip_borrowed_slice(ty) {
1436 elem.clone()
1437 } else {
1438 return None;
1439 };
1440
1441 let syn::Type::Path(p) = &elem else { return None; };
1442 let last = p.path.segments.last()?;
1443 // f16 lives in the `half` crate, so the element path can
1444 // be `f16`, `half::f16`, etc. — match by last segment.
1445 match last.ident.to_string().as_str() {
1446 "f32" => Some(WrapperWire::VecF32),
1447 "i32" => Some(WrapperWire::VecI32),
1448 "f64" => Some(WrapperWire::VecF64),
1449 "i64" => Some(WrapperWire::VecI64),
1450 "f16" => Some(WrapperWire::VecF16),
1451 "i16" => Some(WrapperWire::VecI16),
1452 "i8" => Some(WrapperWire::VecI8),
1453 _ => None,
1454 }
1455}
1456
1457fn strip_vec(ty: &Type) -> Option<&Type> {
1458 let syn::Type::Path(p) = ty else { return None; };
1459 let last = p.path.segments.last()?;
1460 if last.ident != "Vec" { return None; }
1461 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1462 args.args.iter().find_map(|a| match a {
1463 syn::GenericArgument::Type(t) => Some(t),
1464 _ => None,
1465 })
1466}
1467
1468fn strip_slice_arc(ty: &Type) -> Option<&Type> {
1469 let syn::Type::Path(p) = ty else { return None; };
1470 let last = p.path.segments.last()?;
1471 if last.ident != "SliceArc" { return None; }
1472 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1473 args.args.iter().find_map(|a| match a {
1474 syn::GenericArgument::Type(t) => Some(t),
1475 _ => None,
1476 })
1477}
1478
1479fn strip_borrowed_slice(ty: &Type) -> Option<&Type> {
1480 let syn::Type::Reference(r) = ty else { return None; };
1481 if r.mutability.is_some() { return None; }
1482 let syn::Type::Slice(slc) = &*r.elem else { return None; };
1483 Some(&slc.elem)
1484}
1485
1486/// Detect `&[T]` (variadic) in arg-type position. SRD-80 PR B.9.
1487/// Returns the recognised element type for the supported primitive
1488/// element set; `None` otherwise (bare reference, non-slice, or
1489/// unsupported element type). Structural match — works regardless
1490/// of how the inner type is written (`Value` / `polydat::ast::Value`).
1491fn classify_variadic(ty: &Type) -> Option<VariadicElement> {
1492 let syn::Type::Reference(r) = ty else { return None; };
1493 if r.mutability.is_some() { return None; }
1494 let syn::Type::Slice(s) = &*r.elem else { return None; };
1495
1496 // `&[&str]` — element is a Type::Reference to a path "str".
1497 if let syn::Type::Reference(inner_r) = &*s.elem
1498 && inner_r.mutability.is_none()
1499 && let syn::Type::Path(p) = &*inner_r.elem
1500 && p.path.is_ident("str")
1501 {
1502 return Some(VariadicElement::BorrowedStr);
1503 }
1504
1505 // Bare-path element types — match by last path segment ident.
1506 let syn::Type::Path(p) = &*s.elem else { return None; };
1507 let last = p.path.segments.last()?;
1508 if !last.arguments.is_empty() { return None; }
1509 match last.ident.to_string().as_str() {
1510 "u64" => Some(VariadicElement::U64),
1511 // NOTE: `&[f64]` is deliberately NOT variadic — it is the
1512 // `VecF64` vector wire, uniform with every other lane
1513 // element (`&[f32]`/`&[i32]`/…). A variadic run of f64
1514 // wires would need an explicit `Variadic<f64>` spelling.
1515 "bool" => Some(VariadicElement::Bool),
1516 "String" => Some(VariadicElement::OwnedString),
1517 "Value" => Some(VariadicElement::Value),
1518 _ => None,
1519 }
1520}
1521
1522/// SRD-80b Phase 5 S16 — detect `Result<T, E>` return type for
1523/// fallible-construction nodes. Returns `Some(T)` (the Ok type)
1524/// when the return is a `Result<T, _>`; `None` otherwise. Matches
1525/// any path ending in `Result` so both bare `Result` and fully
1526/// qualified `std::result::Result` work.
1527///
1528/// The Err arm is consumed for its `Into<String>` projection at
1529/// emission time, so we don't pin its shape here — any E that
1530/// satisfies `Into<String>` (including `String` itself) is fine.
1531fn classify_result_return(ty: &Type) -> Option<Type> {
1532 let syn::Type::Path(p) = ty else { return None; };
1533 let last = p.path.segments.last()?;
1534 if last.ident != "Result" { return None; }
1535 let syn::PathArguments::AngleBracketed(args) = &last.arguments else { return None; };
1536 // Two args expected: <Ok, Err>. Tolerate `Result<T>` (rare alias)
1537 // by requiring at least one type arg.
1538 let mut tys = args.args.iter().filter_map(|a| match a {
1539 syn::GenericArgument::Type(t) => Some(t.clone()),
1540 _ => None,
1541 });
1542 tys.next()
1543}
1544
1545fn generate(
1546 func: ItemFn,
1547 attrs: NodeAttrs,
1548 dsl_name_override: Option<String>,
1549) -> syn::Result<TokenStream2> {
1550 let fn_name = &func.sig.ident;
1551 // SRD-80 PR B.7: strip `r#` from raw identifiers (`fn r#mod`,
1552 // `fn r#type`, etc.) so the Rust struct name comes out clean.
1553 let fn_name_raw = fn_name.to_string();
1554 let rust_name_str = fn_name_raw
1555 .strip_prefix("r#")
1556 .unwrap_or(&fn_name_raw)
1557 .to_string();
1558 let struct_name = format_ident!("{}", to_camel_case(&rust_name_str));
1559 // SRD-80b Phase D1 — when instantiating a generic-over-Wire
1560 // function, the per-instantiation copies have suffixed Rust
1561 // names (`passthrough_u64`, `passthrough_f64`) but share a
1562 // single DSL function name from the original declaration.
1563 let is_instantiation = dsl_name_override.is_some();
1564 let func_name_str = dsl_name_override.unwrap_or_else(|| rust_name_str.clone());
1565 let category = &attrs.category;
1566
1567 // Classify each function arg: wire or const? Reject any
1568 // unsupported pattern (self, complex destructuring, bare-
1569 // type wires the macro doesn't recognize).
1570 let mut args: Vec<ClassifiedArg> = Vec::new();
1571 for input in &func.sig.inputs {
1572 match input {
1573 FnArg::Receiver(r) => {
1574 return Err(syn::Error::new_spanned(
1575 r,
1576 "#[polydat_node] does not support `self` parameters yet; \
1577 state-bearing nodes are deferred to a later PR.",
1578 ));
1579 }
1580 FnArg::Typed(pat_ty) => {
1581 let ident = match &*pat_ty.pat {
1582 Pat::Ident(p) => p.ident.clone(),
1583 other => {
1584 return Err(syn::Error::new_spanned(
1585 other,
1586 "#[polydat_node] requires plain identifier parameters; \
1587 pattern matching in argument position isn't supported.",
1588 ));
1589 }
1590 };
1591 let declared_ty = (*pat_ty.ty).clone();
1592 let default_value = parse_poly_default(&pat_ty.attrs)?;
1593 let setup_attr = parse_poly_const(&pat_ty.attrs)?;
1594 let wire_constraint = parse_wire_constraint(&pat_ty.attrs)?;
1595 let is_polywire = classify_polywire(&declared_ty);
1596 let variadic_elem = classify_variadic(&declared_ty);
1597
1598 let kind = if let Some(elem) = variadic_elem {
1599 if default_value.is_some() || setup_attr.is_some() || is_polywire {
1600 return Err(syn::Error::new_spanned(
1601 pat_ty,
1602 "variadic `&[T]` args don't combine with \
1603 #[poly_default(...)], #[poly_const(...)], or `Value`.",
1604 ));
1605 }
1606 ArgKind::Variadic(elem)
1607 } else if is_polywire {
1608 if default_value.is_some() || setup_attr.is_some() {
1609 return Err(syn::Error::new_spanned(
1610 pat_ty,
1611 "`Value` args (PolyWire) don't combine with \
1612 #[poly_default(...)] or #[poly_const(...)]; \
1613 the runtime port type comes from the upstream wire \
1614 at construction time.",
1615 ));
1616 }
1617 ArgKind::PolyWire
1618 } else if let Some((setup_fn, source_args)) = setup_attr {
1619 // `#[poly_const(...)]` requires `&T` arg type.
1620 let inner_ty = classify_borrowed(&declared_ty)
1621 .ok_or_else(|| syn::Error::new_spanned(
1622 &declared_ty,
1623 "#[poly_const(...)] requires the argument type to be \
1624 a borrow `&T` — the macro stores the computed `T` \
1625 in a struct field and hands the body a borrow each \
1626 eval.",
1627 ))?;
1628 if default_value.is_some() {
1629 return Err(syn::Error::new_spanned(
1630 pat_ty,
1631 "#[poly_default(...)] cannot combine with \
1632 #[poly_const(...)]; defaults belong on the source \
1633 Const arg, not on the derived setup arg.",
1634 ));
1635 }
1636 ArgKind::Setup(Box::new(SetupSpec { inner_ty, setup_fn, source_args }))
1637 } else if let Some(inner) = classify_const_vec(&declared_ty) {
1638 // SRD-80b Phase C — `Const<Vec<C>>` variadic
1639 // workload-list. `poly_default` doesn't apply
1640 // (the empty list IS the default); other
1641 // attributes don't compose.
1642 if default_value.is_some() {
1643 return Err(syn::Error::new_spanned(
1644 pat_ty,
1645 "#[poly_default(...)] cannot combine with \
1646 `Const<Vec<C>>`; the empty Vec IS the implicit \
1647 default. Use `Const<C>` with a poly_default \
1648 literal for a single-value default instead.",
1649 ));
1650 }
1651 if setup_attr.is_some() {
1652 return Err(syn::Error::new_spanned(
1653 pat_ty,
1654 "`Const<Vec<C>>` doesn't combine with \
1655 #[poly_const(...)]; route the derived state \
1656 from a scalar `Const<C>` source instead.",
1657 ));
1658 }
1659 ArgKind::ConstVec(inner)
1660 } else {
1661 match classify_type(&declared_ty) {
1662 Some(shape) => ArgKind::Const(shape),
1663 None => {
1664 if default_value.is_some() {
1665 return Err(syn::Error::new_spanned(
1666 pat_ty,
1667 "#[poly_default(...)] only applies to const args \
1668 (`Const<T>`); bare-type wire args don't have \
1669 assembly-time defaults.",
1670 ));
1671 }
1672 ArgKind::Wire
1673 }
1674 }
1675 };
1676 args.push(ClassifiedArg { name: ident, declared_ty, kind, default_value, wire_constraint });
1677 }
1678 }
1679 }
1680
1681 // SRD-80b Phase C — `Const<Vec<C>>` consumes the tail of
1682 // `consts[..]` at build time, so at most one ConstVec arg is
1683 // allowed per node and it must be the last const arg in
1684 // declaration order. Validate before emission.
1685 {
1686 let const_vec_positions: Vec<usize> = args.iter().enumerate()
1687 .filter_map(|(i, a)| if matches!(a.kind, ArgKind::ConstVec(_)) { Some(i) } else { None })
1688 .collect();
1689 if const_vec_positions.len() > 1 {
1690 return Err(syn::Error::new_spanned(
1691 &args[const_vec_positions[1]].declared_ty,
1692 "#[polydat_node] supports at most one `Const<Vec<C>>` arg \
1693 per function; the variadic-const surface consumes the \
1694 tail of the consts slice and a second one would have no \
1695 entries to claim.",
1696 ));
1697 }
1698 if let Some(&pos) = const_vec_positions.first() {
1699 // Any Const(_) declared AFTER the ConstVec would never
1700 // bind (its index ≥ ConstVec's tail-start).
1701 for later in &args[pos + 1..] {
1702 if matches!(later.kind, ArgKind::Const(_)) {
1703 return Err(syn::Error::new_spanned(
1704 &later.declared_ty,
1705 "scalar `Const<T>` arg declared after a \
1706 `Const<Vec<C>>` arg is unreachable — the variadic \
1707 consumes everything from its position to the end \
1708 of the consts slice. Move the scalar consts BEFORE \
1709 the `Const<Vec<C>>` in the function signature.",
1710 ));
1711 }
1712 }
1713 }
1714 }
1715
1716 // Map a bare wire-arg type to a PortType expression.
1717 //
1718 // SRD-80b: the canonical answer is `<#ty as Wire>::PORT` —
1719 // any owned type that impls [`Wire`] is admitted, and adding
1720 // a new wire type means adding one Wire impl (no macro
1721 // source change). Three exceptions stay structural because
1722 // they can't be expressed through the trait:
1723 //
1724 // 1. Borrow shapes (`&str`, `&[u8]`, `&[T]`,
1725 // `&serde_json::Value`) — `Wire` is `Sized + 'static`
1726 // so borrowed refs can't impl it. The macro emits the
1727 // literal `PortType` here and direct `match`-on-`Value`
1728 // extraction elsewhere.
1729 //
1730 // 2. `Arc<T>` Handle (non-special T) — would conflict with
1731 // the concrete `Arc<[u8]>` / `Arc<serde_json::Value>`
1732 // impls if expressed as a blanket. Kept as inline
1733 // downcast at the extract site; port type is the static
1734 // `Handle`.
1735 //
1736 // 3. PolyWire (`Value`-typed wire) — polymorphic at
1737 // runtime; no static `PortType`. The `ArgKind::PolyWire`
1738 // path handles this independently of `wire_port_type_for`.
1739 //
1740 // Everything else — including `Option<T>`, `Ext<T>`, and any
1741 // future combinator added by impl'ing `Wire` — flows through
1742 // trait dispatch.
1743 let wire_port_type_for = |ty: &Type| -> syn::Result<TokenStream2> {
1744 if let Some(kind) = classify_wrapper_wire(ty) {
1745 return Ok(match kind {
1746 WrapperWire::Bytes => quote!(polydat::ast::PortType::Bytes),
1747 WrapperWire::Json => quote!(polydat::ast::PortType::Json),
1748 WrapperWire::Handle => quote!(polydat::ast::PortType::Handle),
1749 WrapperWire::VecF32 => quote!(polydat::ast::PortType::VecF32),
1750 WrapperWire::VecI32 => quote!(polydat::ast::PortType::VecI32),
1751 WrapperWire::VecF64 => quote!(polydat::ast::PortType::VecF64),
1752 WrapperWire::VecI64 => quote!(polydat::ast::PortType::VecI64),
1753 WrapperWire::VecF16 => quote!(polydat::ast::PortType::VecF16),
1754 WrapperWire::VecI16 => quote!(polydat::ast::PortType::VecI16),
1755 WrapperWire::VecI8 => quote!(polydat::ast::PortType::VecI8),
1756 });
1757 }
1758 if let Some(borrow) = is_borrow_wire_shape(ty) {
1759 return Ok(borrow_port_type(&borrow));
1760 }
1761 // Fall through to trait dispatch — `<T as Wire>::PORT` is
1762 // a const associated, evaluable at codegen time. Types
1763 // without a `Wire` impl produce a clean E0277 at the
1764 // function's call site, naming the missing trait bound.
1765 Ok(quote!(<#ty as polydat::derive_support::Wire>::PORT))
1766 };
1767
1768 // Build the NodeMeta `ins` slot list — one entry per arg,
1769 // dispatched by kind. Wire args get `Slot::Wire(...)`;
1770 // const args get `Slot::Const { ... }` populated with the
1771 // captured field value at construction time.
1772 let mut slot_exprs: Vec<TokenStream2> = Vec::new();
1773 for a in &args {
1774 let name_str = a.name.to_string();
1775 match &a.kind {
1776 ArgKind::Wire => {
1777 let pt = wire_port_type_for(&a.declared_ty)?;
1778 let ty = &a.declared_ty;
1779 // SRD-80 PR B.14: optional `#[constraint(Variant)]`.
1780 let constraint_chain = if let Some(variant) = &a.wire_constraint {
1781 quote! {
1782 .with_constraint(
1783 polydat::dsl::const_constraints::ConstConstraint::#variant)
1784 }
1785 } else {
1786 quote!()
1787 };
1788 // SRD-80b in-spirit — `Wire::WIRE_COST` is read
1789 // from the trait at codegen. Owned/non-borrow
1790 // wire types route here; borrow shapes don't
1791 // impl Wire so they get the default Data cost
1792 // (the WireCost::Config opt-in only applies to
1793 // owned types wrapped in `Config<T>`).
1794 let cost_chain = if is_borrow_wire_shape(ty).is_none()
1795 && classify_wrapper_wire(ty) != Some(WrapperWire::Handle)
1796 {
1797 quote! {
1798 .with_cost(<#ty as polydat::derive_support::Wire>::WIRE_COST)
1799 }
1800 } else {
1801 quote!()
1802 };
1803 slot_exprs.push(quote! {
1804 polydat::ast::Slot::Wire(
1805 polydat::ast::Port::new(#name_str, #pt)
1806 #constraint_chain
1807 #cost_chain
1808 )
1809 });
1810 }
1811 ArgKind::Const(shape) => {
1812 let field_name = &a.name;
1813 let const_value_ctor = match shape {
1814 ConstShape::U64 => quote!(polydat::ast::ConstValue::U64(#field_name)),
1815 ConstShape::F64 => quote!(polydat::ast::ConstValue::F64(#field_name)),
1816 ConstShape::Bool => quote!(polydat::ast::ConstValue::U64(if #field_name { 1 } else { 0 })),
1817 ConstShape::Str => quote!(polydat::ast::ConstValue::Str(#field_name.clone())),
1818 };
1819 slot_exprs.push(quote! {
1820 polydat::ast::Slot::Const {
1821 name: #name_str.into(),
1822 value: #const_value_ctor,
1823 }
1824 });
1825 }
1826 ArgKind::Setup(_) => {
1827 // Setup args don't appear in NodeMeta.ins —
1828 // they're derived state, not declared params.
1829 // The source Const arg already carries the
1830 // introspectable value.
1831 }
1832 ArgKind::PolyWire => {
1833 // Port type is the `<argname>_type` parameter
1834 // passed to `new()`; the variable is in scope
1835 // because the macro emits it as a `new()` param.
1836 let pt_param = format_ident!("{}_type", a.name);
1837 slot_exprs.push(quote! {
1838 polydat::ast::Slot::Wire(polydat::ast::Port::new(
1839 #name_str, #pt_param))
1840 });
1841 }
1842 ArgKind::Variadic(_) => {
1843 // Variadic emits per-element slots at construction.
1844 // The macro generates `extend` into the slot vec
1845 // from a 0..n_wires loop. Each slot is named
1846 // `<argname>_<i>` to keep the meta diff-friendly.
1847 // (Handled in the new() body via a separate pass —
1848 // see `variadic_slot_extends` below.)
1849 }
1850 ArgKind::ConstVec(inner) => {
1851 // SRD-80b — `Const<Vec<C>>` emits a `Slot::Const`
1852 // entry when the inner element has a matching
1853 // `ConstValue::Vec*` variant (u64, f64). This
1854 // makes the captured list visible to JIT slot-
1855 // walkers and introspection (`jit_constants_from_slots`).
1856 // For element types without a parallel
1857 // `ConstValue` variant (bool, Str), no slot is
1858 // emitted; the FuncSig's `Arity::VariadicConsts`
1859 // tracks the surface and the stored Vec<C> field
1860 // is the canonical storage.
1861 let field_name = &a.name;
1862 match inner {
1863 ConstShape::U64 => slot_exprs.push(quote! {
1864 polydat::ast::Slot::Const {
1865 name: #name_str.into(),
1866 value: polydat::ast::ConstValue::VecU64(#field_name.clone()),
1867 }
1868 }),
1869 ConstShape::F64 => slot_exprs.push(quote! {
1870 polydat::ast::Slot::Const {
1871 name: #name_str.into(),
1872 value: polydat::ast::ConstValue::VecF64(#field_name.clone()),
1873 }
1874 }),
1875 _ => {}
1876 }
1877 }
1878 }
1879 }
1880 // For each variadic arg, also emit a runtime loop that
1881 // appends N slots to the `Slot` vec.
1882 let variadic_slot_extends: Vec<TokenStream2> = args.iter()
1883 .filter_map(|a| match &a.kind {
1884 ArgKind::Variadic(elem) => {
1885 let name_str = a.name.to_string();
1886 let pt = elem.port_type_tokens();
1887 Some(quote! {
1888 for __i in 0..n_wires {
1889 ins.push(polydat::ast::Slot::Wire(
1890 polydat::ast::Port::new(
1891 format!("{}_{__i}", #name_str),
1892 #pt,
1893 )));
1894 }
1895 })
1896 }
1897 _ => None,
1898 })
1899 .collect();
1900
1901 // Build the FuncSig.params static slice — one ParamSpec
1902 // per declared arg (Wire and Const). Setup args don't
1903 // appear in the FuncSig surface — they're macro-internal
1904 // derived state.
1905 let param_specs: Vec<TokenStream2> = args.iter()
1906 .filter_map(|a| {
1907 let name_str = a.name.to_string();
1908 // Variadic args declare `required: false` — they accept
1909 // any count from `variadic_min` (default 0) upward.
1910 // `ConstVec` follows the same pattern (empty is valid).
1911 let required = match &a.kind {
1912 ArgKind::Variadic(_) | ArgKind::ConstVec(_) => false,
1913 _ => a.default_value.is_none(),
1914 };
1915 let slot_type = match &a.kind {
1916 ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => quote!(polydat::ast::SlotType::Wire),
1917 ArgKind::Const(shape) => shape.slot_type_tokens(),
1918 ArgKind::ConstVec(inner) => inner.slot_type_tokens(),
1919 ArgKind::Setup(_) => return None,
1920 };
1921 Some(quote! {
1922 polydat::dsl::registry::ParamSpec {
1923 name: #name_str,
1924 slot_type: #slot_type,
1925 required: #required,
1926 example: #name_str,
1927 constraint: None,
1928 }
1929 })
1930 })
1931 .collect();
1932
1933 // Output type. The simple case requires a concrete return
1934 // type (-> T); unit / unspecified isn't supported.
1935 let declared_ret_ty = match &func.sig.output {
1936 ReturnType::Default => {
1937 return Err(syn::Error::new_spanned(
1938 &func.sig,
1939 "#[polydat_node] requires an explicit return type; \
1940 nodes always produce a value.",
1941 ));
1942 }
1943 ReturnType::Type(_, t) => (**t).clone(),
1944 };
1945 // SRD-80b Phase 5 S16 — fallible construction. When the body
1946 // returns `Result<T, E>`, the macro treats T as the effective
1947 // node-output type and emits a `try_new(...) -> Result<Self,
1948 // String>` constructor that runs the body once at
1949 // construction, caches the Ok value, and propagates Err. Only
1950 // valid for nodes with no wire/polywire inputs — the body has
1951 // to be fully resolvable at construction.
1952 let fallible_inner_ty: Option<Type> = classify_result_return(&declared_ret_ty);
1953 let is_fallible = fallible_inner_ty.is_some();
1954 let ret_ty = fallible_inner_ty.clone().unwrap_or_else(|| declared_ret_ty.clone());
1955 let ret_is_polywire = classify_polywire(&ret_ty);
1956
1957 if is_fallible {
1958 // Wire / polywire / variadic inputs are not supported in
1959 // fallible mode: the body executes once at construction,
1960 // not per-eval. Const args are fine — they're all known
1961 // by the time `try_new` runs.
1962 for a in &args {
1963 match &a.kind {
1964 ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => {
1965 return Err(syn::Error::new_spanned(
1966 &a.declared_ty,
1967 "fallible-construction nodes (-> Result<T, E>) must \
1968 have only Const args. Wire/PolyWire/variadic inputs \
1969 can't be evaluated at construction time. Use the \
1970 #[poly_const(setup_fn, from = ...)] shape instead \
1971 when per-eval inputs are needed.",
1972 ));
1973 }
1974 ArgKind::Setup(_) | ArgKind::Const(_) | ArgKind::ConstVec(_) => {}
1975 }
1976 }
1977 }
1978
1979 // SRD-80 PR B.10: detect tuple-typed return for multi-output.
1980 let tuple_ret_elems: Option<Vec<Type>> = match &ret_ty {
1981 syn::Type::Tuple(t) => Some(t.elems.iter().cloned().collect()),
1982 _ => None,
1983 };
1984
1985 // SRD-80b dynamic-output shape — detect `DynamicOutputs<T>`
1986 // return and locate the `Const<Vec<C>>` arg whose length
1987 // drives the output port count at construction.
1988 let dynamic_outputs_inner: Option<Type> = classify_dynamic_outputs(&ret_ty);
1989 let dynamic_outputs_count_arg: Option<syn::Ident> = if dynamic_outputs_inner.is_some() {
1990 let const_vec_args: Vec<&syn::Ident> = args.iter()
1991 .filter_map(|a| match &a.kind {
1992 ArgKind::ConstVec(_) => Some(&a.name),
1993 _ => None,
1994 })
1995 .collect();
1996 if const_vec_args.len() != 1 {
1997 return Err(syn::Error::new_spanned(
1998 &ret_ty,
1999 format!(
2000 "`DynamicOutputs<T>` return requires exactly one \
2001 `Const<Vec<C>>` arg to drive the output port count \
2002 (got {}). Declare one `Const<Vec<C>>` arg whose length \
2003 determines the number of output ports.",
2004 const_vec_args.len(),
2005 ),
2006 ));
2007 }
2008 Some(const_vec_args[0].clone())
2009 } else {
2010 None
2011 };
2012
2013 if tuple_ret_elems.is_some() && ret_is_polywire {
2014 // Type::Tuple isn't Type::Path so this is impossible, but
2015 // belt-and-suspenders for future return-shape changes.
2016 return Err(syn::Error::new_spanned(
2017 &ret_ty,
2018 "tuple return + PolyWire don't compose (SameAsInput is a \
2019 single-output dispatch).",
2020 ));
2021 }
2022
2023 // SRD-80 PR B.8: when the return type is `Value`, the
2024 // output port type tracks the first PolyWire arg's runtime
2025 // port type (SameAsInput). Otherwise it's the primitive's
2026 // fixed PortType.
2027 let first_polywire_idx: Option<usize> = args.iter()
2028 .enumerate()
2029 .find(|(_, a)| matches!(a.kind, ArgKind::PolyWire))
2030 .map(|(i, _)| i);
2031
2032 // Per-output port-type token streams, indexed positionally.
2033 // Single-output → 1-element vec; tuple → N elements.
2034 let output_port_types: Vec<TokenStream2> = if let Some(elems) = &tuple_ret_elems {
2035 elems.iter()
2036 .map(wire_port_type_for)
2037 .collect::<syn::Result<Vec<_>>>()?
2038 } else if ret_is_polywire {
2039 // Prefer a singleton PolyWire arg for SameAsInput
2040 // dispatch; fall back to a variadic `&[Value]` arg
2041 // (split-halves shape) whose runtime element types
2042 // drive the output polymorphism. The static slot
2043 // gets a `PortType::U64` placeholder (assembler skips
2044 // type-check for these); eval enforces uniformity.
2045 if let Some(polywire_arg) = args.iter().find(|a| matches!(a.kind, ArgKind::PolyWire)) {
2046 let pt_ident = format_ident!("{}_type", polywire_arg.name);
2047 vec![quote!(#pt_ident)]
2048 } else if args.iter().any(|a| matches!(&a.kind, ArgKind::Variadic(VariadicElement::Value))) {
2049 vec![quote!(polydat::ast::PortType::U64)]
2050 } else {
2051 return Err(syn::Error::new_spanned(
2052 &ret_ty,
2053 "function returns `Value` but has no `Value` arg — the macro \
2054 needs at least one PolyWire (`Value`) arg or a `&[Value]` \
2055 variadic to source the runtime port type for the output.",
2056 ));
2057 }
2058 } else if let Some(inner) = &dynamic_outputs_inner {
2059 // Single per-element port type for the dynamic case.
2060 // The count is determined at construction time; this
2061 // entry is used by the codegen as the port type each
2062 // output port carries.
2063 vec![wire_port_type_for(inner)?]
2064 } else {
2065 vec![wire_port_type_for(&ret_ty)?]
2066 };
2067
2068 // SRD-80 PR B.10: output names. Operator-supplied via
2069 // `output_names(a, b, c)`; falls back to `out_0`, `out_1`, ...
2070 // for tuple returns; just "output" for single returns.
2071 let output_names_strs: Vec<String> = match (&tuple_ret_elems, &attrs.output_names) {
2072 (Some(elems), Some(names)) => {
2073 if names.len() != elems.len() {
2074 return Err(syn::Error::new_spanned(
2075 &ret_ty,
2076 format!(
2077 "tuple return has {} elements but `output_names(...)` \
2078 lists {}; lengths must match.",
2079 elems.len(), names.len(),
2080 ),
2081 ));
2082 }
2083 names.iter().map(|n| n.to_string()).collect()
2084 }
2085 (Some(elems), None) => (0..elems.len()).map(|i| format!("out_{i}")).collect(),
2086 (None, Some(names)) if names.len() != 1 => {
2087 return Err(syn::Error::new_spanned(
2088 &ret_ty,
2089 "single-output return doesn't accept multi-name `output_names(...)`.",
2090 ));
2091 }
2092 (None, Some(names)) => vec![names[0].to_string()],
2093 (None, None) => vec!["output".to_string()],
2094 };
2095
2096 // FuncSig::output_port — the statically-known return port for
2097 // single fixed-output nodes; None for tuple / polymorphic /
2098 // dynamic shapes (the DSL type inference then falls back to
2099 // its heuristic).
2100 let output_port_field: TokenStream2 = if tuple_ret_elems.is_some()
2101 || ret_is_polywire
2102 || dynamic_outputs_inner.is_some()
2103 {
2104 quote!(None)
2105 } else {
2106 let pt = &output_port_types[0];
2107 quote!(Some(#pt))
2108 };
2109
2110 let output_count = if dynamic_outputs_inner.is_some() { 0 } else { output_port_types.len() };
2111 // SRD-80b: `0` in the FuncSig signals "dynamic, determined at
2112 // compile time" (existing FuncSig convention from the doc).
2113 let output_count_lit = syn::LitInt::new(&output_count.to_string(), proc_macro2::Span::call_site());
2114
2115 // When return is `Value`, prefer SameAsInput dispatch
2116 // against a singleton PolyWire arg; for the split-halves
2117 // `&[Value]` case there's no singleton to point at, so
2118 // fall back to OutputType::Fixed (the static slot's
2119 // placeholder PortType is used and eval enforces type
2120 // uniformity).
2121 let output_type_tokens: TokenStream2 = match (ret_is_polywire, first_polywire_idx) {
2122 (true, Some(idx)) => {
2123 let i = syn::Index::from(idx);
2124 quote!(polydat::dsl::registry::OutputType::SameAsInput(#i))
2125 }
2126 _ => quote!(polydat::dsl::registry::OutputType::Fixed),
2127 };
2128
2129 // Struct fields. Wire/PolyWire/Variadic → no field (arity
2130 // reflected in `meta.ins.len()`); Const → owned-typed field;
2131 // ConstVec → Vec<inner>; Setup → field of the borrowed
2132 // inner type.
2133 let struct_fields: Vec<TokenStream2> = args.iter()
2134 .filter_map(|a| match &a.kind {
2135 ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2136 ArgKind::Const(shape) => {
2137 let n = &a.name;
2138 let ft = shape.field_type_tokens();
2139 Some(quote!(pub #n: #ft))
2140 }
2141 ArgKind::ConstVec(inner) => {
2142 let n = &a.name;
2143 let ft = inner.field_type_tokens();
2144 Some(quote!(pub #n: Vec<#ft>))
2145 }
2146 ArgKind::Setup(spec) => {
2147 let n = &a.name;
2148 let ty = &spec.inner_ty;
2149 Some(quote!(pub #n: #ty))
2150 }
2151 })
2152 .collect();
2153
2154 // `new(<polywire_types..>, <consts..>)` constructor params, in
2155 // declaration order. Const args contribute their owned-typed
2156 // value; PolyWire args contribute a `<argname>_type: PortType`
2157 // parameter that names the runtime port type the assembler
2158 // resolved for the upstream wire. Setup args are computed
2159 // inside new(), not parameters.
2160 let new_params: Vec<TokenStream2> = args.iter()
2161 .filter_map(|a| match &a.kind {
2162 ArgKind::Wire => None,
2163 ArgKind::Const(shape) => {
2164 let n = &a.name;
2165 let ft = shape.field_type_tokens();
2166 Some(quote!(#n: #ft))
2167 }
2168 ArgKind::ConstVec(inner) => {
2169 let n = &a.name;
2170 let ft = inner.field_type_tokens();
2171 Some(quote!(#n: Vec<#ft>))
2172 }
2173 ArgKind::Setup(_) => None,
2174 ArgKind::PolyWire => {
2175 let n = format_ident!("{}_type", a.name);
2176 Some(quote!(#n: polydat::ast::PortType))
2177 }
2178 // Variadic args don't add their OWN per-arg param —
2179 // the variadic-arity is supplied via a SINGLE
2180 // `n_wires: usize` parameter appended once at the end
2181 // (see `variadic_n_wires_param` below).
2182 ArgKind::Variadic(_) => None,
2183 })
2184 .collect();
2185
2186 // SRD-80 PR B.9: append a single `n_wires: usize` parameter
2187 // to `new()` when the function declares any variadic arg.
2188 // SRD-80b split-halves variadic: TWO variadics in succession
2189 // share a single `n_wires` param (interpreted as "count per
2190 // half"). The macro emits 2*n_wires wire slots and slices
2191 // the inputs at the midpoint at eval time. Used by `pick`'s
2192 // `(b0,...,bN,v0,...,vN)` workload syntax per SRD-66.
2193 let has_variadic = args.iter().any(|a| matches!(a.kind, ArgKind::Variadic(_)));
2194 let variadic_count = args.iter().filter(|a| matches!(a.kind, ArgKind::Variadic(_))).count();
2195 if variadic_count > 2 {
2196 return Err(syn::Error::new_spanned(
2197 &func.sig,
2198 "`#[polydat_node]` supports at most two variadic `&[T]` args (split-halves shape). \
2199 Functions declaring more than two are not expressible in any SRD-80b shape.",
2200 ));
2201 }
2202 let is_split_halves = variadic_count == 2;
2203 // Positional index of each Variadic arg in declaration
2204 // order, used by `arg_bindings` to slice `inputs` at the
2205 // midpoint in split-halves mode.
2206 let variadic_positions: std::collections::HashMap<String, usize> = args.iter()
2207 .filter(|a| matches!(a.kind, ArgKind::Variadic(_)))
2208 .enumerate()
2209 .map(|(i, a)| (a.name.to_string(), i))
2210 .collect();
2211 let new_params: Vec<TokenStream2> = if has_variadic {
2212 let mut v = new_params;
2213 v.push(quote!(n_wires: usize));
2214 v
2215 } else {
2216 new_params
2217 };
2218
2219 // Build a lookup from arg name → const-shape category so the
2220 // Setup pre-compute step can dispatch on the source's shape
2221 // to produce the right access expression.
2222 #[derive(Clone, Copy)]
2223 enum ConstSourceShape {
2224 /// Scalar `Const<u64>` / `Const<f64>` / `Const<bool>`.
2225 ScalarValue,
2226 /// `Const<&str>` / `Const<String>` — backing field is
2227 /// `String`; setup fn typically wants `&str`.
2228 ScalarStr,
2229 /// `Const<Vec<C>>` — backing field is `Vec<C>`; setup fn
2230 /// typically wants `&Vec<C>` or `&[C]`.
2231 VecValues,
2232 }
2233 let const_shape_by_name: std::collections::HashMap<String, ConstSourceShape> = args.iter()
2234 .filter_map(|a| match &a.kind {
2235 ArgKind::Const(ConstShape::Str) => Some((a.name.to_string(), ConstSourceShape::ScalarStr)),
2236 ArgKind::Const(_) => Some((a.name.to_string(), ConstSourceShape::ScalarValue)),
2237 ArgKind::ConstVec(_) => Some((a.name.to_string(), ConstSourceShape::VecValues)),
2238 _ => None,
2239 })
2240 .collect();
2241
2242 // Setup pre-compute lines, emitted at the top of `new()`
2243 // BEFORE `Self { ... }` so they can borrow the const
2244 // locals before those values are moved into self.
2245 let setup_precomputes: Vec<TokenStream2> = args.iter()
2246 .filter_map(|a| match &a.kind {
2247 ArgKind::Wire | ArgKind::Const(_) | ArgKind::ConstVec(_) | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2248 ArgKind::Setup(spec) => {
2249 let n = &a.name;
2250 let setup_fn = &spec.setup_fn;
2251 // SRD-80b amendment — `source_args` may be empty
2252 // (session-static setup), single (the common
2253 // case), or multi (joint derivation). Per-source
2254 // access dispatch reads each named const's
2255 // shape and emits the right body-side expression.
2256 let mut src_exprs: Vec<TokenStream2> = Vec::new();
2257 let mut err: Option<TokenStream2> = None;
2258 for src in &spec.source_args {
2259 let shape = const_shape_by_name.get(&src.to_string());
2260 let expr = match shape {
2261 Some(ConstSourceShape::ScalarStr) => quote!(#src.as_str()),
2262 Some(ConstSourceShape::ScalarValue) => quote!(#src),
2263 // ConstVec source: pass a borrow of the
2264 // Vec. Setup fn signatures like
2265 // `fn build(w: &Vec<f64>)` or
2266 // `fn build(w: &[f64])` both work via
2267 // Deref / unsized coercion.
2268 Some(ConstSourceShape::VecValues) => quote!(&#src),
2269 None => {
2270 err = Some(syn::Error::new(
2271 src.span(),
2272 format!(
2273 "#[poly_const(... from = ... {src} ...)] — \
2274 `{src}` is not declared as a `Const<T>` \
2275 arg in the same function signature."),
2276 ).to_compile_error());
2277 break;
2278 }
2279 };
2280 src_exprs.push(expr);
2281 }
2282 if let Some(e) = err { return Some(e); }
2283 let call = quote!(#setup_fn( #( #src_exprs ),* ));
2284 Some(quote! {
2285 let #n = #call;
2286 })
2287 }
2288 })
2289 .collect();
2290
2291 // Self { ... } field-init list. Const args use field-name
2292 // shorthand; Setup args use the local computed above.
2293 // Wire/PolyWire contribute nothing (no field).
2294 let new_field_inits: Vec<TokenStream2> = args.iter()
2295 .filter_map(|a| match &a.kind {
2296 ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2297 ArgKind::Const(_) | ArgKind::ConstVec(_) | ArgKind::Setup(_) => {
2298 let n = &a.name;
2299 Some(quote!(#n))
2300 }
2301 })
2302 .collect();
2303
2304 // Per-arg bindings the eval body sees. Wire args unbox via
2305 // FromValue; const args wrap the struct field as `Const<T>`
2306 // so the user's body code sees the wrapper type matching
2307 // its function signature.
2308 let mut wire_idx = 0usize;
2309 let arg_bindings: Vec<TokenStream2> = args.iter()
2310 .map(|a| {
2311 let n = &a.name;
2312 match &a.kind {
2313 ArgKind::Wire => {
2314 let idx = syn::Index::from(wire_idx);
2315 wire_idx += 1;
2316 let ty = &a.declared_ty;
2317 // SRD-80b Phase B — dispatch:
2318 // 1. `Arc<T>` Handle (non-special T) → inline
2319 // downcast (no blanket impl works).
2320 // 2. Borrow shape (`&str`, `&[u8]`, `&[T]`,
2321 // `&serde_json::Value`) → direct
2322 // `match`-on-`Value`. Lifetime is naturally
2323 // `&inputs[i]`'s; no `unsafe` transmute.
2324 // 3. Otherwise → `<#ty as Wire>::extract`.
2325 if classify_wrapper_wire(ty) == Some(WrapperWire::Handle) {
2326 let inner = extract_handle_inner(ty)
2327 .expect("Handle classification implies Arc<T> shape");
2328 quote! {
2329 let #n: std::sync::Arc<#inner> = match &inputs[#idx] {
2330 polydat::ast::Value::Handle(arc) => arc.clone()
2331 .downcast::<#inner>()
2332 .expect("Handle type mismatch — wiring bug"),
2333 other => panic!("expected Handle, got {other:?}"),
2334 };
2335 }
2336 } else if let Some(borrow) = is_borrow_wire_shape(ty) {
2337 let extract = borrow_extract_tokens(borrow, quote!(&inputs[#idx]));
2338 quote! {
2339 let #n = #extract;
2340 }
2341 } else {
2342 quote! {
2343 let #n = <#ty as polydat::derive_support::Wire>::extract(&inputs[#idx]);
2344 }
2345 }
2346 }
2347 ArgKind::Const(shape) => {
2348 let wrap = shape.wrap_as_const(quote!(self.#n));
2349 quote! {
2350 let #n = #wrap;
2351 }
2352 }
2353 ArgKind::Setup(_) => {
2354 // Setup arg: body sees a borrow of the
2355 // construction-time computed field. No
2356 // wrapping needed — the field is the
2357 // user's named type and `&T` matches the
2358 // function-signature borrow.
2359 quote! {
2360 let #n = &self.#n;
2361 }
2362 }
2363 ArgKind::PolyWire => {
2364 // SRD-80 PR B.8: PolyWire — clone the
2365 // `Value` directly into a local. Body sees
2366 // an owned `Value`.
2367 let idx = syn::Index::from(wire_idx);
2368 wire_idx += 1;
2369 quote! {
2370 let #n: polydat::ast::Value = inputs[#idx].clone();
2371 }
2372 }
2373 ArgKind::Variadic(elem) => {
2374 // SRD-80 PR B.9 + SRD-80b split-halves —
2375 // materialise a Vec<T> from the inputs
2376 // slice (per-element extraction), then bind
2377 // the body local as `&[T]`. In single-
2378 // variadic mode, the slice is `inputs` (all
2379 // of them after the leading wires consumed
2380 // their indices). In split-halves mode, the
2381 // first variadic gets `inputs[0..n_wires]`
2382 // and the second gets `inputs[n_wires..]`.
2383 let extractor = elem.extract_from_value();
2384 let owned = format_ident!("__{}_owned", a.name);
2385 // Split-halves divides `inputs` at the
2386 // midpoint at eval time. `inputs.len() / 2`
2387 // is the per-half count; first variadic
2388 // gets the low half, second gets the high.
2389 let slice_expr = if is_split_halves {
2390 let pos = variadic_positions[&a.name.to_string()];
2391 if pos == 0 {
2392 quote!({ let __half = inputs.len() / 2; &inputs[..__half] })
2393 } else {
2394 quote!({ let __half = inputs.len() / 2; &inputs[__half..] })
2395 }
2396 } else {
2397 quote!(inputs)
2398 };
2399 quote! {
2400 let #owned: Vec<_> = #slice_expr.iter().map(#extractor).collect();
2401 let #n: &[_] = #owned.as_slice();
2402 }
2403 }
2404 ArgKind::ConstVec(_) => {
2405 // SRD-80b Phase C — `Const<Vec<C>>` body view:
2406 // clone the cached Vec and wrap in `Const`.
2407 // (Per-cycle clone matches the Wire-trait
2408 // convention; JIT-ineligible by design.)
2409 quote! {
2410 let #n = polydat::derive_support::Const(self.#n.clone());
2411 }
2412 }
2413 }
2414 })
2415 .collect();
2416
2417 // Build closure const-extraction logic. For each const arg
2418 // (in declaration order), pull from `consts: &[ConstArg]`
2419 // by index; fall back to the `poly_default` value if the
2420 // slice is shorter than the const arg list.
2421 //
2422 // For `ConstVec` args, collect every remaining entry from
2423 // `consts[i..]` into a `Vec<inner>` via the inner shape's
2424 // extractor — this consumes the tail of the consts slice
2425 // (only one ConstVec arg per function, enforced earlier).
2426 let mut const_idx_for_extract = 0usize;
2427 let const_extracts: Vec<TokenStream2> = args.iter()
2428 .filter_map(|a| match &a.kind {
2429 ArgKind::Wire | ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2430 ArgKind::Const(shape) => {
2431 let n = &a.name;
2432 let i = const_idx_for_extract;
2433 const_idx_for_extract += 1;
2434 let i_lit = syn::Index::from(i);
2435 let extract_present = shape.extract_from_const_arg(quote!(c));
2436 let fallback = match &a.default_value {
2437 Some(default_expr) => {
2438 // Default is an expression evaluating to
2439 // the field type (`u64`, `f64`, `bool`,
2440 // `String`). For Str: the expression
2441 // should produce a `&str` or `String`; we
2442 // call `.to_string()` to land on owned.
2443 match shape {
2444 ConstShape::Str => quote!((#default_expr).to_string()),
2445 _ => quote!(#default_expr),
2446 }
2447 }
2448 None => {
2449 let msg = format!(
2450 "missing required const arg '{n}' for function '{func_name_str}'");
2451 quote!(return Some(Err(#msg.to_string())))
2452 }
2453 };
2454 Some(quote! {
2455 let #n: _ = match consts.get(#i_lit) {
2456 Some(c) => #extract_present,
2457 None => #fallback,
2458 };
2459 })
2460 }
2461 ArgKind::ConstVec(inner) => {
2462 let n = &a.name;
2463 let i = const_idx_for_extract;
2464 // ConstVec consumes everything from index `i`
2465 // onward. const_idx_for_extract is intentionally
2466 // NOT bumped — by construction (validated below)
2467 // there's at most one ConstVec arg and it must be
2468 // the last arg, so no subsequent Const reads need
2469 // a higher base index.
2470 let i_lit = syn::LitInt::new(&i.to_string(), proc_macro2::Span::call_site());
2471 let extract_one = inner.extract_from_const_arg(quote!(c));
2472 Some(quote! {
2473 let #n: Vec<_> = consts[#i_lit..].iter()
2474 .map(|c| #extract_one)
2475 .collect();
2476 })
2477 }
2478 })
2479 .collect();
2480
2481 // Names to pass to `Self::new(...)` from the build closure,
2482 // in declaration order. Const → `<name>`; PolyWire →
2483 // `<name>_type` (the local extracted from `wire_types`).
2484 let mut new_call_args: Vec<TokenStream2> = args.iter()
2485 .filter_map(|a| match &a.kind {
2486 ArgKind::Wire | ArgKind::Setup(_) | ArgKind::Variadic(_) => None,
2487 ArgKind::Const(_) | ArgKind::ConstVec(_) => {
2488 let n = &a.name;
2489 Some(quote!(#n))
2490 }
2491 ArgKind::PolyWire => {
2492 let n = format_ident!("{}_type", a.name);
2493 Some(quote!(#n))
2494 }
2495 })
2496 .collect();
2497 if has_variadic {
2498 new_call_args.push(quote!(n_wires));
2499 }
2500
2501 // SRD-80 PR B.9: when the function has a variadic arg,
2502 // extract `n_wires` from the `_wires: &[WireRef]` slice in
2503 // the build closure. The whole `_wires.len()` is the variadic
2504 // count (this PR supports one variadic arg only — when
2505 // multi-variadic lands, this extraction needs the per-arg
2506 // split logic).
2507 let variadic_n_wires_extract: TokenStream2 = if has_variadic {
2508 // Split-halves: assembler hands TOTAL wires; new() takes
2509 // the per-half count, so divide by 2 here too (matches
2510 // the variadic_ctor field's `n / 2`).
2511 if is_split_halves {
2512 quote! { let n_wires: usize = _wires.len() / 2; }
2513 } else {
2514 quote! { let n_wires: usize = _wires.len(); }
2515 }
2516 } else {
2517 quote!()
2518 };
2519
2520 // SRD-80 PR B.8: extract resolved PolyWire port types from
2521 // the `wire_types: &[PortType]` slice the assembler hands
2522 // the build closure. Wire/PolyWire share the same slot
2523 // counter (both consume a wire input position); we count
2524 // through args in declaration order.
2525 let polywire_extracts: Vec<TokenStream2> = {
2526 let mut wire_idx = 0usize;
2527 let mut out = Vec::new();
2528 for a in &args {
2529 match &a.kind {
2530 ArgKind::Wire => { wire_idx += 1; }
2531 ArgKind::Variadic(_) => {
2532 // Variadic args consume the REMAINDER of the
2533 // wire slots. Only one variadic arg supported
2534 // in this PR.
2535 wire_idx += 0; // no positional increment
2536 }
2537 ArgKind::PolyWire => {
2538 let pt_ident = format_ident!("{}_type", a.name);
2539 let i = syn::Index::from(wire_idx);
2540 let n_str = a.name.to_string();
2541 let err = format!(
2542 "polywire arg '{n_str}' for '{func_name_str}': assembler \
2543 did not resolve a port type at wire index {wire_idx}");
2544 out.push(quote! {
2545 let #pt_ident: polydat::ast::PortType = match _wire_types.get(#i) {
2546 Some(t) => *t,
2547 None => return Some(Err(#err.to_string())),
2548 };
2549 });
2550 wire_idx += 1;
2551 }
2552 ArgKind::Const(_) | ArgKind::ConstVec(_) | ArgKind::Setup(_) => {}
2553 }
2554 }
2555 out
2556 };
2557
2558 let block = &func.block;
2559
2560 // SRD-80b in-spirit `default_resolver` emission. Each wire
2561 // arg's `Wire::RESOLVER` const exposes the auto-resolver
2562 // intent at codegen time; the cascade picks the first
2563 // non-None among the wire-typed args. Non-Resolved wire
2564 // types contribute `None` (the trait default), so this
2565 // collapses cleanly to a no-resolver FuncSig for the
2566 // overwhelming majority of nodes.
2567 let default_resolver_field: TokenStream2 = {
2568 // Borrow shapes (`&str`, `&[u8]`, ...) don't impl `Wire`,
2569 // and `PolyWire` is excluded by ArgKind; only the
2570 // owned-type wire args contribute resolver intent.
2571 let wire_tys: Vec<&Type> = args.iter()
2572 .filter_map(|a| match &a.kind {
2573 ArgKind::Wire if is_borrow_wire_shape(&a.declared_ty).is_none()
2574 && classify_wrapper_wire(&a.declared_ty) != Some(WrapperWire::Handle)
2575 => Some(&a.declared_ty),
2576 _ => None,
2577 })
2578 .collect();
2579 if wire_tys.is_empty() {
2580 quote!(None)
2581 } else {
2582 // Build a right-to-left match cascade so the first
2583 // wire arg with a Some(_) resolver wins. Each step:
2584 // match <ty as Wire>::RESOLVER { Some(r) => Some(r), None => <rest> }
2585 let mut acc = quote!(None);
2586 for ty in wire_tys.iter().rev() {
2587 acc = quote! {
2588 match <#ty as polydat::derive_support::Wire>::RESOLVER {
2589 Some(__r) => Some(__r),
2590 None => #acc,
2591 }
2592 };
2593 }
2594 acc
2595 }
2596 };
2597
2598 // SRD-80b Phase D1 — when multiple instantiations share a
2599 // DSL function name, each per-instantiation build closure
2600 // must claim only the call that matches its own concrete
2601 // wire types. The guard checks `wire_types[i]` against
2602 // `<#ty as Wire>::PORT` for every wire-position arg. The
2603 // factory walks all matching registrations and the first to
2604 // accept (return `Some(Ok(...))`) wins; mismatches fall
2605 // through to the next instantiation.
2606 let port_guard: TokenStream2 = if is_instantiation {
2607 let mut wi: usize = 0;
2608 let mut checks: Vec<TokenStream2> = Vec::new();
2609 for a in &args {
2610 match &a.kind {
2611 ArgKind::Wire | ArgKind::PolyWire => {
2612 let i = syn::Index::from(wi);
2613 let ty = &a.declared_ty;
2614 // PolyWire stays opaque (Value isn't a Wire impl);
2615 // skip it from the guard.
2616 if !classify_polywire(ty) {
2617 checks.push(quote! {
2618 if _wire_types.get(#i) != Some(&<#ty as polydat::derive_support::Wire>::PORT) {
2619 return None;
2620 }
2621 });
2622 }
2623 wi += 1;
2624 }
2625 ArgKind::Variadic(_) => {
2626 // Variadic — claims any tail; instantiation
2627 // selection on variadic generic-over-Wire
2628 // isn't supported in this pass.
2629 }
2630 _ => {}
2631 }
2632 }
2633 quote! { #( #checks )* }
2634 } else {
2635 quote!()
2636 };
2637
2638 // Emit `Default` only when there are no const args AND no
2639 // setup args. Both require captured values to construct.
2640 let has_non_wire = args.iter().any(|a| !matches!(a.kind, ArgKind::Wire));
2641 let default_impl = if has_non_wire {
2642 quote!()
2643 } else {
2644 quote! {
2645 impl Default for #struct_name {
2646 fn default() -> Self { Self::new() }
2647 }
2648 }
2649 };
2650
2651 // SRD-80b Phase F (S18) — `#[polydat_node(decompose =
2652 // path)]` emits the FusedNode impl by delegating to the
2653 // named free function. Operators with bespoke fusion
2654 // logic (e.g. WeightedPick whose `decomposed()` body
2655 // builds a spec string) can still write their own
2656 // `impl FusedNode` block alongside the macro emission;
2657 // both compose because `decompose` is opt-in.
2658 let fused_node_impl: TokenStream2 = if let Some(path) = &attrs.decompose {
2659 quote! {
2660 impl polydat::compile::fusion::FusedNode for #struct_name {
2661 fn decomposed(&self) -> polydat::compile::fusion::DecomposedGraph {
2662 #path(self)
2663 }
2664 }
2665 }
2666 } else {
2667 quote!()
2668 };
2669
2670 // ── SRD-80 PR B.7 — JIT eligibility + hook emission ──
2671 //
2672 // A node is Phase-2 eligible when every arg + return maps
2673 // to a `JitType` and no `Setup<T>` arg is declared (Setup
2674 // carries non-primitive derived state that can't fit a u64
2675 // buffer). Override attributes (`compiled_u64 = ...`,
2676 // `jit_constants = ...`) bypass eligibility — they win
2677 // unconditionally. `no_jit` blocks macro emission when no
2678 // override is present.
2679
2680 let has_setup = args.iter().any(|a| matches!(a.kind, ArgKind::Setup(_)));
2681 let ret_jit_type = wire_type_to_jit_type(&ret_ty);
2682
2683 let arg_jit_types: Option<Vec<JitType>> = if has_setup {
2684 None
2685 } else {
2686 args.iter()
2687 .map(|a| match &a.kind {
2688 ArgKind::Wire => wire_type_to_jit_type(&a.declared_ty),
2689 ArgKind::Const(shape) => const_shape_to_jit_type(*shape),
2690 // ConstVec is JIT-ineligible (the JIT u64 buffer
2691 // has no slot shape for a variable-length list).
2692 ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(_) => None,
2693 // SRD-80 PR B.9: variadic JIT — only `&[u64]`
2694 // rides the Phase 2 closure cleanly (the buffer
2695 // IS the slice). For f64/bool/Str variadics
2696 // the closure would need a per-call Vec
2697 // allocation to bit-reinterpret; skip in this PR.
2698 ArgKind::Variadic(elem) => match elem {
2699 VariadicElement::U64 => Some(JitType::U64),
2700 _ => None,
2701 },
2702 })
2703 .collect()
2704 };
2705
2706 // SRD-80 PR B.10/B.15: tuple return becomes JIT-eligible
2707 // when every element is JIT-eligible. The compiled_u64
2708 // closure destructures the result and writes each element
2709 // to its `outputs[i]` slot via the matching JitType.
2710 let tuple_ret_jit_types: Option<Vec<JitType>> = tuple_ret_elems.as_ref()
2711 .and_then(|elems| {
2712 elems.iter()
2713 .map(wire_type_to_jit_type)
2714 .collect::<Option<Vec<_>>>()
2715 });
2716
2717 let jit_eligible = !is_fallible
2718 && arg_jit_types.is_some()
2719 && (ret_jit_type.is_some() || tuple_ret_jit_types.is_some());
2720
2721 // §8.4 layer 3 — slot eligibility: at least one typed-slice
2722 // arg or `Vec<elem>` return, every other wire arg jit-able,
2723 // consts capturable, single return, no setup/polywire/
2724 // variadic shapes. Slot-compiled nodes read slice inputs as
2725 // `(ptr, len)` slot pairs and write vector outputs into
2726 // kernel-owned scratch.
2727 let slice_arg_elem = |ty: &Type| -> Option<&'static str> {
2728 match is_borrow_wire_shape(ty) {
2729 Some(BorrowWire::Vec(variant, _)) => match variant {
2730 "VecF32" => Some("F32"),
2731 "VecF64" => Some("F64"),
2732 "VecF16" => Some("F16"),
2733 "VecI8" => Some("I8"),
2734 "VecI16" => Some("I16"),
2735 "VecI32" => Some("I32"),
2736 "VecI64" => Some("I64"),
2737 _ => None,
2738 },
2739 _ => None,
2740 }
2741 };
2742 let vec_ret_elem: Option<&'static str> = {
2743 let flat: String = type_to_string(&ret_ty).split_whitespace().collect();
2744 match flat.as_str() {
2745 "Vec<f32>" => Some("F32"),
2746 "Vec<f64>" => Some("F64"),
2747 "Vec<half::f16>" | "Vec<f16>" => Some("F16"),
2748 "Vec<i8>" => Some("I8"),
2749 "Vec<i16>" => Some("I16"),
2750 "Vec<i32>" => Some("I32"),
2751 "Vec<i64>" => Some("I64"),
2752 _ => None,
2753 }
2754 };
2755 enum SlotArgRead {
2756 Jit(JitType),
2757 Slice(&'static str),
2758 Const,
2759 }
2760 let slot_arg_reads: Option<Vec<SlotArgRead>> = if has_setup
2761 || tuple_ret_elems.is_some()
2762 || is_fallible
2763 {
2764 None
2765 } else {
2766 args.iter()
2767 .map(|a| match &a.kind {
2768 ArgKind::Wire => wire_type_to_jit_type(&a.declared_ty)
2769 .map(SlotArgRead::Jit)
2770 .or_else(|| slice_arg_elem(&a.declared_ty).map(SlotArgRead::Slice)),
2771 ArgKind::Const(_) => Some(SlotArgRead::Const),
2772 _ => None,
2773 })
2774 .collect()
2775 };
2776 let has_slice_shape = slot_arg_reads
2777 .as_ref()
2778 .map(|v| v.iter().any(|r| matches!(r, SlotArgRead::Slice(_))))
2779 .unwrap_or(false)
2780 || vec_ret_elem.is_some();
2781 let slot_eligible = !jit_eligible
2782 && has_slice_shape
2783 && slot_arg_reads.is_some()
2784 && (ret_jit_type.is_some() || vec_ret_elem.is_some());
2785
2786 let emit_compiled_u64 = attrs.compiled_u64_override.is_some()
2787 || (jit_eligible && !attrs.no_jit);
2788 let emit_jit_constants = attrs.jit_constants_override.is_some()
2789 || (jit_eligible && !attrs.no_jit);
2790
2791 // Body sharing: extract the function body into a private
2792 // associated fn `__polydat_body` when JIT is emitted. Both
2793 // `eval()` (Value boxing path) and `compiled_u64()` (u64
2794 // buffer path) call it. Single source of truth.
2795 //
2796 // When JIT is not emitted, the body stays inlined inside
2797 // `eval()`'s current `#[allow(unused_variables)]` block
2798 // (Setup-bearing nodes need this — their body references
2799 // setup-derived locals via `let n = &self.n` bindings).
2800
2801 let use_shared_body = (jit_eligible && (emit_compiled_u64 || !attrs.no_jit))
2802 || (slot_eligible && !attrs.no_jit);
2803
2804 // Body-fn parameter list — every arg in its DECLARED form
2805 // (wire as bare type, const as `Const<T>`, setup as `&T`).
2806 let body_params: Vec<TokenStream2> = args.iter()
2807 .map(|a| {
2808 let n = &a.name;
2809 let t = &a.declared_ty;
2810 quote!(#n: #t)
2811 })
2812 .collect();
2813
2814 let body_fn_def: TokenStream2 = if is_fallible {
2815 // SRD-80b Phase 5 S16 — fallible body. Body returns the
2816 // declared Result<T, E>; try_new runs it once at
2817 // construction and propagates Err as String via Into.
2818 quote! {
2819 #[inline(always)]
2820 #[allow(unused_variables)]
2821 fn __polydat_body( #( #body_params ),* ) -> #declared_ret_ty #block
2822 }
2823 } else if use_shared_body {
2824 quote! {
2825 #[inline(always)]
2826 #[allow(unused_variables)]
2827 fn __polydat_body( #( #body_params ),* ) -> #ret_ty #block
2828 }
2829 } else {
2830 quote!()
2831 };
2832
2833 // Helper: emit `outputs[idx] = <conversion>(value)` for a
2834 // given element type. SRD-80b Phase B — owned types route
2835 // through `<T as Wire>::inject`; Handle keeps its inline
2836 // upcast (no blanket impl works). Returning a borrow shape
2837 // (`&str`, `&[u8]`, etc.) from a node body is unusual but
2838 // supported: the borrow's `into()` already exists for the
2839 // canonical `Value` constructor; we emit that directly.
2840 let output_assign = |idx_lit: TokenStream2, elem_ty: &Type, local: TokenStream2| -> TokenStream2 {
2841 if classify_wrapper_wire(elem_ty) == Some(WrapperWire::Handle) {
2842 quote! {
2843 outputs[#idx_lit] = polydat::ast::Value::handle(#local);
2844 }
2845 } else if classify_polywire(elem_ty) {
2846 // PolyWire return: body returns `Value` directly, move
2847 // it into the outputs slot. No trait dispatch — Value
2848 // has no static port type (it's polymorphic at runtime).
2849 quote! {
2850 outputs[#idx_lit] = #local;
2851 }
2852 } else if let Some(borrow) = is_borrow_wire_shape(elem_ty) {
2853 // Borrow-typed returns: construct the matching Value
2854 // variant from the borrow via the existing
2855 // `Into<Value>` / Arc::from path. `&str` →
2856 // `Value::Str(arc)`; `&[u8]` → `Value::Bytes(arc)`;
2857 // typed-vec borrows → `Value::Vec*(SliceArc::from(slice))`.
2858 match borrow {
2859 BorrowWire::Str => quote! {
2860 outputs[#idx_lit] = polydat::ast::Value::Str((#local).into());
2861 },
2862 BorrowWire::Bytes => quote! {
2863 outputs[#idx_lit] = polydat::ast::Value::Bytes((#local).into());
2864 },
2865 BorrowWire::Json => quote! {
2866 outputs[#idx_lit] = polydat::ast::Value::Json(::std::sync::Arc::new((#local).clone()));
2867 },
2868 BorrowWire::Vec(variant, _) => {
2869 let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
2870 quote! {
2871 outputs[#idx_lit] = polydat::ast::Value::#v(polydat::ast::SliceArc::from_vec((#local).to_vec()));
2872 }
2873 }
2874 }
2875 } else {
2876 quote! {
2877 outputs[#idx_lit] = <#elem_ty as polydat::derive_support::Wire>::inject(#local);
2878 }
2879 }
2880 };
2881
2882 // SRD-80b `DynamicOutputs<T>` — build the `outs:` vec at
2883 // construction from the driving `Const<Vec<C>>` arg's
2884 // length. Used by both the infallible `new()` and the
2885 // fallible `try_new()` paths below.
2886 let outs_build: TokenStream2 = if let (Some(inner), Some(count_arg)) =
2887 (&dynamic_outputs_inner, &dynamic_outputs_count_arg)
2888 {
2889 quote! {
2890 let outs: Vec<polydat::ast::Port> = (0..#count_arg.len())
2891 .map(|__i| polydat::ast::Port::new(
2892 format!("d{}", __i),
2893 <#inner as polydat::derive_support::Wire>::PORT,
2894 ))
2895 .collect();
2896 }
2897 } else {
2898 quote! {
2899 let outs = vec![ #(
2900 polydat::ast::Port::new(#output_names_strs, #output_port_types)
2901 ),* ];
2902 }
2903 };
2904
2905 // SRD-80 PR B.10/B.11: result → outputs translation. For
2906 // single-output, write `outputs[0] = ...(result)`. For
2907 // tuple-output, destructure and per-element write. For
2908 // SRD-80b `DynamicOutputs<T>`, iterate the returned Vec
2909 // and inject each element via the inner type's Wire impl.
2910 let result_to_outputs: TokenStream2 = if let Some(inner) = &dynamic_outputs_inner {
2911 let inject_one = if classify_polywire(inner) {
2912 quote!(__elem)
2913 } else if let Some(borrow) = is_borrow_wire_shape(inner) {
2914 match borrow {
2915 BorrowWire::Str => quote!(polydat::ast::Value::Str((__elem).into())),
2916 BorrowWire::Bytes => quote!(polydat::ast::Value::Bytes((__elem).into())),
2917 BorrowWire::Json => quote!(polydat::ast::Value::Json(::std::sync::Arc::new((__elem).clone()))),
2918 BorrowWire::Vec(variant, _) => {
2919 let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
2920 quote!(polydat::ast::Value::#v(polydat::ast::SliceArc::from_vec((__elem).to_vec())))
2921 }
2922 }
2923 } else {
2924 quote!(<#inner as polydat::derive_support::Wire>::inject(__elem))
2925 };
2926 quote! {
2927 for (__i, __elem) in result.0.into_iter().enumerate() {
2928 outputs[__i] = #inject_one;
2929 }
2930 }
2931 } else if let Some(elems) = &tuple_ret_elems {
2932 let locals: Vec<Ident> = (0..elems.len())
2933 .map(|i| format_ident!("__r_{}", i))
2934 .collect();
2935 let writes: Vec<TokenStream2> = elems.iter().enumerate()
2936 .map(|(i, elem_ty)| {
2937 let local = &locals[i];
2938 let idx = syn::Index::from(i);
2939 output_assign(quote!(#idx), elem_ty, quote!(#local))
2940 })
2941 .collect();
2942 quote! {
2943 let ( #( #locals ),* ) = result;
2944 #( #writes )*
2945 }
2946 } else {
2947 output_assign(quote!(0), &ret_ty, quote!(result))
2948 };
2949
2950 // Eval-path arg bindings + body-call. When JIT is emitted,
2951 // eval() unboxes from Values and calls `__polydat_body`.
2952 // When JIT is not emitted, the body stays inline in
2953 // `eval()` for back-compat with Setup-bearing nodes.
2954 let eval_body: TokenStream2 = if use_shared_body {
2955 let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
2956 quote! {
2957 #[allow(unused_variables)]
2958 {
2959 #( #arg_bindings )*
2960 let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
2961 #result_to_outputs
2962 }
2963 }
2964 } else {
2965 quote! {
2966 #[allow(unused_variables)]
2967 {
2968 #( #arg_bindings )*
2969 let result: #ret_ty = (|| #block)();
2970 #result_to_outputs
2971 }
2972 }
2973 };
2974
2975 // compiled_u64() emission. Three cases:
2976 // (a) Override path supplied → call it.
2977 // (b) JIT eligible and not opted out → emit closure that
2978 // reads from u64 buffer, captures const fields by
2979 // Copy, calls __polydat_body, writes back.
2980 // (c) Otherwise → don't override the trait default
2981 // (returns None).
2982 let compiled_u64_impl: TokenStream2 = if let Some(path) = &attrs.compiled_u64_override {
2983 // SRD-80b in-spirit refinement — pass `&self` to the
2984 // override fn so setup-derived state (round_keys,
2985 // half_bits, etc.) is reachable. The override fn
2986 // signature is now `fn(&Self) -> CompiledU64Op`.
2987 quote! {
2988 fn compiled_u64(&self) -> Option<polydat::ast::CompiledU64Op> {
2989 Some(#path(self))
2990 }
2991 }
2992 } else if jit_eligible && !attrs.no_jit {
2993 // Per-arg jit handling. Wire args read from inputs at
2994 // the next sequential index. Const args capture by Copy
2995 // from self at closure-creation time, then re-wrap as
2996 // `Const<T>` inside the closure for handoff to body.
2997 let jit_types = arg_jit_types.as_ref().unwrap();
2998 let mut wire_buf_idx = 0usize;
2999
3000 let captures: Vec<TokenStream2> = args.iter()
3001 .filter_map(|a| match &a.kind {
3002 ArgKind::Wire | ArgKind::Variadic(_) => None,
3003 ArgKind::Const(_) => {
3004 let n = &a.name;
3005 Some(quote!(let #n = self.#n.clone();))
3006 }
3007 ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(_) => {
3008 unreachable!("setup/polywire/constvec excludes JIT eligibility")
3009 }
3010 })
3011 .collect();
3012
3013 let arg_reads: Vec<TokenStream2> = args.iter().zip(jit_types.iter())
3014 .map(|(a, jt)| {
3015 let n = &a.name;
3016 let _ = jt;
3017 match &a.kind {
3018 ArgKind::Wire => {
3019 let read = jt.read_from_u64_buffer(wire_buf_idx);
3020 wire_buf_idx += jt.width();
3021 quote!(let #n = #read;)
3022 }
3023 ArgKind::Const(shape) => {
3024 if *shape == ConstShape::Str {
3025 quote!(let #n = polydat::derive_support::Const(#n.as_str());)
3026 } else {
3027 quote!(let #n = polydat::derive_support::Const(#n);)
3028 }
3029 }
3030 ArgKind::Variadic(_) => {
3031 // SRD-80 PR B.9: u64 variadic — pass the
3032 // whole `inputs: &[u64]` buffer directly
3033 // to the body. Zero allocation, zero conversion.
3034 // (Non-u64 variadics aren't JIT-eligible —
3035 // this branch is only reached for u64 elems.)
3036 quote!(let #n: &[u64] = inputs;)
3037 }
3038 ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(_) => unreachable!(),
3039 }
3040 })
3041 .collect();
3042
3043 let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
3044 // SRD-80 PR B.15: multi-output write. For single-output
3045 // ret, `write` emits `outputs[0] = bits(result)`. For
3046 // tuple-output, destructure into locals and emit a
3047 // per-element write line.
3048 let write = if let Some(tuple_jits) = &tuple_ret_jit_types {
3049 let locals: Vec<Ident> = (0..tuple_jits.len())
3050 .map(|i| format_ident!("__jit_r_{}", i))
3051 .collect();
3052 // Per-element write at the element's slot OFFSET (the
3053 // prefix sum of preceding element widths — §8.4 L1).
3054 let mut out_off = 0usize;
3055 let writes: Vec<TokenStream2> = tuple_jits.iter().enumerate()
3056 .map(|(i, jt)| {
3057 let local = &locals[i];
3058 let w = jt.write_to_u64_buffer_at(out_off, quote!(#local));
3059 out_off += jt.width();
3060 w
3061 })
3062 .collect();
3063 quote! {
3064 let ( #( #locals ),* ) = result;
3065 #( #writes )*
3066 }
3067 } else {
3068 let ret_jit = ret_jit_type.unwrap();
3069 ret_jit.write_to_u64_buffer(quote!(result))
3070 };
3071
3072 quote! {
3073 fn compiled_u64(&self) -> Option<polydat::ast::CompiledU64Op> {
3074 #( #captures )*
3075 Some(Box::new(move |inputs: &[u64], outputs: &mut [u64]| {
3076 #( #arg_reads )*
3077 let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
3078 #write
3079 }))
3080 }
3081 }
3082 } else {
3083 quote!()
3084 };
3085
3086 // compiled_slot() emission (§8.4 layer 3). Slice inputs read
3087 // (ptr, len) slot pairs; a Vec return moves into scratch[0]
3088 // and publishes its (ptr, len). Scalar args/returns reuse the
3089 // width-aware JitType buffer tokens.
3090 let compiled_slot_impl: TokenStream2 = if slot_eligible && !attrs.no_jit {
3091 let reads_spec = slot_arg_reads.as_ref().unwrap();
3092 let elem_ty_tokens = |elem: &str| -> TokenStream2 {
3093 match elem {
3094 "F32" => quote!(f32),
3095 "F64" => quote!(f64),
3096 "F16" => quote!(polydat::half::f16),
3097 "I8" => quote!(i8),
3098 "I16" => quote!(i16),
3099 "I32" => quote!(i32),
3100 "I64" => quote!(i64),
3101 _ => unreachable!(),
3102 }
3103 };
3104 let captures: Vec<TokenStream2> = args.iter()
3105 .filter_map(|a| match &a.kind {
3106 ArgKind::Const(_) => {
3107 let n = &a.name;
3108 Some(quote!(let #n = self.#n.clone();))
3109 }
3110 _ => None,
3111 })
3112 .collect();
3113 let mut off = 0usize;
3114 let arg_reads: Vec<TokenStream2> = args.iter().zip(reads_spec.iter())
3115 .map(|(a, spec)| {
3116 let n = &a.name;
3117 match spec {
3118 SlotArgRead::Jit(jt) => {
3119 let read = jt.read_from_u64_buffer(off);
3120 off += jt.width();
3121 quote!(let #n = #read;)
3122 }
3123 SlotArgRead::Slice(elem) => {
3124 let et = elem_ty_tokens(elem);
3125 let i = syn::Index::from(off);
3126 let i1 = syn::Index::from(off + 1);
3127 off += 2;
3128 // SAFETY: the (ptr, len) pair was published
3129 // by an upstream slot-op into kernel-owned
3130 // scratch (or by the host for the eval call's
3131 // duration); the layer-3 ownership rule keeps
3132 // it alive until this step's producer reruns.
3133 quote! {
3134 let #n: &[#et] = unsafe {
3135 ::core::slice::from_raw_parts(
3136 inputs[#i] as usize as *const #et,
3137 inputs[#i1] as usize,
3138 )
3139 };
3140 }
3141 }
3142 SlotArgRead::Const => {
3143 quote!(let #n = polydat::derive_support::Const(#n.clone());)
3144 }
3145 }
3146 })
3147 .collect();
3148 let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
3149 let (scratch_decl, write) = if let Some(elem) = vec_ret_elem {
3150 let se = syn::Ident::new(elem, proc_macro2::Span::call_site());
3151 (
3152 quote!(vec![polydat::ast::ScratchElem::#se]),
3153 quote! {
3154 let polydat::ast::ScratchBuf::#se(__buf) = &mut scratch[0] else {
3155 unreachable!("scratch element type mismatch");
3156 };
3157 *__buf = result;
3158 outputs[0] = __buf.as_ptr() as usize as u64;
3159 outputs[1] = __buf.len() as u64;
3160 },
3161 )
3162 } else {
3163 let ret_jit = ret_jit_type.unwrap();
3164 (quote!(vec![]), ret_jit.write_to_u64_buffer(quote!(result)))
3165 };
3166 quote! {
3167 fn compiled_slot(&self) -> Option<polydat::ast::CompiledSlotKit> {
3168 #( #captures )*
3169 Some(polydat::ast::CompiledSlotKit {
3170 scratch: #scratch_decl,
3171 op: Box::new(move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [polydat::ast::ScratchBuf]| {
3172 #( #arg_reads )*
3173 let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
3174 #write
3175 }),
3176 })
3177 }
3178 }
3179 } else {
3180 quote!()
3181 };
3182
3183 // jit_constants() emission. Three cases:
3184 // (a) Override path supplied → call it with `&self`.
3185 // (b) JIT eligible and not opted out → emit a Vec<u64>
3186 // built from const fields in declaration order,
3187 // bit-reinterpreting f64 and 0/1-encoding bool.
3188 // (c) Otherwise → don't override the trait default.
3189 let jit_constants_impl: TokenStream2 = if let Some(path) = &attrs.jit_constants_override {
3190 quote! {
3191 fn jit_constants(&self) -> Vec<u64> {
3192 #path(self)
3193 }
3194 }
3195 } else if emit_jit_constants {
3196 let const_encodings: Vec<TokenStream2> = args.iter()
3197 .filter_map(|a| match &a.kind {
3198 ArgKind::Const(shape) => {
3199 let jt = const_shape_to_jit_type(*shape)?;
3200 let n = &a.name;
3201 Some(jt.const_field_as_u64(quote!(self.#n)))
3202 }
3203 _ => None,
3204 })
3205 .collect();
3206
3207 quote! {
3208 fn jit_constants(&self) -> Vec<u64> {
3209 vec![ #( #const_encodings ),* ]
3210 }
3211 }
3212 } else {
3213 quote!()
3214 };
3215
3216 // purity() emission — only when attribute is set; otherwise
3217 // the trait default (`Pure`) is used.
3218 //
3219 // Two attribute shapes:
3220 // - `Expr::Path` (e.g. `Nondeterministic`)
3221 // → `Purity::Nondeterministic`
3222 // - `Expr::Call` (e.g. `SideChannel(LogBuffer)`)
3223 // → `Purity::SideChannel { sink: SideChannelSink::LogBuffer }`
3224 let purity_impl: TokenStream2 = match &attrs.purity {
3225 None => quote!(),
3226 Some(syn::Expr::Path(p)) => {
3227 let variant = &p.path;
3228 quote! {
3229 fn purity(&self) -> polydat::ast::Purity {
3230 polydat::ast::Purity::#variant
3231 }
3232 }
3233 }
3234 Some(syn::Expr::Call(c)) => {
3235 // SRD-80 PR B.7/B.11: dispatch on the variant head.
3236 // SideChannel(<SideChannelSink variant>) →
3237 // Purity::SideChannel { sink: SideChannelSink::<arg> }
3238 // Nondeterministic(<&'static str reason>) →
3239 // Purity::Nondeterministic { reason: <arg> }
3240 let syn::Expr::Path(head_path) = &*c.func else {
3241 return Err(syn::Error::new_spanned(
3242 &c.func,
3243 "purity call-form expects a Purity variant ident as the head.",
3244 ));
3245 };
3246 let head_ident = head_path.path.get_ident().ok_or_else(|| {
3247 syn::Error::new_spanned(
3248 &c.func,
3249 "purity call-form head must be a single Purity variant ident.",
3250 )
3251 })?;
3252 let arg = c.args.first().ok_or_else(|| {
3253 syn::Error::new_spanned(
3254 c,
3255 "purity call-form requires one argument.")
3256 })?;
3257 match head_ident.to_string().as_str() {
3258 "SideChannel" => quote! {
3259 fn purity(&self) -> polydat::ast::Purity {
3260 polydat::ast::Purity::SideChannel {
3261 sink: polydat::ast::SideChannelSink::#arg,
3262 }
3263 }
3264 },
3265 "Nondeterministic" => quote! {
3266 fn purity(&self) -> polydat::ast::Purity {
3267 polydat::ast::Purity::Nondeterministic { reason: #arg }
3268 }
3269 },
3270 other => return Err(syn::Error::new_spanned(
3271 head_ident,
3272 format!(
3273 "purity call-form head `{other}` not recognized. \
3274 Use `SideChannel(<sink>)` or `Nondeterministic(<reason>)`."),
3275 )),
3276 }
3277 }
3278 Some(other) => {
3279 return Err(syn::Error::new_spanned(
3280 other,
3281 "purity attribute must be a Purity variant path or call form",
3282 ));
3283 }
3284 };
3285
3286 let _ = emit_compiled_u64; // referenced via the conditionals above
3287
3288 // SRD-80 PR B.9: conditional FuncSig fields.
3289 let identity_field: TokenStream2 = if let Some(expr) = &attrs.identity {
3290 quote!(Some(#expr))
3291 } else {
3292 quote!(None)
3293 };
3294
3295 // `variadic_ctor` only emitted for pure-variadic nodes (no
3296 // const args, no PolyWire). Const+variadic mixing would need
3297 // the ctor to thread the const values through — defer to a
3298 // future PR.
3299 let has_const_arg = args.iter().any(|a| matches!(a.kind, ArgKind::Const(_)));
3300 let has_polywire = args.iter().any(|a| matches!(a.kind, ArgKind::PolyWire));
3301 let variadic_ctor_field: TokenStream2 = if has_variadic && !has_const_arg && !has_polywire {
3302 // Split-halves: assembler passes TOTAL wire count; the
3303 // struct's `new()` takes per-half count, so divide by 2.
3304 if is_split_halves {
3305 quote!(Some(|n| Box::new(#struct_name::new(n / 2))))
3306 } else {
3307 quote!(Some(|n| Box::new(#struct_name::new(n))))
3308 }
3309 } else {
3310 quote!(None)
3311 };
3312
3313 // SRD-80b Phase C — `Option<T>` arg auto-emits
3314 // `accepts_none_inputs() -> true`. The runtime kernel's
3315 // SRD-74 Rule 1 propagation short-circuits `Value::None`
3316 // inputs by default; `Option<T>` is the canonical opt-in
3317 // shape that wants None routed to the body instead.
3318 // SRD-80b in-spirit rule — `Option<T>` wire args declare
3319 // None-tolerance via the type system; PolyWire (`Value`) args
3320 // ARE inherently None-tolerant (`Value::None` is just one of
3321 // the polymorphic variants). Both opt the node out of the
3322 // kernel-Rule-1 short-circuit.
3323 let has_none_aware_arg = args.iter().any(|a| match &a.kind {
3324 ArgKind::Wire => is_option_arg(&a.declared_ty),
3325 ArgKind::PolyWire => true,
3326 _ => false,
3327 });
3328 let accepts_none_impl: TokenStream2 = if has_none_aware_arg {
3329 quote! {
3330 fn accepts_none_inputs(&self) -> bool { true }
3331 }
3332 } else {
3333 quote!()
3334 };
3335
3336 // SRD-80b Phase C — `Const<Vec<C>>` implies
3337 // `Arity::VariadicConsts`. Mutually exclusive with the
3338 // wire-variadic case (the macro rejects mixing them earlier).
3339 let has_const_vec = args.iter().any(|a| matches!(a.kind, ArgKind::ConstVec(_)));
3340 let arity_field: TokenStream2 = if has_variadic {
3341 // SRD-80b split-halves: `variadic_min` is interpreted
3342 // as PAIRS count; the FuncSig advertises 2× as total
3343 // wires so the assembler enforces the right floor.
3344 let min_wires = match (&attrs.variadic_min, is_split_halves) {
3345 (Some(v), true) => quote!(2 * (#v)),
3346 (Some(v), false) => quote!(#v),
3347 (None, _) => quote!(0),
3348 };
3349 quote!(polydat::dsl::registry::Arity::VariadicWires { min_wires: #min_wires })
3350 } else if has_const_vec {
3351 // min_consts = 0 by default; the workload-list shape
3352 // permits empty lists. Authors who want a minimum
3353 // declare it via `#[poly_default]` on the inner type or
3354 // by validating in the body.
3355 quote!(polydat::dsl::registry::Arity::VariadicConsts { min_consts: 0 })
3356 } else {
3357 quote!(polydat::dsl::registry::Arity::Fixed)
3358 };
3359
3360 let commutativity_field: TokenStream2 = if let Some(c) = &attrs.commutativity {
3361 quote!(polydat::ast::Commutativity::#c)
3362 } else {
3363 quote!(polydat::ast::Commutativity::Positional)
3364 };
3365
3366 // SRD-80b Phase 5 S16 — fallible-mode emission. When the body
3367 // returns Result<T, E>, the macro:
3368 // * adds a cached `__polydat_cached: T` struct field,
3369 // * replaces `new(...)` with `try_new(...) -> Result<Self, String>`,
3370 // * runs the body once inside try_new, captures Ok into the
3371 // cache, propagates Err via Into<String>,
3372 // * makes eval read the cached value (no per-eval body call).
3373 let (ctor_emission, eval_emission, build_call_emission): (TokenStream2, TokenStream2, TokenStream2) = if is_fallible {
3374 // body-arg pass list. In try_new() Const args arrive as
3375 // their `field_type_tokens()` form (String for Str, raw
3376 // primitive otherwise) and need wrapping as `Const<T>` for
3377 // the body's declared signature. Setup args are locals
3378 // produced by `setup_precomputes` — body takes `&local`.
3379 let body_arg_passes: Vec<TokenStream2> = args.iter()
3380 .map(|a| {
3381 let n = &a.name;
3382 match &a.kind {
3383 ArgKind::Const(shape) => shape.wrap_as_const(quote!(#n)),
3384 ArgKind::Setup(_) => quote!(&#n),
3385 // Wire / PolyWire / Variadic are rejected
3386 // earlier for fallible nodes — unreachable.
3387 _ => quote!(#n),
3388 }
3389 })
3390 .collect();
3391 // Local wrapping: each Const arg comes in as the wrapper
3392 // (matching new_params), so we forward it directly. The
3393 // body receives `Const<T>` and unwraps via .0 or .as_str()
3394 // in its own code.
3395 let try_new = quote! {
3396 pub fn try_new( #( #new_params ),* ) -> ::std::result::Result<Self, String> {
3397 #( #setup_precomputes )*
3398 let mut ins: Vec<polydat::ast::Slot> = vec![ #( #slot_exprs ),* ];
3399 #( #variadic_slot_extends )*
3400 #outs_build
3401 // Invoke the body once; propagate Err as String.
3402 let __polydat_cached = match Self::__polydat_body( #( #body_arg_passes ),* ) {
3403 Ok(v) => v,
3404 Err(e) => return Err(Into::<String>::into(e)),
3405 };
3406 Ok(Self {
3407 meta: polydat::ast::NodeMeta {
3408 name: #func_name_str.into(),
3409 ins,
3410 outs,
3411 },
3412 #( #new_field_inits, )*
3413 __polydat_cached,
3414 })
3415 }
3416 };
3417 // eval reads the cached value; no body call.
3418 let out_assign = output_assign(quote!(0), &ret_ty, quote!(self.__polydat_cached.clone()));
3419 let ev = quote! {
3420 #[allow(unused_variables)]
3421 { #out_assign }
3422 };
3423 // build closure: call try_new and propagate Err.
3424 let bc = quote! {
3425 Some(match #struct_name::try_new( #( #new_call_args ),* ) {
3426 Ok(n) => Ok(Box::new(n) as Box<dyn polydat::ast::PolydatNode>),
3427 Err(e) => Err(e),
3428 })
3429 };
3430 (try_new, ev, bc)
3431 } else {
3432 let ctor = quote! {
3433 pub fn new( #( #new_params ),* ) -> Self {
3434 // SRD-80 PR B.6: setup pre-computes (FnOnce-
3435 // equivalent — emitted once by the macro,
3436 // never reachable by any other code path).
3437 #( #setup_precomputes )*
3438 // Build the `ins` slot list. Const args and
3439 // singleton wires already appear in `slot_exprs`;
3440 // variadic args append N slots per `n_wires`.
3441 let mut ins: Vec<polydat::ast::Slot> = vec![ #( #slot_exprs ),* ];
3442 #( #variadic_slot_extends )*
3443 #outs_build
3444 Self {
3445 meta: polydat::ast::NodeMeta {
3446 name: #func_name_str.into(),
3447 ins,
3448 outs,
3449 },
3450 #( #new_field_inits, )*
3451 }
3452 }
3453 };
3454 let ev = quote!(#eval_body);
3455 // Wrap `new()` in `catch_unwind` so that panics from
3456 // `#[poly_const]` setup functions (Regex parse failures,
3457 // file-not-found from filename consts, "value:weight"
3458 // parse failures, etc.) surface as build-closure `Err`
3459 // values rather than unwinding through the compile path.
3460 // The runtime sees `name` here as the DSL-registered
3461 // function name; the message is prefixed for traceability.
3462 let bc = quote! {
3463 Some(match ::std::panic::catch_unwind(
3464 ::std::panic::AssertUnwindSafe(|| #struct_name::new( #( #new_call_args ),* ))
3465 ) {
3466 Ok(node) => Ok(Box::new(node) as Box<dyn polydat::ast::PolydatNode>),
3467 Err(panic) => {
3468 let msg = panic.downcast_ref::<&str>().copied()
3469 .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
3470 .unwrap_or("<non-string panic>");
3471 Err(format!("{}: construction failed: {}", #func_name_str, msg))
3472 }
3473 })
3474 };
3475 (ctor, ev, bc)
3476 };
3477
3478 // Cached field for fallible mode. T = `ret_ty` (the Ok inner).
3479 let cached_field: TokenStream2 = if is_fallible {
3480 quote!(__polydat_cached: #ret_ty,)
3481 } else {
3482 quote!()
3483 };
3484
3485 let result = quote! {
3486 pub struct #struct_name {
3487 meta: polydat::ast::NodeMeta,
3488 #( #struct_fields, )*
3489 #cached_field
3490 }
3491
3492 #default_impl
3493
3494 #fused_node_impl
3495
3496 impl #struct_name {
3497 #ctor_emission
3498
3499 // SRD-80 PR B.7: shared `__polydat_body` extracted
3500 // when the node is JIT-eligible. Both `eval()` and
3501 // `compiled_u64()` call it. Empty token stream when
3502 // JIT is not emitted (body stays inlined in eval).
3503 #body_fn_def
3504 }
3505
3506 impl polydat::ast::PolydatNode for #struct_name {
3507 fn meta(&self) -> &polydat::ast::NodeMeta { &self.meta }
3508
3509 fn eval(
3510 &self,
3511 inputs: &[polydat::ast::Value],
3512 outputs: &mut [polydat::ast::Value],
3513 ) {
3514 #eval_emission
3515 }
3516
3517 #compiled_u64_impl
3518 #compiled_slot_impl
3519 #jit_constants_impl
3520 #purity_impl
3521 #accepts_none_impl
3522 }
3523
3524 // SRD-80 PR B.2/B.3/B.5 — link-time registration via
3525 // the existing `NodeRegistration` inventory channel.
3526 // The build closure pulls const args from the runtime
3527 // `consts` slice, falling back to per-arg
3528 // `#[poly_default(...)]` values if the slice is short.
3529 const _: () = {
3530 static SIGS: &[polydat::dsl::registry::FuncSig] = &[
3531 polydat::dsl::registry::FuncSig {
3532 name: #func_name_str,
3533 category: polydat::dsl::registry::FuncCategory::#category,
3534 outputs: #output_count_lit,
3535 description: "",
3536 help: "",
3537 identity: #identity_field,
3538 variadic_ctor: #variadic_ctor_field,
3539 params: &[ #( #param_specs ),* ],
3540 arity: #arity_field,
3541 commutativity: #commutativity_field,
3542 default_resolver: #default_resolver_field,
3543 output_type: #output_type_tokens,
3544 output_port: #output_port_field,
3545 },
3546 ];
3547
3548 fn signatures() -> &'static [polydat::dsl::registry::FuncSig] { SIGS }
3549
3550 fn build(
3551 name: &str,
3552 _wires: &[polydat::compile::assembly::WireRef],
3553 _wire_types: &[polydat::ast::PortType],
3554 consts: &[polydat::dsl::factory::ConstArg],
3555 ) -> Option<Result<Box<dyn polydat::ast::PolydatNode>, String>> {
3556 if name != #func_name_str { return None; }
3557 #port_guard
3558 #( #const_extracts )*
3559 #( #polywire_extracts )*
3560 #variadic_n_wires_extract
3561 #build_call_emission
3562 }
3563
3564 ::polydat::inventory::submit! {
3565 polydat::dsl::registry::NodeRegistration {
3566 signatures,
3567 build,
3568 validate: None,
3569 }
3570 }
3571 };
3572 };
3573
3574 Ok(result)
3575}
3576
3577/// `snake_case` → `PascalCase` (for the generated struct name).
3578fn to_camel_case(s: &str) -> String {
3579 let mut out = String::with_capacity(s.len());
3580 let mut up = true;
3581 for c in s.chars() {
3582 if c == '_' { up = true; continue; }
3583 if up { out.extend(c.to_uppercase()); up = false; }
3584 else { out.push(c); }
3585 }
3586 out
3587}
3588
3589/// Stringify a `syn::Type` minimally — used for primitive-type
3590/// dispatch. Not a robust pretty-printer; only handles the
3591/// shapes the simple-case allows (bare path, `&str`, `String`).
3592fn type_to_string(ty: &Type) -> String {
3593 use quote::ToTokens;
3594 let mut s = String::new();
3595 for t in ty.to_token_stream() {
3596 s.push_str(&t.to_string());
3597 s.push(' ');
3598 }
3599 s.trim().to_string()
3600}