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