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