behavior-contracts 0.2.4

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
//! codec — conformance runner adapter COMMON part (PROTOCOL §2).
//!
//! Converts the golden-vector typed wire encoding (`{int:"…"}` / `{float:n}` /
//! `{nan}` / `{inf}` / bare JSON number classification) into the runtime [`Value`],
//! and back. All language runners must decode identically to produce identical
//! pass/fail results.

use crate::expr::FORBIDDEN_OBJECT_KEY;
use crate::value::Value;
use serde_json::Value as J;

/// Error decoding a wire value into a runtime value.
///
/// `code` carries a stable conformance Failure code when the decode failure is a
/// normative one (e.g. `"FORBIDDEN_KEY"` for a `"__proto__"` own key); otherwise
/// it is `None` (a plain malformed-wire error).
#[derive(Debug)]
pub struct DecodeError {
    pub msg: String,
    pub code: Option<&'static str>,
}

impl DecodeError {
    fn new(msg: impl Into<String>) -> Self {
        DecodeError {
            msg: msg.into(),
            code: None,
        }
    }
    fn coded(code: &'static str, msg: impl Into<String>) -> Self {
        DecodeError {
            msg: msg.into(),
            code: Some(code),
        }
    }
}

impl std::fmt::Display for DecodeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "decode error: {}", self.msg)
    }
}
impl std::error::Error for DecodeError {}

/// Decode the golden-vector JSON value representation into a runtime [`Value`]
/// (expression-ir.md §2.3 inverse map).
///
/// - bare JSON number: integral → int, otherwise → float.
/// - `{"int":"…"}` → int (decimal string; may exceed the i64 safe range — the
///   value is parsed as i64 here since the runtime is i64; literals beyond i64 are
///   handled by the evaluator's `{int:…}` literal path, not by the decoder — the
///   decoder is only used for scope/params/expect where i64 is sufficient).
/// - `{"float":n}` → float (explicit integral-valued float).
/// - `{"nan"}` / `{"inf":±1}` → NaN / ±Inf (canonical Failure fixtures only).
pub fn decode_value(x: &J) -> Result<Value, DecodeError> {
    match x {
        J::Null => Ok(Value::Null),
        J::Bool(b) => Ok(Value::Bool(*b)),
        J::String(s) => Ok(Value::Str(s.clone())),
        J::Number(n) => {
            if let Some(i) = n.as_i64() {
                // serde_json treats "1" as integer and "1.0" as float; but a bare
                // integral JSON number in a vector means int (PROTOCOL §2).
                if n.is_i64() || n.is_u64() {
                    return Ok(Value::Int(i));
                }
                Ok(Value::Int(i))
            } else if n.is_u64() {
                Err(DecodeError::new(format!("integer {n} exceeds i64")))
            } else {
                // has a fractional part / is float
                let f = n
                    .as_f64()
                    .ok_or_else(|| DecodeError::new(format!("bad number {n}")))?;
                Ok(Value::Float(f))
            }
        }
        J::Array(a) => {
            let mut out = Vec::with_capacity(a.len());
            for e in a {
                out.push(decode_value(e)?);
            }
            Ok(Value::Arr(out))
        }
        J::Object(o) => {
            if o.len() == 1 {
                if let Some(J::String(s)) = o.get("int") {
                    let i: i64 = s
                        .parse()
                        .map_err(|_| DecodeError::new(format!("int literal '{s}' not i64")))?;
                    return Ok(Value::Int(i));
                }
                if let Some(J::Number(n)) = o.get("float") {
                    let f = n
                        .as_f64()
                        .ok_or_else(|| DecodeError::new(format!("bad float {n}")))?;
                    return Ok(Value::Float(f));
                }
                if o.contains_key("nan") {
                    return Ok(Value::Float(f64::NAN));
                }
                if let Some(J::Number(n)) = o.get("inf") {
                    let sign = n.as_f64().unwrap_or(1.0);
                    return Ok(Value::Float(if sign < 0.0 {
                        f64::NEG_INFINITY
                    } else {
                        f64::INFINITY
                    }));
                }
            }
            let mut out = Vec::with_capacity(o.len());
            for (k, v) in o {
                // fail-closed: own key "__proto__" diverges across languages
                // (expression-ir.md §2.3/§8).
                if k == FORBIDDEN_OBJECT_KEY {
                    return Err(DecodeError::coded(
                        "FORBIDDEN_KEY",
                        format!("object key \"{FORBIDDEN_OBJECT_KEY}\" is forbidden (fail-closed)"),
                    ));
                }
                out.push((k.clone(), decode_value(v)?));
            }
            Ok(Value::Obj(out))
        }
    }
}

/// Encode a runtime [`Value`] back into the golden wire representation
/// (for producing diagnostic detail on a failing vector; mirrors TS `encodeValue`).
pub fn encode_value(v: &Value) -> J {
    match v {
        Value::Null => J::Null,
        Value::Bool(b) => J::Bool(*b),
        Value::Str(s) => J::String(s.clone()),
        Value::Int(i) => {
            const SAFE: i64 = 9_007_199_254_740_991; // 2^53 - 1
            if *i >= -SAFE && *i <= SAFE {
                J::Number((*i).into())
            } else {
                let mut m = serde_json::Map::new();
                m.insert("int".into(), J::String(i.to_string()));
                J::Object(m)
            }
        }
        Value::Float(f) => {
            if f.fract() == 0.0 && f.is_finite() {
                let mut m = serde_json::Map::new();
                m.insert(
                    "float".into(),
                    serde_json::Number::from_f64(*f)
                        .map(J::Number)
                        .unwrap_or(J::Null),
                );
                J::Object(m)
            } else {
                serde_json::Number::from_f64(*f)
                    .map(J::Number)
                    .unwrap_or(J::Null)
            }
        }
        Value::Arr(a) => J::Array(a.iter().map(encode_value).collect()),
        Value::Obj(o) => {
            let mut m = serde_json::Map::new();
            for (k, val) in o {
                m.insert(k.clone(), encode_value(val));
            }
            J::Object(m)
        }
    }
}