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