Skip to main content

behavior_contracts/
guard.rs

1//! guard — Portability Guard (`assert_portable`).
2//!
3//! DSL-agnostic IR portability invariant: a portable IR must not contain
4//! non-serializable values. In Rust the runtime works over `serde_json::Value`
5//! (already JSON-serializable) plus the native [`crate::value::Value`], so the
6//! only structurally non-portable case is a non-finite float (NaN/±Inf) which
7//! cannot be represented in JSON. `assert_portable` walks a value and rejects it.
8
9use crate::value::Value;
10
11#[derive(Debug, Clone)]
12pub struct PortabilityError {
13    pub path: String,
14    pub message: String,
15}
16
17impl std::fmt::Display for PortabilityError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        write!(f, "Portability Guard at {}: {}", self.path, self.message)
20    }
21}
22impl std::error::Error for PortabilityError {}
23
24/// Assert that a runtime [`Value`] is portable (JSON-serializable). The only
25/// non-portable scalar in the Rust value model is a non-finite float.
26pub fn assert_portable(value: &Value) -> Result<(), PortabilityError> {
27    assert_portable_at(value, "$")
28}
29
30fn assert_portable_at(value: &Value, path: &str) -> Result<(), PortabilityError> {
31    match value {
32        Value::Float(f) if !f.is_finite() => Err(PortabilityError {
33            path: path.to_string(),
34            message: format!("non-finite float {f} cannot be placed in portable IR"),
35        }),
36        Value::Arr(a) => {
37            for (i, v) in a.iter().enumerate() {
38                assert_portable_at(v, &format!("{path}[{i}]"))?;
39            }
40            Ok(())
41        }
42        Value::Obj(o) => {
43            for (k, v) in o {
44                assert_portable_at(v, &format!("{path}.{k}"))?;
45            }
46            Ok(())
47        }
48        _ => Ok(()),
49    }
50}