Skip to main content

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//! The attribute turns a typed free function into a polydat node.
8//! From one `fn` it emits the node struct (named after the function
9//! in PascalCase), its `new()` constructor, the `PolydatNode` impl
10//! (`meta`, `eval`, and the compiled forms the signature allows),
11//! and a link-time `NodeRegistration` carrying the `FuncSig` the
12//! DSL registry serves. The function's `///` comment becomes the
13//! struct's documentation and the signature's `description` (first
14//! paragraph) and `help` (the rest).
15//!
16//! ## Arguments
17//!
18//! Each argument is classified by its type and attributes:
19//!
20//! - **Wire** — a per-cycle input. Scalars (`u64`, `i64`, `f64`,
21//!   `bool`, the narrower ints, `f32`, `f16`, `u128`, `i128`),
22//!   strings (`&str`, `String`, `Arc<str>`), bytes (`&[u8]`,
23//!   `Vec<u8>`, `Arc<[u8]>`), JSON (`&serde_json::Value`,
24//!   `Arc<serde_json::Value>`), typed vectors (`&[f32]`, `Vec<i64>`,
25//!   ...), SIMD registers (`Bits128`, `[i32; 4]`, ...), `Arc<T>`
26//!   handles, and host `Ext` types. `Option<T>` marks an input that
27//!   may be unset; `Config<T>` marks a configuration-cost wire.
28//!   `#[constraint(Variant)]` attaches a `ConstConstraint` to a
29//!   wire input.
30//! - **PolyWire** — a `Value` argument: any runtime type; the output
31//!   type of a `Value` return tracks the first PolyWire input.
32//! - **Variadic** — a `&[T]` argument for `T` in `u64`, `bool`,
33//!   `&str`, `String`, `Value`; two consecutive slices form a
34//!   split-halves shape. `variadic_min` and `identity` describe the
35//!   arity.
36//! - **Const** — `Const<u64 | f64 | bool | &str>`, a workload
37//!   constant captured at construction; `#[poly_default(EXPR)]`
38//!   supplies its default. `Const<Vec<C>>` (last) captures every
39//!   trailing constant of the call.
40//! - **Setup** — a `&T` argument with
41//!   `#[poly_const(setup_fn, from = source)]`: derived state
42//!   computed once in `new()` from the named const arguments
43//!   (`from = ()` for none, `from = (a, b)` for several). `T`
44//!   implements `PolydatSetup`.
45//!
46//! ## Returns
47//!
48//! A single wire type; a tuple of wire types (multi-output, named
49//! by `output_names(...)`); `Value` (polymorphic); `Result<T, E>`
50//! for a body that runs once at construction and caches its value;
51//! or a dynamic-output list over a `Const<Vec<C>>` argument.
52//!
53//! ## Attribute parameters
54//!
55//! - `category = <FuncCategory>` — required.
56//! - `struct_name = <Ident>` — the Rust name of the node struct.
57//! - `compiled_u64 = <path>` — `fn(&Node) -> CompiledU64Op`,
58//!   replacing the macro's u64-buffer closure.
59//! - `compiled_slot = <path>` — `fn(&Node, &[PortType]) ->
60//!   CompiledSlotKit`, replacing the slot kit's closure.
61//! - `state = <path>` — per-state scratch: `scratch_layout` and
62//!   `eval_in` delegate to `<path>::layout` / `<path>::eval`.
63//! - `jit_constants = <path>` — `fn(&Node) -> Vec<u64>`.
64//! - `decompose = <path>` — `fn(&Node) -> DecomposedGraph`, emitting
65//!   `impl FusedNode`.
66//! - `simd = "<node>"`, `simd_total` — an exact register-typed
67//!   implementation of the scalar function.
68//! - `purity = <Purity>`, `identity = <expr>`,
69//!   `commutativity = <Commutativity>`, `variadic_min = <int>`.
70//! - `output_names(a, b, ...)` — the ports of a tuple return.
71
72use proc_macro::TokenStream;
73use proc_macro2::TokenStream as TokenStream2;
74use quote::{format_ident, quote};
75use syn::{
76    FnArg, Ident, ItemFn, Meta, Pat, ReturnType, Token, Type, parse::Parser, parse_macro_input,
77    punctuated::Punctuated,
78};
79
80/// `#[polydat_node]` — derive a polydat node from a typed Rust
81/// function signature.
82///
83/// See the crate docs for the argument and return shapes the
84/// macro accepts.
85///
86/// ## Attribute parameters
87///
88/// - `category = <ident>` — the polydat `FuncCategory` variant
89///   the node belongs to (`Comparison`, `Math`, `String`, etc.).
90///   Required; there is no default.
91/// - `struct_name = <Ident>` — the Rust name of the generated node
92///   struct. Defaults to the function name in PascalCase, so a node
93///   `fn geo_cell` produces `struct GeoCell`; set this when that name
94///   is already taken, typically by the value type the node returns.
95///   The DSL name is always the function name.
96/// - `simd = "<node-name>"` declares an exact, lane-independent
97///   register-typed implementation of the scalar function.
98/// - `simd_total` certifies that the declared SIMD implementation is defined
99///   for the complete scalar input domain. It requires `simd`.
100#[proc_macro_attribute]
101pub fn polydat_node(attr: TokenStream, item: TokenStream) -> TokenStream {
102    let func = parse_macro_input!(item as ItemFn);
103
104    let attrs = match parse_attrs(attr.into()) {
105        Ok(a) => a,
106        Err(e) => return e.to_compile_error().into(),
107    };
108
109    match generate(func, attrs) {
110        Ok(ts) => ts.into(),
111        Err(e) => e.to_compile_error().into(),
112    }
113}
114
115/// Parsed `#[polydat_node(...)]` attribute parameters.
116struct NodeAttrs {
117    /// `FuncCategory` variant name — required (no default).
118    /// Forcing the operator to declare the category keeps the
119    /// `describe` / help / categorization surface coherent.
120    category: Ident,
121    /// SRD-80 PR B.7 — override path for `compiled_u64()`. When
122    /// set, the macro emits `compiled_u64(&self) -> Some(<path>(self))`
123    /// instead of building the closure from the body. Free-fn
124    /// signature: `fn(&Node) -> CompiledU64Op`, so setup-derived
125    /// state on the node is reachable. Escape hatch for hand-tuned
126    /// SIMD / FFI / unusual carriers.
127    compiled_u64_override: Option<syn::ExprPath>,
128    /// Override path for `compiled_slot()`. When set, the macro emits
129    /// `compiled_slot(&self, wire_types) -> Some(<path>(self,
130    /// wire_types))` instead of the slot kit's closure. Free-fn
131    /// signature: `fn(&Node, &[PortType]) -> CompiledSlotKit`. For a
132    /// node whose closure reads its slots as borrowed views.
133    /// `validate = <path>`: a node-level check the factory runs when
134    /// the node is built.
135    validate_fn: Option<syn::ExprPath>,
136    compiled_slot_override: Option<syn::ExprPath>,
137    /// SRD-80 PR B.7 — override path for `jit_constants()`.
138    /// Free-fn signature: `fn(&Node) -> Vec<u64>`. Macro emits
139    /// `jit_constants(&self) -> <path>(self)`.
140    jit_constants_override: Option<syn::ExprPath>,
141    /// `state = <path>`: the node keeps state of its own per
142    /// evaluating kernel state (axiom S3: storage belongs to the
143    /// state, never to the shared node). The macro emits
144    /// `scratch_layout` delegating to `<path>::layout(&self)` and
145    /// `eval_in` delegating to `<path>::eval(&self, scratch, inputs,
146    /// outputs)`; the plain `eval` stays the body over fresh scratch.
147    state: Option<syn::ExprPath>,
148    /// SRD-80b Phase F (S18) — `decompose = path`. When set, the
149    /// macro emits `impl FusedNode for <Struct>` whose
150    /// `decomposed(&self)` delegates to the named free function.
151    /// Free-fn signature: `fn(&Self) -> DecomposedGraph`. The
152    /// fusion compiler reaches the equivalent unfused subgraph
153    /// through this path. Operators with bespoke fusion logic
154    /// can still `impl FusedNode` by hand alongside the macro
155    /// emission — the attribute is the canonical sugar for the
156    /// "decompose by calling one free fn" case.
157    decompose: Option<syn::ExprPath>,
158    /// SRD-80 PR B.7 — declared `Purity` (Pure / SideChannel /
159    /// Nondeterministic). Defaults to `Pure` (the trait
160    /// default). Macro emits `fn purity(&self) -> Purity::<expr>`
161    /// when present.
162    ///
163    /// Two attribute forms recognized:
164    ///
165    /// - `purity = Nondeterministic` (path) — emits `Purity::Nondeterministic`.
166    /// - `purity = SideChannel(LogBuffer)` (call) — emits the
167    ///   struct-variant form `Purity::SideChannel { sink:
168    ///   SideChannelSink::LogBuffer }`. The call-form variant
169    ///   makes the struct-variant inline attribute parse-able
170    ///   (Rust attribute grammar doesn't accept inline `{ ... }`
171    ///   struct literals as attribute values).
172    purity: Option<syn::Expr>,
173    /// DSL name of an exact, lane-wise register implementation.
174    simd: Option<syn::LitStr>,
175    /// Declares the SIMD variant total over the scalar input domain. Without
176    /// this flag the variant remains usable only after range/error proof.
177    simd_total: bool,
178    /// SRD-80 PR B.9 — variadic node identity value (the result
179    /// when called with zero inputs). Emitted into
180    /// `FuncSig.identity: Option<u64>`. Required for variadic
181    /// numeric reductions whose group has an identity (sum=0,
182    /// product=1, min=u64::MAX, max=0). Skip for variadics with
183    /// no meaningful identity (str_concat — empty list yields "").
184    identity: Option<syn::Expr>,
185    /// SRD-80 PR B.9 — `Commutativity` variant. Defaults to
186    /// `Positional`. Variadic reductions typically pass
187    /// `AllCommutative` (sum/product/min/max all hold regardless
188    /// of input order).
189    commutativity: Option<Ident>,
190    /// SRD-80 PR B.9 — minimum required wire count for variadic
191    /// nodes. Defaults to 0 (callable with zero inputs).
192    variadic_min: Option<syn::LitInt>,
193    /// SRD-80 PR B.10 — names for the elements of a tuple
194    /// return type, paired positionally with the tuple
195    /// elements. Defaults to `out_0`, `out_1`, ... when
196    /// absent. Length must match tuple arity — operator gets a
197    /// compile error otherwise.
198    output_names: Option<Vec<Ident>>,
199    /// Rust name for the generated node struct. Defaults to the
200    /// function name in PascalCase; set it when that name would
201    /// collide with a type the operator already has in scope,
202    /// such as a `ReflectedValue` type the node produces.
203    struct_name: Option<Ident>,
204}
205
206fn parse_attrs(attr: TokenStream2) -> syn::Result<NodeAttrs> {
207    if attr.is_empty() {
208        return Err(syn::Error::new(
209            proc_macro2::Span::call_site(),
210            "#[polydat_node] requires `category = <FuncCategory variant>`. \
211             Example: #[polydat_node(category = Comparison)]",
212        ));
213    }
214
215    let parser = Punctuated::<Meta, Token![,]>::parse_terminated;
216    let items = parser.parse2(attr)?;
217
218    let mut category: Option<Ident> = None;
219    let mut compiled_u64_override: Option<syn::ExprPath> = None;
220    let mut state: Option<syn::ExprPath> = None;
221    // A node-level `validate = <path>`: checked by the factory when
222    // the node is built, so a rule that no single parameter can state
223    // — a relation between two of them — is enforced once and on every
224    // engine, rather than from a body that native code never runs.
225    let mut validate_fn: Option<syn::ExprPath> = None;
226    let mut compiled_slot_override: Option<syn::ExprPath> = None;
227    let mut jit_constants_override: Option<syn::ExprPath> = None;
228    let mut decompose: Option<syn::ExprPath> = None;
229    let mut purity: Option<syn::Expr> = None;
230    let mut simd: Option<syn::LitStr> = None;
231    let mut simd_total = false;
232    let mut identity: Option<syn::Expr> = None;
233    let mut commutativity: Option<Ident> = None;
234    let mut variadic_min: Option<syn::LitInt> = None;
235    let mut output_names: Option<Vec<Ident>> = None;
236    let mut struct_name: Option<Ident> = None;
237
238    for item in items {
239        match item {
240            Meta::Path(p) => {
241                let key = p
242                    .get_ident()
243                    .ok_or_else(|| {
244                        syn::Error::new_spanned(
245                            &p,
246                            "#[polydat_node] flag keys must be bare identifiers",
247                        )
248                    })?
249                    .clone();
250                match key.to_string().as_str() {
251                    "simd_total" => {
252                        simd_total = true;
253                    }
254                    other => {
255                        return Err(syn::Error::new_spanned(
256                            &key,
257                            format!(
258                                "#[polydat_node] does not recognize flag `{other}`. \
259                                 Flags: `simd_total`.",
260                            ),
261                        ));
262                    }
263                }
264            }
265            Meta::NameValue(nv) => {
266                let key = nv
267                    .path
268                    .get_ident()
269                    .ok_or_else(|| {
270                        syn::Error::new_spanned(
271                            &nv.path,
272                            "#[polydat_node] parameter keys must be bare identifiers",
273                        )
274                    })?
275                    .clone();
276                match key.to_string().as_str() {
277                    "category" => {
278                        let syn::Expr::Path(p) = &nv.value else {
279                            return Err(syn::Error::new_spanned(
280                                &nv.value,
281                                "`category` value must be a bare identifier \
282                                 (a polydat `FuncCategory` variant name).",
283                            ));
284                        };
285                        category = Some(
286                            p.path
287                                .get_ident()
288                                .ok_or_else(|| {
289                                    syn::Error::new_spanned(
290                                        &nv.value,
291                                        "`category` value must be a single identifier.",
292                                    )
293                                })?
294                                .clone(),
295                        );
296                    }
297                    "compiled_u64" => {
298                        let syn::Expr::Path(p) = &nv.value else {
299                            return Err(syn::Error::new_spanned(
300                                &nv.value,
301                                "`compiled_u64` value must be a path to a free \
302                                 function with signature `fn(&Node) -> CompiledU64Op`.",
303                            ));
304                        };
305                        compiled_u64_override = Some(p.clone());
306                    }
307                    "state" => {
308                        let syn::Expr::Path(p) = &nv.value else {
309                            return Err(syn::Error::new_spanned(
310                                &nv.value,
311                                "`state` value must be a path to a module with \
312                                 `layout(&Node) -> Vec<ScratchElem>` and \
313                                 `eval(&Node, &mut [ScratchBuf], &[Value], &mut [Value])`.",
314                            ));
315                        };
316                        state = Some(p.clone());
317                    }
318                    "validate" => {
319                        let syn::Expr::Path(p) = &nv.value else {
320                            return Err(syn::Error::new_spanned(
321                                &nv.value,
322                                "`validate` value must be a path to a free function \
323                                 with signature \
324                                 `fn(&str, &[ConstArg]) -> Result<(), String>`.",
325                            ));
326                        };
327                        validate_fn = Some(p.clone());
328                    }
329                    "compiled_slot" => {
330                        let syn::Expr::Path(p) = &nv.value else {
331                            return Err(syn::Error::new_spanned(
332                                &nv.value,
333                                "`compiled_slot` value must be a path to a free \
334                                 function with signature \
335                                 `fn(&Node, &[PortType]) -> CompiledSlotKit`.",
336                            ));
337                        };
338                        compiled_slot_override = Some(p.clone());
339                    }
340                    "jit_constants" => {
341                        let syn::Expr::Path(p) = &nv.value else {
342                            return Err(syn::Error::new_spanned(
343                                &nv.value,
344                                "`jit_constants` value must be a path to a free \
345                                 function with signature `fn(&Node) -> Vec<u64>`.",
346                            ));
347                        };
348                        jit_constants_override = Some(p.clone());
349                    }
350                    "decompose" => {
351                        let syn::Expr::Path(p) = &nv.value else {
352                            return Err(syn::Error::new_spanned(
353                                &nv.value,
354                                "`decompose` value must be a path to a free \
355                                 function with signature \
356                                 `fn(&Self) -> DecomposedGraph`.",
357                            ));
358                        };
359                        decompose = Some(p.clone());
360                    }
361                    "purity" => {
362                        // Accept either:
363                        //   purity = Nondeterministic         (path)
364                        //   purity = SideChannel(LogBuffer)   (call)
365                        // The codegen dispatches on the shape.
366                        match &nv.value {
367                            syn::Expr::Path(_) | syn::Expr::Call(_) => {
368                                purity = Some(nv.value.clone());
369                            }
370                            _ => {
371                                return Err(syn::Error::new_spanned(
372                                    &nv.value,
373                                    "`purity` value must be a Purity variant: \
374                                     `Pure`, `Nondeterministic`, or \
375                                     `SideChannel(<sink>)` where `<sink>` is a \
376                                     `SideChannelSink` variant ident.",
377                                ));
378                            }
379                        }
380                    }
381                    "simd" => {
382                        let syn::Expr::Lit(syn::ExprLit {
383                            lit: syn::Lit::Str(name),
384                            ..
385                        }) = &nv.value
386                        else {
387                            return Err(syn::Error::new_spanned(
388                                &nv.value,
389                                "`simd` value must be the string name of a register-typed node.",
390                            ));
391                        };
392                        simd = Some(name.clone());
393                    }
394                    "identity" => {
395                        // SRD-80 PR B.9 — variadic identity element.
396                        // Any constant-evaluable expression is fine.
397                        identity = Some(nv.value.clone());
398                    }
399                    "commutativity" => {
400                        let syn::Expr::Path(p) = &nv.value else {
401                            return Err(syn::Error::new_spanned(
402                                &nv.value,
403                                "`commutativity` value must be a `Commutativity` \
404                                 variant ident (Positional / AllCommutative / ...).",
405                            ));
406                        };
407                        commutativity = Some(
408                            p.path
409                                .get_ident()
410                                .ok_or_else(|| {
411                                    syn::Error::new_spanned(
412                                        &nv.value,
413                                        "`commutativity` value must be a single identifier.",
414                                    )
415                                })?
416                                .clone(),
417                        );
418                    }
419                    "variadic_min" => {
420                        let syn::Expr::Lit(syn::ExprLit {
421                            lit: syn::Lit::Int(n),
422                            ..
423                        }) = &nv.value
424                        else {
425                            return Err(syn::Error::new_spanned(
426                                &nv.value,
427                                "`variadic_min` value must be an integer literal.",
428                            ));
429                        };
430                        variadic_min = Some(n.clone());
431                    }
432                    "struct_name" => {
433                        // The generated Rust struct is named after the
434                        // function in PascalCase by default; a host whose
435                        // module already has a type of that name picks
436                        // another one here. The DSL name is unchanged.
437                        let syn::Expr::Path(p) = &nv.value else {
438                            return Err(syn::Error::new_spanned(
439                                &nv.value,
440                                "`struct_name` value must be a bare identifier, \
441                                 e.g. `struct_name = GeoCellNode`.",
442                            ));
443                        };
444                        struct_name = Some(p.path.get_ident()
445                            .ok_or_else(|| syn::Error::new_spanned(
446                                &nv.value,
447                                "`struct_name` value must be a single identifier, not a path.",
448                            ))?
449                            .clone());
450                    }
451                    other => {
452                        return Err(syn::Error::new_spanned(
453                            &key,
454                            format!(
455                                "#[polydat_node] does not recognize parameter `{other}`. \
456                                 Registration: `category = <FuncCategory>`, \
457                                 `struct_name = <Ident>`. \
458                                 Engines: `compiled_u64 = <path>`, \
459                                 `compiled_slot = <path>`, `state = <path>`, \
460                                 `jit_constants = <path>`, `decompose = <path>`, \
461                                 `simd = \"<node>\"`, `simd_total`. \
462                                 Semantics: `purity = <Purity>`, `identity = <expr>`, \
463                                 `commutativity = <Commutativity>`, `variadic_min = <int>`. \
464                                 Shapes: `output_names(...)`.",
465                            ),
466                        ));
467                    }
468                }
469            }
470            Meta::List(list) => {
471                let key = list
472                    .path
473                    .get_ident()
474                    .ok_or_else(|| {
475                        syn::Error::new_spanned(
476                            &list.path,
477                            "#[polydat_node] list-form keys must be bare identifiers",
478                        )
479                    })?
480                    .clone();
481                match key.to_string().as_str() {
482                    "output_names" => {
483                        let names: Punctuated<Ident, Token![,]> =
484                            list.parse_args_with(Punctuated::parse_terminated)?;
485                        if names.is_empty() {
486                            return Err(syn::Error::new_spanned(
487                                &list,
488                                "`output_names(...)` requires at least one name.",
489                            ));
490                        }
491                        output_names = Some(names.into_iter().collect());
492                    }
493                    other => {
494                        return Err(syn::Error::new_spanned(
495                            &key,
496                            format!(
497                                "#[polydat_node] does not recognize list-form key `{other}`. \
498                                 Recognised: `output_names(...)`.",
499                            ),
500                        ));
501                    }
502                }
503            }
504        }
505    }
506
507    let category = category.ok_or_else(|| {
508        syn::Error::new(
509            proc_macro2::Span::call_site(),
510            "#[polydat_node] requires `category = <FuncCategory variant>`.",
511        )
512    })?;
513
514    if simd_total && simd.is_none() {
515        return Err(syn::Error::new(
516            proc_macro2::Span::call_site(),
517            "`simd_total` requires `simd = \"<register node>\"`.",
518        ));
519    }
520
521    Ok(NodeAttrs {
522        category,
523        compiled_u64_override,
524        compiled_slot_override,
525        validate_fn,
526        jit_constants_override,
527        state,
528        decompose,
529        purity,
530        simd,
531        simd_total,
532        identity,
533        commutativity,
534        variadic_min,
535        output_names,
536        struct_name,
537    })
538}
539
540/// One classified function argument. Drives every downstream
541/// piece of the generated output: NodeMeta slot, FuncSig
542/// param, struct field (for consts), build closure const
543/// extraction, eval-time wrapper construction.
544struct ClassifiedArg {
545    name: syn::Ident,
546    /// Original Rust type from the function signature.
547    declared_ty: Type,
548    /// Whether the arg was declared as `Const<T>`.
549    kind: ArgKind,
550    /// For const args: optional default value expression parsed
551    /// from `#[poly_default(VAL)]`. Present → the const is
552    /// optional in FuncSig and the build closure falls back to
553    /// the default when the consts slice doesn't supply one.
554    default_value: Option<syn::Expr>,
555    /// SRD-80 PR B.14 — `#[constraint(<Variant>)]` on a wire
556    /// arg. The variant name maps to `ConstConstraint::*`; the
557    /// emitted `Port` carries the constraint so strict-wire
558    /// mode can auto-insert upstream assertion nodes.
559    /// `#[constraint(...)]`, if the argument declared one.
560    constraint: Option<syn::Expr>,
561}
562
563/// How a `Const` list argument asked for its elements: borrowed from
564/// the node's own field, or cloned out of it for each evaluation.
565#[derive(Clone, Copy, PartialEq)]
566enum ListForm {
567    /// `Const<&[C]>` — a borrow, free per evaluation.
568    Borrowed,
569    /// `Const<Vec<C>>` — an owned clone, the older spelling.
570    Owned,
571}
572#[derive(Clone)]
573enum ArgKind {
574    Wire,
575    Const(ConstShape),
576    /// SRD-80b Phase C — `Const<Vec<C>>` workload-list const.
577    /// Inner ConstShape gives the element type (u64/f64/bool/Str).
578    /// The macro emits ONE ParamSpec in the FuncSig with the
579    /// inner element's slot type, sets `Arity::VariadicConsts`,
580    /// and at build time collects every matching ConstArg from
581    /// the tail of `consts[..]` into a `Vec<inner>` field.
582    ///
583    /// The flag is how the body asked for the list. `Const<&[C]>`
584    /// borrows the field, which costs nothing per evaluation and is
585    /// what the borrowed string shape `Const<&str>` already does;
586    /// `Const<Vec<C>>` clones it, which is an allocation per
587    /// evaluation of a list that never changes after construction.
588    /// Both are accepted and the owned one is the older spelling.
589    ConstVec(ConstShape, ListForm),
590    /// `&T` argument with `#[poly_const(<fn_path>, from = <arg>)]`.
591    /// Generates a struct field of type `T`, computed once in
592    /// `new()` by calling `<fn_path>(<source>)` where `<source>`
593    /// is the field-access expression for the named `from` arg.
594    /// Boxed: `SetupSpec` is ~424 bytes, dwarfing the other
595    /// variants — indirection keeps `ArgKind` small.
596    Setup(Box<SetupSpec>),
597    /// SRD-80 PR B.8 — `Value` argument. Polymorphic wire whose
598    /// port type is resolved at construction (`new()` takes a
599    /// runtime `PortType`). Body sees a cloned `Value`; eval
600    /// box/unboxes via the trivial `FromValue<Value>` impl.
601    /// Triggers `OutputType::SameAsInput(<this idx>)` when the
602    /// return type is also `Value`.
603    PolyWire,
604    /// SRD-80 PR B.9 — `&[T]` argument (variadic wire). Construction
605    /// is runtime-arity (`new(n_wires)`); the macro emits N wire
606    /// slots, an `Arity::VariadicWires { min_wires }` FuncSig
607    /// entry, and a `variadic_ctor` thunk that builds with `n`
608    /// at compile time.
609    Variadic(VariadicElement),
610}
611
612/// Element type of a `&[T]` variadic arg. Determines the
613/// per-element port type, whether the node stays JIT-eligible,
614/// and how `eval()` materialises the slice for the body call.
615#[derive(Clone, Copy, PartialEq, Eq)]
616enum VariadicElement {
617    U64,
618    Bool,
619    BorrowedStr,
620    OwnedString,
621    /// `&[Value]` — polymorphic per-element type. The body sees
622    /// each element as the polydat runtime carrier; type
623    /// inspection / coercion is the body's responsibility.
624    Value,
625}
626
627impl VariadicElement {
628    fn port_type_tokens(self) -> TokenStream2 {
629        // A `Value` variadic's per-slot type is nominal: the slot is
630        // marked `accepts_any_type`, so the assembler reads the wire
631        // and inserts nothing, and the body dispatches on the `Value`
632        // variant itself. `Str` is the spelling of the placeholder,
633        // not a claim about the wire.
634        match self {
635            VariadicElement::U64 => quote!(polydat::ast::PortType::U64),
636            VariadicElement::Bool => quote!(polydat::ast::PortType::Bool),
637            VariadicElement::BorrowedStr => quote!(polydat::ast::PortType::Str),
638            VariadicElement::OwnedString => quote!(polydat::ast::PortType::Str),
639            VariadicElement::Value => quote!(polydat::ast::PortType::Str),
640        }
641    }
642
643    /// Expression that converts a single `&Value` to the body's
644    /// element type. Used to build the per-call slice in eval().
645    fn extract_from_value(self) -> TokenStream2 {
646        match self {
647            VariadicElement::U64 => quote!(|v: &polydat::ast::Value| v.as_u64()),
648            VariadicElement::Bool => quote!(|v: &polydat::ast::Value| v.as_bool()),
649            VariadicElement::BorrowedStr => quote!(|v: &polydat::ast::Value| v.as_str()),
650            VariadicElement::OwnedString => {
651                quote!(|v: &polydat::ast::Value| v.as_str().to_string())
652            }
653            VariadicElement::Value => quote!(|v: &polydat::ast::Value| v.clone()),
654        }
655    }
656}
657
658#[derive(Clone)]
659struct SetupSpec {
660    /// `T` — the type the field stores (inner type of `&T`).
661    inner_ty: Type,
662    /// Operator-provided constructor path, e.g.
663    /// `ParsedPattern::from_pattern`.
664    setup_fn: syn::Expr,
665    /// Names of the const args whose field-values are passed to
666    /// `setup_fn`. Empty when declared as `from = ()` — the
667    /// setup fn takes no arguments and captures session-static
668    /// state (env, system clock, etc.). Length 1 for the common
669    /// single-source case (`from = ident`); length N for
670    /// multi-source `from = (a, b, c)` per SRD-80b amendment.
671    source_args: Vec<syn::Ident>,
672}
673
674#[derive(Clone, Copy, PartialEq, Eq)]
675enum ConstShape {
676    U64,
677    F64,
678    Bool,
679    Str,
680    /// `Const<Arc<T>>` — a value the compiler built, carried as
681    /// [`ConstArg::Opaque`] and downcast to `T` at construction. The
682    /// concrete `T` comes from the argument's declared type, so this
683    /// variant carries none and the enum stays `Copy`.
684    Opaque,
685}
686
687impl ConstShape {
688    /// Token stream for the `SlotType::Const*` variant.
689    fn slot_type_tokens(self) -> TokenStream2 {
690        match self {
691            ConstShape::U64 => quote!(polydat::ast::SlotType::ConstU64),
692            ConstShape::F64 => quote!(polydat::ast::SlotType::ConstF64),
693            ConstShape::Bool => quote!(polydat::ast::SlotType::ConstU64),
694            ConstShape::Str => quote!(polydat::ast::SlotType::ConstStr),
695            // An opaque const has no literal form, so it is not a
696            // const slot on the node's meta; the assembler and the
697            // JIT walkers see the wires and nothing else.
698            ConstShape::Opaque => quote!(polydat::ast::SlotType::ConstStr),
699        }
700    }
701
702    /// Token stream for the struct field type that stores the
703    /// captured const value. `Const<&str>` → `String` (owned
704    /// backing store). Other shapes are Copy and stored
705    /// directly.
706    fn field_type_tokens(self, opaque: Option<&Type>) -> TokenStream2 {
707        match self {
708            ConstShape::U64 => quote!(u64),
709            ConstShape::F64 => quote!(f64),
710            ConstShape::Bool => quote!(bool),
711            ConstShape::Str => quote!(String),
712            ConstShape::Opaque => {
713                let t = opaque.expect("an opaque const names its type");
714                quote!(std::sync::Arc<#t>)
715            }
716        }
717    }
718
719    /// Token stream that extracts a value from a `ConstArg`.
720    /// `c` is the `ConstArg` binding in scope at the call site.
721    fn extract_from_const_arg(self, c: TokenStream2, opaque: Option<&Type>) -> TokenStream2 {
722        match self {
723            ConstShape::U64 => quote!(#c.as_u64()),
724            ConstShape::F64 => quote!(#c.as_f64()),
725            ConstShape::Bool => quote!(#c.as_u64() != 0),
726            ConstShape::Str => quote!(#c.as_str().to_string()),
727            ConstShape::Opaque => {
728                let t = opaque.expect("an opaque const names its type");
729                quote!(match #c.as_opaque::<#t>() {
730                    Some(v) => v,
731                    None => return Some(Err(format!(
732                        "{}: expected a compiler-built {} for this argument",
733                        name,
734                        std::any::type_name::<#t>()
735                    ))),
736                })
737            }
738        }
739    }
740
741    /// Token stream that wraps a struct-field expression as
742    /// `Const<T>` for handoff into the user's function body.
743    /// `field_ref` is the borrow / value expression for the
744    /// stored field (e.g. `&self.pattern` or `self.seed`).
745    fn wrap_as_const(self, field_ref: TokenStream2) -> TokenStream2 {
746        match self {
747            ConstShape::U64 => quote!(polydat::derive_support::Const(#field_ref)),
748            ConstShape::F64 => quote!(polydat::derive_support::Const(#field_ref)),
749            ConstShape::Bool => quote!(polydat::derive_support::Const(#field_ref)),
750            ConstShape::Str => quote!(polydat::derive_support::Const(#field_ref.as_str())),
751            ConstShape::Opaque => quote!(polydat::derive_support::Const(#field_ref.clone())),
752        }
753    }
754}
755
756/// SRD-80 PR B.7 — primitive types that fit the JIT u64 buffer.
757/// A node is Phase-2 eligible iff every wire arg / const arg /
758/// return type maps to a `JitType` and no `#[poly_const]` setup arg
759/// is declared (setup carries non-primitive derived state).
760#[derive(Clone, Copy, PartialEq, Eq)]
761enum JitType {
762    U64,
763    I64,
764    F64,
765    Bool,
766    // Narrow widths (alignment §8.1): each rides the u64 slot per
767    // its Wire storage convention — unsigned zero-extended, signed
768    // sign-extended (through the i64 carrier), floats bit-stuffed.
769    // The variant carries enough width information for the buffer
770    // read/write tokens to emit the exact narrowing/widening casts.
771    U8,
772    U16,
773    U32,
774    I8,
775    I16,
776    I32,
777    F32,
778    F16,
779    // Two-slot values (alignment §8.4 layer 1): 128-bit integers
780    // and register words ride two consecutive u64 slots in
781    // little-endian limb order, reconstructed through
782    // `polydat::ast::Bits128`.
783    U128,
784    I128,
785    RegRaw,
786    RegI8x16,
787    RegI16x8,
788    RegI32x4,
789    RegI64x2,
790    RegF16x8,
791    RegF32x4,
792    RegF64x2,
793}
794
795impl JitType {
796    /// Buffer slots this carrier occupies (alignment §8.4 layer
797    /// 1): 1 for everything riding a single u64; 2 for 128-bit
798    /// values (limb pairs).
799    fn width(self) -> usize {
800        match self {
801            JitType::U128
802            | JitType::I128
803            | JitType::RegRaw
804            | JitType::RegI8x16
805            | JitType::RegI16x8
806            | JitType::RegI32x4
807            | JitType::RegI64x2
808            | JitType::RegF16x8
809            | JitType::RegF32x4
810            | JitType::RegF64x2 => 2,
811            _ => 1,
812        }
813    }
814
815    /// Tokens reading a typed value from the Phase-2 u64 buffer
816    /// at slot offset `idx` (the prefix sum of the widths of all
817    /// preceding wire args). f64/bool are bit-reinterpreted from
818    /// the u64 carrier (the buffer-level convention shared with
819    /// every existing hand-written `compiled_u64`); two-slot
820    /// values reassemble through `Bits128`.
821    fn read_from_u64_buffer(self, idx: usize) -> TokenStream2 {
822        let i = syn::Index::from(idx);
823        let i1 = syn::Index::from(idx + 1);
824        let limbs = quote!(polydat::ast::Bits128([inputs[#i], inputs[#i1]]));
825        match self {
826            JitType::U64 => quote!(inputs[#i]),
827            JitType::I64 => quote!(inputs[#i] as i64),
828            JitType::F64 => quote!(f64::from_bits(inputs[#i])),
829            JitType::Bool => quote!(inputs[#i] != 0),
830            JitType::U8 => quote!(inputs[#i] as u8),
831            JitType::U16 => quote!(inputs[#i] as u16),
832            JitType::U32 => quote!(inputs[#i] as u32),
833            JitType::I8 => quote!((inputs[#i] as i64) as i8),
834            JitType::I16 => quote!((inputs[#i] as i64) as i16),
835            JitType::I32 => quote!((inputs[#i] as i64) as i32),
836            JitType::F32 => quote!(f32::from_bits(inputs[#i] as u32)),
837            JitType::F16 => quote!(polydat::half::f16::from_bits(inputs[#i] as u16)),
838            JitType::U128 => quote!((#limbs).as_u128()),
839            JitType::I128 => quote!((#limbs).as_i128()),
840            JitType::RegRaw => limbs,
841            JitType::RegI8x16 => quote!((#limbs).lanes_i8()),
842            JitType::RegI16x8 => quote!((#limbs).lanes_i16()),
843            JitType::RegI32x4 => quote!((#limbs).lanes_i32()),
844            JitType::RegI64x2 => quote!((#limbs).lanes_i64()),
845            JitType::RegF16x8 => quote!((#limbs).lanes_f16()),
846            JitType::RegF32x4 => quote!((#limbs).lanes_f32()),
847            JitType::RegF64x2 => quote!((#limbs).lanes_f64()),
848        }
849    }
850
851    /// Tokens writing a typed value into the Phase-2 u64 output
852    /// buffer at slot offset `base`. Inverse of the read.
853    fn write_to_u64_buffer_at(self, base: usize, result: TokenStream2) -> TokenStream2 {
854        let o = syn::Index::from(base);
855        let o1 = syn::Index::from(base + 1);
856        let write_limbs = |from: TokenStream2| {
857            quote! {{
858                let __limbs = #from;
859                outputs[#o] = __limbs.0[0];
860                outputs[#o1] = __limbs.0[1];
861            }}
862        };
863        match self {
864            JitType::U64 => quote!(outputs[#o] = #result;),
865            JitType::I64 => quote!(outputs[#o] = (#result) as u64;),
866            JitType::F64 => quote!(outputs[#o] = (#result).to_bits();),
867            JitType::Bool => quote!(outputs[#o] = if #result { 1 } else { 0 };),
868            JitType::U8 | JitType::U16 | JitType::U32 => quote!(outputs[#o] = (#result) as u64;),
869            JitType::I8 | JitType::I16 | JitType::I32 => {
870                quote!(outputs[#o] = ((#result) as i64) as u64;)
871            }
872            JitType::F32 => quote!(outputs[#o] = (#result).to_bits() as u64;),
873            JitType::F16 => quote!(outputs[#o] = (#result).to_bits() as u64;),
874            JitType::U128 => write_limbs(quote!(polydat::ast::Bits128::from_u128(#result))),
875            JitType::I128 => write_limbs(quote!(polydat::ast::Bits128::from_i128(#result))),
876            JitType::RegRaw => write_limbs(quote!(#result)),
877            JitType::RegI8x16 => write_limbs(quote!(polydat::ast::Bits128::from_lanes_i8(#result))),
878            JitType::RegI16x8 => {
879                write_limbs(quote!(polydat::ast::Bits128::from_lanes_i16(#result)))
880            }
881            JitType::RegI32x4 => {
882                write_limbs(quote!(polydat::ast::Bits128::from_lanes_i32(#result)))
883            }
884            JitType::RegI64x2 => {
885                write_limbs(quote!(polydat::ast::Bits128::from_lanes_i64(#result)))
886            }
887            JitType::RegF16x8 => {
888                write_limbs(quote!(polydat::ast::Bits128::from_lanes_f16(#result)))
889            }
890            JitType::RegF32x4 => {
891                write_limbs(quote!(polydat::ast::Bits128::from_lanes_f32(#result)))
892            }
893            JitType::RegF64x2 => {
894                write_limbs(quote!(polydat::ast::Bits128::from_lanes_f64(#result)))
895            }
896        }
897    }
898
899    /// Single-return write at offset 0.
900    fn write_to_u64_buffer(self, result: TokenStream2) -> TokenStream2 {
901        self.write_to_u64_buffer_at(0, result)
902    }
903
904    /// Tokens encoding the captured Copy value of a const field
905    /// as a `u64` for `jit_constants()` (Phase-3 classifier).
906    fn const_field_as_u64(self, field_ref: TokenStream2) -> TokenStream2 {
907        match self {
908            JitType::U64 => quote!(#field_ref),
909            JitType::I64 => quote!((#field_ref) as u64),
910            JitType::F64 => quote!((#field_ref).to_bits()),
911            JitType::Bool => quote!(if #field_ref { 1 } else { 0 }),
912            JitType::U8 | JitType::U16 | JitType::U32 => quote!((#field_ref) as u64),
913            JitType::I8 | JitType::I16 | JitType::I32 => quote!(((#field_ref) as i64) as u64),
914            JitType::F32 | JitType::F16 => quote!((#field_ref).to_bits() as u64),
915            // ConstShape has no 128-bit / register forms, so these
916            // never appear in const position.
917            JitType::U128
918            | JitType::I128
919            | JitType::RegRaw
920            | JitType::RegI8x16
921            | JitType::RegI16x8
922            | JitType::RegI32x4
923            | JitType::RegI64x2
924            | JitType::RegF16x8
925            | JitType::RegF32x4
926            | JitType::RegF64x2 => {
927                unreachable!("128-bit/register types have no const shape")
928            }
929        }
930    }
931}
932
933/// A `#[poly_default(...)]` expression as the literal a program writes:
934/// `4u64` is `4`, `100.0f64` is `100.0`, `-1.5` is `-1.5`, and a string
935/// keeps its quotes. `None` for an expression that is not a literal.
936fn program_literal(e: &syn::Expr) -> Option<String> {
937    match e {
938        syn::Expr::Lit(l) => match &l.lit {
939            syn::Lit::Int(i) => Some(i.base10_digits().to_string()),
940            syn::Lit::Float(f) => {
941                let d = f.base10_digits();
942                // The program reads a number without a point as an
943                // integer, so a float default keeps one.
944                Some(if d.contains(['.', 'e', 'E']) {
945                    d.to_string()
946                } else {
947                    format!("{d}.0")
948                })
949            }
950            syn::Lit::Str(s) => Some(format!("{:?}", s.value())),
951            // A `Const<bool>` rides the integer slot, and the grammar
952            // reads `true` and `false` in an argument as wire names, so
953            // the value a program passes is `1` or `0`.
954            syn::Lit::Bool(b) => Some(if b.value { "1" } else { "0" }.to_string()),
955            _ => None,
956        },
957        syn::Expr::Unary(u) if matches!(u.op, syn::UnOp::Neg(_)) => {
958            program_literal(&u.expr).map(|v| format!("-{v}"))
959        }
960        syn::Expr::Group(g) => program_literal(&g.expr),
961        syn::Expr::Paren(p) => program_literal(&p.expr),
962        _ => None,
963    }
964}
965
966/// Map a `ConstShape` to its JIT-compatible primitive carrier,
967/// or `None` if the shape can't live in the u64 buffer.
968fn const_shape_to_jit_type(s: ConstShape) -> Option<JitType> {
969    match s {
970        ConstShape::U64 => Some(JitType::U64),
971        ConstShape::F64 => Some(JitType::F64),
972        ConstShape::Bool => Some(JitType::Bool),
973        // A string constant never rides the buffer: the kits capture
974        // it by clone, and native lowerings read it from the node.
975        // Nor does a compiler-built value, which is an `Arc` the node
976        // holds rather than bits.
977        ConstShape::Str | ConstShape::Opaque => None,
978    }
979}
980
981/// Map a wire arg's declared Rust type to its JIT carrier, or
982/// `None` for types that can't fit in the buffer.
983fn wire_type_to_jit_type(ty: &Type) -> Option<JitType> {
984    // type_to_string joins every token with a space, and a token
985    // may itself be a bracketed group (`& [u8]`, `half : : f16`,
986    // `[ f32 ; 4 ]`), so every form compares whitespace-stripped.
987    // The two-slot types ride limb pairs per alignment §8.4 layer 1.
988    let flat: String = type_to_string(ty).split_whitespace().collect();
989    match flat.as_str() {
990        "u64" => Some(JitType::U64),
991        "i64" => Some(JitType::I64),
992        "f64" => Some(JitType::F64),
993        "bool" => Some(JitType::Bool),
994        "u8" => Some(JitType::U8),
995        "u16" => Some(JitType::U16),
996        "u32" => Some(JitType::U32),
997        "i8" => Some(JitType::I8),
998        "i16" => Some(JitType::I16),
999        "i32" => Some(JitType::I32),
1000        "f32" => Some(JitType::F32),
1001        "u128" => Some(JitType::U128),
1002        "i128" => Some(JitType::I128),
1003        "half::f16" | "f16" => Some(JitType::F16),
1004        "Bits128" | "crate::ast::Bits128" | "polydat::ast::Bits128" | "ast::Bits128" => {
1005            Some(JitType::RegRaw)
1006        }
1007        "[i8;16]" => Some(JitType::RegI8x16),
1008        "[i16;8]" => Some(JitType::RegI16x8),
1009        "[i32;4]" => Some(JitType::RegI32x4),
1010        "[i64;2]" => Some(JitType::RegI64x2),
1011        "[half::f16;8]" | "[f16;8]" => Some(JitType::RegF16x8),
1012        "[f32;4]" => Some(JitType::RegF32x4),
1013        "[f64;2]" => Some(JitType::RegF64x2),
1014        _ => None,
1015    }
1016}
1017
1018/// The `T` of an `Option<T>` argument, by its last path segment.
1019fn option_inner(ty: &Type) -> Option<&Type> {
1020    generic_inner(ty, "Option")
1021}
1022
1023/// The `T` of a `Config<T>` argument, by its last path segment.
1024fn config_inner(ty: &Type) -> Option<&Type> {
1025    generic_inner(ty, "Config")
1026}
1027
1028fn generic_inner<'a>(ty: &'a Type, wrapper: &str) -> Option<&'a Type> {
1029    let syn::Type::Path(p) = ty else {
1030        return None;
1031    };
1032    let last = p.path.segments.last()?;
1033    if last.ident != wrapper {
1034        return None;
1035    }
1036    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1037        return None;
1038    };
1039    args.args.iter().find_map(|a| match a {
1040        syn::GenericArgument::Type(t) => Some(t),
1041        _ => None,
1042    })
1043}
1044
1045/// Detect `Const<T>` in arg-type position. Returns `Some(shape)`
1046/// for recognized inner types; `None` for bare types (wire) or
1047/// unrecognized shapes. The recognition is structural — matches
1048/// the last segment of the path as `Const` with a single
1049/// generic argument resolving to a primitive type the macro
1050/// supports.
1051fn classify_type(ty: &Type) -> Option<ConstShape> {
1052    let syn::Type::Path(p) = ty else {
1053        return None;
1054    };
1055    let last = p.path.segments.last()?;
1056    if last.ident != "Const" {
1057        return None;
1058    }
1059    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1060        return None;
1061    };
1062    let inner = args.args.iter().find_map(|a| {
1063        if let syn::GenericArgument::Type(t) = a {
1064            Some(t)
1065        } else {
1066            None
1067        }
1068    })?;
1069    let s = type_to_string(inner);
1070    match s.as_str() {
1071        "u64" => Some(ConstShape::U64),
1072        "f64" => Some(ConstShape::F64),
1073        "bool" => Some(ConstShape::Bool),
1074        "& str" | "&str" => Some(ConstShape::Str),
1075        // `Const<Arc<T>>` — a value the compiler built and hands the
1076        // node as it is, rather than a literal the source wrote. The
1077        // concrete `T` is read from the declared type where it is
1078        // needed; the shape itself carries no type so it stays `Copy`
1079        // with the rest.
1080        _ if s.starts_with("Arc <") || s.starts_with("std :: sync :: Arc <") => {
1081            Some(ConstShape::Opaque)
1082        }
1083        _ => None,
1084    }
1085}
1086
1087/// The `T` of a `Const<Arc<T>>` argument's declared type.
1088fn opaque_inner_type(ty: &Type) -> Option<Type> {
1089    let Type::Path(p) = ty else { return None };
1090    let last = p.path.segments.last()?;
1091    if last.ident != "Const" {
1092        return None;
1093    }
1094    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1095        return None;
1096    };
1097    let inner = args.args.iter().find_map(|a| match a {
1098        syn::GenericArgument::Type(t) => Some(t),
1099        _ => None,
1100    })?;
1101    let Type::Path(arc) = inner else { return None };
1102    let arc_last = arc.path.segments.last()?;
1103    if arc_last.ident != "Arc" {
1104        return None;
1105    }
1106    let syn::PathArguments::AngleBracketed(arc_args) = &arc_last.arguments else {
1107        return None;
1108    };
1109    arc_args.args.iter().find_map(|a| match a {
1110        syn::GenericArgument::Type(t) => Some(t.clone()),
1111        _ => None,
1112    })
1113}
1114
1115/// SRD-80b Phase C — detect `Const<Vec<T>>` in arg position.
1116/// Returns the inner element shape on match. Distinct path
1117/// from [`classify_type`]: the macro recognises the variadic-
1118/// const shape before the scalar `Const<T>` shape, so a
1119/// signature using `Const<Vec<u64>>` doesn't get misclassified.
1120fn classify_const_vec(ty: &Type) -> Option<(ConstShape, ListForm)> {
1121    // Outer must be Const<...>.
1122    let syn::Type::Path(p) = ty else {
1123        return None;
1124    };
1125    let last = p.path.segments.last()?;
1126    if last.ident != "Const" {
1127        return None;
1128    }
1129    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1130        return None;
1131    };
1132    let inner = args.args.iter().find_map(|a| {
1133        if let syn::GenericArgument::Type(t) = a {
1134            Some(t)
1135        } else {
1136            None
1137        }
1138    })?;
1139    // Inner is `&[X]`, the borrowed list, or `Vec<X>`, the owned one.
1140    if let syn::Type::Reference(r) = inner
1141        && let syn::Type::Slice(slice) = r.elem.as_ref()
1142    {
1143        return const_shape_of(&slice.elem).map(|s| (s, ListForm::Borrowed));
1144    }
1145    let syn::Type::Path(vp) = inner else {
1146        return None;
1147    };
1148    let vlast = vp.path.segments.last()?;
1149    if vlast.ident != "Vec" {
1150        return None;
1151    }
1152    let syn::PathArguments::AngleBracketed(vargs) = &vlast.arguments else {
1153        return None;
1154    };
1155    let velem = vargs.args.iter().find_map(|a| {
1156        if let syn::GenericArgument::Type(t) = a {
1157            Some(t)
1158        } else {
1159            None
1160        }
1161    })?;
1162    const_shape_of(velem).map(|s| (s, ListForm::Owned))
1163}
1164
1165/// The element shape of a const list, from the element type as
1166/// written. Shared by the borrowed and owned spellings so the two
1167/// accept exactly the same element types.
1168fn const_shape_of(elem: &Type) -> Option<ConstShape> {
1169    match type_to_string(elem).as_str() {
1170        "u64" => Some(ConstShape::U64),
1171        "f64" => Some(ConstShape::F64),
1172        "bool" => Some(ConstShape::Bool),
1173        "String" => Some(ConstShape::Str),
1174        "& str" | "&str" => Some(ConstShape::Str),
1175        _ => None,
1176    }
1177}
1178
1179/// SRD-80b dynamic-output shape — detect
1180/// `DynamicOutputs<T>` in return position. Returns the inner
1181/// element type `T` on match. The macro pairs this with the
1182/// function's `Const<Vec<C>>` arg to compute the output port
1183/// count at construction time.
1184fn classify_dynamic_outputs(ty: &Type) -> Option<Type> {
1185    let syn::Type::Path(p) = ty else {
1186        return None;
1187    };
1188    let last = p.path.segments.last()?;
1189    if last.ident != "DynamicOutputs" {
1190        return None;
1191    }
1192    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1193        return None;
1194    };
1195    args.args.iter().find_map(|a| {
1196        if let syn::GenericArgument::Type(t) = a {
1197            Some(t.clone())
1198        } else {
1199            None
1200        }
1201    })
1202}
1203
1204/// Extract a `#[poly_default(EXPR)]` attribute from an arg's
1205/// outer attributes, if present. Returns the inner expression
1206/// token stream so the build closure can use it as the
1207/// fallback when the runtime `consts` slice is shorter than
1208/// the declared param list.
1209fn parse_poly_default(attrs: &[syn::Attribute]) -> syn::Result<Option<syn::Expr>> {
1210    for attr in attrs {
1211        if !attr.path().is_ident("poly_default") {
1212            continue;
1213        }
1214        let expr: syn::Expr = attr.parse_args()?;
1215        return Ok(Some(expr));
1216    }
1217    Ok(None)
1218}
1219
1220/// Extract a `#[constraint(<Variant>)]` attribute, on a wire or a
1221/// const argument. What follows `ConstConstraint::` — a bare variant
1222/// such as `NonZero`, or one with fields such as
1223/// `RangeF64 { min: 0.0, max: 1.0 }`.
1224///
1225/// On a **wire** arg it is the strict-wire metadata of SRD-80 PR B.14:
1226/// strict mode reads it and inserts an assertion node upstream.
1227///
1228/// On a **const** arg it lands in the parameter's `ParamSpec`, and the
1229/// factory checks it when the node is built, before the node exists.
1230/// That is the difference between a program that fails to assemble
1231/// with the parameter named and one that panics on some later cycle
1232/// from inside a node body — which is what a node had to do before
1233/// this, since a declared constraint was the one thing the macro could
1234/// not emit (F-N6).
1235fn parse_constraint(attrs: &[syn::Attribute]) -> syn::Result<Option<syn::Expr>> {
1236    for attr in attrs {
1237        if !attr.path().is_ident("constraint") {
1238            continue;
1239        }
1240        let expr: syn::Expr = attr.parse_args()?;
1241        return Ok(Some(expr));
1242    }
1243    Ok(None)
1244}
1245
1246/// Extract a `#[poly_const(<fn_expr>, from = <source>)]` attribute
1247/// from an arg's outer attributes, if present. Returns the
1248/// constructor expression and the source identifiers.
1249///
1250/// SRD-80b — `from` accepts three shapes:
1251///   - `from = ()` — empty source. Setup fn takes no args;
1252///     captures session-static state (env, system clock).
1253///   - `from = ident` — single source. Setup fn called as
1254///     `setup_fn(ident_value)`.
1255///   - `from = (a, b, c)` — multi-source (SRD-80b amendment).
1256///     Setup fn called as `setup_fn(a_value, b_value, c_value)`.
1257///     Order matches the tuple. Each name must reference a
1258///     `Const<T>` arg declared in the same function signature.
1259fn parse_poly_const(attrs: &[syn::Attribute]) -> syn::Result<Option<(syn::Expr, Vec<syn::Ident>)>> {
1260    for attr in attrs {
1261        if !attr.path().is_ident("poly_const") {
1262            continue;
1263        }
1264        let parser = |input: syn::parse::ParseStream| -> syn::Result<(syn::Expr, Vec<syn::Ident>)> {
1265            let fn_expr: syn::Expr = input.parse()?;
1266            let _comma: Token![,] = input.parse()?;
1267            let from_kw: syn::Ident = input.parse()?;
1268            if from_kw != "from" {
1269                return Err(syn::Error::new_spanned(
1270                    from_kw,
1271                    "#[poly_const(...)] requires a `from = <source>` clause. \
1272                     Supported shapes: `from = ()` (empty), `from = ident` \
1273                     (single), `from = (a, b, c)` (multi-source).",
1274                ));
1275            }
1276            let _eq: Token![=] = input.parse()?;
1277            // Parenthesised forms: `from = ()` or `from = (a, b, c)`.
1278            if input.peek(syn::token::Paren) {
1279                let inner;
1280                let _paren = syn::parenthesized!(inner in input);
1281                if inner.is_empty() {
1282                    return Ok((fn_expr, Vec::new()));
1283                }
1284                let parsed: Punctuated<syn::Ident, Token![,]> =
1285                    Punctuated::parse_terminated(&inner)?;
1286                if parsed.is_empty() {
1287                    return Err(syn::Error::new_spanned(
1288                        from_kw,
1289                        "#[poly_const(..., from = (...))] — the parenthesised \
1290                         form expects a comma-separated list of source-arg \
1291                         identifiers, or an empty `()` for session-static \
1292                         setup.",
1293                    ));
1294                }
1295                return Ok((fn_expr, parsed.into_iter().collect()));
1296            }
1297            // Bare `from = ident` — single source.
1298            let source: syn::Ident = input.parse()?;
1299            Ok((fn_expr, vec![source]))
1300        };
1301        let parsed = attr.parse_args_with(parser)?;
1302        return Ok(Some(parsed));
1303    }
1304    Ok(None)
1305}
1306
1307/// Detect `&T` for some `T` in arg-type position. Returns
1308/// `Some(inner_t)` on match, `None` otherwise. Used for the
1309/// PR B.6 setup-arg dispatch.
1310fn classify_borrowed(ty: &Type) -> Option<Type> {
1311    let syn::Type::Reference(r) = ty else {
1312        return None;
1313    };
1314    if r.mutability.is_some() {
1315        return None;
1316    }
1317    Some((*r.elem).clone())
1318}
1319
1320/// Detect `Value` in arg-type position. SRD-80 PR B.8 —
1321/// polymorphic wire dispatch. Matches the last path segment
1322/// being `Value`, so both `Value` and `polydat::ast::Value`
1323/// (and any other fully-qualified path ending in `Value`) work.
1324fn classify_polywire(ty: &Type) -> bool {
1325    let syn::Type::Path(p) = ty else {
1326        return false;
1327    };
1328    p.path
1329        .segments
1330        .last()
1331        .map(|s| s.ident == "Value")
1332        .unwrap_or(false)
1333}
1334
1335/// SRD-80 PR B.11/B.13 — structural classifier for the
1336/// wrapper-typed wire arg shapes. Returns the matching wire
1337/// kind, or `None` if the type isn't one of the recognised
1338/// wrapper shapes.
1339#[derive(Clone, Copy, PartialEq, Eq)]
1340enum WrapperWire {
1341    Bytes,
1342    Json,
1343    /// `Arc<T>` for some T that isn't `[u8]` or `serde_json::Value`.
1344    /// Inline-downcast in arg_bindings; inline-upcast in
1345    /// result_to_outputs. Handle dispatch.
1346    Handle,
1347    /// One of the seven typed vector variants: `VecF32` / `VecI32`
1348    /// / `VecF64` / `VecI64` / `VecF16` / `VecI16` / `VecI8`. The
1349    /// macro emits the matching `PortType::Vec*`; the Wire impls in
1350    /// derive_support are autogenerated from a macro_rules!
1351    /// expansion per element type.
1352    VecF32,
1353    VecI32,
1354    VecF64,
1355    VecI64,
1356    VecF16,
1357    VecI16,
1358    VecI8,
1359}
1360
1361fn classify_wrapper_wire(ty: &Type) -> Option<WrapperWire> {
1362    // SRD-80 PR B.13 — typed vectors. Check first to catch
1363    // `Vec<f32>` etc. before they fall into Handle territory
1364    // (which is the catch-all for Arc<T>).
1365    if let Some(kind) = classify_vec_wire(ty) {
1366        return Some(kind);
1367    }
1368
1369    // `Arc<[u8]>` — Arc with [u8] generic.
1370    if let Some(inner) = strip_arc(ty)
1371        && let syn::Type::Slice(slc) = inner
1372        && let syn::Type::Path(p) = &*slc.elem
1373        && p.path.is_ident("u8")
1374    {
1375        return Some(WrapperWire::Bytes);
1376    }
1377    // `Arc<serde_json::Value>` / `Arc<Value>` (last segment).
1378    if let Some(inner) = strip_arc(ty)
1379        && let syn::Type::Path(p) = inner
1380        && last_segment_is(p, "Value")
1381        && path_contains_segment(p, "serde_json")
1382    {
1383        return Some(WrapperWire::Json);
1384    }
1385    // `Arc<str>` — Str port via the dedicated Wire impl. Don't
1386    // route through Handle (str isn't Sized so the Handle's
1387    // `Value::handle<T: Sized>` constructor would reject it).
1388    if let Some(inner) = strip_arc(ty)
1389        && let syn::Type::Path(p) = inner
1390        && p.path.is_ident("str")
1391    {
1392        return None;
1393    }
1394    // `Arc<dyn Any + Send + Sync>` — Handle via the dedicated
1395    // Wire impl. Fall through to trait dispatch rather than
1396    // the structural Handle path (which expects a concrete
1397    // Arc<ConcreteT> for the downcast).
1398    if let Some(inner) = strip_arc(ty)
1399        && matches!(inner, syn::Type::TraitObject(_))
1400    {
1401        return None;
1402    }
1403    // Any other `Arc<T>` is a Handle.
1404    if strip_arc(ty).is_some() {
1405        return Some(WrapperWire::Handle);
1406    }
1407    // `Vec<u8>`.
1408    if let syn::Type::Path(p) = ty
1409        && let Some(last) = p.path.segments.last()
1410        && last.ident == "Vec"
1411        && let syn::PathArguments::AngleBracketed(args) = &last.arguments
1412        && let Some(syn::GenericArgument::Type(syn::Type::Path(elem))) = args.args.first()
1413        && elem.path.is_ident("u8")
1414    {
1415        return Some(WrapperWire::Bytes);
1416    }
1417    // `&[u8]` — borrowed bytes.
1418    if let syn::Type::Reference(r) = ty
1419        && r.mutability.is_none()
1420        && let syn::Type::Slice(slc) = &*r.elem
1421        && let syn::Type::Path(p) = &*slc.elem
1422        && p.path.is_ident("u8")
1423    {
1424        return Some(WrapperWire::Bytes);
1425    }
1426    // `&serde_json::Value`.
1427    if let syn::Type::Reference(r) = ty
1428        && r.mutability.is_none()
1429        && let syn::Type::Path(p) = &*r.elem
1430        && last_segment_is(p, "Value")
1431        && path_contains_segment(p, "serde_json")
1432    {
1433        return Some(WrapperWire::Json);
1434    }
1435    None
1436}
1437
1438fn strip_arc(ty: &Type) -> Option<&Type> {
1439    let syn::Type::Path(p) = ty else {
1440        return None;
1441    };
1442    let last = p.path.segments.last()?;
1443    if last.ident != "Arc" {
1444        return None;
1445    }
1446    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1447        return None;
1448    };
1449    args.args.iter().find_map(|a| match a {
1450        syn::GenericArgument::Type(t) => Some(t),
1451        _ => None,
1452    })
1453}
1454
1455fn last_segment_is(p: &syn::TypePath, name: &str) -> bool {
1456    p.path
1457        .segments
1458        .last()
1459        .map(|s| s.ident == name)
1460        .unwrap_or(false)
1461}
1462
1463fn path_contains_segment(p: &syn::TypePath, name: &str) -> bool {
1464    p.path.segments.iter().any(|s| s.ident == name)
1465}
1466
1467/// For a `Handle` arg, extract the inner T (the downcast target).
1468fn extract_handle_inner(ty: &Type) -> Option<Type> {
1469    strip_arc(ty).cloned()
1470}
1471
1472/// `Option<T>` recognition. Returns `true` if the type's last
1473/// path segment is `Option` with a single generic argument. Used
1474/// to decide whether to auto-emit `accepts_none_inputs() -> true`
1475/// — the runtime kernel's SRD-74 Rule 1 short-circuits `Value::None`
1476/// inputs on opt-in nodes; `Option<T>` wires are the canonical
1477/// opt-in shape.
1478fn is_option_arg(ty: &Type) -> bool {
1479    let syn::Type::Path(p) = ty else {
1480        return false;
1481    };
1482    let Some(last) = p.path.segments.last() else {
1483        return false;
1484    };
1485    if last.ident != "Option" {
1486        return false;
1487    }
1488    matches!(&last.arguments,
1489        syn::PathArguments::AngleBracketed(args)
1490            if args.args.iter().any(|a| matches!(a, syn::GenericArgument::Type(_))))
1491}
1492
1493/// Borrow-shape detection for SRD-80b Wire cutover. The macro
1494/// dispatches owned types through `<T as Wire>::extract` / `::inject`;
1495/// borrow shapes are recognised syntactically and emitted as
1496/// direct `match`-on-`Value` extraction at the eval call site.
1497/// This keeps the [`Wire`] trait bound at `Sized + 'static` without
1498/// needing lifetime parameters.
1499///
1500/// Returns the matched `Value::<Variant>(inner)` pattern and the
1501/// accessor expression that yields the body's expected borrow.
1502#[derive(Clone)]
1503enum BorrowWire {
1504    /// `&str`  → `Value::Str(arc)` → `arc.as_ref()` (`&str`).
1505    Str,
1506    /// `&[u8]` → `Value::Bytes(arc)` → `arc.as_ref()` (`&[u8]`).
1507    Bytes,
1508    /// `&serde_json::Value` → `Value::Json(j)` → `j.as_ref()`.
1509    Json,
1510    /// `&[T]` for T in {f32, i32, f64, i64, f16, i16} — typed
1511    /// vector borrow. Variant tracked separately so we can emit
1512    /// the right `Value::Vec*` arm; element type is recovered
1513    /// from the syntactic recognition.
1514    Vec(
1515        &'static str, /* variant name */
1516        TokenStream2, /* PortType expr */
1517    ),
1518}
1519
1520/// `Ext<T>` for some `T`: an extension value that rides a `Ref2`
1521/// pair into step-owned scratch (SRD 115) and reaches the body
1522/// through `Wire::extract`. The generic path already handles it on
1523/// the interpreter; this recognizer lets the slot kit carry it too.
1524fn is_ext_wire(ty: &Type) -> bool {
1525    let s = type_to_string(ty);
1526    s.starts_with("Ext <") || s.contains(":: Ext <")
1527}
1528
1529fn is_borrow_wire_shape(ty: &Type) -> Option<BorrowWire> {
1530    let syn::Type::Reference(r) = ty else {
1531        return None;
1532    };
1533    if r.mutability.is_some() {
1534        return None;
1535    }
1536    match &*r.elem {
1537        // `&str`
1538        syn::Type::Path(p) if p.path.is_ident("str") => Some(BorrowWire::Str),
1539        // `&[T]` — bytes (T=u8) and typed vectors.
1540        syn::Type::Slice(slc) => {
1541            if let syn::Type::Path(p) = &*slc.elem {
1542                if p.path.is_ident("u8") {
1543                    return Some(BorrowWire::Bytes);
1544                }
1545                let elem_name = p.path.segments.last()?.ident.to_string();
1546                let (variant, port_expr) = match elem_name.as_str() {
1547                    "f32" => ("VecF32", quote!(polydat::ast::PortType::VecF32)),
1548                    "i32" => ("VecI32", quote!(polydat::ast::PortType::VecI32)),
1549                    "f64" => ("VecF64", quote!(polydat::ast::PortType::VecF64)),
1550                    "i64" => ("VecI64", quote!(polydat::ast::PortType::VecI64)),
1551                    "f16" => ("VecF16", quote!(polydat::ast::PortType::VecF16)),
1552                    "i16" => ("VecI16", quote!(polydat::ast::PortType::VecI16)),
1553                    "i8" => ("VecI8", quote!(polydat::ast::PortType::VecI8)),
1554                    _ => return None,
1555                };
1556                return Some(BorrowWire::Vec(variant, port_expr));
1557            }
1558            None
1559        }
1560        // `&serde_json::Value` — recognise by last segment `Value`
1561        // alongside `serde_json` somewhere in the path.
1562        syn::Type::Path(p)
1563            if last_segment_is(p, "Value") && path_contains_segment(p, "serde_json") =>
1564        {
1565            Some(BorrowWire::Json)
1566        }
1567        _ => None,
1568    }
1569}
1570
1571/// Token stream for extracting a borrow-shape wire from
1572/// `&inputs[idx]`. The macro emits this directly (no trait
1573/// dispatch) so the borrow's lifetime is bound to the eval
1574/// call's `&inputs` borrow naturally — no `unsafe transmute`.
1575fn borrow_extract_tokens(shape: BorrowWire, input_expr: TokenStream2) -> TokenStream2 {
1576    match shape {
1577        BorrowWire::Str => quote! {
1578            match #input_expr {
1579                polydat::ast::Value::Str(__arc) => __arc.as_ref(),
1580                __other => panic!("expected Str wire, got {__other:?}"),
1581            }
1582        },
1583        BorrowWire::Bytes => quote! {
1584            match #input_expr {
1585                polydat::ast::Value::Bytes(__arc) => __arc.as_ref(),
1586                __other => panic!("expected Bytes wire, got {__other:?}"),
1587            }
1588        },
1589        BorrowWire::Json => quote! {
1590            match #input_expr {
1591                polydat::ast::Value::Json(__arc) => __arc.as_ref(),
1592                __other => panic!("expected Json wire, got {__other:?}"),
1593            }
1594        },
1595        BorrowWire::Vec(variant, _port) => {
1596            let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
1597            quote! {
1598                match #input_expr {
1599                    polydat::ast::Value::#v(__arc) => __arc.as_slice(),
1600                    __other => panic!(
1601                        concat!("expected ", stringify!(#v), " wire, got {:?}"),
1602                        __other),
1603                }
1604            }
1605        }
1606    }
1607}
1608
1609/// Token stream for the static `PortType` of a borrow-shape wire.
1610fn borrow_port_type(shape: &BorrowWire) -> TokenStream2 {
1611    match shape {
1612        BorrowWire::Str => quote!(polydat::ast::PortType::Str),
1613        BorrowWire::Bytes => quote!(polydat::ast::PortType::Bytes),
1614        BorrowWire::Json => quote!(polydat::ast::PortType::Json),
1615        BorrowWire::Vec(_, port_expr) => port_expr.clone(),
1616    }
1617}
1618
1619/// SRD-80 PR B.13 — typed-vector classifier. Recognises three
1620/// input shapes per element type: `SliceArc<T>`, `Vec<T>`,
1621/// `&[T]`. The element type's last path segment selects the
1622/// `WrapperWire::Vec*` variant.
1623fn classify_vec_wire(ty: &Type) -> Option<WrapperWire> {
1624    // Extract the element type from whichever of the three shapes
1625    // matches; none matching means this is not a typed vector.
1626    let elem: Type = strip_vec(ty)
1627        .or_else(|| strip_slice_arc(ty))
1628        .or_else(|| strip_borrowed_slice(ty))?
1629        .clone();
1630
1631    let syn::Type::Path(p) = &elem else {
1632        return None;
1633    };
1634    let last = p.path.segments.last()?;
1635    // f16 lives in the `half` crate, so the element path can
1636    // be `f16`, `half::f16`, etc. — match by last segment.
1637    match last.ident.to_string().as_str() {
1638        "f32" => Some(WrapperWire::VecF32),
1639        "i32" => Some(WrapperWire::VecI32),
1640        "f64" => Some(WrapperWire::VecF64),
1641        "i64" => Some(WrapperWire::VecI64),
1642        "f16" => Some(WrapperWire::VecF16),
1643        "i16" => Some(WrapperWire::VecI16),
1644        "i8" => Some(WrapperWire::VecI8),
1645        _ => None,
1646    }
1647}
1648
1649fn strip_vec(ty: &Type) -> Option<&Type> {
1650    let syn::Type::Path(p) = ty else {
1651        return None;
1652    };
1653    let last = p.path.segments.last()?;
1654    if last.ident != "Vec" {
1655        return None;
1656    }
1657    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1658        return None;
1659    };
1660    args.args.iter().find_map(|a| match a {
1661        syn::GenericArgument::Type(t) => Some(t),
1662        _ => None,
1663    })
1664}
1665
1666fn strip_slice_arc(ty: &Type) -> Option<&Type> {
1667    let syn::Type::Path(p) = ty else {
1668        return None;
1669    };
1670    let last = p.path.segments.last()?;
1671    if last.ident != "SliceArc" {
1672        return None;
1673    }
1674    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1675        return None;
1676    };
1677    args.args.iter().find_map(|a| match a {
1678        syn::GenericArgument::Type(t) => Some(t),
1679        _ => None,
1680    })
1681}
1682
1683fn strip_borrowed_slice(ty: &Type) -> Option<&Type> {
1684    let syn::Type::Reference(r) = ty else {
1685        return None;
1686    };
1687    if r.mutability.is_some() {
1688        return None;
1689    }
1690    let syn::Type::Slice(slc) = &*r.elem else {
1691        return None;
1692    };
1693    Some(&slc.elem)
1694}
1695
1696/// Detect `&[T]` (variadic) in arg-type position. SRD-80 PR B.9.
1697/// Returns the recognised element type for the supported primitive
1698/// element set; `None` otherwise (bare reference, non-slice, or
1699/// unsupported element type). Structural match — works regardless
1700/// of how the inner type is written (`Value` / `polydat::ast::Value`).
1701fn classify_variadic(ty: &Type) -> Option<VariadicElement> {
1702    let syn::Type::Reference(r) = ty else {
1703        return None;
1704    };
1705    if r.mutability.is_some() {
1706        return None;
1707    }
1708    let syn::Type::Slice(s) = &*r.elem else {
1709        return None;
1710    };
1711
1712    // `&[&str]` — element is a Type::Reference to a path "str".
1713    if let syn::Type::Reference(inner_r) = &*s.elem
1714        && inner_r.mutability.is_none()
1715        && let syn::Type::Path(p) = &*inner_r.elem
1716        && p.path.is_ident("str")
1717    {
1718        return Some(VariadicElement::BorrowedStr);
1719    }
1720
1721    // Bare-path element types — match by last path segment ident.
1722    let syn::Type::Path(p) = &*s.elem else {
1723        return None;
1724    };
1725    let last = p.path.segments.last()?;
1726    if !last.arguments.is_empty() {
1727        return None;
1728    }
1729    match last.ident.to_string().as_str() {
1730        "u64" => Some(VariadicElement::U64),
1731        // NOTE: `&[f64]` is deliberately NOT variadic — it is the
1732        // `VecF64` vector wire, uniform with every other lane
1733        // element (`&[f32]`/`&[i32]`/…). A variadic run of f64
1734        // wires would need an explicit `Variadic<f64>` spelling.
1735        "bool" => Some(VariadicElement::Bool),
1736        "String" => Some(VariadicElement::OwnedString),
1737        "Value" => Some(VariadicElement::Value),
1738        _ => None,
1739    }
1740}
1741
1742/// SRD-80b Phase 5 S16 — detect `Result<T, E>` return type for
1743/// fallible-construction nodes. Returns `Some(T)` (the Ok type)
1744/// when the return is a `Result<T, _>`; `None` otherwise. Matches
1745/// any path ending in `Result` so both bare `Result` and fully
1746/// qualified `std::result::Result` work.
1747///
1748/// The Err arm is consumed for its `Into<String>` projection at
1749/// emission time, so we don't pin its shape here — any E that
1750/// satisfies `Into<String>` (including `String` itself) is fine.
1751fn classify_result_return(ty: &Type) -> Option<Type> {
1752    let syn::Type::Path(p) = ty else {
1753        return None;
1754    };
1755    let last = p.path.segments.last()?;
1756    if last.ident != "Result" {
1757        return None;
1758    }
1759    let syn::PathArguments::AngleBracketed(args) = &last.arguments else {
1760        return None;
1761    };
1762    // Two args expected: <Ok, Err>. Tolerate `Result<T>` (rare alias)
1763    // by requiring at least one type arg.
1764    let mut tys = args.args.iter().filter_map(|a| match a {
1765        syn::GenericArgument::Type(t) => Some(t.clone()),
1766        _ => None,
1767    });
1768    tys.next()
1769}
1770
1771fn generate(func: ItemFn, attrs: NodeAttrs) -> syn::Result<TokenStream2> {
1772    let fn_name = &func.sig.ident;
1773    // SRD-80 PR B.7: strip `r#` from raw identifiers (`fn r#mod`,
1774    // `fn r#type`, etc.) so the Rust struct name comes out clean.
1775    let fn_name_raw = fn_name.to_string();
1776    let rust_name_str = fn_name_raw
1777        .strip_prefix("r#")
1778        .unwrap_or(&fn_name_raw)
1779        .to_string();
1780    let struct_name = attrs
1781        .struct_name
1782        .clone()
1783        .unwrap_or_else(|| format_ident!("{}", to_camel_case(&rust_name_str)));
1784    let func_name_str = rust_name_str.clone();
1785    let category = &attrs.category;
1786
1787    // Classify each function arg: wire or const? Reject any
1788    // unsupported pattern (self, complex destructuring, bare-
1789    // type wires the macro doesn't recognize).
1790    let mut args: Vec<ClassifiedArg> = Vec::new();
1791    for input in &func.sig.inputs {
1792        match input {
1793            FnArg::Receiver(r) => {
1794                return Err(syn::Error::new_spanned(
1795                    r,
1796                    "#[polydat_node] does not support `self` parameters yet; \
1797                     state-bearing nodes are deferred to a later PR.",
1798                ));
1799            }
1800            FnArg::Typed(pat_ty) => {
1801                let ident = match &*pat_ty.pat {
1802                    Pat::Ident(p) => p.ident.clone(),
1803                    other => {
1804                        return Err(syn::Error::new_spanned(
1805                            other,
1806                            "#[polydat_node] requires plain identifier parameters; \
1807                             pattern matching in argument position isn't supported.",
1808                        ));
1809                    }
1810                };
1811                let declared_ty = (*pat_ty.ty).clone();
1812                let default_value = parse_poly_default(&pat_ty.attrs)?;
1813                let setup_attr = parse_poly_const(&pat_ty.attrs)?;
1814                let constraint = parse_constraint(&pat_ty.attrs)?;
1815                let is_polywire = classify_polywire(&declared_ty);
1816                let variadic_elem = classify_variadic(&declared_ty);
1817
1818                let kind = if let Some(elem) = variadic_elem {
1819                    if default_value.is_some() || setup_attr.is_some() || is_polywire {
1820                        return Err(syn::Error::new_spanned(
1821                            pat_ty,
1822                            "variadic `&[T]` args don't combine with \
1823                             #[poly_default(...)], #[poly_const(...)], or `Value`.",
1824                        ));
1825                    }
1826                    ArgKind::Variadic(elem)
1827                } else if is_polywire {
1828                    if default_value.is_some() || setup_attr.is_some() {
1829                        return Err(syn::Error::new_spanned(
1830                            pat_ty,
1831                            "`Value` args (PolyWire) don't combine with \
1832                             #[poly_default(...)] or #[poly_const(...)]; \
1833                             the runtime port type comes from the upstream wire \
1834                             at construction time.",
1835                        ));
1836                    }
1837                    ArgKind::PolyWire
1838                } else if let Some((setup_fn, source_args)) = setup_attr {
1839                    // `#[poly_const(...)]` requires `&T` arg type.
1840                    let inner_ty = classify_borrowed(&declared_ty).ok_or_else(|| {
1841                        syn::Error::new_spanned(
1842                            &declared_ty,
1843                            "#[poly_const(...)] requires the argument type to be \
1844                             a borrow `&T` — the macro stores the computed `T` \
1845                             in a struct field and hands the body a borrow each \
1846                             eval.",
1847                        )
1848                    })?;
1849                    if default_value.is_some() {
1850                        return Err(syn::Error::new_spanned(
1851                            pat_ty,
1852                            "#[poly_default(...)] cannot combine with \
1853                             #[poly_const(...)]; defaults belong on the source \
1854                             Const arg, not on the derived setup arg.",
1855                        ));
1856                    }
1857                    ArgKind::Setup(Box::new(SetupSpec {
1858                        inner_ty,
1859                        setup_fn,
1860                        source_args,
1861                    }))
1862                } else if let Some(list) = classify_const_vec(&declared_ty) {
1863                    // SRD-80b Phase C — `Const<Vec<C>>` variadic
1864                    // workload-list. `poly_default` doesn't apply
1865                    // (the empty list IS the default); other
1866                    // attributes don't compose.
1867                    if default_value.is_some() {
1868                        return Err(syn::Error::new_spanned(
1869                            pat_ty,
1870                            "#[poly_default(...)] cannot combine with \
1871                             `Const<Vec<C>>`; the empty Vec IS the implicit \
1872                             default. Use `Const<C>` with a poly_default \
1873                             literal for a single-value default instead.",
1874                        ));
1875                    }
1876                    if setup_attr.is_some() {
1877                        return Err(syn::Error::new_spanned(
1878                            pat_ty,
1879                            "`Const<Vec<C>>` doesn't combine with \
1880                             #[poly_const(...)]; route the derived state \
1881                             from a scalar `Const<C>` source instead.",
1882                        ));
1883                    }
1884                    ArgKind::ConstVec(list.0, list.1)
1885                } else {
1886                    match classify_type(&declared_ty) {
1887                        Some(shape) => ArgKind::Const(shape),
1888                        None => {
1889                            if default_value.is_some() {
1890                                return Err(syn::Error::new_spanned(
1891                                    pat_ty,
1892                                    "#[poly_default(...)] only applies to const args \
1893                                     (`Const<T>`); bare-type wire args don't have \
1894                                     assembly-time defaults.",
1895                                ));
1896                            }
1897                            ArgKind::Wire
1898                        }
1899                    }
1900                };
1901                args.push(ClassifiedArg {
1902                    name: ident,
1903                    declared_ty,
1904                    kind,
1905                    default_value,
1906                    constraint,
1907                });
1908            }
1909        }
1910    }
1911
1912    // SRD-80b Phase C — `Const<Vec<C>>` consumes the tail of
1913    // `consts[..]` at build time, so at most one ConstVec arg is
1914    // allowed per node and it must be the last const arg in
1915    // declaration order. Validate before emission.
1916    {
1917        let const_vec_positions: Vec<usize> = args
1918            .iter()
1919            .enumerate()
1920            .filter_map(|(i, a)| {
1921                if matches!(a.kind, ArgKind::ConstVec(..)) {
1922                    Some(i)
1923                } else {
1924                    None
1925                }
1926            })
1927            .collect();
1928        if const_vec_positions.len() > 1 {
1929            return Err(syn::Error::new_spanned(
1930                &args[const_vec_positions[1]].declared_ty,
1931                "#[polydat_node] supports at most one `Const<Vec<C>>` arg \
1932                 per function; the variadic-const surface consumes the \
1933                 tail of the consts slice and a second one would have no \
1934                 entries to claim.",
1935            ));
1936        }
1937        if let Some(&pos) = const_vec_positions.first() {
1938            // Any Const(_) declared AFTER the ConstVec would never
1939            // bind (its index ≥ ConstVec's tail-start).
1940            for later in &args[pos + 1..] {
1941                if matches!(later.kind, ArgKind::Const(_)) {
1942                    return Err(syn::Error::new_spanned(
1943                        &later.declared_ty,
1944                        "scalar `Const<T>` arg declared after a \
1945                         `Const<Vec<C>>` arg is unreachable — the variadic \
1946                         consumes everything from its position to the end \
1947                         of the consts slice. Move the scalar consts BEFORE \
1948                         the `Const<Vec<C>>` in the function signature.",
1949                    ));
1950                }
1951            }
1952        }
1953    }
1954
1955    // Map a bare wire-arg type to a PortType expression.
1956    //
1957    // SRD-80b: the canonical answer is `<#ty as Wire>::PORT` —
1958    // any owned type that impls [`Wire`] is admitted, and adding
1959    // a new wire type means adding one Wire impl (no macro
1960    // source change). Three exceptions stay structural because
1961    // they can't be expressed through the trait:
1962    //
1963    //   1. Borrow shapes (`&str`, `&[u8]`, `&[T]`,
1964    //      `&serde_json::Value`) — `Wire` is `Sized + 'static`
1965    //      so borrowed refs can't impl it. The macro emits the
1966    //      literal `PortType` here and direct `match`-on-`Value`
1967    //      extraction elsewhere.
1968    //
1969    //   2. `Arc<T>` Handle (non-special T) — would conflict with
1970    //      the concrete `Arc<[u8]>` / `Arc<serde_json::Value>`
1971    //      impls if expressed as a blanket. Kept as inline
1972    //      downcast at the extract site; port type is the static
1973    //      `Handle`.
1974    //
1975    //   3. PolyWire (`Value`-typed wire) — polymorphic at
1976    //      runtime; no static `PortType`. The `ArgKind::PolyWire`
1977    //      path handles this independently of `wire_port_type_for`.
1978    //
1979    // Everything else — including `Option<T>`, `Ext<T>`, and any
1980    // future combinator added by impl'ing `Wire` — flows through
1981    // trait dispatch.
1982    let wire_port_type_for = |ty: &Type| -> syn::Result<TokenStream2> {
1983        if let Some(kind) = classify_wrapper_wire(ty) {
1984            return Ok(match kind {
1985                WrapperWire::Bytes => quote!(polydat::ast::PortType::Bytes),
1986                WrapperWire::Json => quote!(polydat::ast::PortType::Json),
1987                WrapperWire::Handle => quote!(polydat::ast::PortType::Handle),
1988                WrapperWire::VecF32 => quote!(polydat::ast::PortType::VecF32),
1989                WrapperWire::VecI32 => quote!(polydat::ast::PortType::VecI32),
1990                WrapperWire::VecF64 => quote!(polydat::ast::PortType::VecF64),
1991                WrapperWire::VecI64 => quote!(polydat::ast::PortType::VecI64),
1992                WrapperWire::VecF16 => quote!(polydat::ast::PortType::VecF16),
1993                WrapperWire::VecI16 => quote!(polydat::ast::PortType::VecI16),
1994                WrapperWire::VecI8 => quote!(polydat::ast::PortType::VecI8),
1995            });
1996        }
1997        if let Some(borrow) = is_borrow_wire_shape(ty) {
1998            return Ok(borrow_port_type(&borrow));
1999        }
2000        // Fall through to trait dispatch — `<T as Wire>::PORT` is
2001        // a const associated, evaluable at codegen time. Types
2002        // without a `Wire` impl produce a clean E0277 at the
2003        // function's call site, naming the missing trait bound.
2004        Ok(quote!(<#ty as polydat::derive_support::Wire>::PORT))
2005    };
2006
2007    // Build the NodeMeta `ins` slot list — one entry per arg,
2008    // dispatched by kind. Wire args get `Slot::Wire(...)`;
2009    // const args get `Slot::Const { ... }` populated with the
2010    // captured field value at construction time.
2011    let mut slot_exprs: Vec<TokenStream2> = Vec::new();
2012    for a in &args {
2013        let name_str = a.name.to_string();
2014        match &a.kind {
2015            ArgKind::Wire => {
2016                let pt = wire_port_type_for(&a.declared_ty)?;
2017                let ty = &a.declared_ty;
2018                // SRD-80 PR B.14: optional `#[constraint(Variant)]`.
2019                let constraint_chain = if let Some(variant) = &a.constraint {
2020                    quote! {
2021                        .with_constraint(
2022                            polydat::dsl::const_constraints::ConstConstraint::#variant)
2023                    }
2024                } else {
2025                    quote!()
2026                };
2027                // SRD-80b in-spirit — `Wire::WIRE_COST` is read
2028                // from the trait at codegen. Owned/non-borrow
2029                // wire types route here; borrow shapes don't
2030                // impl Wire so they get the default Data cost
2031                // (the WireCost::Config opt-in only applies to
2032                // owned types wrapped in `Config<T>`).
2033                let cost_chain = if is_borrow_wire_shape(ty).is_none()
2034                    && classify_wrapper_wire(ty) != Some(WrapperWire::Handle)
2035                {
2036                    quote! {
2037                        .with_cost(<#ty as polydat::derive_support::Wire>::WIRE_COST)
2038                    }
2039                } else {
2040                    quote!()
2041                };
2042                slot_exprs.push(quote! {
2043                    polydat::ast::Slot::Wire(
2044                        polydat::ast::Port::new(#name_str, #pt)
2045                            #constraint_chain
2046                            #cost_chain
2047                    )
2048                });
2049            }
2050            ArgKind::Const(shape) => {
2051                let field_name = &a.name;
2052                let const_value_ctor = match shape {
2053                    ConstShape::U64 => quote!(polydat::ast::ConstValue::U64(#field_name)),
2054                    ConstShape::F64 => quote!(polydat::ast::ConstValue::F64(#field_name)),
2055                    ConstShape::Bool => {
2056                        quote!(polydat::ast::ConstValue::U64(if #field_name { 1 } else { 0 }))
2057                    }
2058                    ConstShape::Str => quote!(polydat::ast::ConstValue::Str(#field_name.clone())),
2059                    // A compiler-built value has no literal form. Its
2060                    // slot names the argument and says what it is, so
2061                    // the meta reads honestly and no walker mistakes
2062                    // it for a constant it can fold.
2063                    ConstShape::Opaque => {
2064                        quote!(polydat::ast::ConstValue::Str(
2065                            "<compiler-built>".to_string()
2066                        ))
2067                    }
2068                };
2069                slot_exprs.push(quote! {
2070                    polydat::ast::Slot::Const {
2071                        name: #name_str.into(),
2072                        value: #const_value_ctor,
2073                    }
2074                });
2075            }
2076            ArgKind::Setup(_) => {
2077                // Setup args don't appear in NodeMeta.ins —
2078                // they're derived state, not declared params.
2079                // The source Const arg already carries the
2080                // introspectable value.
2081            }
2082            ArgKind::PolyWire => {
2083                // Port type is the `<argname>_type` parameter
2084                // passed to `new()`; the variable is in scope
2085                // because the macro emits it as a `new()` param.
2086                let pt_param = format_ident!("{}_type", a.name);
2087                slot_exprs.push(quote! {
2088                    polydat::ast::Slot::Wire(polydat::ast::Port::new(
2089                        #name_str, #pt_param))
2090                });
2091            }
2092            ArgKind::Variadic(_) => {
2093                // Variadic emits per-element slots at construction.
2094                // The macro generates `extend` into the slot vec
2095                // from a 0..n_wires loop. Each slot is named
2096                // `<argname>_<i>` to keep the meta diff-friendly.
2097                // (Handled in the new() body via a separate pass —
2098                // see `variadic_slot_extends` below.)
2099            }
2100            ArgKind::ConstVec(inner, _) => {
2101                // SRD-80b — `Const<Vec<C>>` emits a `Slot::Const`
2102                // entry when the inner element has a matching
2103                // `ConstValue::Vec*` variant (u64, f64). This
2104                // makes the captured list visible to JIT slot-
2105                // walkers and introspection (`jit_constants_from_slots`).
2106                // For element types without a parallel
2107                // `ConstValue` variant (bool, Str), no slot is
2108                // emitted; the FuncSig's `Arity::VariadicConsts`
2109                // tracks the surface and the stored Vec<C> field
2110                // is the canonical storage.
2111                let field_name = &a.name;
2112                match inner {
2113                    ConstShape::U64 => slot_exprs.push(quote! {
2114                        polydat::ast::Slot::Const {
2115                            name: #name_str.into(),
2116                            value: polydat::ast::ConstValue::VecU64(#field_name.clone()),
2117                        }
2118                    }),
2119                    ConstShape::F64 => slot_exprs.push(quote! {
2120                        polydat::ast::Slot::Const {
2121                            name: #name_str.into(),
2122                            value: polydat::ast::ConstValue::VecF64(#field_name.clone()),
2123                        }
2124                    }),
2125                    _ => {}
2126                }
2127            }
2128        }
2129    }
2130    // For each variadic arg, also emit a runtime loop that
2131    // appends N slots to the `Slot` vec.
2132    let variadic_slot_extends: Vec<TokenStream2> = args
2133        .iter()
2134        .filter_map(|a| match &a.kind {
2135            ArgKind::Variadic(elem) => {
2136                let name_str = a.name.to_string();
2137                let pt = elem.port_type_tokens();
2138                // A `&[Value]` variadic takes each wire as it is: the
2139                // body reads the `Value` variant, so an adapter into
2140                // the slot's nominal type would change what it sees.
2141                let any_type = if matches!(elem, VariadicElement::Value) {
2142                    quote!(.any_type())
2143                } else {
2144                    quote!()
2145                };
2146                Some(quote! {
2147                    for __i in 0..n_wires {
2148                        ins.push(polydat::ast::Slot::Wire(
2149                            polydat::ast::Port::new(
2150                                format!("{}_{__i}", #name_str),
2151                                #pt,
2152                            )#any_type));
2153                    }
2154                })
2155            }
2156            _ => None,
2157        })
2158        .collect();
2159
2160    // Build the FuncSig.params static slice — one ParamSpec
2161    // per declared arg (Wire and Const). Setup args don't
2162    // appear in the FuncSig surface — they're macro-internal
2163    // derived state.
2164    let param_specs: Vec<TokenStream2> = args
2165        .iter()
2166        .filter_map(|a| {
2167            let name_str = a.name.to_string();
2168            // Variadic args declare `required: false` — they accept
2169            // any count from `variadic_min` (default 0) upward.
2170            // `ConstVec` follows the same pattern (empty is valid).
2171            let required = match &a.kind {
2172                ArgKind::Variadic(_) | ArgKind::ConstVec(..) => false,
2173                _ => a.default_value.is_none(),
2174            };
2175            let slot_type = match &a.kind {
2176                ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => {
2177                    quote!(polydat::ast::SlotType::Wire)
2178                }
2179                ArgKind::Const(shape) => shape.slot_type_tokens(),
2180                ArgKind::ConstVec(inner, _) => inner.slot_type_tokens(),
2181                ArgKind::Setup(_) => return None,
2182            };
2183            // A declared `#[constraint(...)]` on a const argument
2184            // reaches the factory, which checks it when the node is
2185            // built. On a wire argument the same attribute is the
2186            // strict-wire metadata and rides on the port instead, so
2187            // it is not repeated here.
2188            let constraint = match (&a.kind, &a.constraint) {
2189                (ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_), _) | (_, None) => {
2190                    quote!(None)
2191                }
2192                (_, Some(c)) => {
2193                    quote!(Some(polydat::dsl::const_constraints::ConstConstraint::#c))
2194                }
2195            };
2196            // The example is a value a program can pass, as the field's
2197            // documentation says: `cycle` for a wire, the declared
2198            // default for a constant that has one, written as program
2199            // text, and empty when there is none to offer. It used to
2200            // be the parameter's own name, which no program can pass.
2201            let example = match &a.kind {
2202                ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => "cycle".to_string(),
2203                _ => a
2204                    .default_value
2205                    .as_ref()
2206                    .and_then(program_literal)
2207                    .unwrap_or_default(),
2208            };
2209            Some(quote! {
2210                polydat::dsl::registry::ParamSpec {
2211                    name: #name_str,
2212                    slot_type: #slot_type,
2213                    required: #required,
2214                    example: #example,
2215                    constraint: #constraint,
2216                }
2217            })
2218        })
2219        .collect();
2220
2221    // Output type. The simple case requires a concrete return
2222    // type (-> T); unit / unspecified isn't supported.
2223    let declared_ret_ty = match &func.sig.output {
2224        ReturnType::Default => {
2225            return Err(syn::Error::new_spanned(
2226                &func.sig,
2227                "#[polydat_node] requires an explicit return type; \
2228                 nodes always produce a value.",
2229            ));
2230        }
2231        ReturnType::Type(_, t) => (**t).clone(),
2232    };
2233    // SRD-80b Phase 5 S16 — fallible construction. When the body
2234    // returns `Result<T, E>`, the macro treats T as the effective
2235    // node-output type and emits a `try_new(...) -> Result<Self,
2236    // String>` constructor that runs the body once at
2237    // construction, caches the Ok value, and propagates Err. Only
2238    // valid for nodes with no wire/polywire inputs — the body has
2239    // to be fully resolvable at construction.
2240    let fallible_inner_ty: Option<Type> = classify_result_return(&declared_ret_ty);
2241    let is_fallible = fallible_inner_ty.is_some();
2242    let ret_ty = fallible_inner_ty
2243        .clone()
2244        .unwrap_or_else(|| declared_ret_ty.clone());
2245    let ret_is_polywire = classify_polywire(&ret_ty);
2246
2247    if is_fallible {
2248        // Wire / polywire / variadic inputs are not supported in
2249        // fallible mode: the body executes once at construction,
2250        // not per-eval. Const args are fine — they're all known
2251        // by the time `try_new` runs.
2252        for a in &args {
2253            match &a.kind {
2254                ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => {
2255                    return Err(syn::Error::new_spanned(
2256                        &a.declared_ty,
2257                        "fallible-construction nodes (-> Result<T, E>) must \
2258                         have only Const args. Wire/PolyWire/variadic inputs \
2259                         can't be evaluated at construction time. Use the \
2260                         #[poly_const(setup_fn, from = ...)] shape instead \
2261                         when per-eval inputs are needed.",
2262                    ));
2263                }
2264                ArgKind::Setup(_) | ArgKind::Const(_) | ArgKind::ConstVec(..) => {}
2265            }
2266        }
2267    }
2268
2269    // SRD-80 PR B.10: detect tuple-typed return for multi-output.
2270    let tuple_ret_elems: Option<Vec<Type>> = match &ret_ty {
2271        syn::Type::Tuple(t) => Some(t.elems.iter().cloned().collect()),
2272        _ => None,
2273    };
2274
2275    // SRD-80b dynamic-output shape — detect `DynamicOutputs<T>`
2276    // return and locate the `Const<Vec<C>>` arg whose length
2277    // drives the output port count at construction.
2278    let dynamic_outputs_inner: Option<Type> = classify_dynamic_outputs(&ret_ty);
2279    let dynamic_outputs_count_arg: Option<syn::Ident> = if dynamic_outputs_inner.is_some() {
2280        let const_vec_args: Vec<&syn::Ident> = args
2281            .iter()
2282            .filter_map(|a| match &a.kind {
2283                ArgKind::ConstVec(..) => Some(&a.name),
2284                _ => None,
2285            })
2286            .collect();
2287        if const_vec_args.len() != 1 {
2288            return Err(syn::Error::new_spanned(
2289                &ret_ty,
2290                format!(
2291                    "`DynamicOutputs<T>` return requires exactly one \
2292                     `Const<Vec<C>>` arg to drive the output port count \
2293                     (got {}). Declare one `Const<Vec<C>>` arg whose length \
2294                     determines the number of output ports.",
2295                    const_vec_args.len(),
2296                ),
2297            ));
2298        }
2299        Some(const_vec_args[0].clone())
2300    } else {
2301        None
2302    };
2303
2304    if tuple_ret_elems.is_some() && ret_is_polywire {
2305        // Type::Tuple isn't Type::Path so this is impossible, but
2306        // belt-and-suspenders for future return-shape changes.
2307        return Err(syn::Error::new_spanned(
2308            &ret_ty,
2309            "tuple return + PolyWire don't compose (SameAsInput is a \
2310             single-output dispatch).",
2311        ));
2312    }
2313
2314    // SRD-80 PR B.8: when the return type is `Value`, the
2315    // output port type tracks the first PolyWire arg's runtime
2316    // port type (SameAsInput). Otherwise it's the primitive's
2317    // fixed PortType.
2318    let first_polywire_idx: Option<usize> = args
2319        .iter()
2320        .enumerate()
2321        .find(|(_, a)| matches!(a.kind, ArgKind::PolyWire))
2322        .map(|(i, _)| i);
2323
2324    // Per-output port-type token streams, indexed positionally.
2325    // Single-output → 1-element vec; tuple → N elements.
2326    let output_port_types: Vec<TokenStream2> = if let Some(elems) = &tuple_ret_elems {
2327        elems
2328            .iter()
2329            .map(wire_port_type_for)
2330            .collect::<syn::Result<Vec<_>>>()?
2331    } else if ret_is_polywire {
2332        // Prefer a singleton PolyWire arg for SameAsInput
2333        // dispatch; fall back to a variadic `&[Value]` arg
2334        // (split-halves shape) whose runtime element types
2335        // drive the output polymorphism. The static slot
2336        // gets a `PortType::U64` placeholder (assembler skips
2337        // type-check for these); eval enforces uniformity.
2338        if let Some(polywire_arg) = args.iter().find(|a| matches!(a.kind, ArgKind::PolyWire)) {
2339            let pt_ident = format_ident!("{}_type", polywire_arg.name);
2340            vec![quote!(#pt_ident)]
2341        } else if args
2342            .iter()
2343            .any(|a| matches!(&a.kind, ArgKind::Variadic(VariadicElement::Value)))
2344        {
2345            // The output type is the type the variadic's wires
2346            // carry, resolved by the assembler and handed to
2347            // `new()` — the same answer a `Value` argument gets.
2348            // It used to be a `PortType::U64` placeholder, which
2349            // downstream read as a fact: a `Str` from `pick` into a
2350            // `Str` port had a `U64ToString` adapter inserted between
2351            // them, and the adapter read the string's pointer as a
2352            // number.
2353            vec![quote!(__variadic_out_type)]
2354        } else {
2355            return Err(syn::Error::new_spanned(
2356                &ret_ty,
2357                "function returns `Value` but has no `Value` arg — the macro \
2358                 needs at least one PolyWire (`Value`) arg or a `&[Value]` \
2359                 variadic to source the runtime port type for the output.",
2360            ));
2361        }
2362    } else if let Some(inner) = &dynamic_outputs_inner {
2363        // Single per-element port type for the dynamic case.
2364        // The count is determined at construction time; this
2365        // entry is used by the codegen as the port type each
2366        // output port carries.
2367        vec![wire_port_type_for(inner)?]
2368    } else {
2369        vec![wire_port_type_for(&ret_ty)?]
2370    };
2371
2372    // SRD-80 PR B.10: output names. Operator-supplied via
2373    // `output_names(a, b, c)`; falls back to `out_0`, `out_1`, ...
2374    // for tuple returns; just "output" for single returns.
2375    let output_names_strs: Vec<String> = match (&tuple_ret_elems, &attrs.output_names) {
2376        (Some(elems), Some(names)) => {
2377            if names.len() != elems.len() {
2378                return Err(syn::Error::new_spanned(
2379                    &ret_ty,
2380                    format!(
2381                        "tuple return has {} elements but `output_names(...)` \
2382                         lists {}; lengths must match.",
2383                        elems.len(),
2384                        names.len(),
2385                    ),
2386                ));
2387            }
2388            names.iter().map(|n| n.to_string()).collect()
2389        }
2390        (Some(elems), None) => (0..elems.len()).map(|i| format!("out_{i}")).collect(),
2391        (None, Some(names)) if names.len() != 1 => {
2392            return Err(syn::Error::new_spanned(
2393                &ret_ty,
2394                "single-output return doesn't accept multi-name `output_names(...)`.",
2395            ));
2396        }
2397        (None, Some(names)) => vec![names[0].to_string()],
2398        (None, None) => vec!["output".to_string()],
2399    };
2400
2401    // FuncSig::output_port — the statically-known return port for
2402    // single fixed-output nodes; None for tuple / polymorphic /
2403    // dynamic shapes (the DSL type inference then falls back to
2404    // its heuristic).
2405    let output_port_field: TokenStream2 =
2406        if tuple_ret_elems.is_some() || ret_is_polywire || dynamic_outputs_inner.is_some() {
2407            quote!(None)
2408        } else {
2409            let pt = &output_port_types[0];
2410            quote!(Some(#pt))
2411        };
2412
2413    let output_count = if dynamic_outputs_inner.is_some() {
2414        0
2415    } else {
2416        output_port_types.len()
2417    };
2418    // SRD-80b: `0` in the FuncSig signals "dynamic, determined at
2419    // compile time" (existing FuncSig convention from the doc).
2420    let output_count_lit =
2421        syn::LitInt::new(&output_count.to_string(), proc_macro2::Span::call_site());
2422
2423    // When return is `Value`, prefer SameAsInput dispatch
2424    // against a singleton PolyWire arg; for the split-halves
2425    // `&[Value]` case there's no singleton to point at, so
2426    // fall back to OutputType::Fixed (the static slot's
2427    // placeholder PortType is used and eval enforces type
2428    // uniformity).
2429    let output_type_tokens: TokenStream2 = match (ret_is_polywire, first_polywire_idx) {
2430        (true, Some(idx)) => {
2431            let i = syn::Index::from(idx);
2432            quote!(polydat::dsl::registry::OutputType::SameAsInput(#i))
2433        }
2434        _ => quote!(polydat::dsl::registry::OutputType::Fixed),
2435    };
2436
2437    // Struct fields. Wire/PolyWire/Variadic → no field (arity
2438    // reflected in `meta.ins.len()`); Const → owned-typed field;
2439    // ConstVec → Vec<inner>; Setup → field of the borrowed
2440    // inner type.
2441    let struct_fields: Vec<TokenStream2> = args
2442        .iter()
2443        .filter_map(|a| match &a.kind {
2444            ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2445            ArgKind::Const(shape) => {
2446                let n = &a.name;
2447                let ft = shape.field_type_tokens(opaque_inner_type(&a.declared_ty).as_ref());
2448                let doc = format!("The `{n}` argument, as given at construction.");
2449                Some(quote!(#[doc = #doc] pub #n: #ft))
2450            }
2451            ArgKind::ConstVec(inner, _) => {
2452                let n = &a.name;
2453                let ft = inner.field_type_tokens(None);
2454                let doc = format!("The `{n}` arguments, as given at construction.");
2455                Some(quote!(#[doc = #doc] pub #n: Vec<#ft>))
2456            }
2457            ArgKind::Setup(spec) => {
2458                let n = &a.name;
2459                let ty = &spec.inner_ty;
2460                let doc = format!("The `{n}` value, computed once at construction.");
2461                Some(quote!(#[doc = #doc] pub #n: #ty))
2462            }
2463        })
2464        .collect();
2465
2466    // `new(<polywire_types..>, <consts..>)` constructor params, in
2467    // declaration order. Const args contribute their owned-typed
2468    // value; PolyWire args contribute a `<argname>_type: PortType`
2469    // parameter that names the runtime port type the assembler
2470    // resolved for the upstream wire. Setup args are computed
2471    // inside new(), not parameters.
2472    let new_params: Vec<TokenStream2> = args
2473        .iter()
2474        .filter_map(|a| match &a.kind {
2475            ArgKind::Wire => None,
2476            ArgKind::Const(shape) => {
2477                let n = &a.name;
2478                let ft = shape.field_type_tokens(opaque_inner_type(&a.declared_ty).as_ref());
2479                Some(quote!(#n: #ft))
2480            }
2481            ArgKind::ConstVec(inner, _) => {
2482                let n = &a.name;
2483                let ft = inner.field_type_tokens(None);
2484                Some(quote!(#n: Vec<#ft>))
2485            }
2486            ArgKind::Setup(_) => None,
2487            ArgKind::PolyWire => {
2488                let n = format_ident!("{}_type", a.name);
2489                Some(quote!(#n: polydat::ast::PortType))
2490            }
2491            // Variadic args don't add their OWN per-arg param —
2492            // the variadic-arity is supplied via a SINGLE
2493            // `n_wires: usize` parameter appended once at the end
2494            // (see `variadic_n_wires_param` below).
2495            ArgKind::Variadic(_) => None,
2496        })
2497        .collect();
2498
2499    // SRD-80 PR B.9: append a single `n_wires: usize` parameter
2500    // to `new()` when the function declares any variadic arg.
2501    // SRD-80b split-halves variadic: TWO variadics in succession
2502    // share a single `n_wires` param (interpreted as "count per
2503    // half"). The macro emits 2*n_wires wire slots and slices
2504    // the inputs at the midpoint at eval time. Used by `pick`'s
2505    // `(b0,...,bN,v0,...,vN)` workload syntax per SRD-66.
2506    let has_variadic = args.iter().any(|a| matches!(a.kind, ArgKind::Variadic(_)));
2507    let variadic_count = args
2508        .iter()
2509        .filter(|a| matches!(a.kind, ArgKind::Variadic(_)))
2510        .count();
2511    if variadic_count > 2 {
2512        return Err(syn::Error::new_spanned(
2513            &func.sig,
2514            "`#[polydat_node]` supports at most two variadic `&[T]` args (split-halves shape). \
2515             Functions declaring more than two are not expressible in any SRD-80b shape.",
2516        ));
2517    }
2518    let is_split_halves = variadic_count == 2;
2519    // Positional index of each Variadic arg in declaration
2520    // order, used by `arg_bindings` to slice `inputs` at the
2521    // midpoint in split-halves mode.
2522    let variadic_positions: std::collections::HashMap<String, usize> = args
2523        .iter()
2524        .filter(|a| matches!(a.kind, ArgKind::Variadic(_)))
2525        .enumerate()
2526        .map(|(i, a)| (a.name.to_string(), i))
2527        .collect();
2528    // A `Value` return sourced from a `&[Value]` variadic takes the
2529    // output's port type as a constructor parameter, resolved from
2530    // the wires by the build closure.
2531    let needs_variadic_out_type = ret_is_polywire
2532        && first_polywire_idx.is_none()
2533        && args
2534            .iter()
2535            .any(|a| matches!(&a.kind, ArgKind::Variadic(VariadicElement::Value)));
2536    let new_params: Vec<TokenStream2> = if has_variadic {
2537        let mut v = new_params;
2538        v.push(quote!(n_wires: usize));
2539        if needs_variadic_out_type {
2540            v.push(quote!(__variadic_out_type: polydat::ast::PortType));
2541        }
2542        v
2543    } else {
2544        new_params
2545    };
2546
2547    // Build a lookup from arg name → const-shape category so the
2548    // Setup pre-compute step can dispatch on the source's shape
2549    // to produce the right access expression.
2550    #[derive(Clone, Copy)]
2551    enum ConstSourceShape {
2552        /// Scalar `Const<u64>` / `Const<f64>` / `Const<bool>`.
2553        ScalarValue,
2554        /// `Const<&str>` / `Const<String>` — backing field is
2555        /// `String`; setup fn typically wants `&str`.
2556        ScalarStr,
2557        /// `Const<Vec<C>>` — backing field is `Vec<C>`; setup fn
2558        /// typically wants `&Vec<C>` or `&[C]`.
2559        VecValues,
2560    }
2561    let const_shape_by_name: std::collections::HashMap<String, ConstSourceShape> = args
2562        .iter()
2563        .filter_map(|a| match &a.kind {
2564            ArgKind::Const(ConstShape::Str) => {
2565                Some((a.name.to_string(), ConstSourceShape::ScalarStr))
2566            }
2567            ArgKind::Const(_) => Some((a.name.to_string(), ConstSourceShape::ScalarValue)),
2568            ArgKind::ConstVec(..) => Some((a.name.to_string(), ConstSourceShape::VecValues)),
2569            _ => None,
2570        })
2571        .collect();
2572
2573    // Setup pre-compute lines, emitted at the top of `new()`
2574    // BEFORE `Self { ... }` so they can borrow the const
2575    // locals before those values are moved into self.
2576    let setup_precomputes: Vec<TokenStream2> = args
2577        .iter()
2578        .filter_map(|a| match &a.kind {
2579            ArgKind::Wire
2580            | ArgKind::Const(_)
2581            | ArgKind::ConstVec(..)
2582            | ArgKind::PolyWire
2583            | ArgKind::Variadic(_) => None,
2584            ArgKind::Setup(spec) => {
2585                let n = &a.name;
2586                let setup_fn = &spec.setup_fn;
2587                // SRD-80b amendment — `source_args` may be empty
2588                // (session-static setup), single (the common
2589                // case), or multi (joint derivation). Per-source
2590                // access dispatch reads each named const's
2591                // shape and emits the right body-side expression.
2592                let mut src_exprs: Vec<TokenStream2> = Vec::new();
2593                let mut err: Option<TokenStream2> = None;
2594                for src in &spec.source_args {
2595                    let shape = const_shape_by_name.get(&src.to_string());
2596                    let expr = match shape {
2597                        Some(ConstSourceShape::ScalarStr) => quote!(#src.as_str()),
2598                        Some(ConstSourceShape::ScalarValue) => quote!(#src),
2599                        // ConstVec source: pass a borrow of the
2600                        // Vec. Setup fn signatures like
2601                        // `fn build(w: &Vec<f64>)` or
2602                        // `fn build(w: &[f64])` both work via
2603                        // Deref / unsized coercion.
2604                        Some(ConstSourceShape::VecValues) => quote!(&#src),
2605                        None => {
2606                            err = Some(
2607                                syn::Error::new(
2608                                    src.span(),
2609                                    format!(
2610                                        "#[poly_const(... from = ... {src} ...)] — \
2611                                     `{src}` is not declared as a `Const<T>` \
2612                                     arg in the same function signature."
2613                                    ),
2614                                )
2615                                .to_compile_error(),
2616                            );
2617                            break;
2618                        }
2619                    };
2620                    src_exprs.push(expr);
2621                }
2622                if let Some(e) = err {
2623                    return Some(e);
2624                }
2625                let call = quote!(#setup_fn( #( #src_exprs ),* ));
2626                Some(quote! {
2627                    let #n = #call;
2628                })
2629            }
2630        })
2631        .collect();
2632
2633    // Self { ... } field-init list. Const args use field-name
2634    // shorthand; Setup args use the local computed above.
2635    // Wire/PolyWire contribute nothing (no field).
2636    let new_field_inits: Vec<TokenStream2> = args
2637        .iter()
2638        .filter_map(|a| match &a.kind {
2639            ArgKind::Wire | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2640            ArgKind::Const(_) | ArgKind::ConstVec(..) | ArgKind::Setup(_) => {
2641                let n = &a.name;
2642                Some(quote!(#n))
2643            }
2644        })
2645        .collect();
2646
2647    // Per-arg bindings the eval body sees. Wire args unbox via
2648    // FromValue; const args wrap the struct field as `Const<T>`
2649    // so the user's body code sees the wrapper type matching
2650    // its function signature.
2651    let mut wire_idx = 0usize;
2652    let arg_bindings: Vec<TokenStream2> = args
2653        .iter()
2654        .map(|a| {
2655            let n = &a.name;
2656            match &a.kind {
2657                ArgKind::Wire => {
2658                    let idx = syn::Index::from(wire_idx);
2659                    wire_idx += 1;
2660                    let ty = &a.declared_ty;
2661                    // SRD-80b Phase B — dispatch:
2662                    //   1. `Arc<T>` Handle (non-special T) → inline
2663                    //      downcast (no blanket impl works).
2664                    //   2. Borrow shape (`&str`, `&[u8]`, `&[T]`,
2665                    //      `&serde_json::Value`) → direct
2666                    //      `match`-on-`Value`. Lifetime is naturally
2667                    //      `&inputs[i]`'s; no `unsafe` transmute.
2668                    //   3. Otherwise → `<#ty as Wire>::extract`.
2669                    if classify_wrapper_wire(ty) == Some(WrapperWire::Handle) {
2670                        let inner = extract_handle_inner(ty)
2671                            .expect("Handle classification implies Arc<T> shape");
2672                        quote! {
2673                            let #n: std::sync::Arc<#inner> = match &inputs[#idx] {
2674                                polydat::ast::Value::Handle(arc) => arc.clone()
2675                                    .downcast::<#inner>()
2676                                    .expect("Handle type mismatch — wiring bug"),
2677                                other => panic!("expected Handle, got {other:?}"),
2678                            };
2679                        }
2680                    } else if let Some(borrow) = is_borrow_wire_shape(ty) {
2681                        let extract = borrow_extract_tokens(borrow, quote!(&inputs[#idx]));
2682                        quote! {
2683                            let #n = #extract;
2684                        }
2685                    } else {
2686                        quote! {
2687                            let #n = <#ty as polydat::derive_support::Wire>::extract(&inputs[#idx]);
2688                        }
2689                    }
2690                }
2691                ArgKind::Const(shape) => {
2692                    let wrap = shape.wrap_as_const(quote!(self.#n));
2693                    quote! {
2694                        let #n = #wrap;
2695                    }
2696                }
2697                ArgKind::Setup(_) => {
2698                    // Setup arg: body sees a borrow of the
2699                    // construction-time computed field. No
2700                    // wrapping needed — the field is the
2701                    // user's named type and `&T` matches the
2702                    // function-signature borrow.
2703                    quote! {
2704                        let #n = &self.#n;
2705                    }
2706                }
2707                ArgKind::PolyWire => {
2708                    // SRD-80 PR B.8: PolyWire — clone the
2709                    // `Value` directly into a local. Body sees
2710                    // an owned `Value`.
2711                    let idx = syn::Index::from(wire_idx);
2712                    wire_idx += 1;
2713                    quote! {
2714                        let #n: polydat::ast::Value = inputs[#idx].clone();
2715                    }
2716                }
2717                ArgKind::Variadic(elem) => {
2718                    // SRD-80 PR B.9 + SRD-80b split-halves —
2719                    // materialise a Vec<T> from the inputs
2720                    // slice (per-element extraction), then bind
2721                    // the body local as `&[T]`. In single-
2722                    // variadic mode, the slice is `inputs` (all
2723                    // of them after the leading wires consumed
2724                    // their indices). In split-halves mode, the
2725                    // first variadic gets `inputs[0..n_wires]`
2726                    // and the second gets `inputs[n_wires..]`.
2727                    let extractor = elem.extract_from_value();
2728                    let owned = format_ident!("__{}_owned", a.name);
2729                    // Split-halves divides `inputs` at the
2730                    // midpoint at eval time. `inputs.len() / 2`
2731                    // is the per-half count; first variadic
2732                    // gets the low half, second gets the high.
2733                    let slice_expr = if is_split_halves {
2734                        let pos = variadic_positions[&a.name.to_string()];
2735                        if pos == 0 {
2736                            quote!({
2737                                let __half = inputs.len() / 2;
2738                                &inputs[..__half]
2739                            })
2740                        } else {
2741                            quote!({
2742                                let __half = inputs.len() / 2;
2743                                &inputs[__half..]
2744                            })
2745                        }
2746                    } else {
2747                        quote!(inputs)
2748                    };
2749                    quote! {
2750                        let #owned: Vec<_> = #slice_expr.iter().map(#extractor).collect();
2751                        let #n: &[_] = #owned.as_slice();
2752                    }
2753                }
2754                // SRD-80b Phase C — a const list's body view. The
2755                // elements live in the node's own field either way; the
2756                // declared type says whether the body wanted a borrow
2757                // of them or a copy.
2758                ArgKind::ConstVec(_, ListForm::Borrowed) => quote! {
2759                    let #n = polydat::derive_support::Const(&self.#n[..]);
2760                },
2761                ArgKind::ConstVec(_, ListForm::Owned) => quote! {
2762                    let #n = polydat::derive_support::Const(self.#n.clone());
2763                },
2764            }
2765        })
2766        .collect();
2767
2768    // Build closure const-extraction logic. For each const arg
2769    // (in declaration order), pull from `consts: &[ConstArg]`
2770    // by index; fall back to the `poly_default` value if the
2771    // slice is shorter than the const arg list.
2772    //
2773    // For `ConstVec` args, collect every remaining entry from
2774    // `consts[i..]` into a `Vec<inner>` via the inner shape's
2775    // extractor — this consumes the tail of the consts slice
2776    // (only one ConstVec arg per function, enforced earlier).
2777    let mut const_idx_for_extract = 0usize;
2778    let const_extracts: Vec<TokenStream2> = args
2779        .iter()
2780        .filter_map(|a| match &a.kind {
2781            ArgKind::Wire | ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::Variadic(_) => None,
2782            ArgKind::Const(shape) => {
2783                let n = &a.name;
2784                let i = const_idx_for_extract;
2785                const_idx_for_extract += 1;
2786                let i_lit = syn::Index::from(i);
2787                let extract_present = shape
2788                    .extract_from_const_arg(quote!(c), opaque_inner_type(&a.declared_ty).as_ref());
2789                let fallback = match &a.default_value {
2790                    Some(default_expr) => {
2791                        // Default is an expression evaluating to
2792                        // the field type (`u64`, `f64`, `bool`,
2793                        // `String`). For Str: the expression
2794                        // should produce a `&str` or `String`; we
2795                        // call `.to_string()` to land on owned.
2796                        match shape {
2797                            ConstShape::Str => quote!((#default_expr).to_string()),
2798                            _ => quote!(#default_expr),
2799                        }
2800                    }
2801                    None => {
2802                        let msg = format!(
2803                            "missing required const arg '{n}' for function '{func_name_str}'"
2804                        );
2805                        quote!(return Some(Err(#msg.to_string())))
2806                    }
2807                };
2808                Some(quote! {
2809                    let #n: _ = match consts.get(#i_lit) {
2810                        Some(c) => #extract_present,
2811                        None => #fallback,
2812                    };
2813                })
2814            }
2815            ArgKind::ConstVec(inner, _) => {
2816                let n = &a.name;
2817                let i = const_idx_for_extract;
2818                // ConstVec consumes everything from index `i`
2819                // onward. const_idx_for_extract is intentionally
2820                // NOT bumped — by construction (validated below)
2821                // there's at most one ConstVec arg and it must be
2822                // the last arg, so no subsequent Const reads need
2823                // a higher base index.
2824                let i_lit = syn::LitInt::new(&i.to_string(), proc_macro2::Span::call_site());
2825                let extract_one = inner.extract_from_const_arg(quote!(c), None);
2826                Some(quote! {
2827                    let #n: Vec<_> = consts[#i_lit..].iter()
2828                        .map(|c| #extract_one)
2829                        .collect();
2830                })
2831            }
2832        })
2833        .collect();
2834
2835    // Names to pass to `Self::new(...)` from the build closure,
2836    // in declaration order. Const → `<name>`; PolyWire →
2837    // `<name>_type` (the local extracted from `wire_types`).
2838    let mut new_call_args: Vec<TokenStream2> = args
2839        .iter()
2840        .filter_map(|a| match &a.kind {
2841            ArgKind::Wire | ArgKind::Setup(_) | ArgKind::Variadic(_) => None,
2842            ArgKind::Const(_) | ArgKind::ConstVec(..) => {
2843                let n = &a.name;
2844                Some(quote!(#n))
2845            }
2846            ArgKind::PolyWire => {
2847                let n = format_ident!("{}_type", a.name);
2848                Some(quote!(#n))
2849            }
2850        })
2851        .collect();
2852    if has_variadic {
2853        new_call_args.push(quote!(n_wires));
2854        if needs_variadic_out_type {
2855            new_call_args.push(quote!(__variadic_out_type));
2856        }
2857    }
2858
2859    // SRD-80 PR B.9: when the function has a variadic arg,
2860    // extract `n_wires` from the `_wires: &[WireRef]` slice in
2861    // the build closure. The whole `_wires.len()` is the variadic
2862    // count (this PR supports one variadic arg only — when
2863    // multi-variadic lands, this extraction needs the per-arg
2864    // split logic).
2865    let variadic_n_wires_extract: TokenStream2 = if has_variadic {
2866        // Split-halves: assembler hands TOTAL wires; new() takes
2867        // the per-half count, so divide by 2 here too (matches
2868        // the variadic_ctor field's `n / 2`).
2869        let n = if is_split_halves {
2870            quote! { let n_wires: usize = _wires.len() / 2; }
2871        } else {
2872            quote! { let n_wires: usize = _wires.len(); }
2873        };
2874        // The wires the variadic's values come from: all of them,
2875        // or the second half under the split-halves shape, whose
2876        // first half is the selectors. Their common type is the
2877        // output's; `eval` enforces that they agree, and reports
2878        // the disagreement by name when they do not.
2879        let out_type = if needs_variadic_out_type {
2880            let first = if is_split_halves {
2881                quote!(_wire_types.get(n_wires))
2882            } else {
2883                quote!(_wire_types.first())
2884            };
2885            quote! {
2886                let __variadic_out_type = #first
2887                    .copied()
2888                    .unwrap_or(polydat::ast::PortType::U64);
2889            }
2890        } else {
2891            quote!()
2892        };
2893        quote! { #n #out_type }
2894    } else {
2895        quote!()
2896    };
2897
2898    // SRD-80 PR B.8: extract resolved PolyWire port types from
2899    // the `wire_types: &[PortType]` slice the assembler hands
2900    // the build closure. Wire/PolyWire share the same slot
2901    // counter (both consume a wire input position); we count
2902    // through args in declaration order.
2903    let polywire_extracts: Vec<TokenStream2> = {
2904        let mut wire_idx = 0usize;
2905        let mut out = Vec::new();
2906        for a in &args {
2907            match &a.kind {
2908                ArgKind::Wire => {
2909                    wire_idx += 1;
2910                }
2911                ArgKind::Variadic(_) => {
2912                    // Variadic args consume the REMAINDER of the
2913                    // wire slots. Only one variadic arg supported
2914                    // in this PR.
2915                    wire_idx += 0; // no positional increment
2916                }
2917                ArgKind::PolyWire => {
2918                    let pt_ident = format_ident!("{}_type", a.name);
2919                    let i = syn::Index::from(wire_idx);
2920                    let n_str = a.name.to_string();
2921                    let err = format!(
2922                        "polywire arg '{n_str}' for '{func_name_str}': assembler \
2923                         did not resolve a port type at wire index {wire_idx}"
2924                    );
2925                    out.push(quote! {
2926                        let #pt_ident: polydat::ast::PortType = match _wire_types.get(#i) {
2927                            Some(t) => *t,
2928                            None => return Some(Err(#err.to_string())),
2929                        };
2930                    });
2931                    wire_idx += 1;
2932                }
2933                ArgKind::Const(_) | ArgKind::ConstVec(..) | ArgKind::Setup(_) => {}
2934            }
2935        }
2936        out
2937    };
2938
2939    let block = &func.block;
2940
2941    // SRD-80b in-spirit `default_resolver` emission. Each wire
2942    // arg's `Wire::RESOLVER` const exposes the auto-resolver
2943    // intent at codegen time; the cascade picks the first
2944    // non-None among the wire-typed args. Non-Resolved wire
2945    // types contribute `None` (the trait default), so this
2946    // collapses cleanly to a no-resolver FuncSig for the
2947    // overwhelming majority of nodes.
2948    let default_resolver_field: TokenStream2 = {
2949        // Borrow shapes (`&str`, `&[u8]`, ...) don't impl `Wire`,
2950        // and `PolyWire` is excluded by ArgKind; only the
2951        // owned-type wire args contribute resolver intent.
2952        let wire_tys: Vec<&Type> = args
2953            .iter()
2954            .filter_map(|a| match &a.kind {
2955                ArgKind::Wire
2956                    if is_borrow_wire_shape(&a.declared_ty).is_none()
2957                        && classify_wrapper_wire(&a.declared_ty) != Some(WrapperWire::Handle) =>
2958                {
2959                    Some(&a.declared_ty)
2960                }
2961                _ => None,
2962            })
2963            .collect();
2964        if wire_tys.is_empty() {
2965            quote!(None)
2966        } else {
2967            // Build a right-to-left match cascade so the first
2968            // wire arg with a Some(_) resolver wins. Each step:
2969            //   match <ty as Wire>::RESOLVER { Some(r) => Some(r), None => <rest> }
2970            let mut acc = quote!(None);
2971            for ty in wire_tys.iter().rev() {
2972                acc = quote! {
2973                    match <#ty as polydat::derive_support::Wire>::RESOLVER {
2974                        Some(__r) => Some(__r),
2975                        None => #acc,
2976                    }
2977                };
2978            }
2979            acc
2980        }
2981    };
2982
2983    // Emit `Default` only when there are no const args AND no
2984    // setup args. Both require captured values to construct.
2985    let has_non_wire = args.iter().any(|a| !matches!(a.kind, ArgKind::Wire));
2986    let default_impl = if has_non_wire {
2987        quote!()
2988    } else {
2989        quote! {
2990            impl Default for #struct_name {
2991                fn default() -> Self { Self::new() }
2992            }
2993        }
2994    };
2995
2996    // SRD-80b Phase F (S18) — `#[polydat_node(decompose =
2997    // path)]` emits the FusedNode impl by delegating to the
2998    // named free function. Operators with bespoke fusion
2999    // logic (e.g. WeightedPick whose `decomposed()` body
3000    // builds a spec string) can still write their own
3001    // `impl FusedNode` block alongside the macro emission;
3002    // both compose because `decompose` is opt-in.
3003    let fused_node_impl: TokenStream2 = if let Some(path) = &attrs.decompose {
3004        quote! {
3005            impl polydat::compile::fusion::FusedNode for #struct_name {
3006                fn decomposed(&self) -> polydat::compile::fusion::DecomposedGraph {
3007                    #path(self)
3008                }
3009            }
3010        }
3011    } else {
3012        quote!()
3013    };
3014
3015    // ── SRD-80 PR B.7 — JIT eligibility + hook emission ──
3016    //
3017    // A node is Phase-2 eligible when every arg + return maps
3018    // to a `JitType` and no `#[poly_const]` setup arg is declared
3019    // (setup carries non-primitive derived state that can't fit a
3020    // u64 buffer). Override attributes (`compiled_u64 = ...`,
3021    // `jit_constants = ...`) bypass eligibility — they win
3022    // unconditionally.
3023
3024    let has_setup = args.iter().any(|a| matches!(a.kind, ArgKind::Setup(_)));
3025    let ret_jit_type = wire_type_to_jit_type(&ret_ty);
3026
3027    let arg_jit_types: Option<Vec<JitType>> = if has_setup {
3028        None
3029    } else {
3030        args.iter()
3031            .map(|a| match &a.kind {
3032                ArgKind::Wire => wire_type_to_jit_type(&a.declared_ty),
3033                // A const is captured by clone and never rides the
3034                // buffer, so its carrier is immaterial to eligibility.
3035                ArgKind::Const(shape) => {
3036                    Some(const_shape_to_jit_type(*shape).unwrap_or(JitType::U64))
3037                }
3038                // ConstVec is JIT-ineligible (the JIT u64 buffer
3039                // has no slot shape for a variable-length list).
3040                ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(..) => None,
3041                // SRD-80 PR B.9: variadic JIT — only `&[u64]`
3042                // rides the Phase 2 closure cleanly (the buffer
3043                // IS the slice). For f64/bool/Str variadics
3044                // the closure would need a per-call Vec
3045                // allocation to bit-reinterpret; skip in this PR.
3046                ArgKind::Variadic(elem) => match elem {
3047                    VariadicElement::U64 => Some(JitType::U64),
3048                    _ => None,
3049                },
3050            })
3051            .collect()
3052    };
3053
3054    // SRD-80 PR B.10/B.15: tuple return becomes JIT-eligible
3055    // when every element is JIT-eligible. The compiled_u64
3056    // closure destructures the result and writes each element
3057    // to its `outputs[i]` slot via the matching JitType.
3058    let tuple_ret_jit_types: Option<Vec<JitType>> = tuple_ret_elems.as_ref().and_then(|elems| {
3059        elems
3060            .iter()
3061            .map(wire_type_to_jit_type)
3062            .collect::<Option<Vec<_>>>()
3063    });
3064
3065    let jit_eligible = !is_fallible
3066        && arg_jit_types.is_some()
3067        && (ret_jit_type.is_some() || tuple_ret_jit_types.is_some());
3068
3069    // ── The slot kit (`compiled_slot`): the general compiled closure
3070    // over the flat slot buffer, for every node the u64 kit does not
3071    // carry (type_system_alignment.md §8.4 layer 3; jit_boundary.md,
3072    // axioms S1–S10). A scalar rides its slots as in the u64 kit.
3073    // Every `Ref2` port rides a `(ptr, len)` pair: a typed vector, a
3074    // string, or a byte string as a slice of its elements, and a JSON,
3075    // extension, or polymorphic value as a one-element slice holding
3076    // the `Value`. A `Ref2` output is written into the step's own
3077    // scratch entry, which the kernel owns and hands the closure
3078    // (axiom S3), and its pair is republished on every run; a `Ref2`
3079    // input is read through one dereference of the pair its producer
3080    // published (axiom S7). A polymorphic port and a variadic decode by
3081    // the wire types the kernel hands the kit, and a polymorphic return
3082    // encodes by the node's resolved output type. A const or const
3083    // list is captured by clone; a setup derived from consts is
3084    // recomputed from the captured consts, and a session-static setup
3085    // is captured from the node by clone; an `Option<T>` or `Config<T>`
3086    // over a carrier is the carrier's slot, wrapped.
3087    enum SlotArg {
3088        Jit(JitType),
3089        /// `Option<T>` over a one-slot carrier. A compiled kernel
3090        /// never carries `None` on a scalar slot (an unset extern is
3091        /// refused before the run), so the value is always present.
3092        Option(JitType),
3093        /// `Config<T>` over a carrier, the same slot wrapped, or over
3094        /// an owned string or byte string, copied out and wrapped.
3095        Config(ConfigInner),
3096        /// A typed vector slice, `&[T]`.
3097        Vec(&'static str),
3098        /// `&str`, or an owned `String` / `Arc<str>` copied out of
3099        /// the producer's bytes.
3100        Str {
3101            owned: bool,
3102        },
3103        /// `&[u8]`, or an owned `Vec<u8>` / `Arc<[u8]>` copied out.
3104        Bytes {
3105            owned: bool,
3106        },
3107        JsonRef,
3108        JsonArc,
3109        Ext,
3110        Poly,
3111        Variadic(VariadicElement),
3112        Const(ConstShape),
3113        /// A const list. The form says whether the body asked to
3114        /// borrow the kit's own copy or to take one of its own.
3115        ConstVec(ListForm),
3116        Setup,
3117        /// A session-static setup (`from = ()`), captured from the
3118        /// node by clone: the closure sees what the node captured at
3119        /// construction, as the native form does through
3120        /// `jit_constants`.
3121        SetupStatic,
3122    }
3123    /// What a `Config<T>` wraps.
3124    #[derive(Clone, Copy)]
3125    enum ConfigInner {
3126        Jit(JitType),
3127        Str,
3128        Bytes,
3129    }
3130    /// One element of a return: a carrier, or a `Ref2` kind that
3131    /// takes a scratch entry of its own.
3132    #[derive(Clone, Copy)]
3133    enum SlotElem {
3134        Jit(JitType),
3135        Vec(&'static str),
3136        Str,
3137        Bytes,
3138        Json,
3139        Ext,
3140    }
3141    impl SlotElem {
3142        fn is_ref(self) -> bool {
3143            !matches!(self, SlotElem::Jit(_))
3144        }
3145        fn width(self) -> usize {
3146            match self {
3147                SlotElem::Jit(jt) => jt.width(),
3148                _ => 2,
3149            }
3150        }
3151        fn scratch_elem(self) -> Option<TokenStream2> {
3152            let name = match self {
3153                SlotElem::Jit(_) => return None,
3154                SlotElem::Vec(e) => e,
3155                SlotElem::Str => "Str",
3156                SlotElem::Bytes => "Bytes",
3157                SlotElem::Json | SlotElem::Ext => "Value",
3158            };
3159            let id = syn::Ident::new(name, proc_macro2::Span::call_site());
3160            Some(quote!(polydat::ast::ScratchElem::#id))
3161        }
3162    }
3163    enum SlotRet {
3164        Elem(SlotElem),
3165        /// A polymorphic `Value` return, encoded by the node's
3166        /// resolved output type.
3167        Poly,
3168        /// A tuple return: each element written by shape.
3169        Tuple(Vec<SlotElem>),
3170    }
3171    impl SlotRet {
3172        /// Whether any element takes a scratch entry.
3173        fn has_ref(&self) -> bool {
3174            match self {
3175                SlotRet::Elem(e) => e.is_ref(),
3176                SlotRet::Poly => true,
3177                SlotRet::Tuple(elems) => elems.iter().any(|e| e.is_ref()),
3178            }
3179        }
3180    }
3181    let owned_str_ty = |ty: &Type| -> bool {
3182        let flat: String = type_to_string(ty).split_whitespace().collect();
3183        matches!(flat.as_str(), "String" | "Arc<str>" | "std::sync::Arc<str>")
3184    };
3185    let owned_bytes_ty = |ty: &Type| -> bool {
3186        let flat: String = type_to_string(ty).split_whitespace().collect();
3187        matches!(
3188            flat.as_str(),
3189            "Vec<u8>" | "Arc<[u8]>" | "std::sync::Arc<[u8]>"
3190        )
3191    };
3192    let vec_ret_elem = |ty: &Type| -> Option<&'static str> {
3193        let flat: String = type_to_string(ty).split_whitespace().collect();
3194        match flat.as_str() {
3195            "Vec<f32>" => Some("F32"),
3196            "Vec<f64>" => Some("F64"),
3197            "Vec<half::f16>" | "Vec<f16>" => Some("F16"),
3198            "Vec<i8>" => Some("I8"),
3199            "Vec<i16>" => Some("I16"),
3200            "Vec<i32>" => Some("I32"),
3201            "Vec<i64>" => Some("I64"),
3202            _ => None,
3203        }
3204    };
3205    let classify_elem = |ty: &Type| -> Option<SlotElem> {
3206        if classify_wrapper_wire(ty) == Some(WrapperWire::Json) {
3207            Some(SlotElem::Json)
3208        } else if is_ext_wire(ty) {
3209            Some(SlotElem::Ext)
3210        } else if let Some(e) = vec_ret_elem(ty) {
3211            Some(SlotElem::Vec(e))
3212        } else if owned_str_ty(ty) {
3213            Some(SlotElem::Str)
3214        } else if owned_bytes_ty(ty) {
3215            Some(SlotElem::Bytes)
3216        } else {
3217            wire_type_to_jit_type(ty).map(SlotElem::Jit)
3218        }
3219    };
3220    // The return shape the kit can write: a carrier, a `Ref2` kind, a
3221    // polymorphic value, or a tuple of carriers and `Ref2` kinds.
3222    let classify_ret_shape = || -> Option<SlotRet> {
3223        if ret_is_polywire {
3224            return Some(SlotRet::Poly);
3225        }
3226        if let Some(elems) = &tuple_ret_elems {
3227            let shapes: Option<Vec<SlotElem>> = elems.iter().map(classify_elem).collect();
3228            return shapes.map(SlotRet::Tuple);
3229        }
3230        classify_elem(&ret_ty).map(SlotRet::Elem)
3231    };
3232    let slot_plan: Option<(Vec<SlotArg>, SlotRet)> = (|| {
3233        if is_fallible || dynamic_outputs_inner.is_some() {
3234            return None;
3235        }
3236        let ret_shape = classify_ret_shape()?;
3237        let mut shapes = Vec::with_capacity(args.len());
3238        for a in &args {
3239            let ty = &a.declared_ty;
3240            let shape = match &a.kind {
3241                ArgKind::Wire => match is_borrow_wire_shape(ty) {
3242                    Some(BorrowWire::Str) => SlotArg::Str { owned: false },
3243                    Some(BorrowWire::Bytes) => SlotArg::Bytes { owned: false },
3244                    Some(BorrowWire::Json) => SlotArg::JsonRef,
3245                    Some(BorrowWire::Vec(variant, _)) => match variant {
3246                        "VecF32" => SlotArg::Vec("F32"),
3247                        "VecF64" => SlotArg::Vec("F64"),
3248                        "VecF16" => SlotArg::Vec("F16"),
3249                        "VecI8" => SlotArg::Vec("I8"),
3250                        "VecI16" => SlotArg::Vec("I16"),
3251                        "VecI32" => SlotArg::Vec("I32"),
3252                        "VecI64" => SlotArg::Vec("I64"),
3253                        _ => return None,
3254                    },
3255                    None => {
3256                        if classify_wrapper_wire(ty) == Some(WrapperWire::Json) {
3257                            SlotArg::JsonArc
3258                        } else if is_ext_wire(ty) {
3259                            SlotArg::Ext
3260                        } else if owned_str_ty(ty) {
3261                            SlotArg::Str { owned: true }
3262                        } else if owned_bytes_ty(ty) {
3263                            SlotArg::Bytes { owned: true }
3264                        } else if let Some(inner) = option_inner(ty) {
3265                            let jt = wire_type_to_jit_type(inner)?;
3266                            if jt.width() != 1 {
3267                                return None;
3268                            }
3269                            SlotArg::Option(jt)
3270                        } else if let Some(inner) = config_inner(ty) {
3271                            SlotArg::Config(if owned_str_ty(inner) {
3272                                ConfigInner::Str
3273                            } else if owned_bytes_ty(inner) {
3274                                ConfigInner::Bytes
3275                            } else {
3276                                ConfigInner::Jit(wire_type_to_jit_type(inner)?)
3277                            })
3278                        } else {
3279                            SlotArg::Jit(wire_type_to_jit_type(ty)?)
3280                        }
3281                    }
3282                },
3283                ArgKind::PolyWire => SlotArg::Poly,
3284                ArgKind::Variadic(elem) => SlotArg::Variadic(*elem),
3285                ArgKind::Const(shape) => SlotArg::Const(*shape),
3286                ArgKind::ConstVec(_, form) => SlotArg::ConstVec(*form),
3287                ArgKind::Setup(spec) => {
3288                    if spec.source_args.is_empty() {
3289                        SlotArg::SetupStatic
3290                    } else {
3291                        SlotArg::Setup
3292                    }
3293                }
3294            };
3295            shapes.push(shape);
3296        }
3297        // The u64 kit carries every node it is eligible for; this
3298        // kit takes the rest.
3299        if jit_eligible {
3300            return None;
3301        }
3302        Some((shapes, ret_shape))
3303    })();
3304    let slot_eligible = slot_plan.is_some();
3305
3306    // A fallible body ran once at construction; its cached value is
3307    // what every run writes. The shape decides which kit carries it.
3308    let fallible_ret: Option<SlotRet> = if is_fallible {
3309        classify_ret_shape()
3310    } else {
3311        None
3312    };
3313
3314    // Publish scratch entry `k`'s pair into the output slots at `o`.
3315    let publish = |k: usize, o: usize| -> TokenStream2 {
3316        let k = syn::Index::from(k);
3317        let o0 = syn::Index::from(o);
3318        let o1 = syn::Index::from(o + 1);
3319        quote! {
3320            let (__ptr, __len) = scratch[#k].ptr_len();
3321            outputs[#o0] = __ptr;
3322            outputs[#o1] = __len;
3323        }
3324    };
3325    // The write of one element `value` (typed `ty`) at output slot
3326    // `o`: a carrier as its bits, a `Ref2` kind into scratch entry
3327    // `k` with its pair republished (axiom S3).
3328    let write_elem =
3329        |e: SlotElem, ty: &Type, k: usize, o: usize, value: TokenStream2| -> TokenStream2 {
3330            let kk = syn::Index::from(k);
3331            let publish = publish(k, o);
3332            match e {
3333                SlotElem::Jit(jt) => jt.write_to_u64_buffer_at(o, value),
3334                SlotElem::Vec(elem) => {
3335                    let se = syn::Ident::new(elem, proc_macro2::Span::call_site());
3336                    quote! {
3337                        {
3338                            let polydat::ast::ScratchBuf::#se(__buf) = &mut scratch[#kk] else {
3339                                unreachable!("scratch element type mismatch");
3340                            };
3341                            *__buf = #value;
3342                        }
3343                        #publish
3344                    }
3345                }
3346                SlotElem::Str => quote! {
3347                    scratch[#kk].set_str(::core::convert::AsRef::<str>::as_ref(&#value));
3348                    #publish
3349                },
3350                SlotElem::Bytes => quote! {
3351                    scratch[#kk].set_bytes(::core::convert::AsRef::<[u8]>::as_ref(&#value));
3352                    #publish
3353                },
3354                SlotElem::Json => quote! {
3355                    scratch[#kk].set_value(polydat::ast::Value::Json(#value));
3356                    #publish
3357                },
3358                SlotElem::Ext => quote! {
3359                    scratch[#kk].set_value(<#ty as polydat::derive_support::Wire>::inject(#value));
3360                    #publish
3361                },
3362            }
3363        };
3364    // The write of `result` (typed `ret_ty`) by shape.
3365    let write_for = |shape: &SlotRet| -> TokenStream2 {
3366        match shape {
3367            SlotRet::Elem(e) => write_elem(*e, &ret_ty, 0, 0, quote!(result)),
3368            SlotRet::Poly => quote! {
3369                polydat::derive_support::write_poly(__out_type, result, scratch, outputs);
3370            },
3371            SlotRet::Tuple(elems) => {
3372                let types = tuple_ret_elems
3373                    .as_ref()
3374                    .expect("a tuple shape comes from a tuple return");
3375                let locals: Vec<Ident> = (0..elems.len())
3376                    .map(|i| format_ident!("__r_{}", i))
3377                    .collect();
3378                let mut k = 0usize;
3379                let mut o = 0usize;
3380                let writes: Vec<TokenStream2> = elems
3381                    .iter()
3382                    .enumerate()
3383                    .map(|(i, e)| {
3384                        let local = &locals[i];
3385                        let w = write_elem(*e, &types[i], k, o, quote!(#local));
3386                        if e.is_ref() {
3387                            k += 1;
3388                        }
3389                        o += e.width();
3390                        w
3391                    })
3392                    .collect();
3393                quote! {
3394                    let ( #( #locals ),* ) = result;
3395                    #( #writes )*
3396                }
3397            }
3398        }
3399    };
3400    // The scratch entries a return shape owns, in port order.
3401    let scratch_for = |shape: &SlotRet| -> TokenStream2 {
3402        match shape {
3403            SlotRet::Elem(e) => {
3404                let elems: Vec<TokenStream2> = e.scratch_elem().into_iter().collect();
3405                quote!(vec![ #( #elems ),* ])
3406            }
3407            SlotRet::Poly => quote!(
3408                polydat::ast::SlotShape::scratch_elem(&__out_type)
3409                    .into_iter()
3410                    .collect::<Vec<_>>()
3411            ),
3412            SlotRet::Tuple(elems) => {
3413                let elems: Vec<TokenStream2> =
3414                    elems.iter().filter_map(|e| e.scratch_elem()).collect();
3415                quote!(vec![ #( #elems ),* ])
3416            }
3417        }
3418    };
3419    // A polymorphic return encodes by the node's resolved output type,
3420    // which for the split-halves shape is the type of the first value
3421    // wire, the graph's own slot for the output being a placeholder
3422    // there. The graph colored the output slot by the declared port,
3423    // so a resolved type of another color has no slot to land in and
3424    // the node stays interpreted.
3425    let out_type_for = |shape: &SlotRet, fixed_ports: usize| -> TokenStream2 {
3426        if !matches!(shape, SlotRet::Poly) {
3427            return quote!();
3428        }
3429        let fixed = syn::Index::from(fixed_ports);
3430        let resolve = if is_split_halves {
3431            quote!(*wire_types.get(#fixed + (wire_types.len() - #fixed) / 2)?)
3432        } else {
3433            quote!(self.meta().outs[0].typ)
3434        };
3435        quote! {
3436            let __out_type: polydat::ast::PortType = #resolve;
3437            if polydat::ast::SlotShape::slot_color(&__out_type) != polydat::ast::SlotShape::slot_color(&self.meta().outs[0].typ) {
3438                return None;
3439            }
3440        }
3441    };
3442    let elem_ty_tokens = |elem: &str| -> TokenStream2 {
3443        match elem {
3444            "F32" => quote!(f32),
3445            "F64" => quote!(f64),
3446            "F16" => quote!(polydat::half::f16),
3447            "I8" => quote!(i8),
3448            "I16" => quote!(i16),
3449            "I32" => quote!(i32),
3450            "I64" => quote!(i64),
3451            _ => unreachable!(),
3452        }
3453    };
3454
3455    let compiled_slot_impl: TokenStream2 = if let Some(path) = &attrs.compiled_slot_override {
3456        quote! {
3457            fn compiled_slot(&self, wire_types: &[polydat::ast::PortType], engine: polydat::Engine) -> Option<polydat::ast::CompiledSlotKit> {
3458                Some(#path(self, wire_types, engine))
3459            }
3460        }
3461    } else if let Some((shapes, ret_shape)) = &slot_plan {
3462        // Captures: consts and const lists by clone, then setups
3463        // recomputed from those captured consts exactly as `new()`
3464        // computes them (a setup is a pure function of its consts).
3465        let mut captures: Vec<TokenStream2> = Vec::new();
3466        for (a, shape) in args.iter().zip(shapes.iter()) {
3467            let n = &a.name;
3468            match shape {
3469                SlotArg::Const(_) | SlotArg::ConstVec(_) | SlotArg::SetupStatic => {
3470                    captures.push(quote!(let #n = self.#n.clone();))
3471                }
3472                _ => {}
3473            }
3474        }
3475        for (a, shape) in args.iter().zip(shapes.iter()) {
3476            if let (SlotArg::Setup, ArgKind::Setup(spec)) = (shape, &a.kind) {
3477                let n = &a.name;
3478                let setup_fn = &spec.setup_fn;
3479                let src_exprs: Vec<TokenStream2> = spec
3480                    .source_args
3481                    .iter()
3482                    .map(|src| match const_shape_by_name.get(&src.to_string()) {
3483                        Some(ConstSourceShape::ScalarStr) => quote!(#src.as_str()),
3484                        Some(ConstSourceShape::ScalarValue) => quote!(#src),
3485                        Some(ConstSourceShape::VecValues) => quote!(&#src),
3486                        None => quote!(#src),
3487                    })
3488                    .collect();
3489                captures.push(quote!(let #n = #setup_fn( #( #src_exprs ),* );));
3490            }
3491        }
3492        // The reads walk the input slots with two run-time counters:
3493        // `__i`, the slot the next read starts at, and `__p`, its port,
3494        // which indexes the wire types the kernel handed the kit. A
3495        // polymorphic port and a variadic element are as wide as the
3496        // wire that feeds them, so their widths are read at run time.
3497        let fixed_ports: usize = shapes
3498            .iter()
3499            .filter(|s| {
3500                matches!(
3501                    s,
3502                    SlotArg::Jit(_)
3503                        | SlotArg::Option(_)
3504                        | SlotArg::Config(_)
3505                        | SlotArg::Vec(_)
3506                        | SlotArg::Str { .. }
3507                        | SlotArg::Bytes { .. }
3508                        | SlotArg::JsonRef
3509                        | SlotArg::JsonArc
3510                        | SlotArg::Ext
3511                        | SlotArg::Poly
3512                )
3513            })
3514            .count();
3515        let fixed = syn::Index::from(fixed_ports);
3516        // SAFETY (emitted): the pair was published by the producing
3517        // step into storage with a proven owner (its own scratch, an
3518        // extern's stored value, an interned constant, or a boundary
3519        // value alive for the call), and the layer-3 ownership rule
3520        // keeps it alive until that producer reruns.
3521        let pair_slice = |elem: TokenStream2| -> TokenStream2 {
3522            quote!(unsafe {
3523                ::core::slice::from_raw_parts(
3524                    inputs[__i] as usize as *const #elem,
3525                    inputs[__i + 1] as usize,
3526                )
3527            })
3528        };
3529        let str_read = {
3530            let s = pair_slice(quote!(u8));
3531            quote!(unsafe { ::core::str::from_utf8_unchecked(#s) })
3532        };
3533        let bytes_read = pair_slice(quote!(u8));
3534        let arg_reads: Vec<TokenStream2> = args
3535            .iter()
3536            .zip(shapes.iter())
3537            .map(|(a, shape)| {
3538                let n = &a.name;
3539                let ty = &a.declared_ty;
3540                match shape {
3541                    SlotArg::Jit(jt) => {
3542                        let read = jt.read_from_u64_buffer(0);
3543                        let w = jt.width();
3544                        quote! {
3545                            let #n = { let inputs = &inputs[__i..]; #read };
3546                            __i += #w;
3547                            __p += 1;
3548                        }
3549                    }
3550                    SlotArg::Option(jt) => {
3551                        let read = jt.read_from_u64_buffer(0);
3552                        quote! {
3553                            let #n: #ty = Some({ let inputs = &inputs[__i..]; #read });
3554                            __i += 1;
3555                            __p += 1;
3556                        }
3557                    }
3558                    SlotArg::Config(ConfigInner::Jit(jt)) => {
3559                        let read = jt.read_from_u64_buffer(0);
3560                        let w = jt.width();
3561                        quote! {
3562                            let #n: #ty = polydat::derive_support::Config({ let inputs = &inputs[__i..]; #read });
3563                            __i += #w;
3564                            __p += 1;
3565                        }
3566                    }
3567                    SlotArg::Config(ConfigInner::Str) => quote! {
3568                        let __s: &str = #str_read;
3569                        let #n: #ty = polydat::derive_support::Config(::core::convert::From::from(__s));
3570                        __i += 2;
3571                        __p += 1;
3572                    },
3573                    SlotArg::Config(ConfigInner::Bytes) => quote! {
3574                        let __b: &[u8] = #bytes_read;
3575                        let #n: #ty = polydat::derive_support::Config(::core::convert::From::from(__b));
3576                        __i += 2;
3577                        __p += 1;
3578                    },
3579                    SlotArg::Vec(elem) => {
3580                        let et = elem_ty_tokens(elem);
3581                        let s = pair_slice(et.clone());
3582                        quote! {
3583                            let #n: &[#et] = #s;
3584                            __i += 2;
3585                            __p += 1;
3586                        }
3587                    }
3588                    SlotArg::Str { owned } => {
3589                        let bind = if *owned {
3590                            quote!(let #n: #ty = ::core::convert::From::from(__s);)
3591                        } else {
3592                            quote!(let #n: &str = __s;)
3593                        };
3594                        quote! {
3595                            let __s: &str = #str_read;
3596                            #bind
3597                            __i += 2;
3598                            __p += 1;
3599                        }
3600                    }
3601                    SlotArg::Bytes { owned } => {
3602                        let bind = if *owned {
3603                            quote!(let #n: #ty = ::core::convert::From::from(__b);)
3604                        } else {
3605                            quote!(let #n: &[u8] = __b;)
3606                        };
3607                        quote! {
3608                            let __b: &[u8] = #bytes_read;
3609                            #bind
3610                            __i += 2;
3611                            __p += 1;
3612                        }
3613                    }
3614                    SlotArg::JsonRef => quote! {
3615                        let #n = match polydat::derive_support::ref_value(&inputs[__i..]) {
3616                            polydat::ast::Value::Json(__j) => &**__j,
3617                            __other => panic!("expected Json wire, got {__other:?}"),
3618                        };
3619                        __i += 2;
3620                        __p += 1;
3621                    },
3622                    SlotArg::JsonArc => quote! {
3623                        let #n = match polydat::derive_support::ref_value(&inputs[__i..]) {
3624                            polydat::ast::Value::Json(__j) => __j.clone(),
3625                            __other => panic!("expected Json wire, got {__other:?}"),
3626                        };
3627                        __i += 2;
3628                        __p += 1;
3629                    },
3630                    SlotArg::Ext => quote! {
3631                        let #n: #ty = <#ty as polydat::derive_support::Wire>::extract(
3632                            polydat::derive_support::ref_value(&inputs[__i..]),
3633                        );
3634                        __i += 2;
3635                        __p += 1;
3636                    },
3637                    SlotArg::Poly => quote! {
3638                        let #n: polydat::ast::Value =
3639                            polydat::derive_support::read_poly(__wire_types[__p], &inputs[__i..]);
3640                        __i += polydat::ast::SlotShape::slot_width(&__wire_types[__p]);
3641                        __p += 1;
3642                    },
3643                    SlotArg::Variadic(elem) => {
3644                        // A variadic takes every remaining port, or in
3645                        // the split-halves shape (`pick`), its half of
3646                        // them: the selectors first, then the values.
3647                        let count = if is_split_halves {
3648                            let pos = variadic_positions[&a.name.to_string()];
3649                            if pos == 0 {
3650                                quote!((__wire_types.len() - #fixed) / 2)
3651                            } else {
3652                                quote!(__wire_types.len() - #fixed - (__wire_types.len() - #fixed) / 2)
3653                            }
3654                        } else {
3655                            quote!(__wire_types.len() - #fixed)
3656                        };
3657                        let owned = format_ident!("__{}_owned", a.name);
3658                        let (elem_ty, extract, width) = match elem {
3659                            VariadicElement::U64 => (quote!(u64), quote!(inputs[__i]), quote!(1)),
3660                            VariadicElement::Bool => (quote!(bool), quote!(inputs[__i] != 0), quote!(1)),
3661                            VariadicElement::BorrowedStr => (quote!(&str), str_read.clone(), quote!(2)),
3662                            VariadicElement::OwnedString => {
3663                                (quote!(String), quote!((#str_read).to_string()), quote!(2))
3664                            }
3665                            VariadicElement::Value => (
3666                                quote!(polydat::ast::Value),
3667                                quote!(polydat::derive_support::read_poly(__wire_types[__p], &inputs[__i..])),
3668                                quote!(polydat::ast::SlotShape::slot_width(&__wire_types[__p])),
3669                            ),
3670                        };
3671                        quote! {
3672                            let mut #owned: Vec<#elem_ty> = Vec::with_capacity(#count);
3673                            for _ in 0..#count {
3674                                let __v: #elem_ty = #extract;
3675                                __i += #width;
3676                                __p += 1;
3677                                #owned.push(__v);
3678                            }
3679                            let #n = &#owned[..];
3680                        }
3681                    }
3682                    SlotArg::Const(shape) => {
3683                        let wrap = shape.wrap_as_const(quote!(#n));
3684                        quote!(let #n = #wrap;)
3685                    }
3686                    SlotArg::ConstVec(ListForm::Borrowed) => {
3687                        quote!(let #n = polydat::derive_support::Const(&#n[..]);)
3688                    }
3689                    SlotArg::ConstVec(ListForm::Owned) => {
3690                        quote!(let #n = polydat::derive_support::Const(#n.clone());)
3691                    }
3692                    SlotArg::Setup | SlotArg::SetupStatic => quote!(let #n = &#n;),
3693                }
3694            })
3695            .collect();
3696        let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
3697        let write = write_for(ret_shape);
3698        let scratch = scratch_for(ret_shape);
3699        let out_type = out_type_for(ret_shape, fixed_ports);
3700        quote! {
3701            #[allow(unused_mut, unused_variables, unused_assignments, clippy::unused_unit)]
3702            fn compiled_slot(&self, wire_types: &[polydat::ast::PortType], _engine: polydat::Engine) -> Option<polydat::ast::CompiledSlotKit> {
3703                #( #captures )*
3704                #out_type
3705                let __wire_types: Vec<polydat::ast::PortType> = wire_types.to_vec();
3706                let __scratch: Vec<polydat::ast::ScratchElem> = #scratch;
3707                Some(polydat::ast::CompiledSlotKit {
3708                    scratch: __scratch,
3709                    op: Box::new(move |inputs: &[u64], outputs: &mut [u64], scratch: &mut [polydat::ast::ScratchBuf]| {
3710                        let mut __i: usize = 0;
3711                        let mut __p: usize = 0;
3712                        #( #arg_reads )*
3713                        let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
3714                        #write
3715                    }),
3716                })
3717            }
3718        }
3719    } else if let Some(shape) = fallible_ret.as_ref().filter(|s| s.has_ref()) {
3720        // A fallible node whose cached value is a `Ref2` kind: the
3721        // closure writes the same value into its scratch every run it
3722        // is asked for, which is once, since nothing reaches it.
3723        let write = write_for(shape);
3724        let scratch = scratch_for(shape);
3725        let out_type = out_type_for(shape, 0);
3726        quote! {
3727            #[allow(unused_variables)]
3728            fn compiled_slot(&self, wire_types: &[polydat::ast::PortType], _engine: polydat::Engine) -> Option<polydat::ast::CompiledSlotKit> {
3729                #out_type
3730                let __cached = self.__polydat_cached.clone();
3731                Some(polydat::ast::CompiledSlotKit {
3732                    scratch: #scratch,
3733                    op: Box::new(move |_inputs: &[u64], outputs: &mut [u64], scratch: &mut [polydat::ast::ScratchBuf]| {
3734                        let result: #ret_ty = __cached.clone();
3735                        #write
3736                    }),
3737                })
3738            }
3739        }
3740    } else {
3741        quote!()
3742    };
3743
3744    let emit_jit_constants = attrs.jit_constants_override.is_some() || jit_eligible;
3745
3746    // Body sharing: extract the function body into a private
3747    // associated fn `__polydat_body` when JIT is emitted. Both
3748    // `eval()` (Value boxing path) and `compiled_u64()` (u64
3749    // buffer path) call it. Single source of truth.
3750    //
3751    // When JIT is not emitted, the body stays inlined inside
3752    // `eval()`'s current `#[allow(unused_variables)]` block
3753    // (Setup-bearing nodes need this — their body references
3754    // setup-derived locals via `let n = &self.n` bindings).
3755
3756    let use_shared_body = jit_eligible || slot_eligible;
3757
3758    // Body-fn parameter list — every arg in its DECLARED form
3759    // (wire as bare type, const as `Const<T>`, setup as `&T`).
3760    let body_params: Vec<TokenStream2> = args
3761        .iter()
3762        .map(|a| {
3763            let n = &a.name;
3764            let t = &a.declared_ty;
3765            // A `Const<T>` is spelled by its bare name in the source
3766            // signature; the shared body must not depend on the
3767            // module having imported it.
3768            if let (ArgKind::Const(_) | ArgKind::ConstVec(..), syn::Type::Path(p)) = (&a.kind, t)
3769                && let Some(last) = p.path.segments.last()
3770                && last.ident == "Const"
3771            {
3772                let generics = &last.arguments;
3773                return quote!(#n: polydat::derive_support::Const #generics);
3774            }
3775            quote!(#n: #t)
3776        })
3777        .collect();
3778
3779    let body_fn_def: TokenStream2 = if is_fallible {
3780        // SRD-80b Phase 5 S16 — fallible body. Body returns the
3781        // declared Result<T, E>; try_new runs it once at
3782        // construction and propagates Err as String via Into.
3783        quote! {
3784            #[inline(always)]
3785            #[allow(unused_variables)]
3786            #[allow(clippy::ptr_arg)]
3787            fn __polydat_body( #( #body_params ),* ) -> #declared_ret_ty #block
3788        }
3789    } else if use_shared_body {
3790        quote! {
3791            #[inline(always)]
3792            #[allow(unused_variables)]
3793            #[allow(clippy::ptr_arg)]
3794            fn __polydat_body( #( #body_params ),* ) -> #ret_ty #block
3795        }
3796    } else {
3797        quote!()
3798    };
3799
3800    // Helper: emit `outputs[idx] = <conversion>(value)` for a
3801    // given element type. SRD-80b Phase B — owned types route
3802    // through `<T as Wire>::inject`; Handle keeps its inline
3803    // upcast (no blanket impl works). Returning a borrow shape
3804    // (`&str`, `&[u8]`, etc.) from a node body is unusual but
3805    // supported: the borrow's `into()` already exists for the
3806    // canonical `Value` constructor; we emit that directly.
3807    let output_assign = |idx_lit: TokenStream2,
3808                         elem_ty: &Type,
3809                         local: TokenStream2|
3810     -> TokenStream2 {
3811        if classify_wrapper_wire(elem_ty) == Some(WrapperWire::Handle) {
3812            quote! {
3813                outputs[#idx_lit] = polydat::ast::Value::handle(#local);
3814            }
3815        } else if classify_polywire(elem_ty) {
3816            // PolyWire return: body returns `Value` directly, move
3817            // it into the outputs slot. No trait dispatch — Value
3818            // has no static port type (it's polymorphic at runtime).
3819            quote! {
3820                outputs[#idx_lit] = #local;
3821            }
3822        } else if let Some(borrow) = is_borrow_wire_shape(elem_ty) {
3823            // Borrow-typed returns: construct the matching Value
3824            // variant from the borrow via the existing
3825            // `Into<Value>` / Arc::from path. `&str` →
3826            // `Value::Str(arc)`; `&[u8]` → `Value::Bytes(arc)`;
3827            // typed-vec borrows → `Value::Vec*(SliceArc::from(slice))`.
3828            match borrow {
3829                BorrowWire::Str => quote! {
3830                    outputs[#idx_lit] = polydat::ast::Value::Str((#local).into());
3831                },
3832                BorrowWire::Bytes => quote! {
3833                    outputs[#idx_lit] = polydat::ast::Value::Bytes((#local).into());
3834                },
3835                BorrowWire::Json => quote! {
3836                    outputs[#idx_lit] = polydat::ast::Value::Json(::std::sync::Arc::new((#local).clone()));
3837                },
3838                BorrowWire::Vec(variant, _) => {
3839                    let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
3840                    quote! {
3841                        outputs[#idx_lit] = polydat::ast::Value::#v(polydat::ast::SliceArc::from_vec((#local).to_vec()));
3842                    }
3843                }
3844            }
3845        } else {
3846            quote! {
3847                outputs[#idx_lit] = <#elem_ty as polydat::derive_support::Wire>::inject(#local);
3848            }
3849        }
3850    };
3851
3852    // SRD-80b `DynamicOutputs<T>` — build the `outs:` vec at
3853    // construction from the driving `Const<Vec<C>>` arg's
3854    // length. Used by both the infallible `new()` and the
3855    // fallible `try_new()` paths below.
3856    let outs_build: TokenStream2 = if let (Some(inner), Some(count_arg)) =
3857        (&dynamic_outputs_inner, &dynamic_outputs_count_arg)
3858    {
3859        quote! {
3860            let outs: Vec<polydat::ast::Port> = (0..#count_arg.len())
3861                .map(|__i| polydat::ast::Port::new(
3862                    format!("d{}", __i),
3863                    <#inner as polydat::derive_support::Wire>::PORT,
3864                ))
3865                .collect();
3866        }
3867    } else {
3868        quote! {
3869            let outs = vec![ #(
3870                polydat::ast::Port::new(#output_names_strs, #output_port_types)
3871            ),* ];
3872        }
3873    };
3874
3875    // SRD-80 PR B.10/B.11: result → outputs translation. For
3876    // single-output, write `outputs[0] = ...(result)`. For
3877    // tuple-output, destructure and per-element write. For
3878    // SRD-80b `DynamicOutputs<T>`, iterate the returned Vec
3879    // and inject each element via the inner type's Wire impl.
3880    let result_to_outputs: TokenStream2 = if let Some(inner) = &dynamic_outputs_inner {
3881        let inject_one = if classify_polywire(inner) {
3882            quote!(__elem)
3883        } else if let Some(borrow) = is_borrow_wire_shape(inner) {
3884            match borrow {
3885                BorrowWire::Str => quote!(polydat::ast::Value::Str((__elem).into())),
3886                BorrowWire::Bytes => quote!(polydat::ast::Value::Bytes((__elem).into())),
3887                BorrowWire::Json => quote!(polydat::ast::Value::Json(::std::sync::Arc::new(
3888                    (__elem).clone()
3889                ))),
3890                BorrowWire::Vec(variant, _) => {
3891                    let v = syn::Ident::new(variant, proc_macro2::Span::call_site());
3892                    quote!(polydat::ast::Value::#v(polydat::ast::SliceArc::from_vec((__elem).to_vec())))
3893                }
3894            }
3895        } else {
3896            quote!(<#inner as polydat::derive_support::Wire>::inject(__elem))
3897        };
3898        quote! {
3899            for (__i, __elem) in result.0.into_iter().enumerate() {
3900                outputs[__i] = #inject_one;
3901            }
3902        }
3903    } else if let Some(elems) = &tuple_ret_elems {
3904        let locals: Vec<Ident> = (0..elems.len())
3905            .map(|i| format_ident!("__r_{}", i))
3906            .collect();
3907        let writes: Vec<TokenStream2> = elems
3908            .iter()
3909            .enumerate()
3910            .map(|(i, elem_ty)| {
3911                let local = &locals[i];
3912                let idx = syn::Index::from(i);
3913                output_assign(quote!(#idx), elem_ty, quote!(#local))
3914            })
3915            .collect();
3916        quote! {
3917            let ( #( #locals ),* ) = result;
3918            #( #writes )*
3919        }
3920    } else {
3921        output_assign(quote!(0), &ret_ty, quote!(result))
3922    };
3923
3924    // Eval-path arg bindings + body-call. When JIT is emitted,
3925    // eval() unboxes from Values and calls `__polydat_body`.
3926    // When JIT is not emitted, the body stays inline in
3927    // `eval()` for back-compat with Setup-bearing nodes.
3928    let eval_body: TokenStream2 = if use_shared_body {
3929        let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
3930        quote! {
3931            #[allow(unused_variables)]
3932            {
3933                #( #arg_bindings )*
3934                let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
3935                #result_to_outputs
3936            }
3937        }
3938    } else {
3939        quote! {
3940            #[allow(unused_variables)]
3941            {
3942                #( #arg_bindings )*
3943                let result: #ret_ty = (|| #block)();
3944                #result_to_outputs
3945            }
3946        }
3947    };
3948
3949    // compiled_u64() emission. Three cases:
3950    //   (a) Override path supplied → call it.
3951    //   (b) JIT eligible and not opted out → emit closure that
3952    //       reads from u64 buffer, captures const fields by
3953    //       Copy, calls __polydat_body, writes back.
3954    //   (c) Otherwise → don't override the trait default
3955    //       (returns None).
3956    let state_impl: TokenStream2 = if let Some(path) = &attrs.state {
3957        quote! {
3958            fn scratch_layout(&self) -> Vec<polydat::ast::ScratchElem> {
3959                #path::layout(self)
3960            }
3961            fn eval_in(
3962                &self,
3963                scratch: &mut [polydat::ast::ScratchBuf],
3964                inputs: &[polydat::ast::Value],
3965                outputs: &mut [polydat::ast::Value],
3966            ) {
3967                #path::eval(self, scratch, inputs, outputs)
3968            }
3969        }
3970    } else {
3971        quote!()
3972    };
3973
3974    let compiled_u64_impl: TokenStream2 = if let Some(path) = &attrs.compiled_u64_override {
3975        // SRD-80b in-spirit refinement — pass `&self` to the
3976        // override fn so setup-derived state (round_keys,
3977        // half_bits, etc.) is reachable. The override fn
3978        // signature is now `fn(&Self) -> CompiledU64Op`.
3979        quote! {
3980            fn compiled_u64(&self) -> Option<polydat::ast::CompiledU64Op> {
3981                Some(#path(self))
3982            }
3983        }
3984    } else if let Some(shape) = fallible_ret.as_ref().filter(|s| !s.has_ref()) {
3985        // A fallible node whose cached value is a carrier (or a tuple
3986        // of carriers): the closure writes it every run.
3987        let write = write_for(shape);
3988        quote! {
3989            fn compiled_u64(&self) -> Option<polydat::ast::CompiledU64Op> {
3990                let __cached = self.__polydat_cached.clone();
3991                Some(Box::new(move |_inputs: &[u64], outputs: &mut [u64]| {
3992                    let result: #ret_ty = __cached.clone();
3993                    #write
3994                }))
3995            }
3996        }
3997    } else if jit_eligible {
3998        // Per-arg jit handling. Wire args read from inputs at
3999        // the next sequential index. Const args capture by Copy
4000        // from self at closure-creation time, then re-wrap as
4001        // `Const<T>` inside the closure for handoff to body.
4002        let jit_types = arg_jit_types.as_ref().unwrap();
4003        let mut wire_buf_idx = 0usize;
4004
4005        let captures: Vec<TokenStream2> = args
4006            .iter()
4007            .filter_map(|a| match &a.kind {
4008                ArgKind::Wire | ArgKind::Variadic(_) => None,
4009                ArgKind::Const(_) => {
4010                    let n = &a.name;
4011                    Some(quote!(let #n = self.#n.clone();))
4012                }
4013                ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(..) => {
4014                    unreachable!("setup/polywire/constvec excludes JIT eligibility")
4015                }
4016            })
4017            .collect();
4018
4019        let arg_reads: Vec<TokenStream2> = args
4020            .iter()
4021            .zip(jit_types.iter())
4022            .map(|(a, jt)| {
4023                let n = &a.name;
4024                let _ = jt;
4025                match &a.kind {
4026                    ArgKind::Wire => {
4027                        let read = jt.read_from_u64_buffer(wire_buf_idx);
4028                        wire_buf_idx += jt.width();
4029                        quote!(let #n = #read;)
4030                    }
4031                    ArgKind::Const(shape) => {
4032                        if *shape == ConstShape::Str {
4033                            quote!(let #n = polydat::derive_support::Const(#n.as_str());)
4034                        } else {
4035                            quote!(let #n = polydat::derive_support::Const(#n);)
4036                        }
4037                    }
4038                    ArgKind::Variadic(_) => {
4039                        // SRD-80 PR B.9: u64 variadic — pass the
4040                        // whole `inputs: &[u64]` buffer directly
4041                        // to the body. Zero allocation, zero conversion.
4042                        // (Non-u64 variadics aren't JIT-eligible —
4043                        // this branch is only reached for u64 elems.)
4044                        quote!(let #n: &[u64] = inputs;)
4045                    }
4046                    ArgKind::Setup(_) | ArgKind::PolyWire | ArgKind::ConstVec(..) => unreachable!(),
4047                }
4048            })
4049            .collect();
4050
4051        let arg_names: Vec<&syn::Ident> = args.iter().map(|a| &a.name).collect();
4052        // SRD-80 PR B.15: multi-output write. For single-output
4053        // ret, `write` emits `outputs[0] = bits(result)`. For
4054        // tuple-output, destructure into locals and emit a
4055        // per-element write line.
4056        let write = if let Some(tuple_jits) = &tuple_ret_jit_types {
4057            let locals: Vec<Ident> = (0..tuple_jits.len())
4058                .map(|i| format_ident!("__jit_r_{}", i))
4059                .collect();
4060            // Per-element write at the element's slot OFFSET (the
4061            // prefix sum of preceding element widths — §8.4 L1).
4062            let mut out_off = 0usize;
4063            let writes: Vec<TokenStream2> = tuple_jits
4064                .iter()
4065                .enumerate()
4066                .map(|(i, jt)| {
4067                    let local = &locals[i];
4068                    let w = jt.write_to_u64_buffer_at(out_off, quote!(#local));
4069                    out_off += jt.width();
4070                    w
4071                })
4072                .collect();
4073            quote! {
4074                let ( #( #locals ),* ) = result;
4075                #( #writes )*
4076            }
4077        } else {
4078            let ret_jit = ret_jit_type.unwrap();
4079            ret_jit.write_to_u64_buffer(quote!(result))
4080        };
4081
4082        quote! {
4083            fn compiled_u64(&self) -> Option<polydat::ast::CompiledU64Op> {
4084                #( #captures )*
4085                Some(Box::new(move |inputs: &[u64], outputs: &mut [u64]| {
4086                    #( #arg_reads )*
4087                    let result: #ret_ty = Self::__polydat_body( #( #arg_names ),* );
4088                    #write
4089                }))
4090            }
4091        }
4092    } else {
4093        quote!()
4094    };
4095
4096    // jit_constants() emission. Three cases:
4097    //   (a) Override path supplied → call it with `&self`.
4098    //   (b) JIT eligible and not opted out → emit a Vec<u64>
4099    //       built from const fields in declaration order,
4100    //       bit-reinterpreting f64 and 0/1-encoding bool.
4101    //   (c) Otherwise → don't override the trait default.
4102    let jit_constants_impl: TokenStream2 = if let Some(path) = &attrs.jit_constants_override {
4103        quote! {
4104            fn jit_constants(&self) -> Vec<u64> {
4105                #path(self)
4106            }
4107        }
4108    } else if emit_jit_constants {
4109        let const_encodings: Vec<TokenStream2> = args
4110            .iter()
4111            .filter_map(|a| match &a.kind {
4112                ArgKind::Const(shape) => {
4113                    let jt = const_shape_to_jit_type(*shape)?;
4114                    let n = &a.name;
4115                    Some(jt.const_field_as_u64(quote!(self.#n)))
4116                }
4117                _ => None,
4118            })
4119            .collect();
4120
4121        quote! {
4122            fn jit_constants(&self) -> Vec<u64> {
4123                vec![ #( #const_encodings ),* ]
4124            }
4125        }
4126    } else {
4127        quote!()
4128    };
4129
4130    // purity() emission — only when attribute is set; otherwise
4131    // the trait default (`Pure`) is used.
4132    //
4133    // Two attribute shapes:
4134    //   - `Expr::Path` (e.g. `Nondeterministic`)
4135    //     → `Purity::Nondeterministic`
4136    //   - `Expr::Call` (e.g. `SideChannel(LogBuffer)`)
4137    //     → `Purity::SideChannel { sink: SideChannelSink::LogBuffer }`
4138    let purity_impl: TokenStream2 = match &attrs.purity {
4139        None => quote!(),
4140        Some(syn::Expr::Path(p)) => {
4141            let variant = &p.path;
4142            quote! {
4143                fn purity(&self) -> polydat::ast::Purity {
4144                    polydat::ast::Purity::#variant
4145                }
4146            }
4147        }
4148        Some(syn::Expr::Call(c)) => {
4149            // SRD-80 PR B.7/B.11: dispatch on the variant head.
4150            //   SideChannel(<SideChannelSink variant>) →
4151            //     Purity::SideChannel { sink: SideChannelSink::<arg> }
4152            //   Nondeterministic(<&'static str reason>) →
4153            //     Purity::Nondeterministic { reason: <arg> }
4154            let syn::Expr::Path(head_path) = &*c.func else {
4155                return Err(syn::Error::new_spanned(
4156                    &c.func,
4157                    "purity call-form expects a Purity variant ident as the head.",
4158                ));
4159            };
4160            let head_ident = head_path.path.get_ident().ok_or_else(|| {
4161                syn::Error::new_spanned(
4162                    &c.func,
4163                    "purity call-form head must be a single Purity variant ident.",
4164                )
4165            })?;
4166            let arg = c.args.first().ok_or_else(|| {
4167                syn::Error::new_spanned(c, "purity call-form requires one argument.")
4168            })?;
4169            match head_ident.to_string().as_str() {
4170                "SideChannel" => quote! {
4171                    fn purity(&self) -> polydat::ast::Purity {
4172                        polydat::ast::Purity::SideChannel {
4173                            sink: polydat::ast::SideChannelSink::#arg,
4174                        }
4175                    }
4176                },
4177                "Nondeterministic" => quote! {
4178                    fn purity(&self) -> polydat::ast::Purity {
4179                        polydat::ast::Purity::Nondeterministic { reason: #arg }
4180                    }
4181                },
4182                other => {
4183                    return Err(syn::Error::new_spanned(
4184                        head_ident,
4185                        format!(
4186                            "purity call-form head `{other}` not recognized. \
4187                         Use `SideChannel(<sink>)` or `Nondeterministic(<reason>)`."
4188                        ),
4189                    ));
4190                }
4191            }
4192        }
4193        Some(other) => {
4194            return Err(syn::Error::new_spanned(
4195                other,
4196                "purity attribute must be a Purity variant path or call form",
4197            ));
4198        }
4199    };
4200
4201    let simd_variant_impl: TokenStream2 = match &attrs.simd {
4202        None => quote!(),
4203        Some(vector_node) if attrs.simd_total => quote! {
4204            fn simd_variant(&self) -> Option<polydat::ast::SimdVariant> {
4205                Some(polydat::ast::SimdVariant::exact_total(#vector_node))
4206            }
4207        },
4208        Some(vector_node) => quote! {
4209            fn simd_variant(&self) -> Option<polydat::ast::SimdVariant> {
4210                Some(polydat::ast::SimdVariant::exact_fallible(#vector_node))
4211            }
4212        },
4213    };
4214
4215    // SRD-80 PR B.9: conditional FuncSig fields.
4216    let identity_field: TokenStream2 = if let Some(expr) = &attrs.identity {
4217        quote!(Some(#expr))
4218    } else {
4219        quote!(None)
4220    };
4221
4222    // `variadic_ctor` only emitted for pure-variadic nodes (no
4223    // const args, no PolyWire). Const+variadic mixing would need
4224    // the ctor to thread the const values through — defer to a
4225    // future PR.
4226    let has_const_arg = args.iter().any(|a| matches!(a.kind, ArgKind::Const(_)));
4227    let has_polywire = args.iter().any(|a| matches!(a.kind, ArgKind::PolyWire));
4228    // A node whose output type is resolved from its wires cannot be
4229    // built from an arity alone, so it has no arity-only thunk; the
4230    // build closure, which has the wire types, is its one path.
4231    let variadic_ctor_field: TokenStream2 =
4232        if has_variadic && !has_const_arg && !has_polywire && !needs_variadic_out_type {
4233            // Split-halves: assembler passes TOTAL wire count; the
4234            // struct's `new()` takes per-half count, so divide by 2.
4235            if is_split_halves {
4236                quote!(Some(|n| Box::new(#struct_name::new(n / 2))))
4237            } else {
4238                quote!(Some(|n| Box::new(#struct_name::new(n))))
4239            }
4240        } else {
4241            quote!(None)
4242        };
4243
4244    // SRD-80b Phase C — `Option<T>` arg auto-emits
4245    // `accepts_none_inputs() -> true`. The runtime kernel's
4246    // SRD-74 Rule 1 propagation short-circuits `Value::None`
4247    // inputs by default; `Option<T>` is the canonical opt-in
4248    // shape that wants None routed to the body instead.
4249    // SRD-80b in-spirit rule — `Option<T>` wire args declare
4250    // None-tolerance via the type system; PolyWire (`Value`) args
4251    // ARE inherently None-tolerant (`Value::None` is just one of
4252    // the polymorphic variants). Both opt the node out of the
4253    // kernel-Rule-1 short-circuit.
4254    let has_none_aware_arg = args.iter().any(|a| match &a.kind {
4255        ArgKind::Wire => is_option_arg(&a.declared_ty),
4256        ArgKind::PolyWire => true,
4257        _ => false,
4258    });
4259    let accepts_none_impl: TokenStream2 = if has_none_aware_arg {
4260        quote! {
4261            fn accepts_none_inputs(&self) -> bool { true }
4262        }
4263    } else {
4264        quote!()
4265    };
4266
4267    // SRD-80b Phase C — `Const<Vec<C>>` implies
4268    // `Arity::VariadicConsts`. Mutually exclusive with the
4269    // wire-variadic case (the macro rejects mixing them earlier).
4270    let has_const_vec = args.iter().any(|a| matches!(a.kind, ArgKind::ConstVec(..)));
4271    let arity_field: TokenStream2 = if has_variadic {
4272        // SRD-80b split-halves: `variadic_min` is interpreted
4273        // as PAIRS count; the FuncSig advertises 2× as total
4274        // wires so the assembler enforces the right floor.
4275        let min_wires = match (&attrs.variadic_min, is_split_halves) {
4276            (Some(v), true) => quote!(2 * (#v)),
4277            (Some(v), false) => quote!(#v),
4278            (None, _) => quote!(0),
4279        };
4280        quote!(polydat::dsl::registry::Arity::VariadicWires { min_wires: #min_wires })
4281    } else if has_const_vec {
4282        // min_consts = 0 by default; the workload-list shape
4283        // permits empty lists. Authors who want a minimum
4284        // declare it via `#[poly_default]` on the inner type or
4285        // by validating in the body.
4286        quote!(polydat::dsl::registry::Arity::VariadicConsts { min_consts: 0 })
4287    } else {
4288        quote!(polydat::dsl::registry::Arity::Fixed)
4289    };
4290
4291    let commutativity_field: TokenStream2 = if let Some(c) = &attrs.commutativity {
4292        quote!(polydat::ast::Commutativity::#c)
4293    } else {
4294        quote!(polydat::ast::Commutativity::Positional)
4295    };
4296
4297    // SRD-80b Phase 5 S16 — fallible-mode emission. When the body
4298    // returns Result<T, E>, the macro:
4299    //   * adds a cached `__polydat_cached: T` struct field,
4300    //   * replaces `new(...)` with `try_new(...) -> Result<Self, String>`,
4301    //   * runs the body once inside try_new, captures Ok into the
4302    //     cache, propagates Err via Into<String>,
4303    //   * makes eval read the cached value (no per-eval body call).
4304    let ctor_doc = format!("A `{func_name_str}` node with the given constant arguments.");
4305    let (ctor_emission, eval_emission, build_call_emission): (
4306        TokenStream2,
4307        TokenStream2,
4308        TokenStream2,
4309    ) = if is_fallible {
4310        // body-arg pass list. In try_new() Const args arrive as
4311        // their `field_type_tokens()` form (String for Str, raw
4312        // primitive otherwise) and need wrapping as `Const<T>` for
4313        // the body's declared signature. Setup args are locals
4314        // produced by `setup_precomputes` — body takes `&local`.
4315        let body_arg_passes: Vec<TokenStream2> = args
4316            .iter()
4317            .map(|a| {
4318                let n = &a.name;
4319                match &a.kind {
4320                    ArgKind::Const(shape) => shape.wrap_as_const(quote!(#n)),
4321                    ArgKind::Setup(_) => quote!(&#n),
4322                    // Wire / PolyWire / Variadic are rejected
4323                    // earlier for fallible nodes — unreachable.
4324                    _ => quote!(#n),
4325                }
4326            })
4327            .collect();
4328        // Local wrapping: each Const arg comes in as the wrapper
4329        // (matching new_params), so we forward it directly. The
4330        // body receives `Const<T>` and unwraps via .0 or .as_str()
4331        // in its own code.
4332        let try_new = quote! {
4333            #[doc = #ctor_doc]
4334            pub fn try_new( #( #new_params ),* ) -> ::std::result::Result<Self, String> {
4335                #( #setup_precomputes )*
4336                let mut ins: Vec<polydat::ast::Slot> = vec![ #( #slot_exprs ),* ];
4337                #( #variadic_slot_extends )*
4338                #outs_build
4339                // Invoke the body once; propagate Err as String.
4340                let __polydat_cached = match Self::__polydat_body( #( #body_arg_passes ),* ) {
4341                    Ok(v) => v,
4342                    Err(e) => return Err(Into::<String>::into(e)),
4343                };
4344                Ok(Self {
4345                    meta: polydat::ast::NodeMeta {
4346                        name: #func_name_str.into(),
4347                        ins,
4348                        outs,
4349                    },
4350                    #( #new_field_inits, )*
4351                    __polydat_cached,
4352                })
4353            }
4354        };
4355        // eval reads the cached value; no body call.
4356        let out_assign = output_assign(quote!(0), &ret_ty, quote!(self.__polydat_cached.clone()));
4357        let ev = quote! {
4358            #[allow(unused_variables)]
4359            { #out_assign }
4360        };
4361        // build closure: call try_new and propagate Err.
4362        let bc = quote! {
4363            Some(match #struct_name::try_new( #( #new_call_args ),* ) {
4364                Ok(n) => Ok(Box::new(n) as Box<dyn polydat::ast::PolydatNode>),
4365                Err(e) => Err(e),
4366            })
4367        };
4368        (try_new, ev, bc)
4369    } else {
4370        let ctor = quote! {
4371            #[doc = #ctor_doc]
4372            pub fn new( #( #new_params ),* ) -> Self {
4373                // SRD-80 PR B.6: setup pre-computes (FnOnce-
4374                // equivalent — emitted once by the macro,
4375                // never reachable by any other code path).
4376                #( #setup_precomputes )*
4377                // Build the `ins` slot list. Const args and
4378                // singleton wires already appear in `slot_exprs`;
4379                // variadic args append N slots per `n_wires`.
4380                let mut ins: Vec<polydat::ast::Slot> = vec![ #( #slot_exprs ),* ];
4381                #( #variadic_slot_extends )*
4382                #outs_build
4383                Self {
4384                    meta: polydat::ast::NodeMeta {
4385                        name: #func_name_str.into(),
4386                        ins,
4387                        outs,
4388                    },
4389                    #( #new_field_inits, )*
4390                }
4391            }
4392        };
4393        let ev = quote!(#eval_body);
4394        // Wrap `new()` in `catch_unwind` so that panics from
4395        // `#[poly_const]` setup functions (Regex parse failures,
4396        // file-not-found from filename consts, "value:weight"
4397        // parse failures, etc.) surface as build-closure `Err`
4398        // values rather than unwinding through the compile path.
4399        // The runtime sees `name` here as the DSL-registered
4400        // function name; the message is prefixed for traceability.
4401        let bc = quote! {
4402            Some(match ::std::panic::catch_unwind(
4403                ::std::panic::AssertUnwindSafe(|| #struct_name::new( #( #new_call_args ),* ))
4404            ) {
4405                Ok(node) => Ok(Box::new(node) as Box<dyn polydat::ast::PolydatNode>),
4406                Err(panic) => {
4407                    let msg = panic.downcast_ref::<&str>().copied()
4408                        .or_else(|| panic.downcast_ref::<String>().map(|s| s.as_str()))
4409                        .unwrap_or("<non-string panic>");
4410                    Err(format!("{}: construction failed: {}", #func_name_str, msg))
4411                }
4412            })
4413        };
4414        (ctor, ev, bc)
4415    };
4416
4417    // Cached field for fallible mode. T = `ret_ty` (the Ok inner).
4418    let cached_field: TokenStream2 = if is_fallible {
4419        quote!(__polydat_cached: #ret_ty,)
4420    } else {
4421        quote!()
4422    };
4423
4424    // The node's documentation: the function's own doc comments on the
4425    // struct the macro generates, or a line naming the node, and a line
4426    // for the constructor, so a generated node is documented as the
4427    // function that defines it is. The same text fills the registered
4428    // signature: the first paragraph is its `description`, the rest
4429    // its `help`.
4430    let fn_docs: Vec<&syn::Attribute> = func
4431        .attrs
4432        .iter()
4433        .filter(|a| a.path().is_ident("doc"))
4434        .collect();
4435    let struct_doc = if fn_docs.is_empty() {
4436        let text = format!("The `{func_name_str}` node.");
4437        quote! { #[doc = #text] }
4438    } else {
4439        quote! { #( #fn_docs )* }
4440    };
4441    let (description, help) = doc_text(&fn_docs);
4442    let description_lit = syn::LitStr::new(&description, proc_macro2::Span::call_site());
4443    let help_lit = syn::LitStr::new(&help, proc_macro2::Span::call_site());
4444    // The node's own validator, when it declared one.
4445    let validate_emission = match &attrs.validate_fn {
4446        Some(p) => quote!(Some(#p as polydat::dsl::const_constraints::NodeValidator)),
4447        None => quote!(None),
4448    };
4449
4450    let result = quote! {
4451        #struct_doc
4452        pub struct #struct_name {
4453            meta: polydat::ast::NodeMeta,
4454            #( #struct_fields, )*
4455            #cached_field
4456        }
4457
4458        #default_impl
4459
4460        #fused_node_impl
4461
4462        impl #struct_name {
4463            #ctor_emission
4464
4465            // SRD-80 PR B.7: shared `__polydat_body` extracted
4466            // when the node is JIT-eligible. Both `eval()` and
4467            // `compiled_u64()` call it. Empty token stream when
4468            // JIT is not emitted (body stays inlined in eval).
4469            #body_fn_def
4470        }
4471
4472        impl polydat::ast::PolydatNode for #struct_name {
4473            fn meta(&self) -> &polydat::ast::NodeMeta { &self.meta }
4474
4475            fn eval(
4476                &self,
4477                inputs: &[polydat::ast::Value],
4478                outputs: &mut [polydat::ast::Value],
4479            ) {
4480                #eval_emission
4481            }
4482
4483            #state_impl
4484            #compiled_u64_impl
4485            #compiled_slot_impl
4486            #jit_constants_impl
4487            #purity_impl
4488            #simd_variant_impl
4489            #accepts_none_impl
4490        }
4491
4492        // SRD-80 PR B.2/B.3/B.5 — link-time registration via
4493        // the existing `NodeRegistration` inventory channel.
4494        // The build closure pulls const args from the runtime
4495        // `consts` slice, falling back to per-arg
4496        // `#[poly_default(...)]` values if the slice is short.
4497        const _: () = {
4498            static SIGS: &[polydat::dsl::registry::FuncSig] = &[
4499                polydat::dsl::registry::FuncSig {
4500                    name: #func_name_str,
4501                    category: polydat::dsl::registry::FuncCategory::#category,
4502                    outputs: #output_count_lit,
4503                    description: #description_lit,
4504                    help: #help_lit,
4505                    identity: #identity_field,
4506                    variadic_ctor: #variadic_ctor_field,
4507                    params: &[ #( #param_specs ),* ],
4508                    arity: #arity_field,
4509                    commutativity: #commutativity_field,
4510                    default_resolver: #default_resolver_field,
4511                    output_type: #output_type_tokens,
4512                    output_port: #output_port_field,
4513                },
4514            ];
4515
4516            fn signatures() -> &'static [polydat::dsl::registry::FuncSig] { SIGS }
4517
4518            fn build(
4519                name: &str,
4520                _wires: &[polydat::compile::assembly::WireRef],
4521                _wire_types: &[polydat::ast::PortType],
4522                consts: &[polydat::dsl::factory::ConstArg],
4523            ) -> Option<Result<Box<dyn polydat::ast::PolydatNode>, String>> {
4524                if name != #func_name_str { return None; }
4525                #( #const_extracts )*
4526                #( #polywire_extracts )*
4527                #variadic_n_wires_extract
4528                #build_call_emission
4529            }
4530
4531            ::polydat::inventory::submit! {
4532                polydat::dsl::registry::NodeRegistration {
4533                    signatures,
4534                    build,
4535                    validate: #validate_emission,
4536                }
4537            }
4538        };
4539    };
4540
4541    Ok(result)
4542}
4543
4544/// Split a function's `///` comments into the registered
4545/// `description` (the first paragraph, joined onto one line) and
4546/// `help` (every paragraph after it, lines kept). Each line loses
4547/// the one space rustdoc puts after `///`.
4548fn doc_text(doc_attrs: &[&syn::Attribute]) -> (String, String) {
4549    let mut lines: Vec<String> = Vec::new();
4550    for attr in doc_attrs {
4551        if let syn::Meta::NameValue(nv) = &attr.meta
4552            && let syn::Expr::Lit(syn::ExprLit {
4553                lit: syn::Lit::Str(s),
4554                ..
4555            }) = &nv.value
4556        {
4557            let raw = s.value();
4558            lines.push(raw.strip_prefix(' ').unwrap_or(&raw).to_string());
4559        }
4560    }
4561    while lines.first().is_some_and(|l| l.trim().is_empty()) {
4562        lines.remove(0);
4563    }
4564    while lines.last().is_some_and(|l| l.trim().is_empty()) {
4565        lines.pop();
4566    }
4567    let split = lines
4568        .iter()
4569        .position(|l| l.trim().is_empty())
4570        .unwrap_or(lines.len());
4571    let description = lines[..split]
4572        .iter()
4573        .map(|l| l.trim())
4574        .collect::<Vec<_>>()
4575        .join(" ");
4576    let rest = &lines[split..];
4577    let rest_start = rest
4578        .iter()
4579        .position(|l| !l.trim().is_empty())
4580        .unwrap_or(rest.len());
4581    let help = rest[rest_start..]
4582        .iter()
4583        .map(|l| l.trim_end())
4584        .collect::<Vec<_>>()
4585        .join("\n");
4586    (description, help)
4587}
4588
4589/// `snake_case` → `PascalCase` (for the generated struct name).
4590fn to_camel_case(s: &str) -> String {
4591    let mut out = String::with_capacity(s.len());
4592    let mut up = true;
4593    for c in s.chars() {
4594        if c == '_' {
4595            up = true;
4596            continue;
4597        }
4598        if up {
4599            out.extend(c.to_uppercase());
4600            up = false;
4601        } else {
4602            out.push(c);
4603        }
4604    }
4605    out
4606}
4607
4608/// Stringify a `syn::Type` minimally — used for primitive-type
4609/// dispatch. Not a robust pretty-printer; only handles the
4610/// shapes the simple-case allows (bare path, `&str`, `String`).
4611fn type_to_string(ty: &Type) -> String {
4612    use quote::ToTokens;
4613    let mut s = String::new();
4614    for t in ty.to_token_stream() {
4615        s.push_str(&t.to_string());
4616        s.push(' ');
4617    }
4618    s.trim().to_string()
4619}