Skip to main content

behavior_contracts/
codec.rs

1//! codec — conformance runner adapter COMMON part (PROTOCOL §2).
2//!
3//! Converts the golden-vector typed wire encoding (`{int:"…"}` / `{float:n}` /
4//! `{nan}` / `{inf}` / bare JSON number classification) into the runtime [`Value`],
5//! and back. All language runners must decode identically to produce identical
6//! pass/fail results.
7
8use crate::value::Value;
9use serde_json::Value as J;
10
11/// Error decoding a wire value into a runtime value.
12#[derive(Debug)]
13pub struct DecodeError(pub String);
14
15impl std::fmt::Display for DecodeError {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        write!(f, "decode error: {}", self.0)
18    }
19}
20impl std::error::Error for DecodeError {}
21
22/// Decode the golden-vector JSON value representation into a runtime [`Value`]
23/// (expression-ir.md §2.3 inverse map).
24///
25/// - bare JSON number: integral → int, otherwise → float.
26/// - `{"int":"…"}` → int (decimal string; may exceed the i64 safe range — the
27///   value is parsed as i64 here since the runtime is i64; literals beyond i64 are
28///   handled by the evaluator's `{int:…}` literal path, not by the decoder — the
29///   decoder is only used for scope/params/expect where i64 is sufficient).
30/// - `{"float":n}` → float (explicit integral-valued float).
31/// - `{"nan"}` / `{"inf":±1}` → NaN / ±Inf (canonical Failure fixtures only).
32pub fn decode_value(x: &J) -> Result<Value, DecodeError> {
33    match x {
34        J::Null => Ok(Value::Null),
35        J::Bool(b) => Ok(Value::Bool(*b)),
36        J::String(s) => Ok(Value::Str(s.clone())),
37        J::Number(n) => {
38            if let Some(i) = n.as_i64() {
39                // serde_json treats "1" as integer and "1.0" as float; but a bare
40                // integral JSON number in a vector means int (PROTOCOL §2).
41                if n.is_i64() || n.is_u64() {
42                    return Ok(Value::Int(i));
43                }
44                Ok(Value::Int(i))
45            } else if n.is_u64() {
46                Err(DecodeError(format!("integer {n} exceeds i64")))
47            } else {
48                // has a fractional part / is float
49                let f = n
50                    .as_f64()
51                    .ok_or_else(|| DecodeError(format!("bad number {n}")))?;
52                Ok(Value::Float(f))
53            }
54        }
55        J::Array(a) => {
56            let mut out = Vec::with_capacity(a.len());
57            for e in a {
58                out.push(decode_value(e)?);
59            }
60            Ok(Value::Arr(out))
61        }
62        J::Object(o) => {
63            if o.len() == 1 {
64                if let Some(J::String(s)) = o.get("int") {
65                    let i: i64 = s
66                        .parse()
67                        .map_err(|_| DecodeError(format!("int literal '{s}' not i64")))?;
68                    return Ok(Value::Int(i));
69                }
70                if let Some(J::Number(n)) = o.get("float") {
71                    let f = n
72                        .as_f64()
73                        .ok_or_else(|| DecodeError(format!("bad float {n}")))?;
74                    return Ok(Value::Float(f));
75                }
76                if o.contains_key("nan") {
77                    return Ok(Value::Float(f64::NAN));
78                }
79                if let Some(J::Number(n)) = o.get("inf") {
80                    let sign = n.as_f64().unwrap_or(1.0);
81                    return Ok(Value::Float(if sign < 0.0 {
82                        f64::NEG_INFINITY
83                    } else {
84                        f64::INFINITY
85                    }));
86                }
87            }
88            let mut out = Vec::with_capacity(o.len());
89            for (k, v) in o {
90                out.push((k.clone(), decode_value(v)?));
91            }
92            Ok(Value::Obj(out))
93        }
94    }
95}
96
97/// Encode a runtime [`Value`] back into the golden wire representation
98/// (for producing diagnostic detail on a failing vector; mirrors TS `encodeValue`).
99pub fn encode_value(v: &Value) -> J {
100    match v {
101        Value::Null => J::Null,
102        Value::Bool(b) => J::Bool(*b),
103        Value::Str(s) => J::String(s.clone()),
104        Value::Int(i) => {
105            const SAFE: i64 = 9_007_199_254_740_991; // 2^53 - 1
106            if *i >= -SAFE && *i <= SAFE {
107                J::Number((*i).into())
108            } else {
109                let mut m = serde_json::Map::new();
110                m.insert("int".into(), J::String(i.to_string()));
111                J::Object(m)
112            }
113        }
114        Value::Float(f) => {
115            if f.fract() == 0.0 && f.is_finite() {
116                let mut m = serde_json::Map::new();
117                m.insert(
118                    "float".into(),
119                    serde_json::Number::from_f64(*f)
120                        .map(J::Number)
121                        .unwrap_or(J::Null),
122                );
123                J::Object(m)
124            } else {
125                serde_json::Number::from_f64(*f)
126                    .map(J::Number)
127                    .unwrap_or(J::Null)
128            }
129        }
130        Value::Arr(a) => J::Array(a.iter().map(encode_value).collect()),
131        Value::Obj(o) => {
132            let mut m = serde_json::Map::new();
133            for (k, val) in o {
134                m.insert(k.clone(), encode_value(val));
135            }
136            J::Object(m)
137        }
138    }
139}