Skip to main content

behavior_contracts/
expr.rs

1//! expr — reference evaluator for expression-ir.md (normative).
2//!
3//! Value model: int = checked i64 (overflow → Failure, never wrap/panic),
4//! float = f64 (NaN/±Inf → Failure), string / bool / null / arr / obj.
5//!
6//! Normative points (§6/§8 traps):
7//!   - int arithmetic is checked i64 (overflow = INT_OVERFLOW).
8//!   - a result that becomes NaN/±Inf is a Failure.
9//!   - mod is truncated division (sign follows dividend; Rust `%` already does this).
10//!   - string comparison is code-point order (Rust `str` Ord = UTF-8 byte order = code point).
11//!   - and/or/coalesce/cond short-circuit.
12//!   - unknown operators fail closed.
13
14use crate::value::{deep_equals, Value};
15use serde_json::Value as J;
16
17/// Expression failure codes (PROTOCOL §3.1).
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ExprFailureCode {
20    IntOverflow,
21    NanOrInf,
22    ModZero,
23    PrecisionLoss,
24    TypeMismatch,
25    NullRef,
26    MissingProp,
27    UnknownBinding,
28    UnknownOp,
29    InvalidNode,
30    InvalidLiteral,
31}
32
33impl ExprFailureCode {
34    /// The stable string form used for conformance code matching.
35    pub fn as_str(self) -> &'static str {
36        match self {
37            ExprFailureCode::IntOverflow => "INT_OVERFLOW",
38            ExprFailureCode::NanOrInf => "NAN_OR_INF",
39            ExprFailureCode::ModZero => "MOD_ZERO",
40            ExprFailureCode::PrecisionLoss => "PRECISION_LOSS",
41            ExprFailureCode::TypeMismatch => "TYPE_MISMATCH",
42            ExprFailureCode::NullRef => "NULL_REF",
43            ExprFailureCode::MissingProp => "MISSING_PROP",
44            ExprFailureCode::UnknownBinding => "UNKNOWN_BINDING",
45            ExprFailureCode::UnknownOp => "UNKNOWN_OP",
46            ExprFailureCode::InvalidNode => "INVALID_NODE",
47            ExprFailureCode::InvalidLiteral => "INVALID_LITERAL",
48        }
49    }
50}
51
52#[derive(Debug, Clone)]
53pub struct ExprFailure {
54    pub code: ExprFailureCode,
55    pub message: String,
56}
57
58impl std::fmt::Display for ExprFailure {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(f, "{}: {}", self.code.as_str(), self.message)
61    }
62}
63impl std::error::Error for ExprFailure {}
64
65type R = Result<Value, ExprFailure>;
66
67fn fail<T>(code: ExprFailureCode, message: impl Into<String>) -> Result<T, ExprFailure> {
68    Err(ExprFailure {
69        code,
70        message: message.into(),
71    })
72}
73
74const WIDEN_EXACT: i64 = 1 << 53; // ±2^53 exact in f64
75
76fn check_finite(v: f64) -> R {
77    if v.is_finite() {
78        Ok(Value::Float(v))
79    } else {
80        fail(ExprFailureCode::NanOrInf, format!("non-finite float: {v}"))
81    }
82}
83
84fn widen_to_float(v: &Value) -> Result<f64, ExprFailure> {
85    match v {
86        Value::Float(f) => Ok(*f),
87        Value::Int(i) => {
88            if *i > WIDEN_EXACT || *i < -WIDEN_EXACT {
89                fail(
90                    ExprFailureCode::PrecisionLoss,
91                    format!("int {i} exceeds exact float range (±2^53)"),
92                )
93            } else {
94                Ok(*i as f64)
95            }
96        }
97        other => fail(
98            ExprFailureCode::TypeMismatch,
99            format!("numeric operand expected, got {}", other.type_name()),
100        ),
101    }
102}
103
104/// Compare two strings by code point. Rust `str` Ord IS UTF-8 byte order which
105/// equals Unicode code-point order, so we can delegate to the native comparison.
106/// Kept as a named function to make the normative choice explicit.
107pub fn cmp_code_points(a: &str, b: &str) -> std::cmp::Ordering {
108    a.cmp(b)
109}
110
111fn require_bool(v: &Value, ctx: &str) -> Result<bool, ExprFailure> {
112    match v {
113        Value::Bool(b) => Ok(*b),
114        other => fail(
115            ExprFailureCode::TypeMismatch,
116            format!(
117                "{ctx}: bool expected, got {} (no truthiness)",
118                other.type_name()
119            ),
120        ),
121    }
122}
123
124/// Evaluate an IR expression node against a scope, producing a runtime [`Value`].
125pub fn evaluate(node: &J, scope: &[(String, Value)]) -> R {
126    match node {
127        J::Null => Ok(Value::Null),
128        J::Bool(b) => Ok(Value::Bool(*b)),
129        J::String(s) => Ok(Value::Str(s.clone())),
130        J::Number(n) => {
131            // Classification (§2.3): integral → int (must be safe-range), fractional → float.
132            if n.is_i64() {
133                let i = n.as_i64().unwrap();
134                // safe integer check: |i| <= 2^53-1
135                const SAFE: i64 = 9_007_199_254_740_991;
136                if !(-SAFE..=SAFE).contains(&i) {
137                    return fail(
138                        ExprFailureCode::InvalidLiteral,
139                        format!("integral literal {i} exceeds safe range; use {{int:\"…\"}}"),
140                    );
141                }
142                Ok(Value::Int(i))
143            } else if n.is_u64() {
144                // u64 beyond i64 => beyond safe range
145                fail(
146                    ExprFailureCode::InvalidLiteral,
147                    format!("integral literal {n} exceeds safe range; use {{int:\"…\"}}"),
148                )
149            } else {
150                let f = n.as_f64().ok_or(ExprFailure {
151                    code: ExprFailureCode::InvalidLiteral,
152                    message: format!("bad number literal {n}"),
153                })?;
154                check_finite(f)
155            }
156        }
157        J::Array(_) => fail(
158            ExprFailureCode::InvalidNode,
159            "bare array is not an expression (use {arr:[...]})",
160        ),
161        J::Object(map) => {
162            if map.len() != 1 {
163                let keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
164                return fail(
165                    ExprFailureCode::InvalidNode,
166                    format!(
167                        "operator node must have exactly one key, got [{}]",
168                        keys.join(", ")
169                    ),
170                );
171            }
172            let (op, arg) = map.iter().next().unwrap();
173            eval_op(op, arg, scope)
174        }
175    }
176}
177
178fn eval_op(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
179    match op {
180        "int" => {
181            let s = arg.as_str().ok_or_else(|| ExprFailure {
182                code: ExprFailureCode::InvalidNode,
183                message: "{int:…} expects a string".into(),
184            })?;
185            match s.parse::<i64>() {
186                Ok(v) => Ok(Value::Int(v)),
187                Err(_) => {
188                    // distinguish overflow (valid decimal beyond i64) from garbage.
189                    if s.trim_start_matches('-')
190                        .chars()
191                        .all(|c| c.is_ascii_digit())
192                        && !s.is_empty()
193                        && s != "-"
194                    {
195                        fail(ExprFailureCode::IntOverflow, format!("i64 overflow: {s}"))
196                    } else {
197                        fail(
198                            ExprFailureCode::InvalidLiteral,
199                            format!("invalid int literal: {s}"),
200                        )
201                    }
202                }
203            }
204        }
205        "float" => {
206            let n = arg.as_f64().ok_or_else(|| ExprFailure {
207                code: ExprFailureCode::InvalidNode,
208                message: "{float:…} expects a number".into(),
209            })?;
210            check_finite(n)
211        }
212        "ref" | "refOpt" => eval_ref(op, arg, scope),
213        "obj" => {
214            let m = arg.as_object().ok_or_else(|| ExprFailure {
215                code: ExprFailureCode::InvalidNode,
216                message: "{obj:…} expects an object".into(),
217            })?;
218            let mut out = Vec::with_capacity(m.len());
219            for (k, v) in m {
220                out.push((k.clone(), evaluate(v, scope)?));
221            }
222            Ok(Value::Obj(out))
223        }
224        "arr" => {
225            let a = arg_array(op, arg)?;
226            let mut out = Vec::with_capacity(a.len());
227            for e in a {
228                out.push(evaluate(e, scope)?);
229            }
230            Ok(Value::Arr(out))
231        }
232        "add" | "sub" | "mul" => {
233            let (a, b) = eval_binary(op, arg, scope)?;
234            match (&a, &b) {
235                (Value::Int(x), Value::Int(y)) => {
236                    let r = match op {
237                        "add" => x.checked_add(*y),
238                        "sub" => x.checked_sub(*y),
239                        _ => x.checked_mul(*y),
240                    };
241                    match r {
242                        Some(v) => Ok(Value::Int(v)),
243                        None => fail(
244                            ExprFailureCode::IntOverflow,
245                            format!("i64 overflow in {op}"),
246                        ),
247                    }
248                }
249                (Value::Float(x), Value::Float(y)) => {
250                    let r = match op {
251                        "add" => x + y,
252                        "sub" => x - y,
253                        _ => x * y,
254                    };
255                    check_finite(r)
256                }
257                _ => fail(
258                    ExprFailureCode::TypeMismatch,
259                    format!(
260                        "{op}: int×int or float×float (got {}×{})",
261                        a.type_name(),
262                        b.type_name()
263                    ),
264                ),
265            }
266        }
267        "neg" => {
268            let a = evaluate(arg_unary(op, arg)?, scope)?;
269            match a {
270                Value::Int(i) => match i.checked_neg() {
271                    Some(v) => Ok(Value::Int(v)),
272                    None => fail(ExprFailureCode::IntOverflow, "i64 overflow in neg"),
273                },
274                Value::Float(f) => check_finite(-f),
275                other => fail(
276                    ExprFailureCode::TypeMismatch,
277                    format!("neg: numeric expected, got {}", other.type_name()),
278                ),
279            }
280        }
281        "div" => {
282            let (a, b) = eval_binary(op, arg, scope)?;
283            let fa = widen_to_float(&a)?;
284            let fb = widen_to_float(&b)?;
285            check_finite(fa / fb)
286        }
287        "mod" => {
288            let (a, b) = eval_binary(op, arg, scope)?;
289            match (&a, &b) {
290                (Value::Int(x), Value::Int(y)) => {
291                    if *y == 0 {
292                        return fail(ExprFailureCode::ModZero, "int mod by zero");
293                    }
294                    // Rust `%` is truncated (sign follows dividend), matching spec.
295                    // checked_rem guards the i64::MIN % -1 overflow edge.
296                    match x.checked_rem(*y) {
297                        Some(v) => Ok(Value::Int(v)),
298                        None => fail(ExprFailureCode::IntOverflow, "i64 overflow in mod"),
299                    }
300                }
301                (Value::Float(x), Value::Float(y)) => check_finite(x % y),
302                _ => fail(
303                    ExprFailureCode::TypeMismatch,
304                    format!(
305                        "mod: int×int or float×float (got {}×{})",
306                        a.type_name(),
307                        b.type_name()
308                    ),
309                ),
310            }
311        }
312        "concat" => {
313            let a = arg_array(op, arg)?;
314            let mut s = String::new();
315            for e in a {
316                match evaluate(e, scope)? {
317                    Value::Str(p) => s.push_str(&p),
318                    other => {
319                        return fail(
320                            ExprFailureCode::TypeMismatch,
321                            format!(
322                                "concat: strings only (got {}; no implicit toString)",
323                                other.type_name()
324                            ),
325                        )
326                    }
327                }
328            }
329            Ok(Value::Str(s))
330        }
331        "eq" | "ne" => {
332            let (a, b) = eval_binary(op, arg, scope)?;
333            let equal = value_equals(&a, &b)?;
334            Ok(Value::Bool(if op == "eq" { equal } else { !equal }))
335        }
336        "lt" | "le" | "gt" | "ge" => {
337            use std::cmp::Ordering;
338            let (a, b) = eval_binary(op, arg, scope)?;
339            let c: Ordering = match (&a, &b) {
340                (Value::Int(x), Value::Int(y)) => x.cmp(y),
341                (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
342                (Value::Str(x), Value::Str(y)) => cmp_code_points(x, y),
343                _ => {
344                    return fail(
345                        ExprFailureCode::TypeMismatch,
346                        format!(
347                            "{op}: same-typed int/float/string only (got {}×{})",
348                            a.type_name(),
349                            b.type_name()
350                        ),
351                    )
352                }
353            };
354            let res = match op {
355                "lt" => c == Ordering::Less,
356                "le" => c != Ordering::Greater,
357                "gt" => c == Ordering::Greater,
358                _ => c != Ordering::Less,
359            };
360            Ok(Value::Bool(res))
361        }
362        "and" | "or" => {
363            let (ea, eb) = raw_binary(op, arg)?;
364            let a = require_bool(&evaluate(ea, scope)?, op)?;
365            if op == "and" && !a {
366                return Ok(Value::Bool(false));
367            }
368            if op == "or" && a {
369                return Ok(Value::Bool(true));
370            }
371            Ok(Value::Bool(require_bool(&evaluate(eb, scope)?, op)?))
372        }
373        "not" => {
374            let a = require_bool(&evaluate(arg_unary(op, arg)?, scope)?, "not")?;
375            Ok(Value::Bool(!a))
376        }
377        "coalesce" => {
378            let (ea, eb) = raw_binary(op, arg)?;
379            let a = evaluate(ea, scope)?;
380            match a {
381                Value::Null => evaluate(eb, scope),
382                other => Ok(other),
383            }
384        }
385        "cond" => {
386            let a = arg
387                .as_array()
388                .filter(|a| a.len() == 3)
389                .ok_or_else(|| ExprFailure {
390                    code: ExprFailureCode::InvalidNode,
391                    message: "cond expects [c, t, e]".into(),
392                })?;
393            let c = require_bool(&evaluate(&a[0], scope)?, "cond")?;
394            evaluate(if c { &a[1] } else { &a[2] }, scope)
395        }
396        "len" => {
397            let a = evaluate(arg_unary(op, arg)?, scope)?;
398            match a {
399                Value::Arr(v) => Ok(Value::Int(v.len() as i64)),
400                other => fail(
401                    ExprFailureCode::TypeMismatch,
402                    format!(
403                        "len: arrays only (string length is not v1; got {})",
404                        other.type_name()
405                    ),
406                ),
407            }
408        }
409        _ => fail(
410            ExprFailureCode::UnknownOp,
411            format!("unknown operator: {op} (fail-closed)"),
412        ),
413    }
414}
415
416fn eval_ref(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
417    let path = arg_array(op, arg)?;
418    if path.is_empty() || !path.iter().all(|p| p.is_string()) {
419        return fail(
420            ExprFailureCode::InvalidNode,
421            format!("{op} expects a non-empty string path"),
422        );
423    }
424    let head = path[0].as_str().unwrap();
425    let mut cur: Value = match scope.iter().find(|(k, _)| k == head) {
426        Some((_, v)) => v.clone(),
427        None => {
428            return fail(
429                ExprFailureCode::UnknownBinding,
430                format!("unknown binding: {head}"),
431            )
432        }
433    };
434    for seg_node in &path[1..] {
435        let seg = seg_node.as_str().unwrap();
436        match cur {
437            Value::Null => {
438                if op == "refOpt" {
439                    return Ok(Value::Null);
440                }
441                return fail(
442                    ExprFailureCode::NullRef,
443                    format!("null intermediate at .{seg} (use ?.)"),
444                );
445            }
446            Value::Obj(ref pairs) => match pairs.iter().find(|(k, _)| k == seg) {
447                Some((_, v)) => {
448                    let next = v.clone();
449                    cur = next;
450                }
451                None => {
452                    return fail(
453                        ExprFailureCode::MissingProp,
454                        format!("missing property .{seg}"),
455                    )
456                }
457            },
458            ref other => {
459                return fail(
460                    ExprFailureCode::TypeMismatch,
461                    format!("cannot access .{seg} on {}", other.type_name()),
462                )
463            }
464        }
465    }
466    Ok(cur)
467}
468
469/// Equality for eq/ne: null-vs-null allowed; otherwise same scalar type only.
470fn value_equals(a: &Value, b: &Value) -> Result<bool, ExprFailure> {
471    if matches!(a, Value::Null) || matches!(b, Value::Null) {
472        return Ok(matches!(a, Value::Null) && matches!(b, Value::Null));
473    }
474    let ta = a.type_name();
475    let tb = b.type_name();
476    if ta != tb {
477        return fail(
478            ExprFailureCode::TypeMismatch,
479            format!("eq/ne: same type only (got {ta}×{tb})"),
480        );
481    }
482    if ta == "arr" || ta == "obj" {
483        return fail(
484            ExprFailureCode::TypeMismatch,
485            "eq/ne: obj/arr equality is undefined in v1",
486        );
487    }
488    Ok(deep_equals(a, b))
489}
490
491// ── arg helpers ──────────────────────────────────────────────────────────────
492fn arg_array<'a>(op: &str, arg: &'a J) -> Result<&'a Vec<J>, ExprFailure> {
493    arg.as_array().ok_or_else(|| ExprFailure {
494        code: ExprFailureCode::InvalidNode,
495        message: format!("{op} expects an args array"),
496    })
497}
498fn arg_unary<'a>(op: &str, arg: &'a J) -> Result<&'a J, ExprFailure> {
499    let a = arg_array(op, arg)?;
500    if a.len() != 1 {
501        return Err(ExprFailure {
502            code: ExprFailureCode::InvalidNode,
503            message: format!("{op} expects 1 arg"),
504        });
505    }
506    Ok(&a[0])
507}
508fn raw_binary<'a>(op: &str, arg: &'a J) -> Result<(&'a J, &'a J), ExprFailure> {
509    let a = arg_array(op, arg)?;
510    if a.len() != 2 {
511        return Err(ExprFailure {
512            code: ExprFailureCode::InvalidNode,
513            message: format!("{op} expects 2 args"),
514        });
515    }
516    Ok((&a[0], &a[1]))
517}
518fn eval_binary(
519    op: &str,
520    arg: &J,
521    scope: &[(String, Value)],
522) -> Result<(Value, Value), ExprFailure> {
523    let (ea, eb) = raw_binary(op, arg)?;
524    Ok((evaluate(ea, scope)?, evaluate(eb, scope)?))
525}