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