1
//! Constant-folding eval handlers for `+ - * / MOD` over the ADR-0028
2
//! lattice. An all-literal call folds to an `Expr::Number` (integer
3
//! operands fold with Index semantics — `/` truncates, `MOD` is `rem` —
4
//! so the eval surface agrees with the `i32.div_s` / `i32.rem_s` codegen);
5
//! otherwise the handler returns `Expr::WasmRuntime(result_dim)`. Real
6
//! cross-strata refusal happens at codegen time in `compile::*`.
7

            
8
use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub};
9

            
10
use crate::ast::{Expr, Fraction, WasmType};
11
use crate::compiler::expr::{eval_value, format_expr};
12
use crate::error::{Error, Result};
13
use crate::runtime::SymbolTable;
14

            
15
29892
pub(super) fn try_fold(symbols: &mut SymbolTable, args: &[Expr]) -> Option<Vec<Fraction>> {
16
    // Probe on a CLONE: this only decides whether the call const-folds; it must
17
    // not apply an operand's compile-time effects to the live table (the actual
18
    // emission re-walks the operands on the live path).
19
29892
    args.iter()
20
41111
        .map(|arg| {
21
41111
            eval_value(&mut symbols.clone(), arg)
22
41111
                .ok()
23
41111
                .and_then(|e| match e {
24
21089
                    Expr::Number(n) => Some(n),
25
19809
                    _ => None,
26
40898
                })
27
41111
        })
28
29892
        .collect()
29
29892
}
30

            
31
99471
pub(super) fn try_fold_resolved(resolved: &[Expr]) -> Option<Vec<Fraction>> {
32
99471
    resolved
33
99471
        .iter()
34
175299
        .map(|e| match e {
35
151798
            Expr::Number(n) => Some(*n),
36
23501
            _ => None,
37
175299
        })
38
99471
        .collect()
39
99471
}
40

            
41
5893
fn all_integers(nums: &[Fraction]) -> bool {
42
11289
    nums.iter().all(|n| *n.denom() == 1)
43
5893
}
44

            
45
/// The runtime dimension of a resolved operand, or `None` for a numeric literal
46
/// (a dimension-flexible token).
47
46576
fn operand_dim(r: &Expr) -> Result<Option<WasmType>> {
48
46576
    match r {
49
20732
        Expr::Number(_) => Ok(None),
50
25844
        _ => match r.wasm_type() {
51
25844
            Some(t @ (WasmType::I32 | WasmType::Ratio | WasmType::Commodity)) => Ok(Some(t)),
52
            _ => Err(Error::Compile(format!(
53
                "expected number arguments, got {}",
54
                format_expr(r)
55
            ))),
56
        },
57
    }
58
46576
}
59

            
60
/// Mirror codegen's refusal (`emit_literal` / `additive_dim`) to mix a runtime
61
/// Index with any Scalar — a runtime Ratio OR a fractional literal. Integer
62
/// literals stay flexible (they coerce to the Index). Without this the eval
63
/// surface would optimistically predict `I32` for `(+ IDX 1/2)` while codegen
64
/// rejects it — eval↔codegen drift. The only legal crossing is an explicit
65
/// `index->scalar` / `scalar->index` bridge.
66
23501
fn reject_index_scalar_mix(resolved: &[Expr]) -> Result<()> {
67
23501
    let has_runtime_index = resolved
68
23501
        .iter()
69
36920
        .any(|r| !matches!(r, Expr::Number(_)) && r.wasm_type() == Some(WasmType::I32));
70
23501
    if !has_runtime_index {
71
13419
        return Ok(());
72
10082
    }
73
20164
    let has_scalar = resolved.iter().any(|r| match r {
74
9940
        Expr::Number(n) => *n.denom() != 1,
75
10224
        other => other.wasm_type() == Some(WasmType::Ratio),
76
20164
    });
77
10082
    if has_scalar {
78
        return Err(Error::Compile(
79
            "cannot mix an index (count) with a scalar; bridge with \
80
             index->scalar / scalar->index"
81
                .to_string(),
82
        ));
83
10082
    }
84
10082
    Ok(())
85
23501
}
86

            
87
/// The dominant runtime dimension across operands: Money > Scalar > Index;
88
/// all-literal defaults to Scalar. Used for `+ - * MOD`, whose result is the
89
/// single shared dimension in the legal (non-mixed) case.
90
23075
fn dominant_result_type(resolved: &[Expr]) -> Result<WasmType> {
91
23075
    reject_index_scalar_mix(resolved)?;
92
23075
    let mut money = false;
93
23075
    let mut scalar = false;
94
23075
    let mut index = false;
95
46150
    for r in resolved {
96
46150
        match operand_dim(r)? {
97
568
            Some(WasmType::Commodity) => money = true,
98
14626
            Some(WasmType::Ratio) => scalar = true,
99
10224
            Some(WasmType::I32) => index = true,
100
20732
            _ => {}
101
        }
102
    }
103
23075
    Ok(if money {
104
284
        WasmType::Commodity
105
22791
    } else if scalar {
106
12709
        WasmType::Ratio
107
10082
    } else if index {
108
10082
        WasmType::I32
109
    } else {
110
        WasmType::Ratio
111
    })
112
23075
}
113

            
114
/// The result dimension of `/`, mirroring codegen's LEFT-associative fold
115
/// (`combine_div`): Index÷Index→Index, Scalar÷Scalar→Scalar, Money÷Scalar→Money,
116
/// Money÷Money→Scalar. A dominant-type shortcut would drift from codegen for
117
/// chains like `(/ money scalar money)` (→ Scalar, not Money).
118
426
fn div_result_type(resolved: &[Expr]) -> Result<WasmType> {
119
426
    reject_index_scalar_mix(resolved)?;
120
    // The result dimension is the LEFT operand's dimension, mirroring codegen's
121
    // left-associative `combine_div`: Index÷Index→Index, Scalar÷*→Scalar, and
122
    // (ADR-0028 E2) Money÷anything→Money — money ÷ money no longer collapses to
123
    // Ratio, it stays Money carrying a dimensionless/compound unit term, in
124
    // lockstep with `commodity_div`. A leading bare literal seeds Index only
125
    // when the dominant runtime dim is Index, else Scalar.
126
426
    let leading = operand_dim(&resolved[0])?;
127
    Ok(match leading {
128
426
        Some(t) => t,
129
        None if dominant_result_type(resolved)? == WasmType::I32 => WasmType::I32,
130
        None => WasmType::Ratio,
131
    })
132
426
}
133

            
134
/// The `Fraction` (`Ratio<i64>`) operators panic (debug / `overflow-checks`) or
135
/// wrap (release) when a cross-multiply exceeds i64 — reachable from any
136
/// all-literal `(* huge huge)` / `(+ a/b c/d)` whose reduced terms overflow. So
137
/// const-fold through the `Checked*` traits and surface a structured
138
/// `Error::Compile` instead, per CLAUDE.md (never a panic / SIGABRT on input).
139
355
fn overflow() -> Error {
140
355
    Error::Compile("arithmetic overflow in constant expression".to_string())
141
355
}
142

            
143
61203
pub(super) fn fold_add(nums: &[Fraction]) -> Result<Fraction> {
144
122903
    nums.iter().try_fold(Fraction::from_integer(0), |a, b| {
145
122903
        a.checked_add(b).ok_or_else(overflow)
146
122903
    })
147
61203
}
148

            
149
13419
pub(super) fn fold_sub(nums: &[Fraction]) -> Result<Fraction> {
150
13419
    if nums.len() == 1 {
151
213
        return Fraction::from_integer(0)
152
213
            .checked_sub(&nums[0])
153
213
            .ok_or_else(overflow);
154
13206
    }
155
13206
    nums[1..]
156
13206
        .iter()
157
13561
        .try_fold(nums[0], |a, b| a.checked_sub(b).ok_or_else(overflow))
158
13419
}
159

            
160
5254
pub(super) fn fold_mul(nums: &[Fraction]) -> Result<Fraction> {
161
10437
    nums.iter().try_fold(Fraction::from_integer(1), |a, b| {
162
10437
        a.checked_mul(b).ok_or_else(overflow)
163
10437
    })
164
5254
}
165

            
166
/// Const-fold `/`: all-integer operands divide as Index (truncating toward
167
/// zero, matching `i32.div_s`); any fractional operand divides rationally.
168
5538
pub(super) fn fold_div(nums: &[Fraction]) -> Result<Fraction> {
169
5538
    if nums.len() == 1 {
170
213
        if *nums[0].numer() == 0 {
171
71
            return Err(Error::Compile("division by zero".to_string()));
172
142
        }
173
142
        return Ok(if all_integers(nums) {
174
            // `1 / numer` can't overflow, but keep the lattice's truncating
175
            // Index semantics.
176
71
            Fraction::from_integer(1 / *nums[0].numer())
177
        } else {
178
            // `recip` swaps numer/denom — infallible for a non-zero ratio.
179
71
            nums[0].recip()
180
        });
181
5325
    }
182
5325
    if all_integers(nums) {
183
4899
        let mut acc = *nums[0].numer();
184
5041
        for n in &nums[1..] {
185
5041
            if *n.numer() == 0 {
186
142
                return Err(Error::Compile("division by zero".to_string()));
187
4899
            }
188
            // `i64::MIN / -1` overflows — `checked_div` catches it.
189
4899
            acc = acc.checked_div(*n.numer()).ok_or_else(overflow)?;
190
        }
191
4757
        return Ok(Fraction::from_integer(acc));
192
426
    }
193
426
    let mut acc = nums[0];
194
426
    for n in &nums[1..] {
195
426
        if *n.numer() == 0 {
196
            return Err(Error::Compile("division by zero".to_string()));
197
426
        }
198
        // Rational division cross-multiplies — can overflow i64.
199
426
        acc = acc.checked_div(n).ok_or_else(overflow)?;
200
    }
201
355
    Ok(acc)
202
5538
}
203

            
204
/// Const-fold `MOD`: all-integer operands use `rem` (sign of the dividend,
205
/// matching `i32.rem_s`); fractional operands use floored modulo.
206
426
pub(super) fn fold_mod(nums: &[Fraction]) -> Result<Fraction> {
207
426
    if *nums[1].numer() == 0 {
208
        return Err(Error::Compile("division by zero in MOD".to_string()));
209
426
    }
210
426
    if all_integers(nums) {
211
        // Raw `%` panics on `i64::MIN % -1` (overflow). wasm `i32.rem_s` defines
212
        // that case as 0 (it does NOT trap, unlike `i32.div_s`), so `checked_rem`
213
        // returning `None` there folds to 0 — keeping the eval surface in lockstep
214
        // with `i32.rem_s` codegen rather than panicking the compiler.
215
355
        let r = nums[0].numer().checked_rem(*nums[1].numer()).unwrap_or(0);
216
355
        return Ok(Fraction::from_integer(r));
217
71
    }
218
    // Floored modulo `a - b*floor(a/b)` cross-multiplies twice — guard both.
219
71
    let quotient = nums[0].checked_div(&nums[1]).ok_or_else(overflow)?.floor();
220
    let scaled = nums[1].checked_mul(&quotient).ok_or_else(overflow)?;
221
    nums[0].checked_sub(&scaled).ok_or_else(overflow)
222
426
}
223

            
224
102737
fn resolve_all(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Vec<Expr>> {
225
201995
    args.iter().map(|arg| eval_value(symbols, arg)).collect()
226
102737
}
227

            
228
70361
pub(super) fn add(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
229
70361
    let resolved = resolve_all(symbols, args)?;
230
67095
    if let Some(nums) = try_fold_resolved(&resolved) {
231
55877
        return Ok(Expr::Number(fold_add(&nums)?));
232
11218
    }
233
11218
    Ok(Expr::WasmRuntime(dominant_result_type(&resolved)?))
234
70361
}
235

            
236
21655
pub(super) fn sub(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
237
21655
    if args.is_empty() {
238
        return Err(Error::Compile("- requires at least 1 argument".to_string()));
239
21655
    }
240
21655
    let resolved = resolve_all(symbols, args)?;
241
21655
    if let Some(nums) = try_fold_resolved(&resolved) {
242
12851
        return Ok(Expr::Number(fold_sub(&nums)?));
243
8804
    }
244
8804
    Ok(Expr::WasmRuntime(dominant_result_type(&resolved)?))
245
21655
}
246

            
247
6319
pub(super) fn mul(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
248
6319
    let resolved = resolve_all(symbols, args)?;
249
6319
    if let Some(nums) = try_fold_resolved(&resolved) {
250
3266
        return Ok(Expr::Number(fold_mul(&nums)?));
251
3053
    }
252
3053
    Ok(Expr::WasmRuntime(dominant_result_type(&resolved)?))
253
6319
}
254

            
255
4331
pub(super) fn div(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
256
4331
    if args.is_empty() {
257
        return Err(Error::Compile("/ requires at least 1 argument".to_string()));
258
4331
    }
259
4331
    let resolved = resolve_all(symbols, args)?;
260
4331
    if let Some(nums) = try_fold_resolved(&resolved) {
261
3905
        return Ok(Expr::Number(fold_div(&nums)?));
262
426
    }
263
426
    Ok(Expr::WasmRuntime(div_result_type(&resolved)?))
264
4331
}
265

            
266
71
pub(super) fn modulo(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
267
71
    if args.len() != 2 {
268
        return Err(Error::Arity {
269
            name: "MOD".to_string(),
270
            expected: 2,
271
            actual: args.len(),
272
        });
273
71
    }
274
71
    let resolved = resolve_all(symbols, args)?;
275
71
    if let Some(nums) = try_fold_resolved(&resolved) {
276
71
        return Ok(Expr::Number(fold_mod(&nums)?));
277
    }
278
    Ok(Expr::WasmRuntime(dominant_result_type(&resolved)?))
279
71
}