1
//! `to_stack` codegen for `+ - * / MOD` over the ADR-0028 dimension
2
//! lattice {Index, Scalar, Money}. An all-literal call const-folds; a
3
//! runtime call compiles each non-literal operand to a local (which
4
//! yields its real wasm type — including closure-call results the eval
5
//! path cannot reduce), classifies the operation's dimension, then emits
6
//! the dimension's native ops: raw `i32` for Index, `ratio_*` for Scalar,
7
//! `commodity_*` for Money. A numeric literal is dimension-flexible and
8
//! crosses the sanctioned Index↔Scalar boundary; everything else is a
9
//! compile-time strata mismatch.
10

            
11
use crate::ast::{Expr, Fraction, WasmType};
12
use crate::compiler::context::CompileContext;
13
use crate::compiler::emit::FunctionEmitter;
14
use crate::compiler::expr::{compile_for_effect, compile_for_stack, eval_value, push_ratio};
15
use crate::error::{Error, Result};
16
use crate::runtime::SymbolTable;
17

            
18
use super::eval::{fold_add, fold_div, fold_mod, fold_mul, fold_sub, try_fold};
19

            
20
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
21
enum Dim {
22
    Index,
23
    Scalar,
24
    Money,
25
}
26

            
27
impl Dim {
28
18520
    fn wasm_type(self) -> WasmType {
29
18520
        match self {
30
9476
            Dim::Index => WasmType::I32,
31
7956
            Dim::Scalar => WasmType::Ratio,
32
1088
            Dim::Money => WasmType::Commodity,
33
        }
34
18520
    }
35

            
36
49212
    fn of(ty: WasmType, op: &str) -> Result<Dim> {
37
49212
        match ty {
38
20720
            WasmType::I32 => Ok(Dim::Index),
39
22372
            WasmType::Ratio => Ok(Dim::Scalar),
40
5848
            WasmType::Commodity => Ok(Dim::Money),
41
272
            other => Err(Error::Compile(format!(
42
272
                "{op} expects numeric arguments, got a {other} value"
43
272
            ))),
44
        }
45
49212
    }
46
}
47

            
48
/// A compiled operand: a dimension-flexible numeric literal (not yet emitted),
49
/// or a runtime value already evaluated into `local` with its wasm type.
50
enum Slot {
51
    Literal(Fraction),
52
    Runtime(u32, WasmType),
53
}
54

            
55
impl Slot {
56
    /// The fixed dimension of a runtime slot, or `None` for a flexible literal.
57
40780
    fn runtime_dim(&self, op: &str) -> Result<Option<Dim>> {
58
40780
        match self {
59
16888
            Slot::Literal(_) => Ok(None),
60
23892
            Slot::Runtime(_, ty) => Dim::of(*ty, op).map(Some),
61
        }
62
40780
    }
63
}
64

            
65
/// Compile each operand once: a literal (syntactic, or a constant that resolves
66
/// to a `Number`) stays flexible; everything else is emitted to a fresh local,
67
/// capturing its real wasm type and its side effects in operand order. The
68
/// literal probe runs on a clone so it applies no live mutation.
69
19200
fn collect_slots(
70
19200
    ctx: &mut CompileContext,
71
19200
    emit: &mut FunctionEmitter,
72
19200
    symbols: &mut SymbolTable,
73
19200
    args: &[Expr],
74
19200
) -> Result<Vec<Slot>> {
75
19200
    let mut slots = Vec::with_capacity(args.len());
76
38400
    for arg in args {
77
38400
        match eval_value(&mut symbols.clone(), arg) {
78
15528
            Ok(Expr::Number(n)) => {
79
                // A non-literal that *resolves* to a number (e.g. `(setf x 1)`)
80
                // still emits its effects on the live table, in operand order,
81
                // before the flexible literal is recorded.
82
15528
                if !matches!(arg, Expr::Number(_)) {
83
2312
                    compile_for_effect(ctx, emit, symbols, arg)?;
84
13216
                }
85
15528
                slots.push(Slot::Literal(n));
86
            }
87
            _ => {
88
22872
                let ty = compile_for_stack(ctx, emit, symbols, arg)?;
89
22804
                let local = ctx.alloc_local(ty)?;
90
22804
                emit.local_set(local);
91
22804
                slots.push(Slot::Runtime(local, ty));
92
            }
93
        }
94
    }
95
19132
    Ok(slots)
96
19200
}
97

            
98
/// Emit a single slot coerced to `dim`. A literal crosses Index↔Scalar; a
99
/// runtime slot must already be that dimension (a runtime value never coerces).
100
37448
fn emit_slot(
101
37448
    ctx: &CompileContext,
102
37448
    emit: &mut FunctionEmitter,
103
37448
    slot: &Slot,
104
37448
    dim: Dim,
105
37448
    op: &str,
106
37448
) -> Result<()> {
107
37448
    match slot {
108
15052
        Slot::Literal(n) => emit_literal(ctx, emit, *n, dim, op),
109
22396
        Slot::Runtime(local, ty) => {
110
22396
            if Dim::of(*ty, op)? == dim {
111
22396
                emit.local_get(*local);
112
22396
                Ok(())
113
            } else {
114
                Err(mix_error(op))
115
            }
116
        }
117
    }
118
37448
}
119

            
120
15052
fn emit_literal(
121
15052
    ctx: &CompileContext,
122
15052
    emit: &mut FunctionEmitter,
123
15052
    n: Fraction,
124
15052
    dim: Dim,
125
15052
    op: &str,
126
15052
) -> Result<()> {
127
15052
    match dim {
128
        Dim::Index => {
129
9000
            if *n.denom() != 1 {
130
272
                return Err(Error::Compile(format!(
131
272
                    "{op}: a fractional literal is a scalar, not an index"
132
272
                )));
133
8728
            }
134
8728
            emit.i32_const(i32::try_from(*n.numer()).map_err(|_| {
135
                Error::Compile(format!("integer literal {} exceeds i32 range", n.numer()))
136
            })?);
137
8728
            Ok(())
138
        }
139
        Dim::Scalar => {
140
6052
            push_ratio(ctx, emit, *n.numer(), *n.denom());
141
6052
            Ok(())
142
        }
143
        Dim::Money => Err(Error::Compile(format!(
144
            "{op} cannot combine a bare literal with a money value (a literal \
145
             has no currency); supply a money value or scale with `*`"
146
        ))),
147
    }
