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