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