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