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