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            PortType::Dyn => "assert_dyn",
91        };
92        Self {
93            meta: NodeMeta {
94                name: name.into(),
95                outs: vec![Port::new("output", typ)],
96                ins: vec![Slot::Wire(Port::new("input", typ))],
97            },
98            expected: typ,
99        }
100    }
101
102    /// Returns the `PortType` this node asserts against.
103    pub fn expected(&self) -> PortType {
104        self.expected
105    }
106}
107
108impl PolydatNode for AssertType {
109    fn meta(&self) -> &NodeMeta {
110        &self.meta
111    }
112
113    fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
114        let v = &inputs[0];
115        if !value_matches(v, self.expected) {
116            panic!(
117                "{}: expected runtime value of type {:?}, got {:?}",
118                self.meta.name, self.expected, v
119            );
120        }
121        outputs[0] = v.clone();
122    }
123
124    /// The compiled form. In a slot buffer a wire's color is its type,
125    /// so the variant check the interpreter makes has nothing to
126    /// observe there; the compiled step is the copy `identity` makes:
127    /// an immediate copied, a `Ref2` value copied into this step's own
128    /// scratch, since a pair is never forwarded (axiom S3).
129    fn compiled_u64(&self) -> Option<crate::ast::CompiledU64Op> {
130        if self.expected.slot_color() == crate::ast::SlotColor::Ref2 {
131            return None;
132        }
133        Some(Box::new(|inputs: &[u64], outputs: &mut [u64]| {
134            outputs.copy_from_slice(inputs)
135        }))
136    }
137
138    fn compiled_slot(
139        &self,
140        _wire_types: &[PortType],
141        _engine: crate::compile::select::Engine,
142    ) -> Option<crate::ast::CompiledSlotKit> {
143        crate::compile::assembly::ref_copy_kit(self.expected)
144    }
145}
146
147fn value_matches(v: &Value, typ: PortType) -> bool {
148    match (v, typ) {
149        (Value::U64(_), PortType::U64) => true,
150        (Value::F64(_), PortType::F64) => true,
151        (Value::Bool(_), PortType::Bool) => true,
152        (Value::Str(_), PortType::Str) => true,
153        (Value::Bytes(_), PortType::Bytes) => true,
154        (Value::Json(_), PortType::Json) => true,
155        // Narrow-int variants ride in the wider variant per the
156        // PortType doc on `node.rs` — accept the natural carrier.
157        (Value::U64(_), PortType::U32) => true,
158        (Value::U64(_), PortType::I32) => true,
159        (Value::U64(_), PortType::I64) => true,
160        (Value::F64(_), PortType::F32) => true,
161        (Value::U64(_), PortType::U8 | PortType::U16) => true,
162        // F16 rides its bit pattern in U64 (same stuffing as F32
163        // node outputs); host-written F64 also satisfies F16.
164        (Value::U64(_), PortType::F16) => true,
165        (Value::F64(_), PortType::F16) => true,
166        // Honest signed carrier serves all signed widths; legacy
167        // stuffed-U64 forms for the narrow signed projections
168        // remain accepted during the alignment migration.
169        (Value::I64(_), PortType::I64 | PortType::I32 | PortType::I8 | PortType::I16) => true,
170        (Value::U64(_), PortType::I8 | PortType::I16) => true,
171        (Value::U128(_), PortType::U128) => true,
172        (Value::I128(_), PortType::I128) => true,
173        // Register views are free bitcasts of one another.
174        (
175            Value::Reg128(_, _),
176            PortType::Reg128
177            | PortType::RegI8x16
178            | PortType::RegI16x8
179            | PortType::RegI32x4
180            | PortType::RegI64x2
181            | PortType::RegF16x8
182            | PortType::RegF32x4
183            | PortType::RegF64x2,
184        ) => true,
185        // Ext is opaque; we accept any concrete reflection.
186        (Value::Ext(_), PortType::Ext) => true,
187        _ => false,
188    }
189}
190
191// =========================================================================
192// Value assertions: type + constraint pair
193// =========================================================================
194
195/// Runtime value-constraint guard. Holds a [`ConstConstraint`]
196/// the value must satisfy each cycle. Panics with a structured
197/// message on violation; passes the value through otherwise.
198///
199/// Constructed with [`assert_value_node`] from the compiler when
200/// the source can't statically be proven to deliver a value
201/// satisfying the sink's constraint. Reuses the same
202/// `ConstConstraint` vocabulary the const-validator uses, so the
203/// two layers speak one language.
204pub struct AssertValue {
205    meta: NodeMeta,
206    typ: PortType,
207    constraint: ConstConstraint,
208}
209
210impl AssertValue {
211    /// A value assertion for `typ` under `constraint`, named by the pair.
212    pub fn new(typ: PortType, constraint: ConstConstraint) -> Self {
213        let name = match (&typ, &constraint) {
214            (PortType::U64, ConstConstraint::NonZeroU64) => "assert_u64_nonzero",
215            (PortType::U64, ConstConstraint::RangeU64 { .. }) => "assert_u64_range",
216            (PortType::U64, ConstConstraint::AllowedU64(_)) => "assert_u64_allowed",
217            (PortType::F64, ConstConstraint::RangeF64 { .. }) => "assert_f64_range",
218            (PortType::Str, ConstConstraint::NonEmptyStr) => "assert_str_non_empty",
219            (PortType::Str, ConstConstraint::StrParser(_)) => "assert_str_parses",
220            // Catch-all for combinations we haven't dedicated a
221            // distinct DSL name to yet.
222            _ => "assert_value",
223        };
224        Self {
225            meta: NodeMeta {
226                name: name.into(),
227                outs: vec![Port::new("output", typ)],
228                ins: vec![Slot::Wire(Port::new("input", typ))],
229            },
230            typ,
231            constraint,
232        }
233    }
234
235    /// The constraint asserted.
236    pub fn constraint(&self) -> &ConstConstraint {
237        &self.constraint
238    }
239
240    /// The type asserted.
241    pub fn port_type(&self) -> PortType {
242        self.typ
243    }
244}
245
246impl PolydatNode for AssertValue {
247    fn meta(&self) -> &NodeMeta {
248        &self.meta
249    }
250
251    fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
252        // Re-route the constraint check through `ConstConstraint::check`
253        // by lifting the value into a `ConstArg` shaped tuple. Avoids
254        // duplicating the per-variant logic between assembly and
255        // runtime.
256        let arg = match &inputs[0] {
257            Value::U64(v) => crate::dsl::factory::ConstArg::Int(*v),
258            Value::F64(v) => crate::dsl::factory::ConstArg::Float(*v),
259            Value::Str(s) => crate::dsl::factory::ConstArg::Str(s.to_string()),
260            other => panic!(
261                "{}: unsupported runtime value variant {:?}",
262                self.meta.name, other
263            ),
264        };
265        if let Err(msg) = self.constraint.check(&arg, "value") {
266            panic!("{}: {msg}", self.meta.name);
267        }
268        outputs[0] = inputs[0].clone();
269    }
270
271    /// The compiled form: the same constraint checked against the slot,
272    /// decoded by the asserted type, with the same message on failure.
273    /// A carrier reads as its integer, a float from its bits; the
274    /// other shapes have no u64 form.
275    fn compiled_u64(&self) -> Option<crate::ast::CompiledU64Op> {
276        use crate::dsl::factory::ConstArg;
277        let lift: fn(u64) -> ConstArg = match self.typ {
278            PortType::U64 | PortType::U32 | PortType::U16 | PortType::U8 => ConstArg::Int,
279            PortType::F64 => |slot| ConstArg::Float(f64::from_bits(slot)),
280            _ => return None,
281        };
282        let name = self.meta.name.clone();
283        let constraint = self.constraint;
284        Some(Box::new(move |inputs: &[u64], outputs: &mut [u64]| {
285            if let Err(msg) = constraint.check(&lift(inputs[0]), "value") {
286                panic!("{name}: {msg}");
287            }
288            outputs[0] = inputs[0];
289        }))
290    }
291
292    /// A string reads through its pair, is checked, and is copied into
293    /// this step's own scratch (axiom S3).
294    fn compiled_slot(
295        &self,
296        _wire_types: &[PortType],
297        _engine: crate::compile::select::Engine,
298    ) -> Option<crate::ast::CompiledSlotKit> {
299        use crate::dsl::factory::ConstArg;
300        if self.typ != PortType::Str {
301            return None;
302        }
303        let name = self.meta.name.clone();
304        let constraint = self.constraint;
305        let copy = crate::compile::assembly::ref_copy_kit(PortType::Str)?;
306        Some(crate::ast::CompiledSlotKit {
307            scratch: copy.scratch,
308            op: Box::new(
309                move |inputs: &[u64],
310                      outputs: &mut [u64],
311                      scratch: &mut [crate::ast::ScratchBuf]| {
312                    // SAFETY: the pair was published by the producing
313                    // step into storage alive until it reruns (S3, S4).
314                    let text = unsafe {
315                        std::str::from_utf8_unchecked(std::slice::from_raw_parts(
316                            inputs[0] as usize as *const u8,
317                            inputs[1] as usize,
318                        ))
319                    };
320                    if let Err(msg) = constraint.check(&ConstArg::Str(text.to_string()), "value") {
321                        panic!("{name}: {msg}");
322                    }
323                    (copy.op)(inputs, outputs, scratch);
324                },
325            ),
326        })
327    }
328}
329
330// =========================================================================
331// Helpers used by the compiler when auto-wiring assertions
332// =========================================================================
333
334/// Construct the right type assertion node for a given `PortType`.
335pub fn assert_type_node(typ: PortType) -> Box<dyn PolydatNode> {
336    Box::new(AssertType::new(typ))
337}
338
339/// Construct a value assertion node for the given (type, constraint) pair.
340pub fn assert_value_node(typ: PortType, constraint: ConstConstraint) -> Box<dyn PolydatNode> {
341    Box::new(AssertValue::new(typ, constraint))
342}
343
344// =========================================================================
345// Tests
346// =========================================================================
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn assert_u64_passes_u64_through() {
354        let node = AssertType::new(PortType::U64);
355        let mut out = [Value::None];
356        node.eval(&[Value::U64(42)], &mut out);
357        assert_eq!(out[0].as_u64(), 42);
358    }
359
360    #[test]
361    #[should_panic(expected = "expected runtime value of type U64")]
362    fn assert_u64_panics_on_string() {
363        let node = AssertType::new(PortType::U64);
364        let mut out = [Value::None];
365        node.eval(&[Value::Str("not a number".into())], &mut out);
366    }
367
368    #[test]
369    fn assert_value_nonzero_passes_nonzero() {
370        let node = AssertValue::new(PortType::U64, ConstConstraint::NonZeroU64);
371        let mut out = [Value::None];
372        node.eval(&[Value::U64(7)], &mut out);
373        assert_eq!(out[0].as_u64(), 7);
374    }
375
376    #[test]
377    #[should_panic(expected = "must be non-zero")]
378    fn assert_value_nonzero_panics_on_zero() {
379        let node = AssertValue::new(PortType::U64, ConstConstraint::NonZeroU64);
380        let mut out = [Value::None];
381        node.eval(&[Value::U64(0)], &mut out);
382    }
383
384    #[test]
385    fn assert_value_range_f64_passes_unit_interval() {
386        let node = AssertValue::new(
387            PortType::F64,
388            ConstConstraint::RangeF64 { min: 0.0, max: 1.0 },
389        );
390        let mut out = [Value::None];
391        node.eval(&[Value::F64(0.5)], &mut out);
392        assert_eq!(out[0].as_f64(), 0.5);
393    }
394
395    #[test]
396    #[should_panic(expected = "must be in [0, 1]")]
397    fn assert_value_range_f64_panics_on_out_of_range() {
398        let node = AssertValue::new(
399            PortType::F64,
400            ConstConstraint::RangeF64 { min: 0.0, max: 1.0 },
401        );
402        let mut out = [Value::None];
403        node.eval(&[Value::F64(1.5)], &mut out);
404    }
405}