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