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
119pub(crate) fn 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// ── 値受けオペレータ core(single-source)─────────────────────────────────────
133// codegen(A2 rust)の native 経路が「被演算子を native 評価済みの Value」で直呼びし、
134// `eval_op` の各アームも同じ core を呼ぶ。意味論(overflow / finite / code-point 順 /
135// FORBIDDEN_KEY 等)は evaluate と完全一致(実装が一本なので divergence 不能)。
136
137/// add / sub / mul(int×int は checked、float×float は finite 検査、混在は TYPE_MISMATCH)。
138pub(crate) fn arith(op: &str, a: &Value, b: &Value) -> R {
139    match (a, b) {
140        (Value::Int(x), Value::Int(y)) => {
141            let r = match op {
142                "add" => x.checked_add(*y),
143                "sub" => x.checked_sub(*y),
144                _ => x.checked_mul(*y),
145            };
146            match r {
147                Some(v) => Ok(Value::Int(v)),
148                None => fail(
149                    ExprFailureCode::IntOverflow,
150                    format!("i64 overflow in {op}"),
151                ),
152            }
153        }
154        (Value::Float(x), Value::Float(y)) => {
155            let r = match op {
156                "add" => x + y,
157                "sub" => x - y,
158                _ => x * y,
159            };
160            check_finite(r)
161        }
162        _ => fail(
163            ExprFailureCode::TypeMismatch,
164            format!(
165                "{op}: int×int or float×float (got {}×{})",
166                a.type_name(),
167                b.type_name()
168            ),
169        ),
170    }
171}
172
173/// neg(int は checked_neg、float は finite)。
174pub(crate) fn neg(a: &Value) -> R {
175    match a {
176        Value::Int(i) => match i.checked_neg() {
177            Some(v) => Ok(Value::Int(v)),
178            None => fail(ExprFailureCode::IntOverflow, "i64 overflow in neg"),
179        },
180        Value::Float(f) => check_finite(-f),
181        other => fail(
182            ExprFailureCode::TypeMismatch,
183            format!("neg: numeric expected, got {}", other.type_name()),
184        ),
185    }
186}
187
188/// div(常に float 除算・0除算は NaN/Inf 規則で Failure)。
189pub(crate) fn div(a: &Value, b: &Value) -> R {
190    let fa = widen_to_float(a)?;
191    let fb = widen_to_float(b)?;
192    check_finite(fa / fb)
193}
194
195/// mod(truncated・符号は被除数。int mod 0 は MOD_ZERO、i64::MIN%-1 は checked_rem で overflow)。
196pub(crate) fn rem(a: &Value, b: &Value) -> R {
197    match (a, b) {
198        (Value::Int(x), Value::Int(y)) => {
199            if *y == 0 {
200                return fail(ExprFailureCode::ModZero, "int mod by zero");
201            }
202            match x.checked_rem(*y) {
203                Some(v) => Ok(Value::Int(v)),
204                None => fail(ExprFailureCode::IntOverflow, "i64 overflow in mod"),
205            }
206        }
207        (Value::Float(x), Value::Float(y)) => check_finite(x % y),
208        _ => fail(
209            ExprFailureCode::TypeMismatch,
210            format!(
211                "mod: int×int or float×float (got {}×{})",
212                a.type_name(),
213                b.type_name()
214            ),
215        ),
216    }
217}
218
219/// eq / ne(value_equals SSoT。arr/obj は TYPE_MISMATCH は value_equals 内で判定)。
220pub(crate) fn eq_ne(op: &str, a: &Value, b: &Value) -> R {
221    let equal = value_equals(a, b)?;
222    Ok(Value::Bool(if op == "eq" { equal } else { !equal }))
223}
224
225/// lt / le / gt / ge(同一型 int/float/string のみ。string は code-point 順)。
226pub(crate) fn compare(op: &str, a: &Value, b: &Value) -> R {
227    use std::cmp::Ordering;
228    let c: Ordering = match (a, b) {
229        (Value::Int(x), Value::Int(y)) => x.cmp(y),
230        (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).unwrap_or(Ordering::Equal),
231        (Value::Str(x), Value::Str(y)) => cmp_code_points(x, y),
232        _ => {
233            return fail(
234                ExprFailureCode::TypeMismatch,
235                format!(
236                    "{op}: same-typed int/float/string only (got {}×{})",
237                    a.type_name(),
238                    b.type_name()
239                ),
240            )
241        }
242    };
243    let res = match op {
244        "lt" => c == Ordering::Less,
245        "le" => c != Ordering::Greater,
246        "gt" => c == Ordering::Greater,
247        _ => c != Ordering::Less,
248    };
249    Ok(Value::Bool(res))
250}
251
252/// obj 構築。`__proto__` キーは fail-closed(FORBIDDEN_KEY)。キーは codegen では静的なので
253/// 生成側が順序どおり本 core を呼ぶ(値は評価済み Value)。
254pub(crate) fn make_obj(pairs: Vec<(String, Value)>) -> R {
255    for (k, _) in &pairs {
256        if k == FORBIDDEN_OBJECT_KEY {
257            return fail(
258                ExprFailureCode::ForbiddenKey,
259                format!("obj key \"{FORBIDDEN_OBJECT_KEY}\" is forbidden (fail-closed)"),
260            );
261        }
262    }
263    Ok(Value::Obj(pairs))
264}
265
266/// len(配列のみ。string length は v1 に無い)。
267pub(crate) fn len(a: &Value) -> R {
268    match a {
269        Value::Arr(v) => Ok(Value::Int(v.len() as i64)),
270        other => fail(
271            ExprFailureCode::TypeMismatch,
272            format!(
273                "len: arrays only (string length is not v1; got {})",
274                other.type_name()
275            ),
276        ),
277    }
278}
279
280const SAFE_INT: i64 = 9_007_199_254_740_991; // 2^53-1
281
282/// `{int:"…"}` リテラル(decimal 文字列 → i64。overflow と garbage を区別)。
283pub(crate) fn int_lit(s: &str) -> R {
284    match s.parse::<i64>() {
285        Ok(v) => Ok(Value::Int(v)),
286        Err(_) => {
287            if s.trim_start_matches('-')
288                .chars()
289                .all(|c| c.is_ascii_digit())
290                && !s.is_empty()
291                && s != "-"
292            {
293                fail(ExprFailureCode::IntOverflow, format!("i64 overflow: {s}"))
294            } else {
295                fail(
296                    ExprFailureCode::InvalidLiteral,
297                    format!("invalid int literal: {s}"),
298                )
299            }
300        }
301    }
302}
303
304/// `{float:n}` リテラル(finite 検査)。
305pub(crate) fn float_lit(n: f64) -> R {
306    check_finite(n)
307}
308
309/// bare number リテラル(§2.3 分類: 整数値かつ安全域 → int、小数 → float、整数だが範囲外 → INVALID_LITERAL)。
310pub(crate) fn number_lit(n: f64) -> R {
311    if n.is_finite() && n.fract() == 0.0 {
312        if n.abs() <= SAFE_INT as f64 {
313            Ok(Value::Int(n as i64))
314        } else {
315            fail(
316                ExprFailureCode::InvalidLiteral,
317                format!("integral literal {n} exceeds safe range; use {{int:\"…\"}}"),
318            )
319        }
320    } else {
321        check_finite(n)
322    }
323}
324
325/// Evaluate an IR expression node against a scope, producing a runtime [`Value`].
326pub fn evaluate(node: &J, scope: &[(String, Value)]) -> R {
327    match node {
328        J::Null => Ok(Value::Null),
329        J::Bool(b) => Ok(Value::Bool(*b)),
330        J::String(s) => Ok(Value::Str(s.clone())),
331        J::Number(n) => {
332            // Classification (§2.3): integral → int (must be safe-range), fractional → float.
333            if n.is_i64() {
334                let i = n.as_i64().unwrap();
335                // safe integer check: |i| <= 2^53-1
336                const SAFE: i64 = 9_007_199_254_740_991;
337                if !(-SAFE..=SAFE).contains(&i) {
338                    return fail(
339                        ExprFailureCode::InvalidLiteral,
340                        format!("integral literal {i} exceeds safe range; use {{int:\"…\"}}"),
341                    );
342                }
343                Ok(Value::Int(i))
344            } else if n.is_u64() {
345                // u64 beyond i64 => beyond safe range
346                fail(
347                    ExprFailureCode::InvalidLiteral,
348                    format!("integral literal {n} exceeds safe range; use {{int:\"…\"}}"),
349                )
350            } else {
351                let f = n.as_f64().ok_or(ExprFailure {
352                    code: ExprFailureCode::InvalidLiteral,
353                    message: format!("bad number literal {n}"),
354                })?;
355                check_finite(f)
356            }
357        }
358        J::Array(_) => fail(
359            ExprFailureCode::InvalidNode,
360            "bare array is not an expression (use {arr:[...]})",
361        ),
362        J::Object(map) => {
363            if map.len() != 1 {
364                let keys: Vec<&str> = map.keys().map(|s| s.as_str()).collect();
365                return fail(
366                    ExprFailureCode::InvalidNode,
367                    format!(
368                        "operator node must have exactly one key, got [{}]",
369                        keys.join(", ")
370                    ),
371                );
372            }
373            let (op, arg) = map.iter().next().unwrap();
374            eval_op(op, arg, scope)
375        }
376    }
377}
378
379fn eval_op(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
380    match op {
381        "int" => {
382            let s = arg.as_str().ok_or_else(|| ExprFailure {
383                code: ExprFailureCode::InvalidNode,
384                message: "{int:…} expects a string".into(),
385            })?;
386            int_lit(s)
387        }
388        "float" => {
389            let n = arg.as_f64().ok_or_else(|| ExprFailure {
390                code: ExprFailureCode::InvalidNode,
391                message: "{float:…} expects a number".into(),
392            })?;
393            float_lit(n)
394        }
395        "ref" | "refOpt" => eval_ref(op, arg, scope),
396        "obj" => {
397            let m = arg.as_object().ok_or_else(|| ExprFailure {
398                code: ExprFailureCode::InvalidNode,
399                message: "{obj:…} expects an object".into(),
400            })?;
401            // 順序どおり: __proto__ は値評価前に fail-closed(make_obj は評価済み対象なので
402            // ここではキー検査を先に行い evaluate の順序を保つ)。
403            let mut out = Vec::with_capacity(m.len());
404            for (k, v) in m {
405                if k == FORBIDDEN_OBJECT_KEY {
406                    return fail(
407                        ExprFailureCode::ForbiddenKey,
408                        format!("obj key \"{FORBIDDEN_OBJECT_KEY}\" is forbidden (fail-closed)"),
409                    );
410                }
411                out.push((k.clone(), evaluate(v, scope)?));
412            }
413            Ok(Value::Obj(out))
414        }
415        "arr" => {
416            let a = arg_array(op, arg)?;
417            let mut out = Vec::with_capacity(a.len());
418            for e in a {
419                out.push(evaluate(e, scope)?);
420            }
421            Ok(Value::Arr(out))
422        }
423        "add" | "sub" | "mul" => {
424            let (a, b) = eval_binary(op, arg, scope)?;
425            arith(op, &a, &b)
426        }
427        "neg" => {
428            let a = evaluate(arg_unary(op, arg)?, scope)?;
429            neg(&a)
430        }
431        "div" => {
432            let (a, b) = eval_binary(op, arg, scope)?;
433            div(&a, &b)
434        }
435        "mod" => {
436            let (a, b) = eval_binary(op, arg, scope)?;
437            rem(&a, &b)
438        }
439        "concat" => {
440            let a = arg_array(op, arg)?;
441            // n-ary(min 2 args)。arity<2 は不正 IR(expression-ir.md §2.1/§3/§6)。
442            if a.len() < 2 {
443                return fail(
444                    ExprFailureCode::InvalidNode,
445                    format!("concat expects >= 2 args, got {}", a.len()),
446                );
447            }
448            let mut s = String::new();
449            for e in a {
450                match evaluate(e, scope)? {
451                    Value::Str(p) => s.push_str(&p),
452                    other => {
453                        return fail(
454                            ExprFailureCode::TypeMismatch,
455                            format!(
456                                "concat: strings only (got {}; no implicit toString)",
457                                other.type_name()
458                            ),
459                        )
460                    }
461                }
462            }
463            Ok(Value::Str(s))
464        }
465        "eq" | "ne" => {
466            let (a, b) = eval_binary(op, arg, scope)?;
467            eq_ne(op, &a, &b)
468        }
469        "lt" | "le" | "gt" | "ge" => {
470            let (a, b) = eval_binary(op, arg, scope)?;
471            compare(op, &a, &b)
472        }
473        "and" | "or" => {
474            let (ea, eb) = raw_binary(op, arg)?;
475            let a = require_bool(&evaluate(ea, scope)?, op)?;
476            if op == "and" && !a {
477                return Ok(Value::Bool(false));
478            }
479            if op == "or" && a {
480                return Ok(Value::Bool(true));
481            }
482            Ok(Value::Bool(require_bool(&evaluate(eb, scope)?, op)?))
483        }
484        "not" => {
485            let a = require_bool(&evaluate(arg_unary(op, arg)?, scope)?, "not")?;
486            Ok(Value::Bool(!a))
487        }
488        "coalesce" => {
489            let (ea, eb) = raw_binary(op, arg)?;
490            let a = evaluate(ea, scope)?;
491            match a {
492                Value::Null => evaluate(eb, scope),
493                other => Ok(other),
494            }
495        }
496        "cond" => {
497            let a = arg
498                .as_array()
499                .filter(|a| a.len() == 3)
500                .ok_or_else(|| ExprFailure {
501                    code: ExprFailureCode::InvalidNode,
502                    message: "cond expects [c, t, e]".into(),
503                })?;
504            let c = require_bool(&evaluate(&a[0], scope)?, "cond")?;
505            evaluate(if c { &a[1] } else { &a[2] }, scope)
506        }
507        "len" => {
508            let a = evaluate(arg_unary(op, arg)?, scope)?;
509            len(&a)
510        }
511        _ => fail(
512            ExprFailureCode::UnknownOp,
513            format!("unknown operator: {op} (fail-closed)"),
514        ),
515    }
516}
517
518fn eval_ref(op: &str, arg: &J, scope: &[(String, Value)]) -> R {
519    let path = arg_array(op, arg)?;
520    if path.is_empty() || !path.iter().all(|p| p.is_string()) {
521        return fail(
522            ExprFailureCode::InvalidNode,
523            format!("{op} expects a non-empty string path"),
524        );
525    }
526    let head = path[0].as_str().unwrap();
527    let mut cur: Value = match scope.iter().find(|(k, _)| k == head) {
528        Some((_, v)) => v.clone(),
529        None => {
530            return fail(
531                ExprFailureCode::UnknownBinding,
532                format!("unknown binding: {head}"),
533            )
534        }
535    };
536    for seg_node in &path[1..] {
537        let seg = seg_node.as_str().unwrap();
538        match cur {
539            Value::Null => {
540                if op == "refOpt" {
541                    return Ok(Value::Null);
542                }
543                return fail(
544                    ExprFailureCode::NullRef,
545                    format!("null intermediate at .{seg} (use ?.)"),
546                );
547            }
548            Value::Obj(ref pairs) => match pairs.iter().find(|(k, _)| k == seg) {
549                Some((_, v)) => {
550                    let next = v.clone();
551                    cur = next;
552                }
553                None => {
554                    return fail(
555                        ExprFailureCode::MissingProp,
556                        format!("missing property .{seg}"),
557                    )
558                }
559            },
560            ref other => {
561                return fail(
562                    ExprFailureCode::TypeMismatch,
563                    format!("cannot access .{seg} on {}", other.type_name()),
564                )
565            }
566        }
567    }
568    Ok(cur)
569}
570
571/// Equality for eq/ne: null-vs-null allowed; otherwise same scalar type only.
572fn value_equals(a: &Value, b: &Value) -> Result<bool, ExprFailure> {
573    if matches!(a, Value::Null) || matches!(b, Value::Null) {
574        return Ok(matches!(a, Value::Null) && matches!(b, Value::Null));
575    }
576    let ta = a.type_name();
577    let tb = b.type_name();
578    if ta != tb {
579        return fail(
580            ExprFailureCode::TypeMismatch,
581            format!("eq/ne: same type only (got {ta}×{tb})"),
582        );
583    }
584    if ta == "arr" || ta == "obj" {
585        return fail(
586            ExprFailureCode::TypeMismatch,
587            "eq/ne: obj/arr equality is undefined in v1",
588        );
589    }
590    Ok(deep_equals(a, b))
591}
592
593// ── arg helpers ──────────────────────────────────────────────────────────────
594fn arg_array<'a>(op: &str, arg: &'a J) -> Result<&'a Vec<J>, ExprFailure> {
595    arg.as_array().ok_or_else(|| ExprFailure {
596        code: ExprFailureCode::InvalidNode,
597        message: format!("{op} expects an args array"),
598    })
599}
600fn arg_unary<'a>(op: &str, arg: &'a J) -> Result<&'a J, ExprFailure> {
601    let a = arg_array(op, arg)?;
602    if a.len() != 1 {
603        return Err(ExprFailure {
604            code: ExprFailureCode::InvalidNode,
605            message: format!("{op} expects 1 arg"),
606        });
607    }
608    Ok(&a[0])
609}
610fn raw_binary<'a>(op: &str, arg: &'a J) -> Result<(&'a J, &'a J), ExprFailure> {
611    let a = arg_array(op, arg)?;
612    if a.len() != 2 {
613        return Err(ExprFailure {
614            code: ExprFailureCode::InvalidNode,
615            message: format!("{op} expects 2 args"),
616        });
617    }
618    Ok((&a[0], &a[1]))
619}
620fn eval_binary(
621    op: &str,
622    arg: &J,
623    scope: &[(String, Value)],
624) -> Result<(Value, Value), ExprFailure> {
625    let (ea, eb) = raw_binary(op, arg)?;
626    Ok((evaluate(ea, scope)?, evaluate(eb, scope)?))
627}