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