Skip to main content

polydat_core/
derive_support.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Trait surface that the `#[polydat_node]` proc-macro
5//! (`polydat-derive`) calls into for boxing / unboxing wire
6//! values.
7//!
8//! ## Canonical trait surface (SRD-80b)
9//!
10//! - [`Wire`] — `Sized + 'static` Rust-type ↔ [`Value`] bridge.
11//!   Owned types only; the macro recognises borrow shapes
12//!   (`&str`, `&[u8]`, `&[T]`, `&serde_json::Value`)
13//!   syntactically and emits direct `match`-on-`Value`
14//!   extraction at the eval call site — no trait dispatch, no
15//!   `unsafe` lifetime transmute.
16//!
17//! Combinator [`Wire`] impls cover [`Option<T>`] (`None`-aware
18//! pass-through) and [`Ext<T>`] (downcast through
19//! [`ReflectedValue`]).
20//!
21//! A `Const<T>` position is not a `Wire`: the macro classifies it
22//! syntactically into a `ConstShape` and emits the extraction from
23//! the `ConstArg` inline, so there is no trait for it to dispatch
24//! through. A `ConstSource` trait once carried that, and stayed here
25//! after the macro stopped emitting calls to it.
26//!
27//! ## Why the trait surface lives here
28//!
29//! `polydat-derive` is a proc-macro crate — it can't define
30//! traits that are visible at the call site, only emit token
31//! streams referencing traits defined elsewhere. The macro
32//! emits `<T as polydat::derive_support::Wire>::extract(...)`
33//! paths; this module is what those paths resolve to.
34
35use std::sync::Arc;
36
37use crate::ast::SlotShape;
38use crate::ast::{PortType, ReflectedValue, SliceArc, Value};
39
40// =====================================================================
41// Wire — Rust-type ↔ Value bridge (owned types only)
42// =====================================================================
43
44/// Rust-type ↔ `Value` bridge.
45///
46/// Every owned Rust type the macro accepts in a wire position
47/// implements this trait. `PORT` is the static [`PortType`] the
48/// DSL type-checker uses to route a wire to this slot; which slots
49/// a value rides and how wide they are follows from that port type
50/// through [`SlotShape`], the one place that mapping lives. A second
51/// `JIT: Option<JitType>` const here said the same thing about the
52/// compiled buffer and nothing ever read it.
53///
54/// Borrow shapes (`&str`, `&[u8]`, `&[T]`,
55/// `&serde_json::Value`) and polymorphic `Value`-typed wires
56/// are NOT covered here — the macro recognises them
57/// syntactically and emits direct `match`-on-`Value` extraction
58/// at the eval call site. This keeps the trait surface free of
59/// lifetime parameters.
60///
61/// `extract` panics on type mismatch — the DSL type-checker is
62/// responsible for routing well-typed `Value`s to each slot
63/// before `eval` runs. A panic here is a "type-checker was
64/// lied to" bug, not a normal path.
65pub trait Wire: Sized + 'static {
66    /// Static port type for the DSL type-checker.
67    const PORT: PortType;
68
69    /// SRD-53 §"Source-string call-site sugar" — auto-resolver
70    /// for `Str`-typed upstream wires feeding this slot. `None`
71    /// (the default) disables auto-promotion; the workload must
72    /// supply the wire's actual port type directly. Set via the
73    /// [`Resolved<R, T>`] marker wrapper.
74    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = None;
75
76    /// SRD-15 §"WireCost::Config" — cost class for this wire.
77    /// Defaults to [`WireCost::Data`](crate::ast::WireCost::Data) (cheap per-cycle input).
78    /// Set to [`WireCost::Config`](crate::ast::WireCost::Config) via the [`Config<T>`] marker
79    /// wrapper to signal that the wire is rarely-changing and
80    /// the compiler should warn on cycle-time binding.
81    const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Data;
82
83    /// Pull a typed value out of a `Value` wire.
84    fn extract(v: &Value) -> Self;
85
86    /// Push a typed value back into the `Value` outputs stream.
87    fn inject(self) -> Value;
88}
89
90// ── Scalar primitives ─────────────────────────────────────────
91
92impl Wire for u64 {
93    const PORT: PortType = PortType::U64;
94    fn extract(v: &Value) -> Self {
95        v.as_u64()
96    }
97    fn inject(self) -> Value {
98        Value::U64(self)
99    }
100}
101
102impl Wire for u32 {
103    const PORT: PortType = PortType::U32;
104    fn extract(v: &Value) -> Self {
105        v.as_u64() as u32
106    }
107    fn inject(self) -> Value {
108        Value::U64(self as u64)
109    }
110}
111
112impl Wire for i32 {
113    const PORT: PortType = PortType::I32;
114    // Lenient extract: honest `Value::I64` (sign-extended I32
115    // storage convention) plus the legacy bit-stuffed `Value::U64`
116    // form during the alignment migration — same precedent as
117    // `Wire<bool>` accepting `U64(n != 0)`.
118    fn extract(v: &Value) -> Self {
119        v.as_i64() as i32
120    }
121    fn inject(self) -> Value {
122        Value::I64(self as i64)
123    }
124}
125
126impl Wire for i64 {
127    const PORT: PortType = PortType::I64;
128    // Lenient extract: see `Wire<i32>` note above.
129    fn extract(v: &Value) -> Self {
130        v.as_i64()
131    }
132    fn inject(self) -> Value {
133        Value::I64(self)
134    }
135}
136
137impl Wire for u8 {
138    const PORT: PortType = PortType::U8;
139    fn extract(v: &Value) -> Self {
140        v.as_u64() as u8
141    }
142    fn inject(self) -> Value {
143        Value::U64(self as u64)
144    }
145}
146
147impl Wire for u16 {
148    const PORT: PortType = PortType::U16;
149    fn extract(v: &Value) -> Self {
150        v.as_u64() as u16
151    }
152    fn inject(self) -> Value {
153        Value::U64(self as u64)
154    }
155}
156
157impl Wire for i8 {
158    const PORT: PortType = PortType::I8;
159    // Lenient extract through as_i64 (honest I64 or legacy
160    // stuffed U64), narrowed by truncation — sign survives
161    // because the storage convention is sign-extension.
162    fn extract(v: &Value) -> Self {
163        v.as_i64() as i8
164    }
165    fn inject(self) -> Value {
166        Value::I64(self as i64)
167    }
168}
169
170impl Wire for i16 {
171    const PORT: PortType = PortType::I16;
172    fn extract(v: &Value) -> Self {
173        v.as_i64() as i16
174    }
175    fn inject(self) -> Value {
176        Value::I64(self as i64)
177    }
178}
179
180impl Wire for u128 {
181    const PORT: PortType = PortType::U128;
182    // No named native lowering: a 128-bit value cannot ride the
183    // one-u64 JIT slot, so it crosses the compiled tiers as a limb
184    // pair (`Imm2`) and a node over it runs its closure on the
185    // closure tier and a slot call of its kit on the native engines
186    // (type_system_alignment.md §2). The two-slot ride is about the
187    // register, not about which engines carry the type — every one
188    // of them does, which `the_128_bit_carriers_agree_on_every_engine`
189    // pins.
190    fn extract(v: &Value) -> Self {
191        v.as_u128()
192    }
193    fn inject(self) -> Value {
194        Value::U128(crate::ast::Bits128::from_u128(self))
195    }
196}
197
198impl Wire for i128 {
199    const PORT: PortType = PortType::I128;
200    fn extract(v: &Value) -> Self {
201        v.as_i128()
202    }
203    fn inject(self) -> Value {
204        Value::I128(crate::ast::Bits128::from_i128(self))
205    }
206}
207
208// ── 128-bit register words (type_system_alignment.md §8.4 L2) ──
209//
210// The raw view extracts/injects the word itself; the lane-typed
211// `[T; N]` views extract through the free-bitcast rule (any
212// register view satisfies any register slot) and inject tagged
213// with their own lane typing.
214
215impl Wire for crate::ast::Bits128 {
216    const PORT: PortType = PortType::Reg128;
217    fn extract(v: &Value) -> Self {
218        v.as_reg_bits()
219    }
220    fn inject(self) -> Value {
221        Value::Reg128(self, crate::ast::RegLanes::Raw)
222    }
223}
224
225macro_rules! impl_wire_reg {
226    ($arr:ty, $port:ident, $view:ident, $to:ident, $from:ident) => {
227        impl Wire for $arr {
228            const PORT: PortType = PortType::$port;
229            fn extract(v: &Value) -> Self {
230                v.as_reg_bits().$to()
231            }
232            fn inject(self) -> Value {
233                Value::Reg128(
234                    crate::ast::Bits128::$from(self),
235                    crate::ast::RegLanes::$view,
236                )
237            }
238        }
239    };
240}
241
242impl_wire_reg!([i8; 16], RegI8x16, I8x16, lanes_i8, from_lanes_i8);
243impl_wire_reg!([i16; 8], RegI16x8, I16x8, lanes_i16, from_lanes_i16);
244impl_wire_reg!([i32; 4], RegI32x4, I32x4, lanes_i32, from_lanes_i32);
245impl_wire_reg!([i64; 2], RegI64x2, I64x2, lanes_i64, from_lanes_i64);
246impl_wire_reg!([half::f16; 8], RegF16x8, F16x8, lanes_f16, from_lanes_f16);
247impl_wire_reg!([f32; 4], RegF32x4, F32x4, lanes_f32, from_lanes_f32);
248impl_wire_reg!([f64; 2], RegF64x2, F64x2, lanes_f64, from_lanes_f64);
249
250impl Wire for f64 {
251    const PORT: PortType = PortType::F64;
252    fn extract(v: &Value) -> Self {
253        v.as_f64()
254    }
255    fn inject(self) -> Value {
256        Value::F64(self)
257    }
258}
259
260impl Wire for f32 {
261    const PORT: PortType = PortType::F32;
262    fn extract(v: &Value) -> Self {
263        f32::from_bits(v.as_u64() as u32)
264    }
265    fn inject(self) -> Value {
266        Value::U64(self.to_bits() as u64)
267    }
268}
269
270impl Wire for half::f16 {
271    const PORT: PortType = PortType::F16;
272    // Same bit-stuffing convention as f32: the binary16 pattern
273    // rides the low 16 bits of the u64 carrier.
274    fn extract(v: &Value) -> Self {
275        half::f16::from_bits(v.as_u64() as u16)
276    }
277    fn inject(self) -> Value {
278        Value::U64(self.to_bits() as u64)
279    }
280}
281
282impl Wire for bool {
283    const PORT: PortType = PortType::Bool;
284    fn extract(v: &Value) -> Self {
285        match v {
286            Value::Bool(b) => *b,
287            Value::U64(n) => *n != 0,
288            other => panic!(
289                "Wire<bool>::extract: type-checker routed {other:?} \
290                 to a Bool slot"
291            ),
292        }
293    }
294    fn inject(self) -> Value {
295        Value::Bool(self)
296    }
297}
298
299impl Wire for String {
300    const PORT: PortType = PortType::Str;
301    fn extract(v: &Value) -> Self {
302        // SRD-80b: panic on shape mismatch — the type-checker is
303        // responsible for routing well-typed values to each slot,
304        // and a non-Str input here is a "type system was lied to"
305        // bug, not a coercion opportunity. Nodes that want a
306        // display rendering of an arbitrary `Value` take a
307        // `Value`-typed (PolyWire) arg instead.
308        match v {
309            Value::Str(s) => s.to_string(),
310            other => panic!("Wire<String>::extract: expected Str, got {other:?}"),
311        }
312    }
313    fn inject(self) -> Value {
314        Value::Str(self.into())
315    }
316}
317
318/// `Arc<str>` — zero-copy shared string handle. Reading
319/// extracts the existing `Arc<str>` from `Value::Str` (refcount
320/// bump only); injecting wraps directly. Nodes whose hot path
321/// emits the same string per cycle (lookup table outputs,
322/// fixed-value selectors) should use this instead of `String`
323/// to avoid the per-cycle `to_string()` allocation.
324impl Wire for std::sync::Arc<str> {
325    const PORT: PortType = PortType::Str;
326    fn extract(v: &Value) -> Self {
327        match v {
328            Value::Str(s) => s.clone(),
329            other => panic!("Wire<Arc<str>>::extract: expected Str, got {other:?}"),
330        }
331    }
332    fn inject(self) -> Value {
333        Value::Str(self)
334    }
335}
336
337/// `Arc<dyn Any + Send + Sync>` — opaque Handle wire. The body
338/// receives the runtime-typed handle directly; downcast is the
339/// operator's responsibility. Use [`Resolved<R, T>`] when the
340/// node wants a typed Handle with SRD-53 source-string
341/// auto-promotion sugar; use this raw shape when the body
342/// needs to handle multiple inner types via runtime dispatch.
343impl Wire for std::sync::Arc<dyn std::any::Any + Send + Sync> {
344    const PORT: PortType = PortType::Handle;
345    fn extract(v: &Value) -> Self {
346        match v {
347            Value::Handle(arc) => arc.clone(),
348            other => panic!("Wire<Arc<dyn Any>>::extract: expected Handle, got {other:?}"),
349        }
350    }
351    fn inject(self) -> Value {
352        Value::Handle(self)
353    }
354}
355
356/// `Box<dyn ReflectedValue>` — Ext (adapter-typed) wire with
357/// dynamic downcast left to the body. Use [`Ext<T>`] when the
358/// inner type is known at codegen; use this when a node needs
359/// to dispatch on the runtime ReflectedValue::type_name.
360impl Wire for Box<dyn ReflectedValue> {
361    const PORT: PortType = PortType::Ext;
362    fn extract(v: &Value) -> Self {
363        match v {
364            Value::Ext(b) => b.clone_reflected(),
365            other => panic!("Wire<Box<dyn ReflectedValue>>::extract: expected Ext, got {other:?}"),
366        }
367    }
368    fn inject(self) -> Value {
369        Value::Ext(self)
370    }
371}
372
373// ── Bytes ──────────────────────────────────────────────────────
374
375impl Wire for Arc<[u8]> {
376    const PORT: PortType = PortType::Bytes;
377    fn extract(v: &Value) -> Self {
378        match v {
379            Value::Bytes(b) => b.clone(),
380            other => panic!("Wire<Arc<[u8]>>::extract: expected Bytes, got {other:?}"),
381        }
382    }
383    fn inject(self) -> Value {
384        Value::Bytes(self)
385    }
386}
387
388impl Wire for Vec<u8> {
389    const PORT: PortType = PortType::Bytes;
390    fn extract(v: &Value) -> Self {
391        match v {
392            Value::Bytes(b) => b.to_vec(),
393            other => panic!("Wire<Vec<u8>>::extract: expected Bytes, got {other:?}"),
394        }
395    }
396    fn inject(self) -> Value {
397        Value::Bytes(self.into())
398    }
399}
400
401// ── Json ───────────────────────────────────────────────────────
402
403impl Wire for Arc<serde_json::Value> {
404    const PORT: PortType = PortType::Json;
405    fn extract(v: &Value) -> Self {
406        match v {
407            Value::Json(j) => j.clone(),
408            other => panic!("Wire<Arc<Json>>::extract: expected Json, got {other:?}"),
409        }
410    }
411    fn inject(self) -> Value {
412        Value::Json(self)
413    }
414}
415
416// ── Typed-element vectors ──────────────────────────────────────
417
418macro_rules! impl_wire_vec {
419    ($elem:ty, $variant:ident, $port:ident) => {
420        impl Wire for SliceArc<$elem> {
421            const PORT: PortType = PortType::$port;
422            fn extract(v: &Value) -> Self {
423                match v {
424                    Value::$variant(arc) => arc.clone(),
425                    other => panic!(
426                        concat!(
427                            "Wire<SliceArc<",
428                            stringify!($elem),
429                            ">>::extract: expected ",
430                            stringify!($variant),
431                            ", got {:?}"
432                        ),
433                        other
434                    ),
435                }
436            }
437            fn inject(self) -> Value {
438                Value::$variant(self)
439            }
440        }
441
442        impl Wire for Vec<$elem> {
443            const PORT: PortType = PortType::$port;
444            fn extract(v: &Value) -> Self {
445                match v {
446                    Value::$variant(arc) => arc.as_slice().to_vec(),
447                    other => panic!(
448                        concat!(
449                            "Wire<Vec<",
450                            stringify!($elem),
451                            ">>::extract: expected ",
452                            stringify!($variant),
453                            ", got {:?}"
454                        ),
455                        other
456                    ),
457                }
458            }
459            fn inject(self) -> Value {
460                Value::$variant(SliceArc::from_vec(self))
461            }
462        }
463    };
464}
465
466impl_wire_vec!(f32, VecF32, VecF32);
467impl_wire_vec!(i32, VecI32, VecI32);
468impl_wire_vec!(f64, VecF64, VecF64);
469impl_wire_vec!(i64, VecI64, VecI64);
470impl_wire_vec!(half::f16, VecF16, VecF16);
471impl_wire_vec!(i16, VecI16, VecI16);
472impl_wire_vec!(i8, VecI8, VecI8);
473
474// ── Phase C combinators ────────────────────────────────────────
475
476/// None-aware wire combinator. Macro auto-emits
477/// `accepts_none_inputs() -> true` when any arg is `Option<_>`.
478impl<T: Wire> Wire for Option<T> {
479    const PORT: PortType = T::PORT;
480    fn extract(v: &Value) -> Self {
481        match v {
482            Value::None => None,
483            _ => Some(T::extract(v)),
484        }
485    }
486    fn inject(self) -> Value {
487        match self {
488            None => Value::None,
489            Some(t) => t.inject(),
490        }
491    }
492}
493
494/// Operator-side wrapper for adapter-typed wire arguments.
495/// `Ext<T>` signals "this arg comes from `Value::Ext(Box<dyn
496/// ReflectedValue>)`; downcast it to `T`." Implements `Deref` /
497/// `DerefMut` like [`Const<T>`] so the body can use `.method()`
498/// directly.
499#[derive(Clone)]
500pub struct Ext<T>(pub T);
501
502impl<T> std::ops::Deref for Ext<T> {
503    type Target = T;
504    fn deref(&self) -> &T {
505        &self.0
506    }
507}
508
509impl<T> std::ops::DerefMut for Ext<T> {
510    fn deref_mut(&mut self) -> &mut T {
511        &mut self.0
512    }
513}
514
515impl<T: ReflectedValue + Clone + 'static> Wire for Ext<T> {
516    const PORT: PortType = PortType::Ext;
517    fn extract(v: &Value) -> Self {
518        match v {
519            Value::Ext(boxed) => {
520                let any = boxed.as_any();
521                match any.downcast_ref::<T>() {
522                    Some(t) => Ext(t.clone()),
523                    None => panic!(
524                        "Wire<Ext<{}>>::extract: ReflectedValue downcast failed; \
525                         got runtime type {:?}",
526                        std::any::type_name::<T>(),
527                        boxed.type_name()
528                    ),
529                }
530            }
531            other => panic!("Wire<Ext>::extract: expected Ext, got {other:?}"),
532        }
533    }
534    fn inject(self) -> Value {
535        Value::Ext(Box::new(self.0))
536    }
537}
538
539// ── DynamicOutputs<T> — variable output port count ────────────
540
541/// Marker wrapper for node return types whose output port
542/// COUNT is determined at construction time from a
543/// const-list arg's length, not at codegen time.
544/// SRD-80b shape extension covering nodes like `mixed_radix`
545/// that emit one output per radix where `radix` count is a
546/// workload-supplied list.
547///
548/// Operator writes:
549///
550/// ```ignore
551/// #[polydat_node(category = Arithmetic)]
552/// fn mixed_radix(
553///     value: u64,
554///     radixes: Const<&[u64]>,
555/// ) -> DynamicOutputs<u64> {
556///     // body returns DynamicOutputs(Vec<u64>) with len == radixes.len()
557/// }
558/// ```
559///
560/// The macro emits one output port per element (named `d0`,
561/// `d1`, ...) at construction time using the const-list
562/// arg's length. `FuncSig.outputs` is `0` signalling dynamic.
563/// Requires exactly one const-list arg per function — `Const<&[C]>`
564/// or its owned spelling `Const<Vec<C>>` — and the
565/// macro errors at compile time otherwise.
566pub struct DynamicOutputs<T>(pub Vec<T>);
567
568impl<T> std::ops::Deref for DynamicOutputs<T> {
569    type Target = Vec<T>;
570    fn deref(&self) -> &Vec<T> {
571        &self.0
572    }
573}
574
575// ── Config<T> — wire arg marked as config-cost ────────────────
576
577/// Marker wrapper signalling that the wrapped wire is a
578/// configuration input — expensive to change because the node
579/// keeps internal state (LUTs, alias tables, parsed specs)
580/// derived from it. The macro emits the matching slot with
581/// `Port::config()` (SRD 15 §"WireCost::Config") so the
582/// compiler warns on cycle-time binding.
583///
584/// In-spirit replacement for a `#[wire_cost(Config)]` arg-level
585/// attribute — operator declares the cost intent via the type
586/// system. Body unwraps with `.0` or via `Deref`.
587pub struct Config<T>(pub T);
588
589impl<T> std::ops::Deref for Config<T> {
590    type Target = T;
591    fn deref(&self) -> &T {
592        &self.0
593    }
594}
595
596impl<T: Wire> Wire for Config<T> {
597    const PORT: PortType = T::PORT;
598    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> = T::RESOLVER;
599    const WIRE_COST: crate::ast::WireCost = crate::ast::WireCost::Config;
600    fn extract(v: &Value) -> Self {
601        Config(T::extract(v))
602    }
603    fn inject(self) -> Value {
604        self.0.inject()
605    }
606}
607
608// ── Resolved<R, T> — Handle wire with SRD-53 auto-resolver ────
609
610/// SRD-80b in-spirit replacement for the `default_resolver`
611/// attribute. The `R` parameter (a [`ResolverKind`] impl) carries
612/// the auto-resolver kind; the `T` parameter is the concrete
613/// `Handle`-inner type the body sees.
614///
615/// Operators write:
616///
617/// ```ignore
618/// fn matching_profiles(
619///     group: Resolved<GroupResolver, DatasetHandle>,
620///     prefix: &str,
621/// ) -> Vec<String> {
622///     let group: &TestDataGroup = group_of(&group);
623///     // ... use group methods directly
624/// }
625/// ```
626///
627/// The macro reads `<Resolved<GroupResolver, T> as Wire>::RESOLVER`
628/// at codegen time and emits the matching `FuncSig.default_resolver`.
629/// No `#[polydat_node(default_resolver = ...)]` attribute is
630/// involved — the resolver information lives in the function
631/// signature where it belongs.
632pub struct Resolved<R: ResolverKind, T: 'static + Send + Sync> {
633    inner: std::sync::Arc<T>,
634    _r: std::marker::PhantomData<fn() -> R>,
635}
636
637impl<R: ResolverKind, T: 'static + Send + Sync> std::ops::Deref for Resolved<R, T> {
638    type Target = T;
639    fn deref(&self) -> &T {
640        &self.inner
641    }
642}
643
644impl<R: ResolverKind, T: 'static + Send + Sync> Resolved<R, T> {
645    /// Construct from a pre-resolved Arc — useful for tests
646    /// and programmatic graph assembly that bypasses the DSL
647    /// auto-resolver.
648    pub fn from_arc(inner: std::sync::Arc<T>) -> Self {
649        Self {
650            inner,
651            _r: std::marker::PhantomData,
652        }
653    }
654    /// Borrow the inner Arc.
655    pub fn as_arc(&self) -> &std::sync::Arc<T> {
656        &self.inner
657    }
658}
659
660/// Marker trait that names a kind of source-string auto-resolver
661/// for [`Resolved<R, T>`] wire args. The variants here mirror
662/// [`crate::dsl::registry::DefaultResolver`]; each impl picks
663/// one of them.
664pub trait ResolverKind: 'static {
665    /// The resolver this kind names.
666    const RESOLVER: crate::dsl::registry::DefaultResolver;
667}
668
669/// Splice `dataset_group_open(<source>)` upstream when the wire
670/// source is a `Str` (SRD-53 `DefaultResolver::Group`).
671pub struct GroupResolver;
672impl ResolverKind for GroupResolver {
673    const RESOLVER: crate::dsl::registry::DefaultResolver =
674        crate::dsl::registry::DefaultResolver::Group;
675}
676
677impl<R: ResolverKind, T: 'static + Send + Sync> Wire for Resolved<R, T> {
678    const PORT: PortType = PortType::Handle;
679    const RESOLVER: Option<crate::dsl::registry::DefaultResolver> =
680        Some(<R as ResolverKind>::RESOLVER);
681    fn extract(v: &Value) -> Self {
682        match v {
683            Value::Handle(arc) => {
684                let inner = arc.clone().downcast::<T>().unwrap_or_else(|_| {
685                    panic!(
686                        "Wire<Resolved<_, {}>>::extract: Handle downcast failed",
687                        std::any::type_name::<T>()
688                    )
689                });
690                Resolved {
691                    inner,
692                    _r: std::marker::PhantomData,
693                }
694            }
695            // The common fault, and the one worth naming: an
696            // upstream open did not resolve, so the handle this
697            // node reads is absent rather than wrong.
698            Value::None => panic!(
699                "a resolved handle is None — the upstream open failed to \
700                 resolve. The audit log carries the underlying error and \
701                 the name it was opening: a catalog miss, a facet missing \
702                 on disk, or a transport failure. This is the most common \
703                 fault when a workload runs on a system whose catalog is \
704                 not configured for the source it asks for."
705            ),
706            other => panic!("Wire<Resolved>::extract: expected Handle, got {other:?}"),
707        }
708    }
709    fn inject(self) -> Value {
710        Value::Handle(self.inner)
711    }
712}
713
714// FromValue / IntoValue retired 2026-06-05 — the `#[polydat_node]`
715// macro now dispatches every owned type through `<T as Wire>::extract`
716// / `::inject` and emits direct `match`-on-`Value` extraction for
717// borrow shapes (`&str`, `&[u8]`, `&[T]`, `&serde_json::Value`).
718// Per SRD-80b Phase B; the old trait pair plus their borrow-impls'
719// `unsafe { transmute }` lifetime-extension hack are gone.
720//
721// [PLACEHOLDER_PHASE_B_DELETE]
722
723/// SRD-80 PR B.5 — marker wrapper for const arguments in
724/// `#[polydat_node]` function signatures.
725///
726/// Use in arg position to signal that the value is captured at
727/// node-construction time (assembly-time) rather than read
728/// per-cycle from a wire. The macro detects `Const<T>` in arg
729/// position and:
730///
731/// - Emits `Slot::Const { ... }` (not `Slot::Wire`) in the
732///   node's NodeMeta.
733/// - Emits `SlotType::ConstU64` / `ConstF64` / `ConstStr` in
734///   the corresponding `FuncSig.params` entry (the const
735///   variant matching `T`).
736/// - Adds a struct field to hold the captured value.
737/// - Generates a `new(const_values...)` constructor.
738/// - Wires the build closure to pull from `consts: &[ConstArg]`
739///   and pass values to `new()`.
740/// - Constructs a `Const<T>(...)` wrapper around the struct
741///   field at eval time so the user's function body sees the
742///   wrapped type matching its signature.
743///
744/// Body code accesses the wrapped value via `.0` or via the
745/// `Deref` impl below:
746///
747/// ```ignore
748/// #[polydat_node(category = String)]
749/// fn combinations(input: u64, pattern: Const<&str>) -> String {
750///     apply(input, pattern.0)  // pattern.0 is &str
751/// }
752/// ```
753///
754/// Type-shape dispatch table:
755///
756/// | `Const<T>` form | `SlotType` variant | Struct field type | ConstArg accessor |
757/// |---|---|---|---|
758/// | `Const<u64>`  | `ConstU64`  | `u64`    | `as_u64()`  |
759/// | `Const<f64>`  | `ConstF64`  | `f64`    | `as_f64()`  |
760/// | `Const<bool>` | `ConstU64`  | `bool`   | `as_u64() != 0` |
761/// | `Const<&str>` | `ConstStr`  | `String` | `as_str().to_string()` |
762pub struct Const<T>(pub T);
763
764impl<T> std::ops::Deref for Const<T> {
765    type Target = T;
766    fn deref(&self) -> &T {
767        &self.0
768    }
769}
770
771impl<T> std::ops::DerefMut for Const<T> {
772    fn deref_mut(&mut self) -> &mut T {
773        &mut self.0
774    }
775}
776
777/// SRD-80 PR B.6 — construction-time setup contract for nodes
778/// that derive a pre-computed runtime state from their const
779/// args (e.g. `combinations` parsing a charset pattern into
780/// segments + modulus, `regex_match` compiling a pattern,
781/// `histribution` parsing a distribution spec).
782///
783/// **The contract**: the operator-provided setup function is
784/// called EXACTLY ONCE per node instance, at construction time
785/// (`new()`). Its result is stored in a struct field; eval-
786/// time access is a plain `&T` borrow.
787///
788/// **Type-level enforcement**: the `#[poly_const(...)]`
789/// attribute on a `&T` argument tells the macro to generate
790/// this construction pattern. The macro is the sole party
791/// emitting `setup_fn(...)` calls and it generates the call
792/// exactly once inside `new()`. The contract is inviolable
793/// because no other code path can reach the setup function —
794/// the macro hides it inside the constructor.
795///
796/// In effect, the function pointer behaves as `FnOnce` —
797/// invoked one time, by one site, never again. The FnOnce
798/// semantics aren't expressed as a trait bound because they
799/// don't need to be: the macro is the only caller, and the
800/// macro respects single-call by construction.
801///
802/// Library author idiom:
803///
804/// ```ignore
805/// pub struct ParsedPattern {
806///     pub segments: Vec<Segment>,
807///     pub modulus: u64,
808/// }
809///
810/// impl ParsedPattern {
811///     /// Single-call setup. Macro invokes once in `new()`.
812///     fn from_pattern(pattern: &str) -> Self { /* parse */ }
813/// }
814///
815/// #[polydat_node(category = String)]
816/// fn combinations(
817///     input: u64,
818///     pattern: Const<&str>,
819///     #[poly_const(ParsedPattern::from_pattern, from = pattern)]
820///     parsed: &ParsedPattern,
821/// ) -> String {
822///     // parsed is a borrow of the cached struct field —
823///     // no recomputation, no clone, no ceremony at the call site.
824///     let mut r = input % parsed.modulus;
825///     /* ... */
826/// }
827/// ```
828///
829/// Marker trait — purely a documentation handle for types
830/// intended to be polydat-setup targets. The macro doesn't
831/// dispatch on this; the attribute is the dispatch surface.
832/// Implementing the trait gives library authors a way to
833/// signal intent and improve `cargo doc` discoverability.
834pub trait PolydatSetup {}
835
836// ── The slot kit's run-time helpers ──────────────────────────────
837// Called by the closures `#[polydat_node]` emits for its `compiled_slot`
838// kit; public because generated code in other crates calls them, not
839// because hosts should.
840
841/// The value a `Ref2` pair at the head of `slots` holds by reference:
842/// the one-element slice a JSON, extension, or handle producer
843/// published (jit_boundary.md, axiom S7: one dereference).
844///
845/// The slots must hold a pair a producer published into storage that
846/// is alive: its own scratch, an extern's stored value, or a boundary
847/// value alive for the call (axioms S3, S4). A pair of length zero,
848/// an unset extern, reads as [`Value::None`].
849#[inline]
850pub fn ref_value(slots: &[u64]) -> &Value {
851    static NONE: Value = Value::None;
852    if slots.get(1).copied().unwrap_or(0) == 0 {
853        return &NONE;
854    }
855    // SAFETY: as documented; the producer's storage outlives the read.
856    unsafe { &*(slots[0] as usize as *const Value) }
857}
858
859/// An empty buffer with room for `n` elements, for a node whose buffer
860/// size comes from a *value* — a wire, a constant, or arithmetic on
861/// either.
862///
863/// `Vec::with_capacity(n as usize)` is the obvious spelling and it is
864/// wrong twice over for a value the node did not choose. A size the
865/// machine cannot hold makes the allocator **abort the process**, which
866/// no `catch_unwind` sees: not an error on the program that asked, but
867/// the host gone. And `as usize` truncates on a 32-bit target, so a
868/// large size silently becomes a small one. Here a size that does not
869/// fit `usize`, or cannot be reserved, is a failure of the node like
870/// any other — caught, attributed to the node and its inputs, and the
871/// same on every engine.
872///
873/// There is no cap. A size that *can* be allocated is allocated, however
874/// slow filling it is; how large a string a host asks for is the host's
875/// business. What this refuses is only what could never have
876/// succeeded. Arithmetic on a size belongs in `u64` with
877/// `saturating_add`, so an overflow reaches here as a size that cannot
878/// be reserved rather than wrapping to a small one first.
879pub fn buffer_for<T>(n: u64, what: &str) -> Vec<T> {
880    try_buffer_for(n, what).unwrap_or_else(|e| panic!("{e}"))
881}
882
883/// [`buffer_for`] for a caller that reports failure as a value rather
884/// than as a node's panic: a spec parser or a compile step, whose size
885/// comes from program text. The error is the same sentence.
886pub fn try_buffer_for<T>(n: u64, what: &str) -> Result<Vec<T>, String> {
887    let mut v = Vec::new();
888    match usize::try_from(n) {
889        Ok(k) if v.try_reserve_exact(k).is_ok() => Ok(v),
890        _ => Err(size_refusal(n, what)),
891    }
892}
893
894/// [`buffer_for`] for text: an empty `String` with room for `n` bytes.
895pub fn string_for(n: u64, what: &str) -> String {
896    let mut s = String::new();
897    match usize::try_from(n) {
898        Ok(n) if s.try_reserve_exact(n).is_ok() => s,
899        _ => refuse_size(n, what),
900    }
901}
902
903/// [`buffer_for`] for a buffer that is reused across evaluations:
904/// `out` is cleared and then has room for `n` elements. A native
905/// producer writing into step-owned scratch takes this shape, so its
906/// refusal reads the same as the node's.
907pub fn reserve_for<T>(out: &mut Vec<T>, n: u64, what: &str) {
908    out.clear();
909    match usize::try_from(n) {
910        Ok(n) if out.try_reserve_exact(n).is_ok() => {}
911        _ => refuse_size(n, what),
912    }
913}
914
915#[cold]
916fn refuse_size(n: u64, what: &str) -> ! {
917    panic!("{}", size_refusal(n, what))
918}
919
920fn size_refusal(n: u64, what: &str) -> String {
921    format!("{what}: a buffer of {n} elements cannot be allocated on this machine")
922}
923
924/// A polymorphic port's slots as the owned `Value` the wire type
925/// names: a scalar from its bits, a `Ref2` kind copied out of the
926/// pair its producer published.
927#[inline]
928pub fn read_poly(ty: PortType, slots: &[u64]) -> Value {
929    crate::compile::marshal::decode_slot(slots, ty)
930}
931
932/// A polymorphic return written by the node's resolved output type: a
933/// scalar as its bits into `outputs[0]`, a `Ref2` kind into
934/// `scratch[0]` with its pair republished (axiom S3). The value must
935/// be the port type's carrier: the graph colored the slot by the
936/// node's resolved output type, and a value of another type would be
937/// read by every consumer as something it is not, where the
938/// interpreter would have carried it. A `None` has no slot form on a
939/// compiled engine (engines.md §3.3).
940///
941/// The comparison is with the carrier, not the port type, because a
942/// `u32` or `f32` value *is* a `U64` in flight. Comparing with the port
943/// type refused every narrow integer and small float that reached a
944/// polymorphic node, on every compiled engine.
945#[inline]
946pub fn write_poly(
947    ty: PortType,
948    v: Value,
949    scratch: &mut [crate::ast::ScratchBuf],
950    outputs: &mut [u64],
951) {
952    // A `Dyn` port carries any value as written (input_variance.md).
953    if ty != PortType::Dyn && v.port_type() != crate::compile::marshal::carrier_port(ty) {
954        panic!(
955            "a node produced a {:?} on an output the graph typed {:?}; a compiled engine \
956             cannot carry a value of another type than the slot's (engines.md §3.4)",
957            v.port_type(),
958            ty
959        );
960    }
961    // The slot form is a property of the *type*, and the type is
962    // resolved: `ty` is what the graph coloured this output. So the
963    // write is chosen by the colour, of which there are three, rather
964    // than by the value's variant — a match over `Value` has to be
965    // extended every time the language grows a carrier, and a missing
966    // arm is not a compile error but a panic at the first pull. This
967    // one went through three rounds of that: the by-reference singles,
968    // then the numeric vectors, then the 128-bit words.
969    match ty.slot_color() {
970        // One slot of immediate data: the carrier's bits.
971        crate::ast::SlotColor::Imm1 => outputs[0] = carrier_slot_bits(&v),
972        // Two slots of immediate limb data, low word first
973        // (`Bits128`), which is what every 128-bit and register value
974        // is underneath.
975        crate::ast::SlotColor::Imm2 => {
976            // `as_reg_bits` is the register reading and refuses a
977            // 128-bit integer, so the limbs are taken from whichever
978            // two-slot carrier this is.
979            let words = match v {
980                Value::U128(b) | Value::I128(b) | Value::Reg128(b, _) => b.0,
981                other => unreachable!("a {:?} is not a two-slot carrier", other.port_type()),
982            };
983            outputs[0] = words[0];
984            outputs[1] = words[1];
985        }
986        // A (ptr, len) pair into the step's own scratch entry. Filling
987        // the entry can move its buffer, so the pair is republished
988        // every time — a slot left pointing at the old allocation is
989        // what the S9 validator catches (axiom S9(a)).
990        crate::ast::SlotColor::Ref2 => {
991            scratch[0].set_from_value(&v);
992            let (p, l) = scratch[0].ptr_len();
993            outputs[0] = p;
994            outputs[1] = l;
995        }
996    }
997}
998
999/// The slot bits of a one-slot carrier.
1000///
1001/// `Value::None` has no slot form on a compiled engine (engines.md
1002/// §3.3); it never reaches here, because an unset output is tracked
1003/// beside the buffer rather than written into it.
1004#[inline]
1005fn carrier_slot_bits(v: &Value) -> u64 {
1006    match v {
1007        Value::U64(x) => *x,
1008        Value::I64(x) => *x as u64,
1009        Value::F64(x) => x.to_bits(),
1010        Value::Bool(b) => *b as u64,
1011        other => panic!(
1012            "a {:?} value is not a one-slot carrier; the graph coloured its output Imm1",
1013            other.port_type()
1014        ),
1015    }
1016}
1017
1018// SRD-80 PR B.2/B.3 — macro-generated nodes register through
1019// the existing `NodeRegistration` inventory channel
1020// (`polydat::dsl::registry::NodeRegistration`), the same
1021// channel `register_nodes!` already uses. The proc-macro
1022// emits a `NodeRegistration` per `#[polydat_node]` site, so
1023// every consumer that already iterates the registry
1024// (`registry()`, `lookup()`, the compile pipeline's
1025// `factory::build_node`) sees macro-generated nodes
1026// automatically — no parallel collection, no separate dispatch
1027// surface. See `polydat::dsl::registry` for the load-bearing
1028// data structures.
1029
1030#[cfg(test)]
1031mod tests {
1032    use super::*;
1033
1034    fn refusal(f: impl FnOnce() + std::panic::UnwindSafe) -> String {
1035        let err = std::panic::catch_unwind(f).expect_err("the size must be refused");
1036        err.downcast_ref::<String>().cloned().unwrap_or_default()
1037    }
1038
1039    /// A size no machine can hold is a caught failure naming the node,
1040    /// not an allocator abort that takes the host down with it.
1041    #[test]
1042    fn an_impossible_size_is_refused_not_aborted() {
1043        let m = refusal(|| drop(buffer_for::<u32>(u64::MAX, "probe")));
1044        assert!(
1045            m.starts_with("probe: a buffer of 18446744073709551615"),
1046            "{m}"
1047        );
1048        let m = refusal(|| drop(string_for((1 << 53) + 1, "probe")));
1049        assert!(m.contains("cannot be allocated"), "{m}");
1050        let m = refusal(|| reserve_for(&mut vec![1.0f32; 4], u64::MAX, "probe"));
1051        assert!(m.contains("cannot be allocated"), "{m}");
1052    }
1053
1054    #[derive(Debug, Clone)]
1055    struct Probe;
1056    impl ReflectedValue for Probe {
1057        fn type_name(&self) -> &str {
1058            "probe"
1059        }
1060        fn display(&self) -> String {
1061            "probe".into()
1062        }
1063        fn as_any(&self) -> &dyn std::any::Any {
1064            self
1065        }
1066        fn clone_reflected(&self) -> Box<dyn ReflectedValue> {
1067            Box::new(self.clone())
1068        }
1069    }
1070
1071    /// A value of every port type, as its producer's `Wire` impl would
1072    /// carry it. The match has no wildcard, so a type added to the
1073    /// language does not compile here until it has one.
1074    fn sample(ty: PortType) -> Value {
1075        use crate::ast::{Bits128, RegLanes};
1076        let limbs = Bits128([0x0123_4567_89ab_cdef, 0xfedc_ba98_7654_3210]);
1077        let reg = |lanes| Value::Reg128(limbs, lanes);
1078        match ty {
1079            PortType::U64 | PortType::U32 | PortType::U16 | PortType::U8 => Value::U64(200),
1080            PortType::F32 => Value::U64(1.5f32.to_bits() as u64),
1081            PortType::F16 => Value::U64(half::f16::from_f32(1.5).to_bits() as u64),
1082            PortType::I64 | PortType::I32 | PortType::I16 | PortType::I8 => Value::I64(-7),
1083            PortType::F64 => Value::F64(-2.25),
1084            PortType::Bool => Value::Bool(true),
1085            PortType::U128 => Value::U128(limbs),
1086            PortType::I128 => Value::I128(limbs),
1087            PortType::Reg128 => reg(RegLanes::Raw),
1088            PortType::RegI8x16 => reg(RegLanes::I8x16),
1089            PortType::RegI16x8 => reg(RegLanes::I16x8),
1090            PortType::RegI32x4 => reg(RegLanes::I32x4),
1091            PortType::RegI64x2 => reg(RegLanes::I64x2),
1092            PortType::RegF16x8 => reg(RegLanes::F16x8),
1093            PortType::RegF32x4 => reg(RegLanes::F32x4),
1094            PortType::RegF64x2 => reg(RegLanes::F64x2),
1095            PortType::Str => Value::Str("héllo".into()),
1096            PortType::Bytes => Value::Bytes(vec![0u8, 1, 255].into()),
1097            PortType::Json => Value::Json(Arc::new(serde_json::json!({"k": [1, 2]}))),
1098            PortType::Ext => Value::Ext(Box::new(Probe)),
1099            PortType::Handle => Value::Handle(Arc::new(42u32)),
1100            PortType::VecF32 => Value::VecF32(SliceArc::from_vec(vec![1.0, -0.5])),
1101            PortType::VecI32 => Value::VecI32(SliceArc::from_vec(vec![-3, 4])),
1102            PortType::VecF64 => Value::VecF64(SliceArc::from_vec(vec![1e300, -0.0])),
1103            PortType::VecI64 => Value::VecI64(SliceArc::from_vec(vec![i64::MIN, 9])),
1104            PortType::VecF16 => Value::VecF16(SliceArc::from_vec(vec![half::f16::from_f32(0.5)])),
1105            PortType::VecI16 => Value::VecI16(SliceArc::from_vec(vec![-300i16, 300])),
1106            PortType::VecI8 => Value::VecI8(SliceArc::from_vec(vec![-8i8, 8])),
1107            // A `Dyn` port carries a value of any type, as written.
1108            PortType::Dyn => Value::Str("any".into()),
1109        }
1110    }
1111
1112    /// Reading a polymorphic port inverts writing one, for every port
1113    /// type the language has: what a polymorphic node returns is what
1114    /// the next polymorphic node takes, on every compiled engine.
1115    ///
1116    /// A register reaching a polymorphic node's input used to arrive as
1117    /// its low limb typed `U64`, because the read had a `U64` fallback
1118    /// arm and the limb reassembly lived only in the output read the
1119    /// host makes. The fuzzer found it on `log_warn(reg_splat_f64(..))`.
1120    #[test]
1121    fn every_port_type_reads_back_what_was_written() {
1122        for &ty in PortType::ALL {
1123            let v = sample(ty);
1124            // A `Dyn` port carries a value of any type, as written.
1125            if ty != PortType::Dyn {
1126                assert_eq!(
1127                    v.port_type(),
1128                    crate::compile::marshal::carrier_port(ty),
1129                    "{ty:?}: the sample is not what the port carries"
1130                );
1131            }
1132            let mut scratch: Vec<crate::ast::ScratchBuf> = ty
1133                .scratch_elem()
1134                .map(crate::ast::ScratchBuf::new)
1135                .into_iter()
1136                .collect();
1137            let mut slots = [0u64; 2];
1138            // Written as the macro writes it: by the port's resolved
1139            // type, which for a narrow or float-in-carrier type is not
1140            // the port type of the carrier value its `Wire` impl injects.
1141            write_poly(ty, v.clone(), &mut scratch, &mut slots);
1142            let back = read_poly(ty, &slots[..ty.slot_width()]);
1143            assert_eq!(back, v, "{ty:?}");
1144        }
1145    }
1146
1147    /// A size that fits is reserved exactly, and a reused buffer comes
1148    /// back cleared.
1149    #[test]
1150    fn a_feasible_size_is_reserved() {
1151        let v: Vec<u8> = buffer_for(1000, "probe");
1152        assert!(v.is_empty() && v.capacity() >= 1000);
1153        let mut w = vec![7u8; 3];
1154        reserve_for(&mut w, 64, "probe");
1155        assert!(w.is_empty() && w.capacity() >= 64);
1156    }
1157}