behavior-contracts 0.1.2

Language-neutral IR runtime core (expression evaluation, template rendering, execution plan, canonical serialization) shared across DSL implementations. Passes the dsl-contracts conformance vectors byte-for-byte.
Documentation
//! guard — Portability Guard (`assert_portable`).
//!
//! DSL-agnostic IR portability invariant: a portable IR must not contain
//! non-serializable values. In Rust the runtime works over `serde_json::Value`
//! (already JSON-serializable) plus the native [`crate::value::Value`], so the
//! only structurally non-portable case is a non-finite float (NaN/±Inf) which
//! cannot be represented in JSON. `assert_portable` walks a value and rejects it.

use crate::value::Value;

#[derive(Debug, Clone)]
pub struct PortabilityError {
    pub path: String,
    pub message: String,
}

impl std::fmt::Display for PortabilityError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Portability Guard at {}: {}", self.path, self.message)
    }
}
impl std::error::Error for PortabilityError {}

/// Assert that a runtime [`Value`] is portable (JSON-serializable). The only
/// non-portable scalar in the Rust value model is a non-finite float.
pub fn assert_portable(value: &Value) -> Result<(), PortabilityError> {
    assert_portable_at(value, "$")
}

fn assert_portable_at(value: &Value, path: &str) -> Result<(), PortabilityError> {
    match value {
        Value::Float(f) if !f.is_finite() => Err(PortabilityError {
            path: path.to_string(),
            message: format!("non-finite float {f} cannot be placed in portable IR"),
        }),
        Value::Arr(a) => {
            for (i, v) in a.iter().enumerate() {
                assert_portable_at(v, &format!("{path}[{i}]"))?;
            }
            Ok(())
        }
        Value::Obj(o) => {
            for (k, v) in o {
                assert_portable_at(v, &format!("{path}.{k}"))?;
            }
            Ok(())
        }
        _ => Ok(()),
    }
}