1
//! Constant-folding regressions: fully-static expressions must
2
//! evaluate at compile time to the same `Value` the runtime would
3
//! produce, and the wasm emitted for them must validate.
4

            
5
use nomiscript::{Fraction, Reader, SymbolTable, Value, eval_program};
6

            
7
use super::common::{compile_and_validate, compile_expect_error};
8

            
9
9
fn eval_to_value(symbols: &mut SymbolTable, code: &str) -> Value {
10
9
    let program = Reader::parse(code).unwrap();
11
9
    eval_program(symbols, &program).unwrap()
12
9
}
13

            
14
#[test]
15
1
fn add_constant_fold() {
16
1
    let mut symbols = SymbolTable::with_builtins();
17
1
    assert_eq!(
18
1
        eval_to_value(&mut symbols, "(+ 1 2 3)"),
19
1
        Value::Number(Fraction::from_integer(6)),
20
    );
21
1
}
22

            
23
#[test]
24
1
fn sub_constant_fold() {
25
1
    let mut symbols = SymbolTable::with_builtins();
26
1
    assert_eq!(
27
1
        eval_to_value(&mut symbols, "(- 10 3)"),
28
1
        Value::Number(Fraction::from_integer(7)),
29
    );
30
1
}
31

            
32
#[test]
33
1
fn mul_constant_fold() {
34
1
    let mut symbols = SymbolTable::with_builtins();
35
1
    assert_eq!(
36
1
        eval_to_value(&mut symbols, "(* 3 4)"),
37
1
        Value::Number(Fraction::from_integer(12)),
38
    );
39
1
}
40

            
41
#[test]
42
1
fn integer_div_constant_fold_truncates() {
43
    // ADR-0028: integer literals are Index, so `(/ 10 3)` is integer division
44
    // (truncating toward zero, matching `i32.div_s`) — not the old rational 10/3.
45
1
    let mut symbols = SymbolTable::with_builtins();
46
1
    assert_eq!(
47
1
        eval_to_value(&mut symbols, "(/ 10 3)"),
48
1
        Value::Number(Fraction::from_integer(3)),
49
    );
50
1
}
51

            
52
#[test]
53
1
fn ratio_arithmetic_constant_fold() {
54
    // Rational arithmetic uses fractional literals (`1/3`, the Scalar idiom);
55
    // `(/ 1 3)` would now be integer division (Index → 0).
56
1
    let mut symbols = SymbolTable::with_builtins();
57
1
    assert_eq!(
58
1
        eval_to_value(&mut symbols, "(+ 1/3 1/4)"),
59
1
        Value::Number(Fraction::new(7, 12)),
60
    );
61
1
}
62

            
63
#[test]
64
1
fn comparison_constant_fold() {
65
1
    let mut symbols = SymbolTable::with_builtins();
66
1
    assert_eq!(eval_to_value(&mut symbols, "(= 1 1)"), Value::Bool(true));
67
1
    assert_eq!(eval_to_value(&mut symbols, "(< 1 2)"), Value::Bool(true));
68
1
    assert_eq!(eval_to_value(&mut symbols, "(> 3 2)"), Value::Bool(true));
69
1
}
70

            
71
#[test]
72
1
fn compile_constant_arithmetic() {
73
1
    compile_and_validate("(+ (/ 1 3) (/ 1 4))");
74
1
}
75

            
76
#[test]
77
1
fn compile_constant_comparison() {
78
1
    compile_and_validate("(= 1 2)");
79
1
}
80

            
81
#[test]
82
1
fn compile_nested_arithmetic() {
83
1
    compile_and_validate("(* (+ 1 2) (- 5 3))");
84
1
}
85

            
86
/// Regression (AFL): const-folding arithmetic over `Ratio<i64>` used the bare
87
/// `+`/`-`/`*`/`/` operators, which panic (debug / overflow-checks) or wrap
88
/// (release) when a cross-multiply exceeds i64. An all-literal overflowing
89
/// expression must now surface a structured compile error, never a SIGABRT.
90
#[test]
91
1
fn constant_arithmetic_overflow_is_a_compile_error_not_a_panic() {
92
5
    for src in [
93
1
        "(* 9999999999 9999999999)",
94
1
        "(+ 1/9999999999 1/9999999998)",
95
1
        "(- 1/9999999999 1/9999999998)",
96
1
        "(/ 1/9999999999 9999999999)",
97
1
        "(mod 1/9999999999 7777777777)",
98
1
    ] {
99
5
        let err = compile_expect_error(src);
100
5
        assert!(
101
5
            err.contains("overflow"),
102
            "{src} should report an overflow compile error, got: {err}"
103
        );
104
    }
105
1
}
106

            
107
/// Regression (adversarial review): integer `MOD` const-folding used the raw
108
/// Rust `%`, which panics on `i64::MIN % -1`. wasm `i32.rem_s` defines that case
109
/// as 0 (it does NOT trap), so the fold must yield 0 to stay in lockstep with
110
/// codegen — never panic. `i64::MIN` is reached by folding `(- 0 i64::MAX 1)`
111
/// since the bare `MIN` literal is out of the reader's range.
112
#[test]
113
1
fn integer_mod_min_by_neg_one_folds_to_zero_not_panic() {
114
1
    let mut symbols = SymbolTable::with_builtins();
115
1
    assert_eq!(
116
1
        eval_to_value(&mut symbols, "(mod (- 0 9223372036854775807 1) -1)"),
117
1
        Value::Number(Fraction::from_integer(0)),
118
    );
119
    // And the same form compiles to validating wasm (no panic on the fold path).
120
1
    compile_and_validate("(mod (- 0 9223372036854775807 1) -1)");
121
1
}