Skip to main content

polydat_core/
ast.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Core types for Polydat nodes: values, ports, metadata, and the evaluation trait.
5//!
6//! The Polydat type system has three layers:
7//!
8//! 1. **Runtime values** ([`Value`]) — the enum that flows through
9//!    the DAG at evaluation time. Every interpreter buffer slot holds
10//!    a `Value`; compiled kernels carry the same values as typed
11//!    `u64` slots.
12//!
13//! 2. **Port types** ([`PortType`]) — compile-time type tags on
14//!    node input/output ports. The assembler validates that wiring
15//!    connects compatible types and auto-inserts adapters when not.
16//!
17//! 3. **Slot types** ([`SlotType`]) — distinguishes wire inputs
18//!    (cycle-time values) from constant parameters (baked at
19//!    construction). The DSL compiler uses these to decide whether
20//!    a literal in a function call is a wire promotion or a const arg.
21//!
22//! The [`PolydatNode`] trait is what every node function implements.
23//! A node declares its port metadata via [`NodeMeta`] and evaluates
24//! via `eval(&[Value], &mut [Value])`.
25
26use std::fmt;
27use std::ops::Deref;
28use std::sync::Arc;
29
30/// Arc-managed typed slice. Holds a borrow into a parent Arc'd
31/// owner — typically either an owned backing buffer (`Arc<[T]>`)
32/// or a long-lived resource like an mmap'd dataset. Cloning is
33/// one `Arc::clone` (atomic increment, zero allocations); the
34/// owner is type-erased as `Arc<dyn Any + Send + Sync>` so the
35/// same `SliceArc<T>` shape covers both modes.
36///
37/// Used by [`Value::VecF32`] / [`Value::VecI32`] to flow vector
38/// data on wires from accessors to native-binding adapters with:
39///   - zero per-cycle allocation when the source supports
40///     zero-copy reads (mmap-backed `VectorReader::get_slice`),
41///   - exactly one allocation when it doesn't (a `Vec<T>` from
42///     `VectorReader::get`, wrapped into an `Arc<[T]>`).
43///
44/// See type_system.md §1.7.
45pub struct SliceArc<T: 'static> {
46    /// Keeps the storage alive. For owned data this is an
47    /// `Arc<OwnedSlice<T>>`; for mmap-backed data this is an
48    /// `Arc<UniformDataset<T>>` (or any other type whose Arc
49    /// keeps the underlying memory mapped).
50    _owner: Arc<dyn std::any::Any + Send + Sync>,
51    ptr: *const T,
52    len: usize,
53}
54
55// Send/Sync: the raw pointer is treated as a borrow into memory
56// owned by `_owner`, which is itself Send+Sync. T must be
57// Send+Sync for the slice contents to be safely shared.
58unsafe impl<T: Send + Sync + 'static> Send for SliceArc<T> {}
59unsafe impl<T: Send + Sync + 'static> Sync for SliceArc<T> {}
60
61/// Type-erasable wrapper for an owned `Arc<[T]>`. Used as the
62/// owner when the source isn't zero-copy — `Arc<[T]>` is unsized
63/// so it can't be cast to `Arc<dyn Any>` directly, but
64/// `OwnedSlice<T>` is sized and the cast works.
65// Field is unused at the type level — its only job is to keep the
66// Arc<[T]> reference count alive while the SliceArc holds the raw
67// pointer into the buffer. Hence the `dead_code` allow.
68#[allow(dead_code)]
69pub(crate) struct OwnedSlice<T: 'static>(pub(crate) Arc<[T]>);
70
71impl<T: Send + Sync + 'static> SliceArc<T> {
72    /// Build from an owned `Vec<T>`. One heap allocation
73    /// (`Vec → Arc<[T]>`); cloning the resulting `SliceArc<T>` is
74    /// one atomic increment.
75    pub fn from_vec(v: Vec<T>) -> Self {
76        let arc: Arc<[T]> = Arc::from(v);
77        let ptr = arc.as_ptr();
78        let len = arc.len();
79        let owner: Arc<dyn std::any::Any + Send + Sync> = Arc::new(OwnedSlice(arc));
80        Self {
81            _owner: owner,
82            ptr,
83            len,
84        }
85    }
86
87    /// Build from a `&[T]` borrowed from `owner`'s data.
88    ///
89    /// # Safety
90    ///
91    /// `slice` must point into memory owned by `owner` and
92    /// remain valid for at least as long as `owner` (i.e., until
93    /// the last clone of this Arc is dropped). The caller asserts
94    /// this — typical use is mmap-backed readers where the slice
95    /// is a view into a memory-mapped page kept alive by the
96    /// dataset Arc.
97    pub unsafe fn from_borrowed(owner: Arc<dyn std::any::Any + Send + Sync>, slice: &[T]) -> Self {
98        Self {
99            _owner: owner,
100            ptr: slice.as_ptr(),
101            len: slice.len(),
102        }
103    }
104}
105
106impl<T: 'static> SliceArc<T> {
107    /// Borrow as `&[T]`. The borrow lives as long as `&self`.
108    /// Defined here without Send+Sync bounds so it's reachable
109    /// from `Deref`/`PartialEq`/`Debug` impls that don't carry
110    /// those bounds.
111    #[inline]
112    pub fn as_slice(&self) -> &[T] {
113        // SAFETY: `_owner` keeps the storage alive; `ptr`/`len`
114        // were validated at construction. The returned reference
115        // is bounded by `&self`'s lifetime.
116        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
117    }
118}
119
120impl<T: Send + Sync + 'static> Clone for SliceArc<T> {
121    fn clone(&self) -> Self {
122        Self {
123            _owner: self._owner.clone(),
124            ptr: self.ptr,
125            len: self.len,
126        }
127    }
128}
129
130impl<T: 'static> Deref for SliceArc<T> {
131    type Target = [T];
132    fn deref(&self) -> &[T] {
133        // SAFETY: identical reasoning to as_slice().
134        unsafe { std::slice::from_raw_parts(self.ptr, self.len) }
135    }
136}
137
138impl<T: PartialEq + 'static> PartialEq for SliceArc<T> {
139    fn eq(&self, other: &Self) -> bool {
140        // Pointer-equal pair → trivially equal (zero-copy from the
141        // same source). Otherwise compare contents — two unrelated
142        // SliceArcs may hold equal data.
143        if std::ptr::eq(self.ptr, other.ptr) && self.len == other.len {
144            return true;
145        }
146        self.as_slice() == other.as_slice()
147    }
148}
149
150impl<T: fmt::Debug + 'static> fmt::Debug for SliceArc<T> {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        f.debug_struct("SliceArc")
153            .field("len", &self.len)
154            .field("first", &self.as_slice().first())
155            .finish_non_exhaustive()
156    }
157}
158
159/// Two-limb carrier for 128-bit integers inside [`Value`].
160///
161/// Limbs are little-endian (`[lo, hi]`). Using `[u64; 2]` instead
162/// of a raw `u128`/`i128` field keeps `Value`'s alignment at 8 and
163/// its size inside the 40-byte buffer-slot envelope; reassembly is
164/// two register moves.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
166pub struct Bits128(pub [u64; 2]);
167
168impl Bits128 {
169    #[inline]
170    /// The two-word form of a `u128`, low word first.
171    pub fn from_u128(v: u128) -> Self {
172        Self([v as u64, (v >> 64) as u64])
173    }
174    #[inline]
175    /// The two-word form of an `i128`, low word first.
176    pub fn from_i128(v: i128) -> Self {
177        Self::from_u128(v as u128)
178    }
179    /// The word as a `u128`.
180    #[inline]
181    pub fn as_u128(self) -> u128 {
182        (self.0[0] as u128) | ((self.0[1] as u128) << 64)
183    }
184    /// The word as an `i128`.
185    #[inline]
186    pub fn as_i128(self) -> i128 {
187        self.as_u128() as i128
188    }
189
190    #[inline]
191    /// The word's sixteen bytes, little-endian.
192    pub fn to_le_bytes(self) -> [u8; 16] {
193        self.as_u128().to_le_bytes()
194    }
195
196    #[inline]
197    /// A word from sixteen little-endian bytes.
198    pub fn from_le_bytes(b: [u8; 16]) -> Self {
199        Self::from_u128(u128::from_le_bytes(b))
200    }
201}
202
203/// Lane-codec macro: `[T; N]` views over the 16-byte word,
204/// little-endian lane order (lane 0 = lowest address).
205macro_rules! bits128_lanes {
206    ($to:ident, $from:ident, $t:ty, $n:expr) => {
207        impl Bits128 {
208            #[inline]
209            /// The word as lanes of one element type, lane 0 at the lowest address.
210            pub fn $to(self) -> [$t; $n] {
211                let b = self.to_le_bytes();
212                let mut out = [<$t>::default(); $n];
213                let w = core::mem::size_of::<$t>();
214                for (i, lane) in out.iter_mut().enumerate() {
215                    let mut lb = [0u8; core::mem::size_of::<$t>()];
216                    lb.copy_from_slice(&b[i * w..(i + 1) * w]);
217                    *lane = <$t>::from_le_bytes(lb);
218                }
219                out
220            }
221            #[inline]
222            /// A word from lanes of one element type, lane 0 at the lowest address.
223            pub fn $from(lanes: [$t; $n]) -> Self {
224                let mut b = [0u8; 16];
225                let w = core::mem::size_of::<$t>();
226                for (i, lane) in lanes.iter().enumerate() {
227                    b[i * w..(i + 1) * w].copy_from_slice(&lane.to_le_bytes());
228                }
229                Self::from_le_bytes(b)
230            }
231        }
232    };
233}
234
235bits128_lanes!(lanes_i8, from_lanes_i8, i8, 16);
236bits128_lanes!(lanes_i16, from_lanes_i16, i16, 8);
237bits128_lanes!(lanes_i32, from_lanes_i32, i32, 4);
238bits128_lanes!(lanes_i64, from_lanes_i64, i64, 2);
239bits128_lanes!(lanes_f32, from_lanes_f32, f32, 4);
240bits128_lanes!(lanes_f64, from_lanes_f64, f64, 2);
241
242impl Bits128 {
243    /// f16 lanes go through the bit-pattern codec (`half::f16`
244    /// has no `to_le_bytes`).
245    #[inline]
246    pub fn lanes_f16(self) -> [half::f16; 8] {
247        self.lanes_i16().map(|b| half::f16::from_bits(b as u16))
248    }
249    #[inline]
250    /// A word from eight `f16` lanes, through the bit-pattern codec.
251    pub fn from_lanes_f16(lanes: [half::f16; 8]) -> Self {
252        Self::from_lanes_i16(lanes.map(|f| f.to_bits() as i16))
253    }
254}
255
256/// Lane-typing view tag for [`Value::Reg128`] — which
257/// interpretation a 128-bit register word currently carries
258/// (type_system_alignment.md §8.4 layer 2). `Raw` is the
259/// algorithm-defined buffer-state view (heterogeneous lane
260/// roles); the typed views are homogeneous `[T; N]` readings.
261/// All views are free bitcasts of one another.
262#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
263pub enum RegLanes {
264    /// The algorithm-defined view: heterogeneous lane roles, no element type.
265    Raw,
266    /// Sixteen `i8` lanes.
267    I8x16,
268    /// Eight `i16` lanes.
269    I16x8,
270    /// Four `i32` lanes.
271    I32x4,
272    /// Two `i64` lanes.
273    I64x2,
274    /// Eight `f16` lanes.
275    F16x8,
276    /// Four `f32` lanes.
277    F32x4,
278    /// Two `f64` lanes.
279    F64x2,
280}
281
282#[derive(Debug, Clone)]
283/// A typed value on a wire: what a node reads and produces on the
284/// interpreter, and what a host sets and pulls on every engine.
285pub enum Value {
286    /// Unsigned 64-bit integer. The workhorse type for deterministic
287    /// data generation: hash outputs, modular arithmetic, bit
288    /// manipulation, cycle counters, primary keys.
289    U64(u64),
290    /// Unsigned 128-bit integer (cranelift I128, unsigned
291    /// interpretation). Carried as two u64 limbs ([`Bits128`],
292    /// little-endian limb order) so `Value` keeps alignment 8 —
293    /// see the `value_size_probe` test. Carried as two immediate
294    /// slots (`SlotColor::Imm2`) in compiled kernels. JSON
295    /// projection is a decimal string (JSON Number cannot carry
296    /// 128-bit magnitude).
297    U128(Bits128),
298    /// Signed 128-bit integer (cranelift I128, signed
299    /// interpretation). Same limb carrier and conventions as
300    /// [`Value::U128`].
301    I128(Bits128),
302    /// 128-bit SIMD register word (type_system_alignment.md
303    /// §8.4 layer 2). The [`RegLanes`] tag records the current
304    /// view — a homogeneous lane typing (`[f32; 4]`, `[i16; 8]`,
305    /// …) or `Raw` (algorithm-defined buffer state with
306    /// heterogeneous lane roles). Views are free bitcasts; the
307    /// word is a plain value (two u64 slots in compiled buffers,
308    /// no pointers, no lifetime).
309    Reg128(Bits128, RegLanes),
310    /// Signed 64-bit integer. The honest runtime carrier for
311    /// `PortType::I64` (and sign-extended `I32`) slots — matching
312    /// `serde_json::Number`'s `NegInt` leaf so display and JSON
313    /// projection render negatives as negatives instead of their
314    /// unsigned bit-reinterpretation. At the JIT boundary the bits
315    /// ride the same u64 slot (`i64 as u64` is a free bitcast), so
316    /// signedness costs nothing in compiled kernels. See
317    /// `polydat/docs/design/type_system_alignment.md` §5.
318    I64(i64),
319    /// IEEE 754 double-precision float. Used for distributions,
320    /// noise functions, trigonometry, interpolation, and any
321    /// computation that needs fractional precision.
322    F64(f64),
323    /// Boolean. Used for conditional ops (`if:` field), selection
324    /// nodes, and flag computation.
325    Bool(bool),
326    /// Shared, immutable UTF-8 string. Used for formatted output,
327    /// weighted string selection, template interpolation, and any
328    /// value that will appear directly in an op statement. Backed
329    /// by `Arc<str>` so cloning is one atomic increment with no
330    /// allocation — the per-cycle reads that materialize a `final`
331    /// or `init` string into op-template substitution are
332    /// pointer-share, not heap-copy.
333    Str(Arc<str>),
334    /// Shared, immutable raw byte buffer. Used for cryptographic
335    /// digests, binary encoding/decoding, and byte-level data
336    /// generation. Backed by `Arc<[u8]>` so cloning is one atomic
337    /// increment.
338    Bytes(Arc<[u8]>),
339    /// Shared, immutable structured JSON value. Used for
340    /// vector representations (JSON arrays), complex structured
341    /// data, and JSON merge ops. Backed by `Arc<serde_json::Value>`
342    /// so cloning is one atomic increment — the per-cycle reads
343    /// of result-body JSON wires (capture extraction, recall
344    /// evaluation, column projection) share the underlying
345    /// allocation rather than deep-cloning the tree. Consumers
346    /// that need an owned `serde_json::Value` (mutation,
347    /// serialization sinks) explicitly deep-clone via
348    /// `(*v).clone()` at the consume site.
349    Json(Arc<serde_json::Value>),
350    /// Adapter-contributed reflected value. Carries type info and
351    /// standard access methods (display, JSON, string, bytes).
352    /// Enables protocol-native types (UUIDs, timestamps, inet
353    /// addresses) to flow through Polydat without boxing to strings.
354    Ext(Box<dyn ReflectedValue>),
355    /// Type-erased Arc handle to a resolved resource (dataset,
356    /// prepared statement, ...). Cloning during input gather is one
357    /// `Arc::clone` — a single atomic increment, zero allocations.
358    /// Produced by resolver nodes (e.g. `dataset_open`) and consumed
359    /// by reader nodes that downcast to the concrete type. See
360    /// type_system.md §1.8 for the canonical use case.
361    Handle(Arc<dyn std::any::Any + Send + Sync>),
362    /// Typed `f32` vector carrier. Flows from vector accessors to
363    /// native-binding adapters without string formatting or byte
364    /// serialization on the cycle path. Cloning is one `Arc::clone`,
365    /// zero allocations. The underlying [`SliceArc`] supports both
366    /// owned (allocated `Arc<[f32]>`) and zero-copy (borrow into a
367    /// long-lived owner like an mmap'd dataset) storage modes.
368    /// `to_display_string()` renders as JSON array.
369    VecF32(SliceArc<f32>),
370    /// Typed `i32` vector carrier (e.g. neighbor indices). Same
371    /// shape as VecF32 — typed slice on the wire.
372    VecI32(SliceArc<i32>),
373    /// Typed `f64` vector carrier (`Arc<[f64]>`). Same shape as
374    /// VecF32. Used for double-precision embeddings / dense
375    /// numeric features bound to CQL `vector<double, N>` etc.
376    VecF64(SliceArc<f64>),
377    /// Typed `i64` vector carrier (`Arc<[i64]>`). 64-bit integer
378    /// vectors for CQL `vector<bigint, N>`.
379    VecI64(SliceArc<i64>),
380    /// Typed half-precision float vector (`Arc<[half::f16]>`).
381    /// 16-bit float carrier — stays at f16 on the wire so
382    /// embeddings stored as half-precision aren't widened on the
383    /// kernel side.
384    VecF16(SliceArc<half::f16>),
385    /// Typed `i16` vector carrier (`Arc<[i16]>`). 16-bit signed
386    /// integer vectors for CQL `vector<smallint, N>`.
387    VecI16(SliceArc<i16>),
388    /// Typed `i8` vector carrier (`Arc<[i8]>`). 8-bit signed
389    /// integer vectors (CQL `vector<tinyint, N>`); completes the
390    /// cranelift lane family {i8, i16, i32, i64, f16, f32, f64}
391    /// (type_system_alignment.md §8.2). Unsigned byte buffers are
392    /// spelled `Bytes`.
393    VecI8(SliceArc<i8>),
394    /// The absent value (none_semantics.md): fresh buffer slots start as
395    /// `None`, and the kernel propagates it through nodes that do
396    /// not `accepts_none_inputs`.
397    None,
398}
399
400impl PartialEq for Value {
401    fn eq(&self, other: &Self) -> bool {
402        match (self, other) {
403            (Value::U64(a), Value::U64(b)) => a == b,
404            (Value::I64(a), Value::I64(b)) => a == b,
405            (Value::U128(a), Value::U128(b)) => a == b,
406            (Value::I128(a), Value::I128(b)) => a == b,
407            (Value::Reg128(a, av), Value::Reg128(b, bv)) => a == b && av == bv,
408            (Value::F64(a), Value::F64(b)) => a == b,
409            (Value::Bool(a), Value::Bool(b)) => a == b,
410            // Arc-backed variants: pointer-eq fast path before
411            // any content compare. Hot per-cycle callers
412            // (notably `PolydatState::reset_inputs_from`'s
413            // "still at default?" probe) typically test a slot
414            // against a value that was Arc-cloned from the same
415            // source — `Arc::ptr_eq` is O(1) and lets the deep
416            // compare drop out of the per-cycle path.
417            (Value::Str(a), Value::Str(b)) => Arc::ptr_eq(a, b) || a == b,
418            (Value::Bytes(a), Value::Bytes(b)) => Arc::ptr_eq(a, b) || a == b,
419            (Value::Json(a), Value::Json(b)) => Arc::ptr_eq(a, b) || a == b,
420            (Value::None, Value::None) => true,
421            (Value::Ext(a), Value::Ext(b)) => {
422                a.type_name() == b.type_name() && a.display() == b.display()
423            }
424            (Value::Handle(a), Value::Handle(b)) => Arc::ptr_eq(a, b),
425            (Value::VecF32(a), Value::VecF32(b)) => a == b,
426            (Value::VecI32(a), Value::VecI32(b)) => a == b,
427            (Value::VecF64(a), Value::VecF64(b)) => a == b,
428            (Value::VecI64(a), Value::VecI64(b)) => a == b,
429            (Value::VecF16(a), Value::VecF16(b)) => a == b,
430            (Value::VecI16(a), Value::VecI16(b)) => a == b,
431            (Value::VecI8(a), Value::VecI8(b)) => a == b,
432            _ => false,
433        }
434    }
435}
436
437/// Trait for adapter-contributed value types.
438///
439/// Any type that flows through the Polydat Kernel as `Value::Ext` must
440/// implement this. It provides standard access patterns that work
441/// across adapter boundaries — stdout can display it, HTTP can
442/// serialize it, model adapter can capture it — without needing
443/// the concrete type.
444///
445/// The producing adapter can downcast via `as_any()` when it needs
446/// native protocol access (e.g., CQL binding a `uuid::Uuid`).
447pub trait ReflectedValue: Send + Sync + std::fmt::Debug {
448    /// Type name for diagnostics and describe output.
449    fn type_name(&self) -> &str;
450
451    /// Human-readable string representation.
452    /// Used by stdout adapter, logging, and diagnostics.
453    fn display(&self) -> String;
454
455    /// JSON representation for serialization and HTTP bodies.
456    fn to_json_value(&self) -> serde_json::Value {
457        serde_json::Value::String(self.display())
458    }
459
460    /// Try to represent as a string. Many types have a canonical
461    /// string form (UUIDs, timestamps, IP addresses).
462    fn try_as_str(&self) -> Option<String> {
463        Some(self.display())
464    }
465
466    /// Try to represent as u64.
467    fn try_as_u64(&self) -> Option<u64> {
468        None
469    }
470
471    /// Try to represent as f64.
472    fn try_as_f64(&self) -> Option<f64> {
473        None
474    }
475
476    /// Try to represent as bytes.
477    fn try_as_bytes(&self) -> Option<&[u8]> {
478        None
479    }
480
481    /// Downcast to the concrete type. Only works when the consuming
482    /// code has the concrete type in scope (same crate or shared dep).
483    fn as_any(&self) -> &dyn std::any::Any;
484
485    /// Clone into a new boxed trait object.
486    fn clone_reflected(&self) -> Box<dyn ReflectedValue>;
487}
488
489impl Clone for Box<dyn ReflectedValue> {
490    fn clone(&self) -> Self {
491        self.clone_reflected()
492    }
493}
494
495impl Value {
496    /// The `U64` payload; panics on any other variant, naming both types.
497    #[inline]
498    pub fn as_u64(&self) -> u64 {
499        match self {
500            Value::U64(v) => *v,
501            _ => panic!("expected U64, got {}", self.type_name()),
502        }
503    }
504
505    /// Read a signed 64-bit integer. Accepts the honest `Value::I64`
506    /// carrier and a bit-stuffed `Value::U64` whose bits are
507    /// reinterpreted as the `i64` they store.
508    #[inline]
509    pub fn as_i64(&self) -> i64 {
510        match self {
511            Value::I64(v) => *v,
512            Value::U64(v) => *v as i64,
513            _ => panic!("expected I64, got {}", self.type_name()),
514        }
515    }
516
517    /// Read an unsigned 128-bit integer. Accepts the honest
518    /// `Value::U128` carrier plus zero-extended `U64` (widening
519    /// is implicit at read sites the way `as_i64` accepts the
520    /// bit-stuffed form).
521    #[inline]
522    pub fn as_u128(&self) -> u128 {
523        match self {
524            Value::U128(b) => b.as_u128(),
525            Value::U64(v) => *v as u128,
526            _ => panic!("expected U128, got {}", self.type_name()),
527        }
528    }
529
530    /// Read a signed 128-bit integer. Accepts `Value::I128` plus
531    /// sign-extended `I64` and zero-extended `U64`.
532    #[inline]
533    pub fn as_i128(&self) -> i128 {
534        match self {
535            Value::I128(b) => b.as_i128(),
536            Value::I64(v) => *v as i128,
537            Value::U64(v) => *v as i128,
538            _ => panic!("expected I128, got {}", self.type_name()),
539        }
540    }
541
542    /// Read a 128-bit register word under any view (views are
543    /// free bitcasts — a consumer declaring a different lane
544    /// typing than the producer is the intended use).
545    #[inline]
546    pub fn as_reg_bits(&self) -> Bits128 {
547        match self {
548            Value::Reg128(b, _) => *b,
549            _ => panic!("expected Reg128, got {}", self.type_name()),
550        }
551    }
552
553    /// The `F64` payload; panics on any other variant, naming both types.
554    #[inline]
555    pub fn as_f64(&self) -> f64 {
556        match self {
557            Value::F64(v) => *v,
558            _ => panic!("expected F64, got {}", self.type_name()),
559        }
560    }
561
562    /// The `Bool` payload; panics on any other variant, naming both types.
563    #[inline]
564    pub fn as_bool(&self) -> bool {
565        match self {
566            Value::Bool(v) => *v,
567            _ => panic!("expected Bool, got {}", self.type_name()),
568        }
569    }
570
571    /// The `Str` payload as a string slice; panics on any other variant.
572    #[inline]
573    pub fn as_str(&self) -> &str {
574        match self {
575            Value::Str(v) => v,
576            _ => panic!("expected Str, got {}", self.type_name()),
577        }
578    }
579
580    /// The `Bytes` payload as a byte slice; panics on any other variant.
581    #[inline]
582    pub fn as_bytes(&self) -> &[u8] {
583        match self {
584            Value::Bytes(v) => v,
585            _ => panic!("expected Bytes, got {}", self.type_name()),
586        }
587    }
588
589    /// The `Json` payload by reference; panics on any other variant.
590    #[inline]
591    pub fn as_json(&self) -> &serde_json::Value {
592        match self {
593            Value::Json(v) => v,
594            _ => panic!("expected Json, got {}", self.type_name()),
595        }
596    }
597
598    /// Borrow the inner `Arc<serde_json::Value>` from a
599    /// `Value::Json` variant. Use when a consumer wants to
600    /// share the JSON tree across kernels without deep-cloning
601    /// the structure — e.g. capture extraction that writes the
602    /// same JSON wire to multiple downstream slots. Panics on
603    /// type mismatch.
604    #[inline]
605    pub fn as_json_arc(&self) -> &Arc<serde_json::Value> {
606        match self {
607            Value::Json(v) => v,
608            _ => panic!("expected Json, got {}", self.type_name()),
609        }
610    }
611
612    /// Return the `PortType` corresponding to this value's variant.
613    #[inline]
614    pub fn port_type(&self) -> PortType {
615        match self {
616            Value::U64(_) => PortType::U64,
617            Value::I64(_) => PortType::I64,
618            Value::U128(_) => PortType::U128,
619            Value::I128(_) => PortType::I128,
620            Value::Reg128(_, v) => match v {
621                RegLanes::Raw => PortType::Reg128,
622                RegLanes::I8x16 => PortType::RegI8x16,
623                RegLanes::I16x8 => PortType::RegI16x8,
624                RegLanes::I32x4 => PortType::RegI32x4,
625                RegLanes::I64x2 => PortType::RegI64x2,
626                RegLanes::F16x8 => PortType::RegF16x8,
627                RegLanes::F32x4 => PortType::RegF32x4,
628                RegLanes::F64x2 => PortType::RegF64x2,
629            },
630            Value::F64(_) => PortType::F64,
631            Value::Bool(_) => PortType::Bool,
632            Value::Str(_) => PortType::Str,
633            Value::Bytes(_) => PortType::Bytes,
634            Value::Json(_) => PortType::Json,
635            Value::Ext(_) => PortType::Ext,
636            Value::Handle(_) => PortType::Handle,
637            Value::VecF32(_) => PortType::VecF32,
638            Value::VecI32(_) => PortType::VecI32,
639            Value::VecF64(_) => PortType::VecF64,
640            Value::VecI64(_) => PortType::VecI64,
641            Value::VecF16(_) => PortType::VecF16,
642            Value::VecI16(_) => PortType::VecI16,
643            Value::VecI8(_) => PortType::VecI8,
644            // `None` is the absence of a value, which no port type
645            // names. `U64` is what this has always answered, and
646            // callers that care read it through
647            // [`Self::type_name`] or test for `None` first
648            // ([`Self::satisfies_slot`] does).
649            Value::None => PortType::U64,
650        }
651    }
652
653    /// The name of this value's type, for a diagnostic.
654    ///
655    /// Distinct from [`Self::port_type`] in the one case that
656    /// matters: an absent value reads as "none" rather than as the
657    /// `u64` its port type answers. A reader told "expected Handle,
658    /// got U64" goes looking for a number; the value was not there
659    /// at all, which is a different fault with a different cause.
660    pub fn type_name(&self) -> String {
661        match self {
662            Value::None => "none".to_string(),
663            other => other.port_type().to_string(),
664        }
665    }
666
667    /// Borrow a `VecF32` value as `&[f32]`. Panics on type mismatch.
668    #[inline]
669    pub fn as_vec_f32(&self) -> &[f32] {
670        match self {
671            Value::VecF32(arc) => arc,
672            _ => panic!("expected VecF32, got {}", self.type_name()),
673        }
674    }
675
676    /// Test whether this value's runtime variant is acceptable
677    /// to a slot declaring `slot_type`. `port_type() == slot_type`
678    /// is the strict case; this method also accepts the
679    /// **bit-stuffing equivalences** documented in
680    /// `polydat/docs/design/type_system.md` §1:
681    ///
682    /// - `Value::U64` is the runtime storage for `PortType` `U64`,
683    ///   `U32`, `I64`, and `I32` (narrow integers carry their
684    ///   bits in the low part of the u64; sign-extension for
685    ///   `I32` is part of the producer convention).
686    /// - `Value::F64` is the runtime storage for `PortType` `F64`
687    ///   and `F32` (`F32` carries its bits in the low 32 via
688    ///   `f32::to_bits() as u64`-style stuffing — but float
689    ///   stuffing uses `Value::F64` for the materialised float
690    ///   value, not the bit pattern).
691    /// - `Value::None` is acceptable for every slot type
692    ///   (the absent sentinel, none_semantics.md).
693    ///
694    /// Every typed input write checks a value with it. The check in
695    /// `adapt_boundary_value` stays strict (`port_type == slot_type`)
696    /// so an unadapted Value::U64 can never silently truncate into a
697    /// narrower slot.
698    #[inline]
699    pub fn satisfies_slot(&self, slot_type: PortType) -> bool {
700        // A `Dyn` slot takes any value as written; its converter node
701        // decides what the value becomes (input_variance.md §5).
702        if matches!(self, Value::None) || slot_type == PortType::Dyn {
703            return true;
704        }
705        let value_type = self.port_type();
706        if value_type == slot_type {
707            return true;
708        }
709        matches!(
710            (value_type, slot_type),
711            // Bit-stuffed forms: U8/U16/U32 zero-extend into U64
712            // storage, the signed narrow types may still arrive as
713            // U64 storage from a pre-alignment producer, and F32 and
714            // F16 ride their bit patterns in U64 (`Wire for f32` and
715            // `Wire for f16` inject them so).
716            (PortType::U64, PortType::U32 | PortType::I64 | PortType::I32
717                | PortType::U8 | PortType::U16 | PortType::I8 | PortType::I16
718                | PortType::F32 | PortType::F16)
719                | (PortType::F64, PortType::F32 | PortType::F16)
720                // Honest signed carrier: I64 storage serves the
721                // I64 slot and the sign-extended narrow signed
722                // projections.
723                | (PortType::I64, PortType::I32 | PortType::I8 | PortType::I16)
724                // Register views are free bitcasts: a word under
725                // any view satisfies a slot declaring any other
726                // (the consumer's declared lane typing IS the
727                // bitcast).
728                | (
729                    PortType::Reg128 | PortType::RegI8x16 | PortType::RegI16x8
730                        | PortType::RegI32x4 | PortType::RegI64x2
731                        | PortType::RegF16x8 | PortType::RegF32x4 | PortType::RegF64x2,
732                    PortType::Reg128 | PortType::RegI8x16 | PortType::RegI16x8
733                        | PortType::RegI32x4 | PortType::RegI64x2
734                        | PortType::RegF16x8 | PortType::RegF32x4 | PortType::RegF64x2,
735                )
736        )
737    }
738
739    /// Borrow a `VecI32` value as `&[i32]`. Panics on type mismatch.
740    #[inline]
741    pub fn as_vec_i32(&self) -> &[i32] {
742        match self {
743            Value::VecI32(arc) => arc,
744            _ => panic!("expected VecI32, got {}", self.type_name()),
745        }
746    }
747
748    /// Borrow a `VecF64` value as `&[f64]`. Panics on type mismatch.
749    #[inline]
750    pub fn as_vec_f64(&self) -> &[f64] {
751        match self {
752            Value::VecF64(arc) => arc,
753            _ => panic!("expected VecF64, got {}", self.type_name()),
754        }
755    }
756
757    /// Borrow a `VecI64` value as `&[i64]`. Panics on type mismatch.
758    #[inline]
759    pub fn as_vec_i64(&self) -> &[i64] {
760        match self {
761            Value::VecI64(arc) => arc,
762            _ => panic!("expected VecI64, got {}", self.type_name()),
763        }
764    }
765
766    /// Borrow a `VecF16` value as `&[half::f16]`. Panics on type mismatch.
767    #[inline]
768    pub fn as_vec_f16(&self) -> &[half::f16] {
769        match self {
770            Value::VecF16(arc) => arc,
771            _ => panic!("expected VecF16, got {}", self.type_name()),
772        }
773    }
774
775    /// Borrow a `VecI16` value as `&[i16]`. Panics on type mismatch.
776    #[inline]
777    pub fn as_vec_i16(&self) -> &[i16] {
778        match self {
779            Value::VecI16(arc) => arc,
780            _ => panic!("expected VecI16, got {}", self.type_name()),
781        }
782    }
783
784    /// Borrow a `VecI8` value as `&[i8]`. Panics on type mismatch.
785    #[inline]
786    pub fn as_vec_i8(&self) -> &[i8] {
787        match self {
788            Value::VecI8(arc) => arc,
789            _ => panic!("expected VecI8, got {}", self.type_name()),
790        }
791    }
792
793    /// Downcast a Handle value to a borrowed reference of its concrete
794    /// type. Panics if the variant isn't `Handle` or the type doesn't
795    /// match. Used by reader nodes that consume a typed-handle wire
796    /// produced by a resolver node (see type_system.md §1.8).
797    ///
798    /// The borrow lasts as long as `self` (the buffer slot's `Value`
799    /// is what holds the `Arc`). For per-cycle reads this is the
800    /// expected pattern — call methods on the borrowed dataset, then
801    /// return.
802    #[inline]
803    pub fn as_handle<T: std::any::Any + Send + Sync>(&self) -> &T {
804        match self {
805            Value::Handle(arc) => arc.downcast_ref::<T>().unwrap_or_else(|| {
806                panic!(
807                    "Handle downcast failed: expected {}",
808                    std::any::type_name::<T>()
809                )
810            }),
811            _ => panic!("expected Handle, got {}", self.type_name()),
812        }
813    }
814
815    /// Construct a `Value::Handle` from a typed `Arc<T>`. Convenience
816    /// wrapper that performs the type-erasure to `Arc<dyn Any + Send + Sync>`.
817    pub fn handle<T: std::any::Any + Send + Sync>(arc: Arc<T>) -> Self {
818        Value::Handle(arc as Arc<dyn std::any::Any + Send + Sync>)
819    }
820
821    /// Best-effort string representation for any value.
822    /// Works across all variants including Ext.
823    pub fn to_display_string(&self) -> String {
824        match self {
825            Value::U64(v) => v.to_string(),
826            Value::I64(v) => v.to_string(),
827            Value::U128(b) => b.as_u128().to_string(),
828            Value::I128(b) => b.as_i128().to_string(),
829            // Lane-typed register views render like the Vec*
830            // display forms; the raw view renders as 32 hex
831            // digits (the full word as buffer state).
832            Value::Reg128(b, view) => match view {
833                RegLanes::Raw => format!("{:032x}", b.as_u128()),
834                RegLanes::I8x16 => format!("{:?}", b.lanes_i8()),
835                RegLanes::I16x8 => format!("{:?}", b.lanes_i16()),
836                RegLanes::I32x4 => format!("{:?}", b.lanes_i32()),
837                RegLanes::I64x2 => format!("{:?}", b.lanes_i64()),
838                RegLanes::F16x8 => format!("{:?}", b.lanes_f16().map(|f| f.to_f32())),
839                RegLanes::F32x4 => format!("{:?}", b.lanes_f32()),
840                RegLanes::F64x2 => format!("{:?}", b.lanes_f64()),
841            },
842            // `{v:?}` (Rust Debug) for f64 always includes at
843            // least one fractional digit, so whole-number floats
844            // render as `1.0` instead of `1` — distinguishing
845            // them from integers in CQL OPTIONS strings, plot
846            // labels, and other surfaces where the type matters.
847            // Display-formatted (`v.to_string()`) strips the
848            // trailing zero, conflating ints with whole-number
849            // floats. Both forms produce identical output for
850            // non-whole floats (`1.5 → "1.5"`).
851            Value::F64(v) => format!("{v:?}"),
852            Value::Bool(v) => v.to_string(),
853            Value::Str(v) => v.to_string(),
854            Value::Bytes(v) => v.iter().map(|b| format!("{b:02x}")).collect(),
855            Value::Json(v) => v.to_string(),
856            Value::Ext(v) => v.display(),
857            Value::Handle(arc) => format!("<handle:{:?}>", arc.type_id()),
858            Value::VecF32(arc) => {
859                // JSON-array text. Per-element format-write into a
860                // pre-sized String avoids the intermediate Vec<String>.
861                // Debug formatter (`{v:?}`) matches the F64 element
862                // rule above: whole-number floats render as `1.0`
863                // so VecF32 stays distinguishable from VecI32 at the
864                // display surface.
865                let mut s = String::with_capacity(arc.len() * 8 + 2);
866                s.push('[');
867                let mut first = true;
868                for v in arc.iter() {
869                    if !first {
870                        s.push(',');
871                    }
872                    first = false;
873                    use std::fmt::Write;
874                    let _ = write!(&mut s, "{v:?}");
875                }
876                s.push(']');
877                s
878            }
879            Value::VecI32(arc) => {
880                let mut s = String::with_capacity(arc.len() * 4 + 2);
881                s.push('[');
882                let mut first = true;
883                for v in arc.iter() {
884                    if !first {
885                        s.push(',');
886                    }
887                    first = false;
888                    use std::fmt::Write;
889                    let _ = write!(&mut s, "{v}");
890                }
891                s.push(']');
892                s
893            }
894            Value::VecF64(arc) => {
895                let mut s = String::with_capacity(arc.len() * 8 + 2);
896                s.push('[');
897                let mut first = true;
898                for v in arc.iter() {
899                    if !first {
900                        s.push(',');
901                    }
902                    first = false;
903                    use std::fmt::Write;
904                    let _ = write!(&mut s, "{v:?}");
905                }
906                s.push(']');
907                s
908            }
909            Value::VecI64(arc) => {
910                let mut s = String::with_capacity(arc.len() * 4 + 2);
911                s.push('[');
912                let mut first = true;
913                for v in arc.iter() {
914                    if !first {
915                        s.push(',');
916                    }
917                    first = false;
918                    use std::fmt::Write;
919                    let _ = write!(&mut s, "{v}");
920                }
921                s.push(']');
922                s
923            }
924            Value::VecF16(arc) => {
925                let mut s = String::with_capacity(arc.len() * 6 + 2);
926                s.push('[');
927                let mut first = true;
928                for v in arc.iter() {
929                    if !first {
930                        s.push(',');
931                    }
932                    first = false;
933                    use std::fmt::Write;
934                    // Render as the f32 widening so the JSON form
935                    // is the standard "1.0" / "1.5" surface — f16
936                    // Display has its own form but it isn't valid
937                    // JSON, so widening makes the array shape
938                    // parseable downstream.
939                    let _ = write!(&mut s, "{:?}", v.to_f32());
940                }
941                s.push(']');
942                s
943            }
944            Value::VecI16(arc) => {
945                let mut s = String::with_capacity(arc.len() * 4 + 2);
946                s.push('[');
947                let mut first = true;
948                for v in arc.iter() {
949                    if !first {
950                        s.push(',');
951                    }
952                    first = false;
953                    use std::fmt::Write;
954                    let _ = write!(&mut s, "{v}");
955                }
956                s.push(']');
957                s
958            }
959            Value::VecI8(arc) => {
960                let mut s = String::with_capacity(arc.len() * 4 + 2);
961                s.push('[');
962                let mut first = true;
963                for v in arc.iter() {
964                    if !first {
965                        s.push(',');
966                    }
967                    first = false;
968                    use std::fmt::Write;
969                    let _ = write!(&mut s, "{v}");
970                }
971                s.push(']');
972                s
973            }
974            Value::None => String::new(),
975        }
976    }
977
978    /// Strict-render variant of [`Self::to_display_string`] for use
979    /// at wire-protocol render sites (op-template substitution,
980    /// adapter byte-emission paths).
981    ///
982    /// Returns `None` for [`Value::None`] instead of converting it
983    /// to `""`. The empty-string mapping in `to_display_string` is
984    /// convenient for diagnostic / log contexts but lethal at the
985    /// wire boundary — it silently coerces "absent" into "present
986    /// but empty," corrupting downstream bytes (e.g. sending
987    /// `'source_model': ''` to a CQL cluster when the intended
988    /// shadow didn't bind). Render paths use this primitive and
989    /// surface a clear error when an unresolved bind-point reaches
990    /// them. See `crates/polydat/docs/design/none_semantics.md`
991    /// (the render-refuses-silent-None rule).
992    pub fn to_display_strict(&self) -> Option<String> {
993        match self {
994            Value::None => None,
995            other => Some(other.to_display_string()),
996        }
997    }
998
999    /// JSON representation for any value. Works across all variants.
1000    pub fn to_json_value(&self) -> serde_json::Value {
1001        match self {
1002            Value::U64(v) => serde_json::Value::from(*v),
1003            Value::I64(v) => serde_json::Value::from(*v),
1004            // JSON Number is bounded by u64/i64/f64 leaves
1005            // (serde_json without arbitrary_precision); 128-bit
1006            // magnitudes project as decimal strings, the same
1007            // string-convention family as Bytes-as-hex.
1008            Value::U128(b) => serde_json::Value::String(b.as_u128().to_string()),
1009            Value::I128(b) => serde_json::Value::String(b.as_i128().to_string()),
1010            // Lane-typed views project as homogeneous arrays
1011            // (same shape as the matching Vec*); the raw view as
1012            // a hex string (lane roles are algorithm-defined, so
1013            // no numeric reading exists).
1014            Value::Reg128(b, view) => match view {
1015                RegLanes::Raw => serde_json::Value::String(format!("{:032x}", b.as_u128())),
1016                RegLanes::I8x16 => serde_json::Value::Array(
1017                    b.lanes_i8()
1018                        .iter()
1019                        .map(|i| serde_json::Value::from(*i as i32))
1020                        .collect(),
1021                ),
1022                RegLanes::I16x8 => serde_json::Value::Array(
1023                    b.lanes_i16()
1024                        .iter()
1025                        .map(|i| serde_json::Value::from(*i as i32))
1026                        .collect(),
1027                ),
1028                RegLanes::I32x4 => serde_json::Value::Array(
1029                    b.lanes_i32()
1030                        .iter()
1031                        .map(|i| serde_json::Value::from(*i))
1032                        .collect(),
1033                ),
1034                RegLanes::I64x2 => serde_json::Value::Array(
1035                    b.lanes_i64()
1036                        .iter()
1037                        .map(|i| serde_json::Value::from(*i))
1038                        .collect(),
1039                ),
1040                RegLanes::F16x8 => serde_json::Value::Array(
1041                    b.lanes_f16()
1042                        .iter()
1043                        .map(|f| serde_json::json!(f.to_f32()))
1044                        .collect(),
1045                ),
1046                RegLanes::F32x4 => serde_json::Value::Array(
1047                    b.lanes_f32()
1048                        .iter()
1049                        .map(|f| serde_json::json!(*f))
1050                        .collect(),
1051                ),
1052                RegLanes::F64x2 => serde_json::Value::Array(
1053                    b.lanes_f64()
1054                        .iter()
1055                        .map(|f| serde_json::json!(*f))
1056                        .collect(),
1057                ),
1058            },
1059            Value::F64(v) => serde_json::json!(*v),
1060            Value::Bool(v) => serde_json::Value::from(*v),
1061            Value::Str(v) => serde_json::Value::from(&**v),
1062            Value::Bytes(v) => {
1063                serde_json::Value::from(v.iter().map(|b| format!("{b:02x}")).collect::<String>())
1064            }
1065            Value::Json(v) => (**v).clone(),
1066            Value::Ext(v) => v.to_json_value(),
1067            Value::Handle(_) => serde_json::Value::Null,
1068            Value::VecF32(arc) => {
1069                serde_json::Value::Array(arc.iter().map(|f| serde_json::json!(*f)).collect())
1070            }
1071            Value::VecI32(arc) => {
1072                serde_json::Value::Array(arc.iter().map(|i| serde_json::Value::from(*i)).collect())
1073            }
1074            Value::VecF64(arc) => {
1075                serde_json::Value::Array(arc.iter().map(|f| serde_json::json!(*f)).collect())
1076            }
1077            Value::VecI64(arc) => {
1078                serde_json::Value::Array(arc.iter().map(|i| serde_json::Value::from(*i)).collect())
1079            }
1080            Value::VecF16(arc) => serde_json::Value::Array(
1081                arc.iter().map(|f| serde_json::json!(f.to_f32())).collect(),
1082            ),
1083            Value::VecI16(arc) => serde_json::Value::Array(
1084                arc.iter()
1085                    .map(|i| serde_json::Value::from(*i as i32))
1086                    .collect(),
1087            ),
1088            Value::VecI8(arc) => serde_json::Value::Array(
1089                arc.iter()
1090                    .map(|i| serde_json::Value::from(*i as i32))
1091                    .collect(),
1092            ),
1093            Value::None => serde_json::Value::Null,
1094        }
1095    }
1096}
1097
1098pub use polydat_grammar::{NumericDomain, PortType};
1099
1100/// What a port type means to a compiled buffer: its slot color, the
1101/// width that follows from it, and the scratch element a by-reference
1102/// producer owns. The type itself is the grammar's
1103/// (`polydat_grammar::PortType`); these are the runtime's reading of
1104/// it, and every layout, codegen, and guard decision derives from
1105/// them.
1106pub trait SlotShape {
1107    /// Slot color in compiled (P2/P3/hybrid) kernel buffers —
1108    /// axiom S1 (`jit_boundary.md` §"Slot-state axioms"). The
1109    /// single chokepoint: width and every layout/codegen/guard
1110    /// decision derive from this, never restate it.
1111    fn slot_color(&self) -> SlotColor;
1112    /// The scratch element a `Ref2`-colored port's producer owns
1113    /// (axiom S3); `None` for an immediate color.
1114    fn scratch_elem(&self) -> Option<ScratchElem>;
1115    /// Buffer slots this type occupies — derived from
1116    /// [`Self::slot_color`] per axiom S1.
1117    fn slot_width(&self) -> usize;
1118}
1119
1120impl SlotShape for PortType {
1121    #[inline]
1122    fn slot_color(&self) -> SlotColor {
1123        match self {
1124            // 128-bit immediates: two slots of limb DATA —
1125            // register words and 128-bit integers are values,
1126            // never addresses.
1127            Self::U128
1128            | Self::I128
1129            | Self::Reg128
1130            | Self::RegI8x16
1131            | Self::RegI16x8
1132            | Self::RegI32x4
1133            | Self::RegI64x2
1134            | Self::RegF16x8
1135            | Self::RegF32x4
1136            | Self::RegF64x2 => SlotColor::Imm2,
1137            // Heap slices: a (ptr, len) reference pair viewing
1138            // kernel-owned scratch (§8.4 layer 3). A string and a
1139            // byte string are slices of bytes; a JSON, extension, or
1140            // handle value is a one-element slice holding the value.
1141            Self::VecF32
1142            | Self::VecI32
1143            | Self::VecF64
1144            | Self::VecI64
1145            | Self::VecF16
1146            | Self::VecI16
1147            | Self::VecI8
1148            | Self::Str
1149            | Self::Bytes
1150            | Self::Json
1151            | Self::Ext
1152            | Self::Handle
1153            | Self::Dyn => SlotColor::Ref2,
1154            // Everything else (incl. all narrow widths riding
1155            // their 64-bit carriers): one slot of immediate data.
1156            _ => SlotColor::Imm1,
1157        }
1158    }
1159
1160    #[inline]
1161    fn scratch_elem(&self) -> Option<ScratchElem> {
1162        Some(match self {
1163            Self::VecF32 => ScratchElem::F32,
1164            Self::VecF64 => ScratchElem::F64,
1165            Self::VecF16 => ScratchElem::F16,
1166            Self::VecI8 => ScratchElem::I8,
1167            Self::VecI16 => ScratchElem::I16,
1168            Self::VecI32 => ScratchElem::I32,
1169            Self::VecI64 => ScratchElem::I64,
1170            Self::Str => ScratchElem::Str,
1171            Self::Bytes => ScratchElem::Bytes,
1172            Self::Json | Self::Ext | Self::Handle | Self::Dyn => ScratchElem::Value,
1173            _ => return None,
1174        })
1175    }
1176
1177    #[inline]
1178    fn slot_width(&self) -> usize {
1179        match self.slot_color() {
1180            SlotColor::Imm1 => 1,
1181            SlotColor::Imm2 | SlotColor::Ref2 => 2,
1182        }
1183    }
1184}
1185
1186/// The lifecycle of a port's value.
1187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1188pub enum Lifecycle {
1189    /// Cycle-time: value changes per evaluation.
1190    Cycle,
1191    /// Init-time: value is frozen at assembly, immutable at runtime.
1192    /// Wiring a cycle-time value to an init port is an assembly error.
1193    Init,
1194}
1195
1196/// Cost class for an input wire, indicating how expensive it is
1197/// to change the value on this port.
1198#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1199pub enum WireCost {
1200    /// Data wire: cheap per-cycle input. The node's primary
1201    /// computation path. Default for most ports.
1202    #[default]
1203    Data,
1204    /// Config wire: changing this input invalidates expensive
1205    /// internal state (LUT, distribution table). Expected to be
1206    /// wired to init-time constants or rarely-changing values.
1207    /// The compiler warns when a config wire connects to a
1208    /// cycle-time binding.
1209    Config,
1210}
1211
1212/// Descriptor for a single input or output port on a node.
1213#[derive(Debug, Clone)]
1214pub struct Port {
1215    /// The port's name, as bindings and diagnostics refer to it.
1216    pub name: String,
1217    /// The port's declared type.
1218    pub typ: PortType,
1219    /// When the port's value changes: per cycle, at init, or as configuration.
1220    pub lifecycle: Lifecycle,
1221    /// Cost class for input ports. Ignored for output ports.
1222    pub wire_cost: WireCost,
1223    /// Optional value contract this wire must satisfy at runtime
1224    /// (graph_compiler.md §2, strict-wire assertions). The compiler uses this to
1225    /// decide whether to auto-insert a value assertion when the
1226    /// upstream source can't statically be proven to deliver a
1227    /// satisfying value. `None` = no constraint declared.
1228    ///
1229    /// Constraints reuse the same vocabulary as
1230    /// [`crate::dsl::const_constraints::ConstConstraint`] — the
1231    /// difference is just where the value comes from (a literal
1232    /// for `ConstU64`, a wire for `Slot::Wire`).
1233    pub constraint: Option<crate::dsl::const_constraints::ConstConstraint>,
1234    /// Whether this port takes the wire's value as it is, whatever
1235    /// type the wire carries — in which case [`Self::typ`] is a
1236    /// nominal placeholder and the assembler inserts no adapter into
1237    /// this port.
1238    ///
1239    /// The one shape that needs it is an element of a `&[Value]`
1240    /// variadic: the node inspects the `Value` variant itself, so
1241    /// converting the wire to the port's declared type would change
1242    /// what the node sees — `json_array(cycle)` would hold the text
1243    /// of a number rather than the number. A plain `Value` argument
1244    /// does not need it, because the assembler resolves that port's
1245    /// type from its wire and hands it to the constructor.
1246    ///
1247    /// The flag is per port rather than per node or per node name:
1248    /// `pick`'s selector wires must be `Bool` while its value wires are
1249    /// polymorphic, and one flag per node cannot say that.
1250    pub accepts_any_type: bool,
1251}
1252
1253impl Port {
1254    /// A cycle-lifecycle port of the given type with no constraint.
1255    pub fn new(name: impl Into<String>, typ: PortType) -> Self {
1256        Self {
1257            name: name.into(),
1258            typ,
1259            lifecycle: Lifecycle::Cycle,
1260            wire_cost: WireCost::Data,
1261            constraint: None,
1262            accepts_any_type: false,
1263        }
1264    }
1265
1266    /// This port, taking the wire as it is whatever its type. See
1267    /// [`Self::accepts_any_type`].
1268    pub fn any_type(mut self) -> Self {
1269        self.accepts_any_type = true;
1270        self
1271    }
1272
1273    /// Create a port with explicit lifecycle.
1274    pub fn with_lifecycle(name: impl Into<String>, typ: PortType, lifecycle: Lifecycle) -> Self {
1275        Self {
1276            name: name.into(),
1277            typ,
1278            lifecycle,
1279            wire_cost: WireCost::Data,
1280            constraint: None,
1281            accepts_any_type: false,
1282        }
1283    }
1284
1285    /// A `u64` port.
1286    pub fn u64(name: impl Into<String>) -> Self {
1287        Self::new(name, PortType::U64)
1288    }
1289
1290    /// An `f64` port.
1291    pub fn f64(name: impl Into<String>) -> Self {
1292        Self::new(name, PortType::F64)
1293    }
1294
1295    /// A string port.
1296    pub fn str(name: impl Into<String>) -> Self {
1297        Self::new(name, PortType::Str)
1298    }
1299
1300    /// A boolean port.
1301    pub fn bool(name: impl Into<String>) -> Self {
1302        Self::new(name, PortType::Bool)
1303    }
1304
1305    /// A JSON port.
1306    pub fn json(name: impl Into<String>) -> Self {
1307        Self::new(name, PortType::Json)
1308    }
1309
1310    /// A handle port.
1311    pub fn handle(name: impl Into<String>) -> Self {
1312        Self::new(name, PortType::Handle)
1313    }
1314
1315    /// An `f32` vector port.
1316    pub fn vec_f32(name: impl Into<String>) -> Self {
1317        Self::new(name, PortType::VecF32)
1318    }
1319
1320    /// An `i32` vector port.
1321    pub fn vec_i32(name: impl Into<String>) -> Self {
1322        Self::new(name, PortType::VecI32)
1323    }
1324
1325    /// Create an init-time port (frozen at assembly).
1326    pub fn init(name: impl Into<String>, typ: PortType) -> Self {
1327        Self::with_lifecycle(name, typ, Lifecycle::Init)
1328    }
1329
1330    /// Attach a value constraint. Used by node authors that want
1331    /// to declare "this wire must satisfy X" so strict-wire-mode
1332    /// can auto-insert the right value assertion. See
1333    /// graph_compiler.md §2.
1334    pub fn with_constraint(mut self, c: crate::dsl::const_constraints::ConstConstraint) -> Self {
1335        self.constraint = Some(c);
1336        self
1337    }
1338
1339    /// Mark this port as a config wire (expensive to change).
1340    pub fn config(mut self) -> Self {
1341        self.wire_cost = WireCost::Config;
1342        self
1343    }
1344
1345    /// Set the wire cost directly. Used by the macro to thread
1346    /// `Wire::WIRE_COST` from the trait through to the slot.
1347    pub fn with_cost(mut self, cost: WireCost) -> Self {
1348        self.wire_cost = cost;
1349        self
1350    }
1351}
1352
1353// ---------------------------------------------------------------------------
1354// Unified slot model (library_catalog.md, "Shapes")
1355// ---------------------------------------------------------------------------
1356
1357/// The type discriminant for a slot: wire or typed constant.
1358///
1359/// This is the shared vocabulary between `FuncSig` (static registry)
1360/// and `NodeMeta` (owned instance). It replaces the former `ParamKind`,
1361/// `ConstType`, and `SlotKind` enums with a single type.
1362#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1363pub enum SlotType {
1364    /// A runtime wire input carrying a value each cycle.
1365    Wire,
1366    /// A u64 constant literal.
1367    ConstU64,
1368    /// An f64 constant literal.
1369    ConstF64,
1370    /// A string constant literal.
1371    ConstStr,
1372    /// A `Vec<u64>` constant (from array literal).
1373    ConstVecU64,
1374    /// A `Vec<f64>` constant (from array literal).
1375    ConstVecF64,
1376    /// Typed-element variadic-const slot for the
1377    /// `Const<Vec<C>>` operator-side shape. Element type
1378    /// discrimination is emitted inline by the macro at the
1379    /// build-closure call site, from the element type it read out of
1380    /// the signature; the slot tag only signals "this is a list" to
1381    /// the DSL type-checker.
1382    ConstVec,
1383}
1384
1385impl SlotType {
1386    /// Whether this is a constant (not a wire).
1387    pub fn is_const(self) -> bool {
1388        !matches!(self, SlotType::Wire)
1389    }
1390
1391    /// Whether this is a wire (not a constant).
1392    pub fn is_wire(self) -> bool {
1393        matches!(self, SlotType::Wire)
1394    }
1395}
1396
1397/// A concrete constant value stored in node metadata.
1398///
1399/// Assembly-time values baked into the node at construction. The
1400/// variant determines the `SlotType` — no separate type discriminant
1401/// is needed.
1402#[derive(Debug, Clone, PartialEq)]
1403pub enum ConstValue {
1404    /// An unsigned integer.
1405    U64(u64),
1406    /// A floating-point number.
1407    F64(f64),
1408    /// A string.
1409    Str(String),
1410    /// A list of unsigned integers.
1411    VecU64(Vec<u64>),
1412    /// A list of floating-point numbers.
1413    VecF64(Vec<f64>),
1414}
1415
1416impl ConstValue {
1417    /// Return the `SlotType` for this value.
1418    pub fn slot_type(&self) -> SlotType {
1419        match self {
1420            ConstValue::U64(_) => SlotType::ConstU64,
1421            ConstValue::F64(_) => SlotType::ConstF64,
1422            ConstValue::Str(_) => SlotType::ConstStr,
1423            ConstValue::VecU64(_) => SlotType::ConstVecU64,
1424            ConstValue::VecF64(_) => SlotType::ConstVecF64,
1425        }
1426    }
1427
1428    /// Encode to the JIT's u64 representation.
1429    pub fn to_jit_u64s(&self) -> Vec<u64> {
1430        match self {
1431            ConstValue::U64(v) => vec![*v],
1432            ConstValue::F64(v) => vec![v.to_bits()],
1433            ConstValue::Str(_) => vec![],
1434            ConstValue::VecU64(v) => v.clone(),
1435            ConstValue::VecF64(v) => v.iter().map(|f| f.to_bits()).collect(),
1436        }
1437    }
1438}
1439
1440/// A single logical input to a node: either a runtime wire or an
1441/// assembly-time constant. The positional order in `NodeMeta.slots`
1442/// matches the function call syntax in the DSL.
1443#[derive(Debug, Clone)]
1444pub enum Slot {
1445    /// A runtime wire input carrying a value each cycle.
1446    Wire(Port),
1447    /// An assembly-time constant, baked into the node at construction.
1448    Const {
1449        /// The constant's name, as the node's signature calls it.
1450        name: String,
1451        /// The baked value.
1452        value: ConstValue,
1453    },
1454}
1455
1456impl Slot {
1457    /// Return the `SlotType` discriminant for this slot.
1458    pub fn slot_type(&self) -> SlotType {
1459        match self {
1460            Slot::Wire(_) => SlotType::Wire,
1461            Slot::Const { value, .. } => value.slot_type(),
1462        }
1463    }
1464
1465    /// Create a wire slot.
1466    pub fn wire(port: Port) -> Self {
1467        Slot::Wire(port)
1468    }
1469
1470    /// Create a u64 constant slot.
1471    pub fn const_u64(name: impl Into<String>, v: u64) -> Self {
1472        Slot::Const {
1473            name: name.into(),
1474            value: ConstValue::U64(v),
1475        }
1476    }
1477
1478    /// Create an f64 constant slot.
1479    pub fn const_f64(name: impl Into<String>, v: f64) -> Self {
1480        Slot::Const {
1481            name: name.into(),
1482            value: ConstValue::F64(v),
1483        }
1484    }
1485
1486    /// Create a string constant slot.
1487    pub fn const_str(name: impl Into<String>, v: impl Into<String>) -> Self {
1488        Slot::Const {
1489            name: name.into(),
1490            value: ConstValue::Str(v.into()),
1491        }
1492    }
1493
1494    /// Create a `Vec<u64>` constant slot.
1495    pub fn const_vec_u64(name: impl Into<String>, v: Vec<u64>) -> Self {
1496        Slot::Const {
1497            name: name.into(),
1498            value: ConstValue::VecU64(v),
1499        }
1500    }
1501
1502    /// Create a `Vec<f64>` constant slot.
1503    pub fn const_vec_f64(name: impl Into<String>, v: Vec<f64>) -> Self {
1504        Slot::Const {
1505            name: name.into(),
1506            value: ConstValue::VecF64(v),
1507        }
1508    }
1509}
1510
1511/// Declares which inputs of a node are interchangeable.
1512///
1513/// Used by the fusion pattern matcher to recognize equivalent
1514/// subgraphs regardless of operand order, and by future passes
1515/// (e.g., canonical ordering, common subexpression elimination).
1516#[derive(Debug, Clone, PartialEq, Eq, Default)]
1517pub enum Commutativity {
1518    /// Input order matters. No permutations attempted during
1519    /// pattern matching. This is the default for unary nodes and
1520    /// any node where operand order affects the result.
1521    ///
1522    /// Examples: `mod(dividend, divisor)`, `div(x, K)`,
1523    /// `concat(left, right)`, `sub(a, b)`.
1524    #[default]
1525    Positional,
1526
1527    /// All inputs are interchangeable, including variadic.
1528    /// For small arity (2-3), the matcher tries all permutations.
1529    /// For larger arity, it uses set-matching.
1530    ///
1531    /// Examples: `sum(a, b, ..., n)`, `product(a, b, ..., n)`,
1532    /// `min(a, b, ..., n)`, `max(a, b, ..., n)`.
1533    AllCommutative,
1534
1535    /// Specific groups of input port indices are interchangeable
1536    /// within each group. Inputs not listed in any group are
1537    /// positional.
1538    ///
1539    /// Example: `fma(x, y, z) = x + y * z`
1540    /// The multiplicands `y` (index 1) and `z` (index 2) commute,
1541    /// but the addend `x` (index 0) does not.
1542    /// `Groups(vec![vec![1, 2]])`
1543    Groups(Vec<Vec<usize>>),
1544}
1545
1546/// Metadata describing a node's interface: its input slots and output ports.
1547///
1548/// Generated per-node-type and queryable at runtime for assembly-time
1549/// validation, compilation, optimization passes, and describe output.
1550///
1551/// Wire inputs are `Slot::Wire(Port)`. Constants are `Slot::Const { name, value }`.
1552/// Use `wire_inputs()` to extract just the wire ports.
1553#[derive(Debug, Clone)]
1554pub struct NodeMeta {
1555    /// The node's function name, as programs call it.
1556    pub name: String,
1557    /// All inputs in positional order: wires and constants.
1558    pub ins: Vec<Slot>,
1559    /// The output ports, in positional order.
1560    pub outs: Vec<Port>,
1561}
1562
1563impl NodeMeta {
1564    /// Wire-only input ports extracted from `ins`.
1565    pub fn wire_inputs(&self) -> Vec<&Port> {
1566        self.ins
1567            .iter()
1568            .filter_map(|s| match s {
1569                Slot::Wire(p) => Some(p),
1570                Slot::Const { .. } => None,
1571            })
1572            .collect()
1573    }
1574
1575    /// Constant names and values extracted from `ins`.
1576    pub fn const_slots(&self) -> Vec<(&str, &ConstValue)> {
1577        self.ins
1578            .iter()
1579            .filter_map(|s| match s {
1580                Slot::Const { name, value } => Some((name.as_str(), value)),
1581                Slot::Wire(_) => None,
1582            })
1583            .collect()
1584    }
1585
1586    /// Encode all constants from `ins` to JIT u64 representation.
1587    pub fn jit_constants_from_slots(&self) -> Vec<u64> {
1588        self.const_slots()
1589            .iter()
1590            .flat_map(|(_, v)| v.to_jit_u64s())
1591            .collect()
1592    }
1593}
1594
1595/// A compiled u64-only evaluation step.
1596///
1597/// The closure captures all assembly-time parameters. At runtime it
1598/// reads from input slots and writes to output slots in a flat `[u64]`
1599/// buffer — no `Value` enum, no virtual dispatch.
1600pub type CompiledU64Op = Box<dyn Fn(&[u64], &mut [u64]) + Send + Sync>;
1601
1602/// Element type of one kernel-owned scratch buffer
1603/// (type_system_alignment.md §8.4 layer 3). One entry per
1604/// `Ref2`-colored output port of a slot-compiled node: a typed
1605/// vector, a string, a byte string, or a value held by reference.
1606#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1607pub enum ScratchElem {
1608    /// `f32` elements.
1609    F32,
1610    /// `f64` elements.
1611    F64,
1612    /// `f16` elements.
1613    F16,
1614    /// `i8` elements.
1615    I8,
1616    /// `i16` elements.
1617    I16,
1618    /// `i32` elements.
1619    I32,
1620    /// `i64` elements.
1621    I64,
1622    /// The UTF-8 bytes of a string.
1623    Str,
1624    /// The bytes of a byte string.
1625    Bytes,
1626    /// One value held by reference (`Json`, `Ext`, `Handle`): the
1627    /// pair is `(&Value, 1)`.
1628    Value,
1629    /// A buffer of 64-bit slots: a native cone's own slot buffer,
1630    /// owned by the state that evaluates it.
1631    Slots,
1632    /// The kernels a tile render keeps over its projection bodies,
1633    /// owned by the state that renders.
1634    Kernels,
1635    /// State a node defines for itself per evaluating kernel state, a
1636    /// memo of what it last derived from its inputs, created by the
1637    /// node on first use; a clone starts empty.
1638    State,
1639}
1640
1641/// Node-defined state held by a kernel state (`ScratchElem::State`):
1642/// what a node keeps between its evaluations in one state, typed by
1643/// the node and never shared between states. Empty until the node
1644/// first fills it; a clone is empty, since a clone of a state is a
1645/// new state (compiled_handles.md §3).
1646#[derive(Default)]
1647pub struct NodeState(Option<Box<dyn std::any::Any + Send + Sync>>);
1648
1649impl NodeState {
1650    /// The state as `T`, created by `init` when the entry is empty or
1651    /// holds another type.
1652    pub fn get_or_insert_with<T: std::any::Any + Send + Sync>(
1653        &mut self,
1654        init: impl FnOnce() -> T,
1655    ) -> &mut T {
1656        if !self.0.as_ref().is_some_and(|b| b.is::<T>()) {
1657            self.0 = Some(Box::new(init()));
1658        }
1659        self.0
1660            .as_mut()
1661            .and_then(|b| b.downcast_mut::<T>())
1662            .expect("the entry holds a T")
1663    }
1664
1665    /// The state as `T`, if the node has filled it with one.
1666    pub fn get<T: std::any::Any + Send + Sync>(&self) -> Option<&T> {
1667        self.0.as_ref().and_then(|b| b.downcast_ref::<T>())
1668    }
1669}
1670
1671impl Clone for NodeState {
1672    fn clone(&self) -> Self {
1673        NodeState(None)
1674    }
1675}
1676
1677impl std::fmt::Debug for NodeState {
1678    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1679        write!(
1680            f,
1681            "NodeState({})",
1682            if self.0.is_some() { "filled" } else { "empty" }
1683        )
1684    }
1685}
1686
1687/// Slot color of a `PortType` in compiled kernel buffers —
1688/// axiom S1: static, total, three-valued. `Imm*` slots carry
1689/// immediate data only (never addresses); `Ref2` pairs carry a
1690/// `(ptr, len)` reference to storage with a proven owner: the
1691/// step's own scratch, an extern's stored value, an interned
1692/// constant, or a boundary value alive for the call. They are
1693/// engine-internal per axiom S2.
1694#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1695pub enum SlotColor {
1696    /// One slot of immediate data.
1697    Imm1,
1698    /// Two slots of immediate limb data (128-bit values).
1699    Imm2,
1700    /// Two slots holding a (ptr, len) reference pair.
1701    Ref2,
1702}
1703
1704/// One kernel-owned scratch buffer. A `Ref2` output port's
1705/// `(ptr, len)` buffer slots view its scratch — the kernel owns
1706/// the allocation, so the pointer is valid exactly as long as the
1707/// producing step doesn't rerun (and a rerun rewrites the slots
1708/// before any consumer reads them). No Arc traffic, no allocation
1709/// after warmup: a string or byte string is rewritten in place, a
1710/// value is replaced.
1711#[derive(Debug, Clone)]
1712pub enum ScratchBuf {
1713    /// An `f32` buffer.
1714    F32(Vec<f32>),
1715    /// An `f64` buffer.
1716    F64(Vec<f64>),
1717    /// An `f16` buffer.
1718    F16(Vec<half::f16>),
1719    /// An `i8` buffer.
1720    I8(Vec<i8>),
1721    /// An `i16` buffer.
1722    I16(Vec<i16>),
1723    /// An `i32` buffer.
1724    I32(Vec<i32>),
1725    /// An `i64` buffer.
1726    I64(Vec<i64>),
1727    /// The UTF-8 bytes of a string.
1728    Str(Vec<u8>),
1729    /// The bytes of a byte string.
1730    Bytes(Vec<u8>),
1731    /// One value held by reference; empty until the step first runs.
1732    Value(Vec<Value>),
1733    /// A buffer of 64-bit slots (a native cone's own).
1734    Slots(Vec<u64>),
1735    /// The kernels a tile render keeps over its projection bodies. A
1736    /// clone is empty: a new state builds its own.
1737    Kernels(crate::library::tile_render::BodyKernels),
1738    /// State a node defines for itself, per kernel state. A clone is
1739    /// empty: a new state derives its own.
1740    State(NodeState),
1741}
1742
1743impl ScratchBuf {
1744    /// The `(ptr, len)` pair this entry currently publishes —
1745    /// the ground truth axiom S9(a)'s validator compares buffer
1746    /// slots against.
1747    pub fn ptr_len(&self) -> (u64, u64) {
1748        match self {
1749            ScratchBuf::F32(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1750            ScratchBuf::F64(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1751            ScratchBuf::F16(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1752            ScratchBuf::I8(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1753            ScratchBuf::I16(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1754            ScratchBuf::I32(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1755            ScratchBuf::I64(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1756            ScratchBuf::Str(v) | ScratchBuf::Bytes(v) => {
1757                (v.as_ptr() as usize as u64, v.len() as u64)
1758            }
1759            ScratchBuf::Value(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1760            ScratchBuf::Slots(v) => (v.as_ptr() as usize as u64, v.len() as u64),
1761            ScratchBuf::Kernels(_) | ScratchBuf::State(_) => (0, 0),
1762        }
1763    }
1764
1765    /// What this entry holds as an owned `Value`, copied out: the
1766    /// typed read of a `Ref2` output on a compiled kernel, which is
1767    /// what the interpreter's `pull` returns for the same port. A
1768    /// value entry that has not been written reads as `None`.
1769    pub fn to_value(&self) -> Value {
1770        match self {
1771            ScratchBuf::F32(v) => Value::VecF32(SliceArc::from_vec(v.clone())),
1772            ScratchBuf::F64(v) => Value::VecF64(SliceArc::from_vec(v.clone())),
1773            ScratchBuf::F16(v) => Value::VecF16(SliceArc::from_vec(v.clone())),
1774            ScratchBuf::I8(v) => Value::VecI8(SliceArc::from_vec(v.clone())),
1775            ScratchBuf::I16(v) => Value::VecI16(SliceArc::from_vec(v.clone())),
1776            ScratchBuf::I32(v) => Value::VecI32(SliceArc::from_vec(v.clone())),
1777            ScratchBuf::I64(v) => Value::VecI64(SliceArc::from_vec(v.clone())),
1778            // SAFETY: a `Str` entry is written only from `&str` bytes.
1779            ScratchBuf::Str(v) => {
1780                Value::Str(Arc::from(unsafe { std::str::from_utf8_unchecked(v) }))
1781            }
1782            ScratchBuf::Bytes(v) => Value::Bytes(Arc::from(&v[..])),
1783            ScratchBuf::Value(v) => v.first().cloned().unwrap_or(Value::None),
1784            ScratchBuf::Slots(_) => panic!("a slot buffer is not a value"),
1785            ScratchBuf::Kernels(_) => panic!("a body kernel set is not a value"),
1786            ScratchBuf::State(_) => panic!("a node's own state is not a value"),
1787        }
1788    }
1789
1790    /// The node-defined state this entry holds. The entry must be a
1791    /// `State` entry.
1792    pub fn node_state(&mut self) -> &mut NodeState {
1793        match self {
1794            ScratchBuf::State(s) => s,
1795            other => panic!("scratch entry holds {other:?}, not a node's state"),
1796        }
1797    }
1798
1799    /// Replace the string this entry holds, reusing its allocation.
1800    /// The entry must be a `Str` entry.
1801    #[inline]
1802    pub fn set_str(&mut self, s: &str) {
1803        match self {
1804            ScratchBuf::Str(v) => {
1805                v.clear();
1806                v.extend_from_slice(s.as_bytes());
1807            }
1808            other => panic!("scratch entry holds {other:?}, not a string"),
1809        }
1810    }
1811
1812    /// Replace the byte string this entry holds, reusing its
1813    /// allocation. The entry must be a `Bytes` entry.
1814    #[inline]
1815    pub fn set_bytes(&mut self, b: &[u8]) {
1816        match self {
1817            ScratchBuf::Bytes(v) => {
1818                v.clear();
1819                v.extend_from_slice(b);
1820            }
1821            other => panic!("scratch entry holds {other:?}, not a byte string"),
1822        }
1823    }
1824
1825    /// Fill this entry from `v`, whatever kind of entry it is.
1826    ///
1827    /// The entry's own variant decides, and it was allocated from the
1828    /// step's declared [`ScratchElem`] — so the type the graph resolved
1829    /// picks the write, rather than a match over `Value` that has to be
1830    /// extended every time the language grows a carrier. A value that
1831    /// does not fit the entry is a graph that mis-typed the slot, and
1832    /// the inner setters say so.
1833    #[inline]
1834    pub fn set_from_value(&mut self, v: &Value) {
1835        match self {
1836            ScratchBuf::Str(_) => self.set_str(v.as_str()),
1837            ScratchBuf::Bytes(_) => self.set_bytes(v.as_bytes()),
1838            ScratchBuf::Value(_) => self.set_value(v.clone()),
1839            ScratchBuf::F32(_)
1840            | ScratchBuf::F64(_)
1841            | ScratchBuf::F16(_)
1842            | ScratchBuf::I8(_)
1843            | ScratchBuf::I16(_)
1844            | ScratchBuf::I32(_)
1845            | ScratchBuf::I64(_) => self.set_vector(v),
1846            other => {
1847                panic!("scratch entry holds {other:?}, which no by-reference value is written into")
1848            }
1849        }
1850    }
1851
1852    /// Replace the numeric vector this entry holds, reusing its
1853    /// allocation. The entry must be the matching element type.
1854    ///
1855    /// The typed write path reaches a vector entry through the node's
1856    /// declared element type; this is the same write for the path that
1857    /// only has a [`Value`] in hand ([`crate::derive_support::write_poly`]),
1858    /// which is how a vector reaches a polymorphic node's output.
1859    #[inline]
1860    pub fn set_vector(&mut self, value: &Value) {
1861        macro_rules! fill {
1862            ($v:expr, $src:expr) => {{
1863                $v.clear();
1864                $v.extend_from_slice($src);
1865            }};
1866        }
1867        match (self, value) {
1868            (ScratchBuf::F32(v), Value::VecF32(s)) => fill!(v, s.as_slice()),
1869            (ScratchBuf::F64(v), Value::VecF64(s)) => fill!(v, s.as_slice()),
1870            (ScratchBuf::F16(v), Value::VecF16(s)) => fill!(v, s.as_slice()),
1871            (ScratchBuf::I8(v), Value::VecI8(s)) => fill!(v, s.as_slice()),
1872            (ScratchBuf::I16(v), Value::VecI16(s)) => fill!(v, s.as_slice()),
1873            (ScratchBuf::I32(v), Value::VecI32(s)) => fill!(v, s.as_slice()),
1874            (ScratchBuf::I64(v), Value::VecI64(s)) => fill!(v, s.as_slice()),
1875            (entry, v) => panic!(
1876                "scratch entry holds {entry:?}, which does not carry a {:?}",
1877                v.port_type()
1878            ),
1879        }
1880    }
1881
1882    /// Replace the value this entry holds. The entry must be a
1883    /// `Value` entry.
1884    #[inline]
1885    pub fn set_value(&mut self, value: Value) {
1886        match self {
1887            ScratchBuf::Value(v) => {
1888                v.clear();
1889                v.push(value);
1890            }
1891            other => panic!("scratch entry holds {other:?}, not a value"),
1892        }
1893    }
1894
1895    /// Replace the value this entry holds with a by-reference value
1896    /// that may be `None`. A `None` leaves the entry empty, so its
1897    /// pair has length zero, which is how a `Ref2` slot reads `None`.
1898    /// The entry must be a `Value` entry.
1899    #[inline]
1900    pub fn set_ref_value(&mut self, value: Value) {
1901        match (self, value) {
1902            (ScratchBuf::Value(v), Value::None) => v.clear(),
1903            (entry, value) => entry.set_value(value),
1904        }
1905    }
1906
1907    /// An empty buffer of the element type.
1908    pub fn new(elem: ScratchElem) -> Self {
1909        match elem {
1910            ScratchElem::F32 => ScratchBuf::F32(Vec::new()),
1911            ScratchElem::F64 => ScratchBuf::F64(Vec::new()),
1912            ScratchElem::F16 => ScratchBuf::F16(Vec::new()),
1913            ScratchElem::I8 => ScratchBuf::I8(Vec::new()),
1914            ScratchElem::I16 => ScratchBuf::I16(Vec::new()),
1915            ScratchElem::I32 => ScratchBuf::I32(Vec::new()),
1916            ScratchElem::I64 => ScratchBuf::I64(Vec::new()),
1917            ScratchElem::Str => ScratchBuf::Str(Vec::new()),
1918            ScratchElem::Bytes => ScratchBuf::Bytes(Vec::new()),
1919            ScratchElem::Value => ScratchBuf::Value(Vec::new()),
1920            ScratchElem::Slots => ScratchBuf::Slots(Vec::new()),
1921            ScratchElem::Kernels => ScratchBuf::Kernels(Default::default()),
1922            ScratchElem::State => ScratchBuf::State(NodeState::default()),
1923        }
1924    }
1925}
1926
1927/// Compiled closure for a node with typed-slice ports (§8.4
1928/// layer 3). Same calling shape as [`CompiledU64Op`] plus the
1929/// step's scratch buffers: slice inputs arrive as `(ptr, len)`
1930/// slot pairs in `inputs`; vector outputs are written into
1931/// scratch and their `(ptr, len)` into `outputs`.
1932pub type CompiledSlotOp = Box<dyn Fn(&[u64], &mut [u64], &mut [ScratchBuf]) + Send + Sync>;
1933
1934/// A slot-compiled node's closure plus its scratch declaration
1935/// (one [`ScratchElem`] per vector-producing output, in port
1936/// order). Returned by [`PolydatNode::compiled_slot`].
1937pub struct CompiledSlotKit {
1938    /// The closure: slice inputs as slot pairs, vector outputs into scratch.
1939    pub op: CompiledSlotOp,
1940    /// One element type per vector-producing output, in port order.
1941    pub scratch: Vec<ScratchElem>,
1942}
1943
1944/// Per-node purity classification per
1945/// [`runtime_model.md`'s D2 axiom][spec] and
1946/// [`composition_substrate.md`'s T1+T2 axioms][substrate].
1947///
1948/// Every node declares its purity status via
1949/// [`PolydatNode::purity`]. The default is [`Purity::Pure`]; nodes
1950/// with observable side channels (logging, file I/O, network)
1951/// or eval-call-spanning state override to declare
1952/// [`Purity::SideChannel`] or [`Purity::Nondeterministic`].
1953///
1954/// **D1 (Typed Return Determinism) holds for every purity
1955/// class.** The slot contract carries only typed return
1956/// values; impure nodes still produce typed-deterministic
1957/// returns. What varies between purity classes is the
1958/// *observable side channels* (D2): pure nodes have none;
1959/// SideChannel nodes have declared side channels; Stateful
1960/// nodes additionally have internal eval-call-spanning state
1961/// that affects future evaluations.
1962///
1963/// [spec]: https://github.com/nosqlbench/polydat/blob/main/crates/polydat/docs/design/runtime_model.md
1964/// [substrate]: https://github.com/nosqlbench/polydat/blob/main/crates/polydat/docs/design/composition_substrate.md
1965#[derive(Debug, Clone, PartialEq, Eq, Hash)]
1966pub enum Purity {
1967    /// Pure function — `eval(inputs)` is a function of inputs,
1968    /// no observable side effects, byte-identical determinism
1969    /// across calls with identical inputs.
1970    Pure,
1971
1972    /// Has an observable side channel (logging, file I/O,
1973    /// network, etc.) but the typed return value is still a
1974    /// function of inputs. Hosts that care about side-channel
1975    /// observability examine the `sink` to know what
1976    /// observable surface this node writes to.
1977    SideChannel {
1978        /// The observable surface the node writes to.
1979        sink: SideChannelSink,
1980    },
1981
1982    /// The typed return value is not a function of declared
1983    /// inputs alone — it depends on external sources (system
1984    /// clock, entropy, thread identity, environment) or on
1985    /// eval-call-spanning internal state mutated by prior calls.
1986    /// In either case, the runtime's `node_clean` caching model
1987    /// must opt the node out of within-cycle memoization
1988    /// suppression; the assembler's lifecycle classes mark the node
1989    /// as nondeterministic (`PolydatProgram::nondeterministic`).
1990    /// The `reason` string documents the source of
1991    /// non-determinism (e.g., "reads system clock",
1992    /// "monotonic counter incremented per call",
1993    /// "accumulates signal buffer across calls").
1994    ///
1995    /// This is the intrinsic-volatility marker referenced by
1996    /// runtime_model.md R1.v: certain library nodes declare
1997    /// themselves volatile via this variant; no user opt-in is
1998    /// required, and the workload author cannot remove the
1999    /// marker. User-opt-in volatility via the `volatile`
2000    /// modifier is a separate surface that produces the same
2001    /// runtime effect (see R1.v).
2002    Nondeterministic {
2003        /// The source of the non-determinism, for diagnostics.
2004        reason: &'static str,
2005    },
2006}
2007
2008/// Where a [`Purity::SideChannel`] node writes its observable
2009/// side effects. Hosts reasoning about side-channel
2010/// determinism (D2) pattern-match on this to know what
2011/// observable surface to expect.
2012#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2013pub enum SideChannelSink {
2014    /// Writes to the process's stderr.
2015    Stderr,
2016    /// Writes to the process's stdout.
2017    Stdout,
2018    /// Writes to a log buffer (e.g. tracing/log crate sink).
2019    LogBuffer,
2020    /// Writes to a file path determined at construction time.
2021    File,
2022    /// Writes to a network endpoint determined at
2023    /// construction time.
2024    Network,
2025    /// Writes to an observable surface not covered by the
2026    /// other variants. The host should consult the node's
2027    /// documentation for the specific contract.
2028    Other,
2029}
2030
2031/// Semantic contract for a scalar node's explicitly registered SIMD variant.
2032///
2033/// This metadata is deliberately attached to the scalar node rather than
2034/// inferred from function names. A promotion pass may use it only after it
2035/// also validates the scalar/register port shapes and proves that the complete
2036/// vector cone lowers for the effective host ISA.
2037#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2038pub struct SimdVariant {
2039    /// DSL name of the register-typed, lane-wise equivalent node.
2040    pub vector_node: &'static str,
2041    /// Whether every lane is exactly equivalent to one scalar invocation.
2042    pub exact: bool,
2043    /// Whether evaluation is total for every bit pattern admitted by the
2044    /// scalar input types. Tier-1 padded execution requires this flag.
2045    pub total: bool,
2046    /// Whether one lane can be evaluated without reading or changing another
2047    /// lane. Scalar-flow auto-promotion requires this flag.
2048    pub lane_independent: bool,
2049}
2050
2051impl SimdVariant {
2052    /// Exact, total, element-wise variant used by the first promotion tier.
2053    pub const fn exact_total(vector_node: &'static str) -> Self {
2054        Self {
2055            vector_node,
2056            exact: true,
2057            total: true,
2058            lane_independent: true,
2059        }
2060    }
2061
2062    /// Exact element-wise variant which may fault for some lane values.
2063    ///
2064    /// Such a variant can be used only when the planner proves the admitted
2065    /// value range or implements ordered lane-error attribution.
2066    pub const fn exact_fallible(vector_node: &'static str) -> Self {
2067        Self {
2068            vector_node,
2069            exact: true,
2070            total: false,
2071            lane_independent: true,
2072        }
2073    }
2074}
2075
2076/// Runtime evaluation interface for a Polydat node.
2077///
2078/// Every engine drives this trait: the interpreter through `eval`,
2079/// the closure and native engines through `compiled_u64` /
2080/// `compiled_slot` where a node offers them and the node's own
2081/// closure elsewhere.
2082pub trait PolydatNode: Send + Sync {
2083    /// Return this node's metadata (port names and types).
2084    fn meta(&self) -> &NodeMeta;
2085
2086    /// Evaluate the node: read from `inputs`, write to `outputs`.
2087    ///
2088    /// The assembly phase guarantees that `inputs` and `outputs` have
2089    /// the correct length and types matching `meta()`.
2090    fn eval(&self, inputs: &[Value], outputs: &mut [Value]);
2091
2092    /// The scratch entries a state owns for this node's evaluation
2093    /// (axiom S3), one per entry in the order the node expects them
2094    /// in [`Self::eval_in`]. Empty for a node that evaluates over
2095    /// `Value`s alone, which is every node but a native cone.
2096    fn scratch_layout(&self) -> Vec<ScratchElem> {
2097        Vec::new()
2098    }
2099
2100    /// [`Self::eval`] with the node's scratch, which the evaluating
2101    /// state owns and hands in: storage belongs to the state, never to
2102    /// the node, which is shared by every state of the program.
2103    fn eval_in(&self, scratch: &mut [ScratchBuf], inputs: &[Value], outputs: &mut [Value]) {
2104        let _ = scratch;
2105        self.eval(inputs, outputs)
2106    }
2107
2108    /// Declare which inputs are interchangeable for this node.
2109    ///
2110    /// Override for commutative operations like `sum`, `product`,
2111    /// `min`, `max`. The default is `Positional` (order matters).
2112    fn commutativity(&self) -> Commutativity {
2113        Commutativity::Positional
2114    }
2115
2116    /// True iff this node should receive `Value::None` inputs
2117    /// directly rather than have the kernel propagate None through
2118    /// it. Default: false — most nodes follow none_semantics.md Rule 1
2119    /// (None in → None out, no eval invocation).
2120    ///
2121    /// Override to true for nodes whose semantics explicitly
2122    /// consume None: coalesce-style fallbacks (`default_or`),
2123    /// optional/maybe handlers, anything that distinguishes
2124    /// "present" from "absent" as part of its contract.
2125    /// Override-true nodes are responsible for handling
2126    /// `Value::None` in their own `eval` implementation.
2127    ///
2128    /// See `crates/polydat/docs/design/none_semantics.md`
2129    /// (string-interpolation propagates None) — the
2130    /// rule is general (lifted to the kernel level) rather than
2131    /// per-node; this flag is the opt-out for legitimate None-
2132    /// aware operators.
2133    fn accepts_none_inputs(&self) -> bool {
2134        false
2135    }
2136
2137    /// Return a compiled u64-only evaluation closure, if this node
2138    /// operates entirely in u64 space.
2139    ///
2140    /// The closure reads from an input slice and writes to an output
2141    /// slice, both `&[u64]` / `&mut [u64]`. Assembly-time parameters
2142    /// are captured in the closure.
2143    ///
2144    /// Return `None` if the node has non-u64 ports or cannot be
2145    /// compiled. The assembly phase will fall back to Phase 1.
2146    fn compiled_u64(&self) -> Option<CompiledU64Op> {
2147        None
2148    }
2149
2150    /// Return a slot-compiled closure for nodes with typed-slice
2151    /// ports (§8.4 layer 3): slice inputs read `(ptr, len)` slot
2152    /// pairs; vector outputs write into kernel-owned scratch.
2153    /// Checked by the compiled-kernel builders AFTER
2154    /// [`Self::compiled_u64`] — pure-scalar nodes never need it.
2155    /// Default `None`: the node stays on typed eval.
2156    ///
2157    /// `engine` is the engine the kit is being built for, which a node
2158    /// needs when its closure runs a program of its own: a tile's
2159    /// projection body belongs to the kernel rendering it, the way a
2160    /// `for` body belongs to the kernel that opened it, and the kit is
2161    /// the only place a closure can learn which that is.
2162    fn compiled_slot(
2163        &self,
2164        _wire_types: &[PortType],
2165        _engine: crate::compile::select::Engine,
2166    ) -> Option<CompiledSlotKit> {
2167        None
2168    }
2169
2170    /// Return assembly-time constants for JIT compilation.
2171    ///
2172    /// Nodes with baked-in constants (Mod's modulus, Add's addend, etc.)
2173    /// override this to expose their constants to the JIT compiler.
2174    /// Returns a list of u64 constants in the order the JIT expects.
2175    ///
2176    /// Default: empty (no constants to expose).
2177    fn jit_constants(&self) -> Vec<u64> {
2178        Vec::new()
2179    }
2180
2181    /// Declare this node's purity status per the
2182    /// [`runtime_model.md`'s D2 axiom][spec]. Default:
2183    /// [`Purity::Pure`]. Override to declare an observable
2184    /// side channel ([`Purity::SideChannel`]) or
2185    /// eval-call-spanning state ([`Purity::Nondeterministic`]).
2186    ///
2187    /// **What this affects:**
2188    ///
2189    /// - The runtime's `node_clean` cache (R1) holds for
2190    ///   `Purity::Pure` and `Purity::SideChannel`. The
2191    ///   typed return value is cached after one eval;
2192    ///   subsequent pulls with identical inputs reuse the
2193    ///   cache. For `SideChannel` nodes, this means the
2194    ///   side channel fires once per dirty-to-clean
2195    ///   transition (not on every pull).
2196    /// - `Purity::Nondeterministic` nodes opt out of `node_clean`
2197    ///   caching at the construction tier (the assembler's
2198    ///   lifecycle classes mark them as nondeterministic,
2199    ///   `PolydatProgram::nondeterministic`).
2200    /// - Hosts inspecting an expression's determinism
2201    ///   profile via D2 read this declaration to know
2202    ///   whether the constituent node has side channels.
2203    ///
2204    /// Default: `Purity::Pure`. Most nodes are pure
2205    /// functions over their inputs.
2206    ///
2207    /// [spec]: https://github.com/nosqlbench/polydat/blob/main/crates/polydat/docs/design/runtime_model.md
2208    fn purity(&self) -> Purity {
2209        Purity::Pure
2210    }
2211
2212    /// Explicit SIMD-native implementation of this scalar node, if one has
2213    /// been registered with a semantic contract.
2214    ///
2215    /// Returning metadata does not itself make a node promotable. The planner
2216    /// must still validate types, purity, source replay, packet ownership, and
2217    /// successful lowering by the same Cranelift ISA used for code generation.
2218    fn simd_variant(&self) -> Option<SimdVariant> {
2219        None
2220    }
2221
2222    /// A synthetic fusion node's view of the subgraph it stands in
2223    /// for (cone extraction, engines.md §2). Program-identity hashing
2224    /// (`PolydatProgram::canonical_hash`) walks THROUGH fusion
2225    /// nodes into this subgraph, so identity is invariant to the
2226    /// engine mix: `jit=off` and `jit=auto` compiles of the same
2227    /// source hash identically, and resume-skip matching survives
2228    /// mode changes. Default `None`: ordinary nodes hash as
2229    /// themselves.
2230    fn fusion_subgraph(&self) -> Option<FusionSubgraph<'_>> {
2231        None
2232    }
2233}
2234
2235/// Borrowed view of the subgraph a fusion node replaced. Local
2236/// wiring convention: `WireSource::Input(i)` refers to the fusion
2237/// node's i-th input wire in the OUTER graph; `NodeOutput(j, p)`
2238/// refers to member `j`'s port `p`.
2239pub struct FusionSubgraph<'a> {
2240    /// The original member nodes, verbatim.
2241    pub members: &'a [Box<dyn PolydatNode>],
2242    /// Per-member local wiring (see convention above).
2243    pub wiring: &'a [Vec<crate::kernel::WireSource>],
2244    /// Per fusion output port: `(member index, member port)` —
2245    /// the original producer behind that port.
2246    pub out_ports: &'a [(usize, usize)],
2247}
2248
2249/// The compile level of a node, given the types of the wires feeding
2250/// it. One call to [`crate::compile::node_tier`], which is the order
2251/// every builder walks; the types are needed because a node's slot kit
2252/// is offered per call site with the types the kernel fixed.
2253///
2254/// Prefer [`crate::kernel::PolydatProgram::node_compile_level`], which
2255/// reads the types out of the program rather than asking the caller
2256/// for them.
2257pub fn compile_level_of(node: &dyn PolydatNode, wire_types: &[PortType]) -> CompileLevel {
2258    crate::compile::node_tier(node, wire_types)
2259}
2260
2261/// The maximum compilation level a node supports.
2262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2263pub enum CompileLevel {
2264    /// Runtime interpreter: `dyn PolydatNode` + `Value` enum.
2265    Phase1,
2266    /// Compiled closure: `Box<dyn Fn(&[u64], &mut [u64])>`.
2267    Phase2,
2268    /// JIT native code via Cranelift.
2269    Phase3,
2270}
2271
2272#[cfg(test)]
2273mod purity_tests {
2274    use super::*;
2275
2276    /// A minimal pure node — defaults to `Purity::Pure` via
2277    /// the trait default impl.
2278    struct DefaultPureNode {
2279        meta: NodeMeta,
2280    }
2281
2282    impl PolydatNode for DefaultPureNode {
2283        fn meta(&self) -> &NodeMeta {
2284            &self.meta
2285        }
2286        fn eval(&self, _inputs: &[Value], outputs: &mut [Value]) {
2287            outputs[0] = Value::U64(42);
2288        }
2289    }
2290
2291    /// A node that explicitly declares a side channel.
2292    struct SideChannelNode {
2293        meta: NodeMeta,
2294    }
2295
2296    impl PolydatNode for SideChannelNode {
2297        fn meta(&self) -> &NodeMeta {
2298            &self.meta
2299        }
2300        fn eval(&self, _inputs: &[Value], _outputs: &mut [Value]) {}
2301        fn purity(&self) -> Purity {
2302            Purity::SideChannel {
2303                sink: SideChannelSink::Stderr,
2304            }
2305        }
2306    }
2307
2308    /// A node that explicitly declares stateful behaviour.
2309    struct StatefulNode {
2310        meta: NodeMeta,
2311    }
2312
2313    impl PolydatNode for StatefulNode {
2314        fn meta(&self) -> &NodeMeta {
2315            &self.meta
2316        }
2317        fn eval(&self, _inputs: &[Value], _outputs: &mut [Value]) {}
2318        fn purity(&self) -> Purity {
2319            Purity::Nondeterministic {
2320                reason: "test fixture",
2321            }
2322        }
2323    }
2324
2325    fn empty_meta() -> NodeMeta {
2326        NodeMeta {
2327            name: "test".into(),
2328            ins: vec![],
2329            outs: vec![Port::u64("out")],
2330        }
2331    }
2332
2333    #[test]
2334    fn default_purity_is_pure() {
2335        let n = DefaultPureNode { meta: empty_meta() };
2336        assert_eq!(n.purity(), Purity::Pure);
2337    }
2338
2339    #[test]
2340    fn side_channel_declaration_is_observable() {
2341        let n = SideChannelNode { meta: empty_meta() };
2342        match n.purity() {
2343            Purity::SideChannel { sink } => assert_eq!(sink, SideChannelSink::Stderr),
2344            other => panic!("expected SideChannel, got {other:?}"),
2345        }
2346    }
2347
2348    #[test]
2349    fn stateful_declaration_is_observable() {
2350        let n = StatefulNode { meta: empty_meta() };
2351        match n.purity() {
2352            Purity::Nondeterministic { reason } => assert_eq!(reason, "test fixture"),
2353            other => panic!("expected Stateful, got {other:?}"),
2354        }
2355    }
2356
2357    #[test]
2358    fn inspect_node_declares_stderr_side_channel() {
2359        let n = crate::library::diagnostic::Inspect::new(PortType::U64, "x".to_string());
2360        match n.purity() {
2361            Purity::SideChannel { sink } => assert_eq!(sink, SideChannelSink::Stderr),
2362            other => panic!("inspect should declare Stderr SideChannel, got {other:?}"),
2363        }
2364    }
2365
2366    #[test]
2367    fn log_passthrough_declares_log_buffer_side_channel() {
2368        let n = crate::library::log_levels::LogInfo::new(PortType::U64);
2369        match n.purity() {
2370            Purity::SideChannel { sink } => assert_eq!(sink, SideChannelSink::LogBuffer),
2371            other => panic!("log_passthrough should declare LogBuffer SideChannel, got {other:?}"),
2372        }
2373    }
2374}
2375
2376#[cfg(test)]
2377mod value_size_probe {
2378    /// The `Value` enum rides per-slot in every node buffer; its
2379    /// size is a load-bearing budget: 40 bytes (the `SliceArc`
2380    /// borrow shape) at alignment 8. The 128-bit integer variants
2381    /// deliberately ride as two u64 limbs ([`super::Bits128`])
2382    /// instead of raw `u128`/`i128` payloads — a native 128-bit
2383    /// field would force the enum to alignment 16 and grow every
2384    /// buffer slot to 48 bytes for a rarely-carried type
2385    /// (type_system_alignment.md §8.1). This test pins the
2386    /// envelope so an accidental payload regression is caught at
2387    /// the door.
2388    #[test]
2389    fn value_fits_size_envelope() {
2390        assert!(
2391            std::mem::size_of::<super::Value>() <= 40,
2392            "Value grew past the 40-byte envelope: {}",
2393            std::mem::size_of::<super::Value>()
2394        );
2395        assert_eq!(
2396            std::mem::align_of::<super::Value>(),
2397            8,
2398            "Value alignment must stay 8 — a 16-aligned payload \
2399             (raw u128/i128?) snuck in"
2400        );
2401    }
2402}
2403
2404/// A borrowed view of a [`Value`] (compiled_handles.md §6): what a compiled helper
2405/// or closure sees for an argument it does not own. A scalar is carried
2406/// by value, a string or byte string by reference into the arena or the
2407/// interner, a JSON value by reference into the value table, and any
2408/// other variant by reference to the `Value` itself. The P1 nodes build
2409/// the same view from their `Value` inputs, so one body serves both
2410/// tiers without copying a string argument to inspect it.
2411#[derive(Clone, Copy, Debug)]
2412pub enum ValueRef<'a> {
2413    /// An unsigned integer.
2414    U64(u64),
2415    /// A signed integer.
2416    I64(i64),
2417    /// A float.
2418    F64(f64),
2419    /// A boolean.
2420    Bool(bool),
2421    /// A string, borrowed from the arena or the interner.
2422    Str(&'a str),
2423    /// A byte string, borrowed.
2424    Bytes(&'a [u8]),
2425    /// A JSON value, by reference into the value table.
2426    Json(&'a serde_json::Value),
2427    /// No value.
2428    None,
2429    /// Any other variant, by reference to the value.
2430    Other(&'a Value),
2431}
2432
2433impl<'a> From<&'a Value> for ValueRef<'a> {
2434    fn from(v: &'a Value) -> Self {
2435        match v {
2436            Value::U64(x) => ValueRef::U64(*x),
2437            Value::I64(x) => ValueRef::I64(*x),
2438            Value::F64(x) => ValueRef::F64(*x),
2439            Value::Bool(b) => ValueRef::Bool(*b),
2440            Value::Str(s) => ValueRef::Str(s),
2441            Value::Bytes(b) => ValueRef::Bytes(b),
2442            Value::Json(j) => ValueRef::Json(j),
2443            Value::None => ValueRef::None,
2444            other => ValueRef::Other(other),
2445        }
2446    }
2447}
2448
2449impl<'a> ValueRef<'a> {
2450    /// The port type of the value viewed.
2451    pub fn port_type(&self) -> PortType {
2452        match self {
2453            ValueRef::U64(_) => PortType::U64,
2454            ValueRef::I64(_) => PortType::I64,
2455            ValueRef::F64(_) => PortType::F64,
2456            ValueRef::Bool(_) => PortType::Bool,
2457            ValueRef::Str(_) => PortType::Str,
2458            ValueRef::Bytes(_) => PortType::Bytes,
2459            ValueRef::Json(_) => PortType::Json,
2460            ValueRef::None => Value::None.port_type(),
2461            ValueRef::Other(v) => v.port_type(),
2462        }
2463    }
2464
2465    /// The display form, exactly as [`Value::to_display_string`] gives
2466    /// it; a string is borrowed rather than copied.
2467    pub fn display(&self) -> std::borrow::Cow<'a, str> {
2468        use std::borrow::Cow;
2469        match self {
2470            ValueRef::Str(s) => Cow::Borrowed(s),
2471            ValueRef::U64(v) => Cow::Owned(v.to_string()),
2472            ValueRef::I64(v) => Cow::Owned(v.to_string()),
2473            ValueRef::F64(v) => Cow::Owned(format!("{v:?}")),
2474            ValueRef::Bool(v) => Cow::Owned(v.to_string()),
2475            ValueRef::Bytes(b) => Cow::Owned(b.iter().map(|b| format!("{b:02x}")).collect()),
2476            ValueRef::Json(j) => Cow::Owned(j.to_string()),
2477            ValueRef::None => Cow::Owned(Value::None.to_display_string()),
2478            ValueRef::Other(v) => Cow::Owned(v.to_display_string()),
2479        }
2480    }
2481
2482    /// The display form as an owned string.
2483    pub fn to_display_string(&self) -> String {
2484        self.display().into_owned()
2485    }
2486
2487    /// The JSON projection, exactly as [`Value::to_json_value`] gives it.
2488    pub fn to_json_value(&self) -> serde_json::Value {
2489        match self {
2490            ValueRef::U64(v) => serde_json::Value::from(*v),
2491            ValueRef::I64(v) => serde_json::Value::from(*v),
2492            ValueRef::F64(v) => serde_json::json!(*v),
2493            ValueRef::Bool(v) => serde_json::Value::from(*v),
2494            ValueRef::Str(s) => serde_json::Value::from(*s),
2495            ValueRef::Bytes(b) => {
2496                serde_json::Value::from(b.iter().map(|b| format!("{b:02x}")).collect::<String>())
2497            }
2498            ValueRef::Json(j) => (*j).clone(),
2499            ValueRef::None => Value::None.to_json_value(),
2500            ValueRef::Other(v) => v.to_json_value(),
2501        }
2502    }
2503}
2504
2505#[cfg(test)]
2506mod satisfies_slot_tests {
2507    use super::*;
2508
2509    /// A float node output rides its bit pattern in `Value::U64`
2510    /// (`Wire for f32` / `Wire for f16` inject it so), and a host may
2511    /// write the materialised `Value::F64` instead; a float slot
2512    /// accepts both, and a `U64` slot does not accept a float.
2513    #[test]
2514    fn float_slots_accept_the_bit_stuffed_and_materialised_forms() {
2515        let f32_bits = Value::U64(1.5f32.to_bits() as u64);
2516        let f16_bits = Value::U64(half::f16::from_f32(1.5).to_bits() as u64);
2517        assert!(f32_bits.satisfies_slot(PortType::F32));
2518        assert!(f16_bits.satisfies_slot(PortType::F16));
2519        assert!(Value::F64(1.5).satisfies_slot(PortType::F32));
2520        assert!(Value::F64(1.5).satisfies_slot(PortType::F16));
2521        assert!(!Value::F64(1.5).satisfies_slot(PortType::U64));
2522        assert!(!Value::Str("1.5".into()).satisfies_slot(PortType::F32));
2523    }
2524}