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