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