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::expr::FORBIDDEN_OBJECT_KEY;
9use crate::value::Value;
10use serde_json::Value as J;
11
12/// Error decoding a wire value into a runtime value.
13///
14/// `code` carries a stable conformance Failure code when the decode failure is a
15/// normative one (e.g. `"FORBIDDEN_KEY"` for a `"__proto__"` own key); otherwise
16/// it is `None` (a plain malformed-wire error).
17#[derive(Debug)]
18pub struct DecodeError {
19    pub msg: String,
20    pub code: Option<&'static str>,
21}
22
23impl DecodeError {
24    fn new(msg: impl Into<String>) -> Self {
25        DecodeError {
26            msg: msg.into(),
27            code: None,
28        }
29    }
30    fn coded(code: &'static str, msg: impl Into<String>) -> Self {
31        DecodeError {
32            msg: msg.into(),
33            code: Some(code),
34        }
35    }
36}
37
38impl std::fmt::Display for DecodeError {
39    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40        write!(f, "decode error: {}", self.msg)
41    }
42}
43impl std::error::Error for DecodeError {}
44
45/// Decode the golden-vector JSON value representation into a runtime [`Value`]
46/// (expression-ir.md §2.3 inverse map).
47///
48/// - bare JSON number: integral → int, otherwise → float.
49/// - `{"int":"…"}` → int (decimal string; may exceed the i64 safe range — the
50///   value is parsed as i64 here since the runtime is i64; literals beyond i64 are
51///   handled by the evaluator's `{int:…}` literal path, not by the decoder — the
52///   decoder is only used for scope/params/expect where i64 is sufficient).
53/// - `{"float":n}` → float (explicit integral-valued float).
54/// - `{"nan"}` / `{"inf":±1}` → NaN / ±Inf (canonical Failure fixtures only).
55pub fn decode_value(x: &J) -> Result<Value, DecodeError> {
56    match x {
57        J::Null => Ok(Value::Null),
58        J::Bool(b) => Ok(Value::Bool(*b)),
59        J::String(s) => Ok(Value::Str(s.clone())),
60        J::Number(n) => {
61            if let Some(i) = n.as_i64() {
62                // serde_json treats "1" as integer and "1.0" as float; but a bare
63                // integral JSON number in a vector means int (PROTOCOL §2).
64                if n.is_i64() || n.is_u64() {
65                    return Ok(Value::Int(i));
66                }
67                Ok(Value::Int(i))
68            } else if n.is_u64() {
69                Err(DecodeError::new(format!("integer {n} exceeds i64")))
70            } else {
71                // has a fractional part / is float
72                let f = n
73                    .as_f64()
74                    .ok_or_else(|| DecodeError::new(format!("bad number {n}")))?;
75                Ok(Value::Float(f))
76            }
77        }
78        J::Array(a) => {
79            let mut out = Vec::with_capacity(a.len());
80            for e in a {
81                out.push(decode_value(e)?);
82            }
83            Ok(Value::Arr(out))
84        }
85        J::Object(o) => {
86            if o.len() == 1 {
87                if let Some(J::String(s)) = o.get("int") {
88                    let i: i64 = s
89                        .parse()
90                        .map_err(|_| DecodeError::new(format!("int literal '{s}' not i64")))?;
91                    return Ok(Value::Int(i));
92                }
93                if let Some(J::Number(n)) = o.get("float") {
94                    let f = n
95                        .as_f64()
96                        .ok_or_else(|| DecodeError::new(format!("bad float {n}")))?;
97                    return Ok(Value::Float(f));
98                }
99                if o.contains_key("nan") {
100                    return Ok(Value::Float(f64::NAN));
101                }
102                if let Some(J::Number(n)) = o.get("inf") {
103                    let sign = n.as_f64().unwrap_or(1.0);
104                    return Ok(Value::Float(if sign < 0.0 {
105                        f64::NEG_INFINITY
106                    } else {
107                        f64::INFINITY
108                    }));
109                }
110            }
111            let mut out = Vec::with_capacity(o.len());
112            for (k, v) in o {
113                // fail-closed: own key "__proto__" diverges across languages
114                // (expression-ir.md §2.3/§8).
115                if k == FORBIDDEN_OBJECT_KEY {
116                    return Err(DecodeError::coded(
117                        "FORBIDDEN_KEY",
118                        format!("object key \"{FORBIDDEN_OBJECT_KEY}\" is forbidden (fail-closed)"),
119                    ));
120                }
121                out.push((k.clone(), decode_value(v)?));
122            }
123            Ok(Value::Obj(out))
124        }
125    }
126}
127
128/// Encode a runtime [`Value`] back into the golden wire representation
129/// (for producing diagnostic detail on a failing vector; mirrors TS `encodeValue`).
130pub fn encode_value(v: &Value) -> J {
131    match v {
132        Value::Null => J::Null,
133        Value::Bool(b) => J::Bool(*b),
134        Value::Str(s) => J::String(s.clone()),
135        Value::Int(i) => {
136            const SAFE: i64 = 9_007_199_254_740_991; // 2^53 - 1
137            if *i >= -SAFE && *i <= SAFE {
138                J::Number((*i).into())
139            } else {
140                let mut m = serde_json::Map::new();
141                m.insert("int".into(), J::String(i.to_string()));
142                J::Object(m)
143            }
144        }
145        Value::Float(f) => {
146            if f.fract() == 0.0 && f.is_finite() {
147                let mut m = serde_json::Map::new();
148                m.insert(
149                    "float".into(),
150                    serde_json::Number::from_f64(*f)
151                        .map(J::Number)
152                        .unwrap_or(J::Null),
153                );
154                J::Object(m)
155            } else {
156                serde_json::Number::from_f64(*f)
157                    .map(J::Number)
158                    .unwrap_or(J::Null)
159            }
160        }
161        Value::Arr(a) => J::Array(a.iter().map(encode_value).collect()),
162        Value::Obj(o) => {
163            let mut m = serde_json::Map::new();
164            for (k, val) in o {
165                m.insert(k.clone(), encode_value(val));
166            }
167            J::Object(m)
168        }
169    }
170}