1
//! Arithmetic with a runtime Ratio operand. Each test wraps its
2
//! expression in a `let*` that binds `X` to a real runtime ratio via
3
//! `(transaction-post-date 0)` — the same shape production scripts
4
//! use to flow host-fn return values into arithmetic. The legacy
5
//! `Symbol::new("X").with_value(Expr::WasmRuntime(Ratio))` pattern
6
//! was a workaround for the silent `compile_for_stack(WasmRuntime)`
7
//! path and is gone.
8

            
9
use super::common::{
10
    compile_and_validate, compile_expect_error, wrap_with_runtime_i32, wrap_with_runtime_ratio,
11
};
12

            
13
#[test]
14
1
fn index_arithmetic_over_runtime_count_compiles() {
15
    // ADR-0028: `(entity-count)` is a runtime Index (I32); Index arithmetic
16
    // accepts it, and the integer literal `1` stays Index. `(+ idx 1)` lowers
17
    // to a raw `i32.add` (it used to be refused as "ratio vs indices").
18
1
    compile_and_validate("(let* ((idx (entity-count))) (+ idx 1))");
19
1
}
20

            
21
#[test]
22
1
fn index_scalar_mix_is_refused() {
23
    // A runtime Index cannot mix with a Scalar — only an explicit bridge
24
    // crosses the strata.
25
1
    let err = compile_expect_error("(let* ((idx (entity-count))) (+ idx 1/2))");
26
1
    assert!(
27
1
        err.contains("mix dimensions") || err.contains("index"),
28
        "expected a strata-mix error, got: {err}",
29
    );
30
1
}
31

            
32
#[test]
33
1
fn runtime_addition() {
34
1
    compile_and_validate(&wrap_with_runtime_ratio("(+ X (/ 1 3))"));
35
1
}
36

            
37
#[test]
38
1
fn runtime_subtraction() {
39
1
    compile_and_validate(&wrap_with_runtime_ratio("(- X 1)"));
40
1
}
41

            
42
#[test]
43
1
fn runtime_multiplication() {
44
1
    compile_and_validate(&wrap_with_runtime_ratio("(* X 2)"));
45
1
}
46

            
47
#[test]
48
1
fn runtime_division() {
49
1
    compile_and_validate(&wrap_with_runtime_ratio("(/ X 3)"));
50
1
}
51

            
52
#[test]
53
1
fn runtime_nested_arithmetic() {
54
1
    compile_and_validate(&wrap_with_runtime_ratio("(+ (* X 2) (/ 1 3))"));
55
1
}
56

            
57
#[test]
58
1
fn index_div_scalar_literal_mix_is_refused_on_both_surfaces() {
59
    // eval↔codegen agreement: `(/ IDX 1/2)` (runtime Index ÷ fractional literal)
60
    // must be refused, not eval-predicted as I32 then codegen-rejected.
61
1
    let err = compile_expect_error(&wrap_with_runtime_i32("(/ IDX 1/2)"));
62
1
    assert!(
63
1
        err.contains("mix") || err.contains("index") || err.contains("scalar"),
64
        "expected a strata-mix error, got: {err}",
65
    );
66
1
}
67

            
68
#[test]
69
1
fn index_mod_scalar_literal_mix_is_refused_on_both_surfaces() {
70
1
    let err = compile_expect_error(&wrap_with_runtime_i32("(mod IDX 1/2)"));
71
1
    assert!(
72
1
        err.contains("mix") || err.contains("index") || err.contains("scalar"),
73
        "expected a strata-mix error, got: {err}",
74
    );
75
1
}
76

            
77
#[test]
78
1
fn index_to_scalar_bridges_runtime_count_into_scalar_arithmetic() {
79
    // ADR-0028 sanctioned crossing: a runtime Index (`(entity-count)`) can be
80
    // lifted to a Scalar explicitly, then mixed with a Scalar literal — the mix
81
    // that `index_scalar_mix_is_refused` forbids without the bridge.
82
1
    compile_and_validate(&wrap_with_runtime_i32("(+ (index->scalar IDX) 1/2)"));
83
1
}
84

            
85
#[test]
86
1
fn scalar_to_index_bridges_runtime_ratio_into_index_arithmetic() {
87
    // The reverse crossing: a runtime Scalar narrows to an Index (truncating
88
    // toward zero), then participates in Index arithmetic with the literal `1`.
89
1
    compile_and_validate(&wrap_with_runtime_ratio("(+ (scalar->index X) 1)"));
90
1
}
91

            
92
#[test]
93
1
fn bridge_round_trip_compiles() {
94
1
    compile_and_validate(&wrap_with_runtime_i32(
95
1
        "(scalar->index (index->scalar IDX))",
96
1
    ));
97
1
}
98

            
99
#[test]
100
1
fn index_to_scalar_rejects_fractional_constant() {
101
    // A constant fractional value is not an index; the bridge refuses it rather
102
    // than silently truncating.
103
1
    let err = compile_expect_error("(index->scalar 1/2)");
104
1
    assert!(
105
1
        err.contains("integer index") || err.contains("index"),
106
        "expected an index-domain error, got: {err}",
107
    );
108
1
}
109

            
110
#[test]
111
1
fn scalar_to_index_folds_constant_toward_zero() {
112
    // `7/2 → 3`: a constant scalar folds to an integer Index at compile time.
113
1
    compile_and_validate("(scalar->index 7/2)");
114
1
}
115

            
116
#[test]
117
1
fn index_to_scalar_rejects_out_of_i32_range_constant() {
118
    // An Index is an i32; a constant above i32::MAX is not an index. eval and
119
    // codegen agree on rejecting it (no eval-accepts/codegen-rejects drift).
120
1
    let err = compile_expect_error("(index->scalar 2147483648)");
121
1
    assert!(
122
1
        err.contains("range"),
123
        "expected an i32-range error, got: {err}"
124
    );
125
1
}
126

            
127
#[test]
128
1
fn scalar_to_index_rejects_out_of_i32_range_constant() {
129
    // A constant scalar whose truncation overflows i32 is a compile error on
130
    // both surfaces — never a silent `i32.wrap_i64` narrowing.
131
1
    let err = compile_expect_error("(scalar->index 4294967296)");
132
1
    assert!(
133
1
        err.contains("range"),
134
        "expected an i32-range error, got: {err}"
135
    );
136
1
}
137

            
138
#[test]
139
1
fn runtime_division_emits_div_by_zero_guard() {
140
    // `ratio_new` (the chokepoint `ratio_div` funnels through) guards a zero
141
    // denominator with a catchable `division-by-zero` throw; validate that the
142
    // guarded body is well-formed wasm (throw structure, stack-polymorphism
143
    // past the throw).
144
1
    compile_and_validate(&wrap_with_runtime_ratio("(/ X X)"));
145
1
}