Skip to main content

polydat_core/library/
assertions.rs

1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Type and value assertion nodes.
5//!
6//! Polydat's runtime contract: node `eval` trusts its inputs. Bad input
7//! panics, by design, because the hot path stays branch-free. The
8//! "guarded" version of a node that would otherwise panic is built
9//! as an *assembly* of two functions — the original node, and an
10//! assertion node spliced in front of one of its inputs. The assertion runs the
11//! check; the downstream node still trusts its inputs.
12//!
13//! Two families:
14//!
15//! * **Type assertions** — one per supported [`PortType`]. They
16//!   confirm the runtime [`Value`] variant matches the static
17//!   port type and pass it through. The compiler inserts none:
18//!   wires are statically typed, and a resolved wire's type is the
19//!   sink port's type, so a runtime type check has nothing to catch.
20//!
21//! * **Value assertions** — one per `PortType`, parameterised by
22//!   a [`ConstConstraint`]. Pass the value through if the
23//!   constraint holds, otherwise panic with a structured message.
24//!   The same vocabulary the const-constraint metadata uses on
25//!   `ParamSpec` is reused on `Port` and on these nodes.
26//!
27//! The compiler inserts value assertions under `pragma strict_values`
28//! (graph_compiler.md §2). Neither family is callable from Polydat
29//! source (library_catalog.md, the hand-written-impl carve-outs).
30
31use crate::ast::SlotShape;
32use crate::ast::{NodeMeta, PolydatNode, Port, PortType, Slot, Value};
33use crate::dsl::const_constraints::ConstConstraint;
34
35// =========================================================================
36// Type assertions: one per PortType
37// =========================================================================
38
39/// Pass-through guard that confirms the runtime value variant
40/// matches a declared `PortType`. Panics on mismatch.
41///
42/// Constructed with [`assert_type_node`]. The compiler inserts none,
43/// under `strict_types` or otherwise: a resolved wire's type is the
44/// sink port's type (graph_compiler.md §2).
45pub struct AssertType {
46    meta: NodeMeta,
47    expected: PortType,
48}
49
50impl AssertType {
51    /// A type assertion for `typ`, named `assert_<type>`.
52    pub fn new(typ: PortType) -> Self {
53        let name = match typ {
54            PortType::U64 => "assert_u64",
55            PortType::F64 => "assert_f64",
56            PortType::Bool => "assert_bool",
57            PortType::Str => "assert_str",
58            PortType::Bytes => "assert_bytes",
59            PortType::Json => "assert_json",
60            PortType::U32 => "assert_u32",
61            PortType::I32 => "assert_i32",
62            PortType::I64 => "assert_i64",
63            PortType::F32 => "assert_f32",
64            PortType::U8 => "assert_u8",
65            PortType::I8 => "assert_i8",
66            PortType::U16 => "assert_u16",
67            PortType::I16 => "assert_i16",
68            PortType::F16 => "assert_f16",
69            PortType::U128 => "assert_u128",
70            PortType::I128 => "assert_i128",
71            PortType::Reg128 => "assert_reg128",
72            PortType::RegI8x16 => "assert_reg_i8x16",
73            PortType::RegI16x8 => "assert_reg_i16x8",
74            PortType::RegI32x4 => "assert_reg_i32x4",
75            PortType::RegI64x2 => "assert_reg_i64x2",
76            PortType::RegF16x8 => "assert_reg_f16x8",
77            PortType::RegF32x4 => "assert_reg_f32x4",
78            PortType::RegF64x2 => "assert_reg_f64x2",
79            PortType::Ext => "assert_ext",
80            PortType::Handle => "assert_handle",
81            PortType::VecF32 => "assert_vec_f32",
82            PortType::VecI32 => "assert_vec_i32",
83            PortType::VecF64 => "assert_vec_f64",
84            PortType::VecI64 => "assert_vec_i64",
85            PortType::VecF16 => "assert_vec_f16",
86            PortType::VecI16 => "assert_vec_i16",
87            PortType::VecI8 => "assert_vec_i8",
88            PortType::Dyn => "assert_dyn",
89        };
90        Self {
91            meta: NodeMeta {
92                name: name.into(),
93                outs: vec![Port::new("output", typ)],
94                ins: vec![Slot::Wire(Port::new("input", typ))],
95            },
96            expected: typ,
97        }
98    }
99
100    /// Returns the `PortType` this node asserts against.
101    pub fn expected(&self) -> PortType {
102        self.expected
103    }
104}
105
106impl PolydatNode for AssertType {
107    fn meta(&self) -> &NodeMeta {
108        &self.meta
109    }
110
111    fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
112        let v = &inputs[0];
113        if !value_matches(v, self.expected) {
114            panic!(
115                "{}: expected runtime value of type {:?}, got {:?}",
116                self.meta.name, self.expected, v
117            );
118        }
119        outputs[0] = v.clone();
120    }
121
122    /// The compiled form. In a slot buffer a wire's color is its type,
123    /// so the variant check the interpreter makes has nothing to
124    /// observe there; the compiled step is the copy `identity` makes:
125    /// an immediate copied, a `Ref2` value copied into this step's own
126    /// scratch, since a pair is never forwarded (axiom S3).
127    fn compiled_u64(&self) -> Option<crate::ast::CompiledU64Op> {
128        if self.expected.slot_color() == crate::ast::SlotColor::Ref2 {
129            return None;
130        }
131        Some(Box::new(|inputs: &[u64], outputs: &mut [u64]| {
132            outputs.copy_from_slice(inputs)
133        }))
134    }
135
136    fn compiled_slot(
137        &self,
138        _wire_types: &[PortType],
139        _engine: crate::compile::select::Engine,
140    ) -> Option<crate::ast::CompiledSlotKit> {
141        crate::compile::assembly::ref_copy_kit(self.expected)
142    }
143}
144
145fn value_matches(v: &Value, typ: PortType) -> bool {
146    match (v, typ) {
147        (Value::U64(_), PortType::U64) => true,
148        (Value::F64(_), PortType::F64) => true,
149        (Value::Bool(_), PortType::Bool) => true,
150        (Value::Str(_), PortType::Str) => true,
151        (Value::Bytes(_), PortType::Bytes) => true,
152        (Value::Json(_), PortType::Json) => true,
153        // Narrow-int variants ride in the wider variant per the
154        // PortType doc on `node.rs` — accept the natural carrier.
155        (Value::U64(_), PortType::U32) => true,
156        (Value::U64(_), PortType::I32) => true,
157        (Value::U64(_), PortType::I64) => true,
158        (Value::F64(_), PortType::F32) => true,
159        (Value::U64(_), PortType::U8 | PortType::U16) => true,
160        // F16 rides its bit pattern in U64 (same stuffing as F32
161        // node outputs); host-written F64 also satisfies F16.
162        (Value::U64(_), PortType::F16) => true,
163        (Value::F64(_), PortType::F16) => true,
164        // Honest signed carrier serves all signed widths; the
165        // bit-stuffed U64 forms of the narrow signed projections are
166        // accepted too.
167        (Value::I64(_), PortType::I64 | PortType::I32 | PortType::I8 | PortType::I16) => true,
168        (Value::U64(_), PortType::I8 | PortType::I16) => true,
169        (Value::U128(_), PortType::U128) => true,
170        (Value::I128(_), PortType::I128) => true,
171        // Register views are free bitcasts of one another.
172        (
173            Value::Reg128(_, _),
174            PortType::Reg128
175            | PortType::RegI8x16
176            | PortType::RegI16x8
177            | PortType::RegI32x4
178            | PortType::RegI64x2
179            | PortType::RegF16x8
180            | PortType::RegF32x4
181            | PortType::RegF64x2,
182        ) => true,
183        // Ext is opaque; we accept any concrete reflection.
184        (Value::Ext(_), PortType::Ext) => true,
185        _ => false,
186    }
187}
188
189// =========================================================================
190// Value assertions: type + constraint pair
191// =========================================================================
192
193/// Runtime value-constraint guard. Holds a [`ConstConstraint`]
194/// the value must satisfy each cycle. Panics with a structured
195/// message on violation; passes the value through otherwise.
196///
197/// Constructed with [`assert_value_node`] from the compiler when
198/// the source can't statically be proven to deliver a value
199/// satisfying the sink's constraint. Reuses the same
200/// `ConstConstraint` vocabulary the const-validator uses, so the
201/// two layers speak one language.
202pub struct AssertValue {
203    meta: NodeMeta,
204    typ: PortType,
205    constraint: ConstConstraint,
206}
207
208impl AssertValue {
209    /// A value assertion for `typ` under `constraint`, named by the pair.
210    pub fn new(typ: PortType, constraint: ConstConstraint) -> Self {
211        let name = match (&typ, &constraint) {
212            (PortType::U64, ConstConstraint::NonZeroU64) => "assert_u64_nonzero",
213            (PortType::U64, ConstConstraint::RangeU64 { .. }) => "assert_u64_range",
214            (PortType::U64, ConstConstraint::AllowedU64(_)) => "assert_u64_allowed",
215            (PortType::F64, ConstConstraint::RangeF64 { .. }) => "assert_f64_range",
216            (PortType::Str, ConstConstraint::NonEmptyStr) => "assert_str_non_empty",
217            (PortType::Str, ConstConstraint::StrParser(_)) => "assert_str_parses",
218            // Catch-all for combinations we haven't dedicated a
219            // distinct DSL name to yet.
220            _ => "assert_value",
221        };
222        Self {
223            meta: NodeMeta {
224                name: name.into(),
225                outs: vec![Port::new("output", typ)],
226                ins: vec![Slot::Wire(Port::new("input", typ))],
227            },
228            typ,
229            constraint,
230        }
231    }
232
233    /// The constraint asserted.
234    pub fn constraint(&self) -> &ConstConstraint {
235        &self.constraint
236    }
237
238    /// The type asserted.
239    pub fn port_type(&self) -> PortType {
240        self.typ
241    }
242}
243
244impl PolydatNode for AssertValue {
245    fn meta(&self) -> &NodeMeta {
246        &self.meta
247    }
248
249    fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
250        // Re-route the constraint check through `ConstConstraint::check`
251        // by lifting the value into a `ConstArg` shaped tuple. Avoids
252        // duplicating the per-variant logic between assembly and
253        // runtime.
254        match check_value(&self.constraint, &inputs[0], "value") {
255            Some(Ok(())) => {}
256            Some(Err(msg)) => panic!("{}: {msg}", self.meta.name),
257            None => panic!(
258                "{}: unsupported runtime value variant {:?}",
259                self.meta.name, inputs[0]
260            ),
261        }
262        outputs[0] = inputs[0].clone();
263    }
264
265    /// The compiled form: the same constraint checked against the slot,
266    /// decoded by the asserted type, with the same message on failure.
267    /// A carrier reads as its integer, a float from its bits; the
268    /// other shapes have no u64 form.
269    fn compiled_u64(&self) -> Option<crate::ast::CompiledU64Op> {
270        use crate::dsl::factory::ConstArg;
271        let lift: fn(u64) -> ConstArg = match self.typ {
272            PortType::U64 | PortType::U32 | PortType::U16 | PortType::U8 => ConstArg::Int,
273            PortType::F64 => |slot| ConstArg::Float(f64::from_bits(slot)),
274            _ => return None,
275        };
276        let name = self.meta.name.clone();
277        let constraint = self.constraint;
278        Some(Box::new(move |inputs: &[u64], outputs: &mut [u64]| {
279            if let Err(msg) = constraint.check(&lift(inputs[0]), "value") {
280                panic!("{name}: {msg}");
281            }
282            outputs[0] = inputs[0];
283        }))
284    }
285
286    /// A string reads through its pair, is checked, and is copied into
287    /// this step's own scratch (axiom S3).
288    fn compiled_slot(
289        &self,
290        _wire_types: &[PortType],
291        _engine: crate::compile::select::Engine,
292    ) -> Option<crate::ast::CompiledSlotKit> {
293        use crate::dsl::factory::ConstArg;
294        if self.typ != PortType::Str {
295            return None;
296        }
297        let name = self.meta.name.clone();
298        let constraint = self.constraint;
299        let copy = crate::compile::assembly::ref_copy_kit(PortType::Str)?;
300        Some(crate::ast::CompiledSlotKit {
301            scratch: copy.scratch,
302            op: Box::new(
303                move |inputs: &[u64],
304                      outputs: &mut [u64],
305                      scratch: &mut [crate::ast::ScratchBuf]| {
306                    // SAFETY: the pair was published by the producing
307                    // step into storage alive until it reruns (S3, S4).
308                    let text = unsafe {
309                        std::str::from_utf8_unchecked(std::slice::from_raw_parts(
310                            inputs[0] as usize as *const u8,
311                            inputs[1] as usize,
312                        ))
313                    };
314                    if let Err(msg) = constraint.check(&ConstArg::Str(text.to_string()), "value") {
315                        panic!("{name}: {msg}");
316                    }
317                    (copy.op)(inputs, outputs, scratch);
318                },
319            ),
320        })
321    }
322}
323
324// =========================================================================
325// Helpers used by the compiler when auto-wiring assertions
326// =========================================================================
327
328/// Check `value` against `constraint`, naming the checked value
329/// `name` in the message. `None` when the value's variant is one the
330/// constraint vocabulary has no reading of (anything but `U64`, `F64`
331/// and `Str`).
332pub fn check_value(
333    constraint: &ConstConstraint,
334    value: &Value,
335    name: &str,
336) -> Option<Result<(), String>> {
337    use crate::dsl::factory::ConstArg;
338    let arg = match value {
339        Value::U64(v) => ConstArg::Int(*v),
340        Value::F64(v) => ConstArg::Float(*v),
341        Value::Str(s) => ConstArg::Str(s.to_string()),
342        _ => return None,
343    };
344    Some(constraint.check(&arg, name))
345}
346
347/// The condition `constraint` states, in words, for diagnostics.
348pub fn describe_constraint(constraint: &ConstConstraint) -> String {
349    match constraint {
350        ConstConstraint::RangeU64 { min, max } => format!("in [{min}, {max}]"),
351        ConstConstraint::RangeF64 { min, max } => format!("in [{min}, {max}]"),
352        ConstConstraint::AllowedU64(allowed) => format!("one of {allowed:?}"),
353        ConstConstraint::NonZeroU64 => "non-zero".into(),
354        ConstConstraint::NonEmptyStr => "non-empty".into(),
355        ConstConstraint::StrParser(_) => "accepted by the port's parser".into(),
356        ConstConstraint::PositiveFiniteF64 => "positive and finite".into(),
357        ConstConstraint::FiniteF64 => "finite".into(),
358    }
359}
360
361/// Whether two constraints state the same condition. A parser
362/// constraint equals another only when both name the same function.
363pub fn same_constraint(a: &ConstConstraint, b: &ConstConstraint) -> bool {
364    use ConstConstraint as C;
365    match (a, b) {
366        (C::RangeU64 { min: a0, max: a1 }, C::RangeU64 { min: b0, max: b1 }) => {
367            a0 == b0 && a1 == b1
368        }
369        (C::RangeF64 { min: a0, max: a1 }, C::RangeF64 { min: b0, max: b1 }) => {
370            a0.to_bits() == b0.to_bits() && a1.to_bits() == b1.to_bits()
371        }
372        (C::AllowedU64(a), C::AllowedU64(b)) => a == b,
373        (C::StrParser(a), C::StrParser(b)) => std::ptr::fn_addr_eq(*a, *b),
374        (C::NonZeroU64, C::NonZeroU64)
375        | (C::NonEmptyStr, C::NonEmptyStr)
376        | (C::PositiveFiniteF64, C::PositiveFiniteF64)
377        | (C::FiniteF64, C::FiniteF64) => true,
378        _ => false,
379    }
380}
381
382/// Construct the right type assertion node for a given `PortType`.
383/// The compiler inserts none: a resolved wire's type is the sink
384/// port's type (graph_compiler.md §2).
385pub fn assert_type_node(typ: PortType) -> Box<dyn PolydatNode> {
386    Box::new(AssertType::new(typ))
387}
388
389/// Construct a value assertion node for the given (type, constraint) pair.
390pub fn assert_value_node(typ: PortType, constraint: ConstConstraint) -> Box<dyn PolydatNode> {
391    Box::new(AssertValue::new(typ, constraint))
392}
393
394// =========================================================================
395// Tests
396// =========================================================================
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn assert_u64_passes_u64_through() {
404        let node = AssertType::new(PortType::U64);
405        let mut out = [Value::None];
406        node.eval(&[Value::U64(42)], &mut out);
407        assert_eq!(out[0].as_u64(), 42);
408    }
409
410    #[test]
411    #[should_panic(expected = "expected runtime value of type U64")]
412    fn assert_u64_panics_on_string() {
413        let node = AssertType::new(PortType::U64);
414        let mut out = [Value::None];
415        node.eval(&[Value::Str("not a number".into())], &mut out);
416    }
417
418    #[test]
419    fn assert_value_nonzero_passes_nonzero() {
420        let node = AssertValue::new(PortType::U64, ConstConstraint::NonZeroU64);
421        let mut out = [Value::None];
422        node.eval(&[Value::U64(7)], &mut out);
423        assert_eq!(out[0].as_u64(), 7);
424    }
425
426    #[test]
427    #[should_panic(expected = "must be non-zero")]
428    fn assert_value_nonzero_panics_on_zero() {
429        let node = AssertValue::new(PortType::U64, ConstConstraint::NonZeroU64);
430        let mut out = [Value::None];
431        node.eval(&[Value::U64(0)], &mut out);
432    }
433
434    #[test]
435    fn assert_value_range_f64_passes_unit_interval() {
436        let node = AssertValue::new(
437            PortType::F64,
438            ConstConstraint::RangeF64 { min: 0.0, max: 1.0 },
439        );
440        let mut out = [Value::None];
441        node.eval(&[Value::F64(0.5)], &mut out);
442        assert_eq!(out[0].as_f64(), 0.5);
443    }
444
445    #[test]
446    #[should_panic(expected = "must be in [0, 1]")]
447    fn assert_value_range_f64_panics_on_out_of_range() {
448        let node = AssertValue::new(
449            PortType::F64,
450            ConstConstraint::RangeF64 { min: 0.0, max: 1.0 },
451        );
452        let mut out = [Value::None];
453        node.eval(&[Value::F64(1.5)], &mut out);
454    }
455}