148
15052
}
149

            
150
/// The single dimension every operand of an additive op (`+`/`-`/`MOD`) shares:
151
/// all runtime operands must agree, and a bare literal next to Money is refused.
152
16208
fn additive_dim(slots: &[Slot], op: &str) -> Result<Dim> {
153
32076
    let mut runtime = slots.iter().filter_map(|s| s.runtime_dim(op).transpose());
154
16208
    let first = match runtime.next() {
155
16208
        Some(d) => d?,
156
        None => Dim::Scalar,
157
    };
158
15936
    for d in runtime {
159
2584
        if d? != first {
160
            return Err(mix_error(op));
161
2584
        }
162
    }
163
15936
    if first == Dim::Money && slots.iter().any(|s| matches!(s, Slot::Literal(_))) {
164
        return Err(Error::Compile(format!(
165
            "{op} cannot combine a bare literal with a money value (a literal \
166
             has no currency); supply a money value"
167
        )));
168
15936
    }
169
15936
    Ok(first)
170
16208
}
171

            
172
8909
fn push_const(ctx: &CompileContext, emit: &mut FunctionEmitter, n: Fraction) -> Result<WasmType> {
173
8909
    if *n.denom() == 1 {
174
8433
        emit.i32_const(i32::try_from(*n.numer()).map_err(|_| {
175
            Error::Compile(format!("integer literal {} exceeds i32 range", n.numer()))
176
        })?);
177
8433
        Ok(WasmType::I32)
178
    } else {
179
476
        push_ratio(ctx, emit, *n.numer(), *n.denom());
180
476
        Ok(WasmType::Ratio)
181
    }
182
8909
}
183

            
184
19473
pub(super) fn compile_add_to_stack(
185
19473
    ctx: &mut CompileContext,
186
19473
    emit: &mut FunctionEmitter,
187
19473
    symbols: &mut SymbolTable,
188
19473
    args: &[Expr],
189
19473
) -> Result<WasmType> {
190
19473
    if let Some(nums) = try_fold(symbols, args) {
191
5101
        return push_const(ctx, emit, fold_add(&nums)?);
192
14372
    }
193
14372
    if args.is_empty() {
194
        emit.i32_const(0);
195
        return Ok(WasmType::I32);
196
14372
    }
197
14372
    compile_additive(ctx, emit, symbols, args, "+", BinOp::Add)
198
19473
}
199

            
200
2312
pub(super) fn compile_sub_to_stack(
201
2312
    ctx: &mut CompileContext,
202
2312
    emit: &mut FunctionEmitter,
203
2312
    symbols: &mut SymbolTable,
204
2312
    args: &[Expr],
205
2312
) -> Result<WasmType> {
206
2312
    if args.is_empty() {
207
68
        return Err(Error::Compile("- requires at least 1 argument".to_string()));
208
2244
    }
209
2244
    if let Some(nums) = try_fold(symbols, args) {
210
544
        return push_const(ctx, emit, fold_sub(&nums)?);
211
1700
    }
212
1700
    let slots = collect_slots(ctx, emit, symbols, args)?;
213
1700
    let dim = additive_dim(&slots, "-")?;
214
1700
    if slots.len() == 1 {
215
68
        return emit_unary_neg(ctx, emit, &slots[0], dim);
216
1632
    }
217
1632
    emit_slot(ctx, emit, &slots[0], dim, "-")?;
218
1632
    for slot in &slots[1..] {
219
1632
        emit_slot(ctx, emit, slot, dim, "-")?;
220
1632
        emit_combine(ctx, emit, dim, BinOp::Sub);
221
    }
222
1632
    Ok(dim.wasm_type())
223
2312
}
224

            
225
4352
pub(super) fn compile_mul_to_stack(
226
4352
    ctx: &mut CompileContext,
227
4352
    emit: &mut FunctionEmitter,
228
4352
    symbols: &mut SymbolTable,
229
4352
    args: &[Expr],
230
4352
) -> Result<WasmType> {
231
4352
    if let Some(nums) = try_fold(symbols, args) {
232
1904
        return push_const(ctx, emit, fold_mul(&nums)?);
233
2448
    }
234
2448
    if args.is_empty() {
235
        emit.i32_const(1);
236
        return Ok(WasmType::I32);
237
2448
    }
238
2448
    let slots = collect_slots(ctx, emit, symbols, args)?;
239
2380
    let seed = scaling_seed_dim(&slots, "*")?;
240
2380
    emit_slot(ctx, emit, &slots[0], seed, "*")?;
241
2380
    let mut acc = seed.wasm_type();
242
2380
    for slot in &slots[1..] {
243
2380
        acc = combine_mul(ctx, emit, acc, slot)?;
244
    }
245
2380
    Ok(acc)
246
4352
}
247

            
248
2176
pub(super) fn compile_div_to_stack(
249
2176
    ctx: &mut CompileContext,
250
2176
    emit: &mut FunctionEmitter,
251
2176
    symbols: &mut SymbolTable,
252
2176
    args: &[Expr],
253
2176
) -> Result<WasmType> {
254
2176
    if args.is_empty() {
255
68
        return Err(Error::Compile("/ requires at least 1 argument".to_string()));
256
2108
    }
257
2108
    if let Some(nums) = try_fold(symbols, args) {
258
1564
        return push_const(ctx, emit, fold_div(&nums)?);
259
544
    }
260
544
    let slots = collect_slots(ctx, emit, symbols, args)?;
261
544
    let seed = scaling_seed_dim(&slots, "/")?;
262
544
    if slots.len() == 1 {
263
        return emit_reciprocal(ctx, emit, &slots[0], seed);
264
544
    }
265
544
    emit_slot(ctx, emit, &slots[0], seed, "/")?;
266
544
    let mut acc = seed.wasm_type();
267
544
    for slot in &slots[1..] {
268
544
        acc = combine_div(ctx, emit, acc, slot)?;
269
    }
270
476
    Ok(acc)
271
2176
}
272

            
273
612
pub(super) fn compile_mod_to_stack(
274
612
    ctx: &mut CompileContext,
275
612
    emit: &mut FunctionEmitter,
276
612
    symbols: &mut SymbolTable,
277
612
    args: &[Expr],
278
612
) -> Result<WasmType> {
279
612
    if args.len() != 2 {
280
136
        return Err(Error::Arity {
281
136
            name: "MOD".to_string(),
282
136
            expected: 2,
283
136
            actual: args.len(),
284
136
        });
285
476
    }
286
476
    if let Some(nums) = try_fold(symbols, args) {
287
340
        return push_const(ctx, emit, fold_mod(&nums)?);
288
136
    }
289
136
    let slots = collect_slots(ctx, emit, symbols, args)?;
290
136
    match additive_dim(&slots, "MOD")? {
291
        Dim::Index => {
292
68
            emit_slot(ctx, emit, &slots[0], Dim::Index, "MOD")?;
293
68
            emit_slot(ctx, emit, &slots[1], Dim::Index, "MOD")?;
294
            emit.i32_rem_s();
295
            Ok(WasmType::I32)
296
        }
297
68
        Dim::Scalar | Dim::Money => Err(Error::Compile(
298
68
            "MOD with runtime non-integer arguments is not yet supported".to_string(),
299
68
        )),
300
    }
301
612
}
302

            
303
#[derive(Clone, Copy)]
304
enum BinOp {
305
    Add,
306
    Sub,
307
}
308

            
309
15596
fn emit_combine(ctx: &CompileContext, emit: &mut FunctionEmitter, dim: Dim, op: BinOp) {
310
15596
    match (dim, op) {
311
9136
        (Dim::Index, BinOp::Add) => emit.i32_add(),
312
272
        (Dim::Index, BinOp::Sub) => emit.i32_sub(),
313
4488
        (Dim::Scalar, BinOp::Add) => emit.call(ctx.ids.ratio_add),
314
1360
        (Dim::Scalar, BinOp::Sub) => emit.call(ctx.ids.ratio_sub),
315
340
        (Dim::Money, BinOp::Add) => emit.call(ctx.ids.commodity_add),
316
        (Dim::Money, BinOp::Sub) => emit.call(ctx.ids.commodity_sub),
317
    }
318
15596
}
319

            
320
14372
fn compile_additive(
321
14372
    ctx: &mut CompileContext,
322
14372
    emit: &mut FunctionEmitter,
323
14372
    symbols: &mut SymbolTable,
324
14372
    args: &[Expr],
325
14372
    op: &str,
326
14372
    binop: BinOp,
327
14372
) -> Result<WasmType> {
328
14372
    let slots = collect_slots(ctx, emit, symbols, args)?;
329
14372
    let dim = additive_dim(&slots, op)?;
330
14100
    emit_slot(ctx, emit, &slots[0], dim, op)?;
331
14032
    for slot in &slots[1..] {
332
14032
        emit_slot(ctx, emit, slot, dim, op)?;
333
13964
        emit_combine(ctx, emit, dim, binop);
334
    }
335
13964
    Ok(dim.wasm_type())
336
14372
}
337

            
338
68
fn emit_unary_neg(
339
68
    ctx: &mut CompileContext,
340
68
    emit: &mut FunctionEmitter,
341
68
    slot: &Slot,
342
68
    dim: Dim,
343
68
) -> Result<WasmType> {
344
68
    match dim {
345
        Dim::Index => {
346
            emit.i32_const(0);
347
            emit_slot(ctx, emit, slot, Dim::Index, "-")?;
348
            emit.i32_sub();
349
            Ok(WasmType::I32)
350
        }
351
        Dim::Scalar => {
352
68
            let r_local = ctx.alloc_local(WasmType::Ratio)?;
353
68
            emit_slot(ctx, emit, slot, Dim::Scalar, "-")?;
354
68
            emit.local_set(r_local);
355
68
            push_ratio(ctx, emit, 0, 1);
356
68
            emit.local_get(r_local);
357
68
            emit.call(ctx.ids.ratio_sub);
358
68
            Ok(WasmType::Ratio)
359
        }
360
        Dim::Money => {
361
            emit_slot(ctx, emit, slot, Dim::Money, "-")?;
362
            emit.call(ctx.ids.commodity_neg);
363
            Ok(WasmType::Commodity)
364
        }
365
    }
366
68
}
367

            
368
/// Seed dimension for the first operand of `*`/`/`: Money if any operand is
369
/// runtime money, Scalar if any runtime scalar, else Index. A literal first
370
/// operand is emitted at this dimension (Scalar when money/scalar is involved).
371
2924
fn scaling_seed_dim(slots: &[Slot], op: &str) -> Result<Dim> {
372
2924
    let mut money = false;
373
2924
    let mut scalar = false;
374
2924
    let mut index = false;
375
5848
    for s in slots {
376
5848
        match s.runtime_dim(op)? {
377
1496
            Some(Dim::Money) => money = true,
378
2448
            Some(Dim::Scalar) => scalar = true,
379
68
            Some(Dim::Index) => index = true,
380
1836
            None => {}
381
        }
382
    }
383
2924
    let dominant = if money {
384
748
        Dim::Money
385
2176
    } else if scalar {
386
2108
        Dim::Scalar
387
68
    } else if index {
388
68
        Dim::Index
389
    } else {
390
        Dim::Scalar
391
    };
392
2924
    Ok(match &slots[0] {
393
2924
        Slot::Runtime(_, ty) => Dim::of(*ty, op)?,
394
        Slot::Literal(_) if dominant == Dim::Index => Dim::Index,
395
        Slot::Literal(_) => Dim::Scalar,
396
    })
397
2924
}
398

            
399
2380
fn combine_mul(
400
2380
    ctx: &mut CompileContext,
401
2380
    emit: &mut FunctionEmitter,
402
2380
    acc: WasmType,
403
2380
    slot: &Slot,
404
2380
) -> Result<WasmType> {
405
2380
    match acc {
406
        WasmType::I32 => {
407
            emit_slot(ctx, emit, slot, Dim::Index, "*")?;
408
            emit.i32_mul();
409
            Ok(WasmType::I32)
410
        }
411
1904
        WasmType::Ratio => match slot.runtime_dim("*")? {
412
            Some(Dim::Money) => {
413
                // scalar × money → money: stash the scalar, push the money, restore.
414
                let scalar_local = ctx.alloc_local(WasmType::Ratio)?;
415
                emit.local_set(scalar_local);
416
                emit_slot(ctx, emit, slot, Dim::Money, "*")?;
417
                emit.local_get(scalar_local);
418
                emit.call(ctx.ids.commodity_mul_by_ratio);
419
                Ok(WasmType::Commodity)
420
            }
421
            _ => {
422
1904
                emit_slot(ctx, emit, slot, Dim::Scalar, "*")?;
423
1904
                emit.call(ctx.ids.ratio_mul);
424
1904
                Ok(WasmType::Ratio)
425
            }
426
        },
427
476
        WasmType::Commodity => match slot.runtime_dim("*")? {
428
            Some(Dim::Money) => {
429
                // money × money → compound money (ADR-0028 E2): the unit terms
430
                // multiply (exponents add).
431
476
                emit_slot(ctx, emit, slot, Dim::Money, "*")?;
432
476
                emit.call(ctx.ids.commodity_mul);
433
476
                Ok(WasmType::Commodity)
434
            }
435
            _ => {
436
                emit_slot(ctx, emit, slot, Dim::Scalar, "*")?;
437
                emit.call(ctx.ids.commodity_mul_by_ratio);
438
                Ok(WasmType::Commodity)
439
            }
440
        },
441
        _ => Err(mix_error("*")),
442
    }
443
2380
}
444

            
445
544
fn combine_div(
446
544
    ctx: &mut CompileContext,
447
544
    emit: &mut FunctionEmitter,
448
544
    acc: WasmType,
449
544
    slot: &Slot,
450
544
) -> Result<WasmType> {
451
544
    match acc {
452
        WasmType::I32 => {
453
68
            emit_slot(ctx, emit, slot, Dim::Index, "/")?;
454
            emit.i32_div_s();
455
            Ok(WasmType::I32)
456
        }
457
204
        WasmType::Ratio => match slot.runtime_dim("/")? {
458
            Some(Dim::Money) => Err(Error::Compile(
459
                "dividing a pure rational by a commodity-bearing value has no \
460
                 dimensional meaning; reverse operands or convert-commodity first"
461
                    .to_string(),
462
            )),
463
            _ => {
464
204
                emit_slot(ctx, emit, slot, Dim::Scalar, "/")?;
465
204
                emit.call(ctx.ids.ratio_div);
466
204
                Ok(WasmType::Ratio)
467
            }
468
        },
469
272
        WasmType::Commodity => match slot.runtime_dim("/")? {
470
            Some(Dim::Money) => {
471
                // money ÷ money → money carrying the divided unit term (ADR-0028
472
                // E2): same currency cancels to a dimensionless term (serializes
473
                // as a Number), cross-currency gives a compound term.
474
272
                emit_slot(ctx, emit, slot, Dim::Money, "/")?;
475
272
                emit.call(ctx.ids.commodity_div);
476
272
                Ok(WasmType::Commodity)
477
            }
478
            _ => {
479
                emit_slot(ctx, emit, slot, Dim::Scalar, "/")?;
480
                emit.call(ctx.ids.commodity_div_by_ratio);
481
                Ok(WasmType::Commodity)
482
            }
483
        },
484
        _ => Err(mix_error("/")),
485
    }
486
544
}
487

            
488
fn emit_reciprocal(
489
    ctx: &mut CompileContext,
490
    emit: &mut FunctionEmitter,
491
    slot: &Slot,
492
    dim: Dim,
493
) -> Result<WasmType> {
494
    match dim {
495
        Dim::Money => Err(Error::Compile(
496
            "reciprocal of a commodity-bearing value has no dimensional meaning; \
497
             divide a commodity by a Ratio scalar instead"
498
                .to_string(),
499
        )),
500
        Dim::Index => {
501
            emit.i32_const(1);
502
            emit_slot(ctx, emit, slot, Dim::Index, "/")?;
503
            emit.i32_div_s();
504
            Ok(WasmType::I32)
505
        }
506
        Dim::Scalar => {
507
            let r_local = ctx.alloc_local(WasmType::Ratio)?;
508
            emit_slot(ctx, emit, slot, Dim::Scalar, "/")?;
509
            emit.local_set(r_local);
510
            push_ratio(ctx, emit, 1, 1);
511
            emit.local_get(r_local);
512
            emit.call(ctx.ids.ratio_div);
513
            Ok(WasmType::Ratio)
514
        }
515
    }
516
}
517

            
518
fn mix_error(op: &str) -> Error {
519
    Error::Compile(format!(
520
        "{op} cannot mix dimensions (index / scalar / money); \
521
         bridge explicitly with `(index->scalar ...)`, `(scalar->index ...)`, \
522
         or `(convert-commodity ...)`"
523
    ))
524
}