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