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