1
//! `IF` special form — eval, effect-compile, and stack-compile paths.
2
//! Constant-folds when the test resolves at compile time; emits a
3
//! WASM `if/else` block when the test is a runtime value. The
4
//! stack variant carries the result type so call sites composing
5
//! `(if ...)` with arithmetic see the right WasmType.
6

            
7
use wasm_encoder::BlockType;
8

            
9
use crate::ast::{Expr, WasmType};
10
use crate::compiler::context::CompileContext;
11
use crate::compiler::emit::FunctionEmitter;
12
use crate::compiler::expr::{
13
    compile_expr, compile_for_stack, compile_for_stack_as, compile_nil, eval_value,
14
    serialize_stack_to_output,
15
};
16
use crate::error::{Error, Result};
17
use crate::runtime::SymbolTable;
18

            
19
use super::is_truthy;
20

            
21
7684
pub(super) fn compile_if(
22
7684
    ctx: &mut CompileContext,
23
7684
    emit: &mut FunctionEmitter,
24
7684
    symbols: &mut SymbolTable,
25
7684
    args: &[Expr],
26
7684
) -> Result<()> {
27
7684
    if args.len() < 2 || args.len() > 3 {
28
136
        return Err(Error::Compile(
29
136
            "IF requires a test, a then-form, and an optional else-form".to_string(),
30
136
        ));
31
7548
    }
32
    // Classify the test on a CLONE: its compile-time side effects (setf /
33
    // macro expansion) must reach the live table exactly once, via the single
34
    // emit below — never twice (classify + emit).
35
7548
    let test = eval_value(&mut symbols.clone(), &args[0])?;
36
7548
    let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
37
7548
    super::reject_non_boolean_runtime_test(&test, test_diverges)?;
38
7548
    if test_diverges {
39
        // The test transfers control before yielding a condition (`(error …)`,
40
        // `(return-from …)`). Compile it for effect so the exit actually
41
        // fires; both branches are then dead. Const-folding past it (the bug
42
        // this guards) would silently drop the exit and pick a branch.
43
        return crate::compiler::expr::compile_for_effect(ctx, emit, symbols, &args[0]);
44
7548
    }
45
7548
    if super::is_runtime_test(&test) {
46
        // Runtime test: emit ONE merged `if (result T)` block via the stack
47
        // path, then serialize the single result. Serializing each branch
48
        // separately (via `compile_expr`) double-advances the compile-time
49
        // output cursor — both branches bake an entity header, but only one
50
        // runs, so the decoder reads a garbage entity slot ("unknown value
51
        // type"). The stack path collapses both arms to one value.
52
1632
        let ty = compile_if_for_stack(ctx, emit, symbols, args)?;
53
1632
        return serialize_stack_to_output(ctx, emit, ty);
54
5916
    }
55
    // Const test: apply its effects to the live table exactly once.
56
5916
    let test = eval_value(symbols, &args[0])?;
57
5916
    if is_truthy(&test) {
58
952
        compile_expr(ctx, emit, symbols, &args[1])
59
4964
    } else if args.len() == 3 {
60
4828
        compile_expr(ctx, emit, symbols, &args[2])
61
    } else {
62
136
        compile_nil(ctx, emit);
63
136
        Ok(())
64
    }
65
7684
}
66

            
67
4692
pub(super) fn compile_if_for_stack(
68
4692
    ctx: &mut CompileContext,
69
4692
    emit: &mut FunctionEmitter,
70
4692
    symbols: &mut SymbolTable,
71
4692
    args: &[Expr],
72
4692
) -> Result<WasmType> {
73
4692
    if args.len() < 2 || args.len() > 3 {
74
        return Err(Error::Compile(
75
            "IF requires a test, a then-form, and an optional else-form".to_string(),
76
        ));
77
4692
    }
78
    // Classify on a CLONE so the test's compile-time effects reach the live
79
    // table only through the single `compile_for_stack` emit below.
80
4692
    let test = eval_value(&mut symbols.clone(), &args[0])?;
81
4692
    let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
82
4692
    super::reject_non_boolean_runtime_test(&test, test_diverges)?;
83
4692
    if test_diverges {
84
        // The test exits before producing a condition; compiling it for the
85
        // stack emits its terminating control flow (`br`/`throw`), leaving a
86
        // polymorphic stack that satisfies any declared result type. Both
87
        // branches are dead. Report I32 as a nominal type for callers.
88
68
        compile_for_stack(ctx, emit, symbols, &args[0])?;
89
68
        return Ok(WasmType::I32);
90
4624
    }
91
4624
    if super::is_runtime_test(&test) {
92
4012
        compile_for_stack(ctx, emit, symbols, &args[0])?;
93
4012
        return compile_runtime_if(ctx, emit, symbols, args);
94
612
    }
95
    // Const test: apply its effects to the live table once, then the live arm.
96
612
    let test = eval_value(symbols, &args[0])?;
97
612
    if is_truthy(&test) {
98
476
        compile_for_stack(ctx, emit, symbols, &args[1])
99
136
    } else if args.len() == 3 {
100
136
        compile_for_stack(ctx, emit, symbols, &args[2])
101
    } else {
102
        // Const-false IF with no else ≡ nil — falsy i31, typed `Bool` so it
103
        // serializes as Nil and matches the eval mirror's `Expr::Nil`.
104
        emit.i32_const(0);
105
        Ok(WasmType::Bool)
106
    }
107
4692
}
108

            
109
/// Emit the runtime `if (result T)` block, with the i32 condition already on
110
/// the stack. Each branch compiles into its own scratch seeded at the
111
/// branch's wasm depth (`parent + 1`) — `compile_for_stack` lowers a
112
/// recursive self-call to `call $idx` and terminates, where an `eval_value`
113
/// peek would inline the body and recurse forever. The real branch types are
114
/// then unified (divergence-aware, widen-to-AnyRef on disagreement), each
115
/// scratch is coerced to `T`, and both are spliced into the live block — so
116
/// each branch is compiled exactly once.
117
4012
fn compile_runtime_if(
118
4012
    ctx: &mut CompileContext,
119
4012
    emit: &mut FunctionEmitter,
120
4012
    symbols: &mut SymbolTable,
121
4012
    args: &[Expr],
122
4012
) -> Result<WasmType> {
123
4012
    let has_else = args.len() == 3;
124
4012
    let then_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[1])?;
125
4012
    let else_diverges =
126
4012
        has_else && super::block_exits::form_diverges(&mut symbols.clone(), &args[2])?;
127

            
128
4012
    let branch_depth = emit.block_depth() + 1;
129

            
130
    // A numeric-literal arm is dimension-flexible: defer compiling it until the
131
    // reconciled result type is known, so it is emitted exactly once at the
132
    // right type (no compile-then-discard, which would double-apply a
133
    // number-resolving arm's compile-time effects). Its type is peeked (no emit).
134
4012
    let then_lit = is_number_literal(symbols, &args[1]);
135
4012
    let else_lit = has_else && is_number_literal(symbols, &args[2]);
136

            
137
4012
    let mut then_scratch = FunctionEmitter::new_seeded(branch_depth);
138
4012
    let then_ty = if then_diverges {
139
204
        compile_for_stack(ctx, &mut then_scratch, symbols, &args[1])?;
140
204
        None
141
3808
    } else if then_lit {
142
1088
        Some(peek_stack_type(&mut symbols.clone(), &args[1])?)
143
    } else {
144
2720
        Some(compile_for_stack(
145
2720
            ctx,
146
2720
            &mut then_scratch,
147
2720
            symbols,
148
2720
            &args[1],
149
        )?)
150
    };
151

            
152
4012
    let mut else_scratch = FunctionEmitter::new_seeded(branch_depth);
153
4012
    let else_ty = if !has_else {
154
        // Missing else ≡ `nil`. Type it to mirror an i32-repr then-branch so
155
        // the block stays homogeneous: an I32 then (a count) keeps Number
156
        // fidelity, a Bool then keeps Nil/Bool fidelity. A ref-typed then
157
        // widens to AnyRef either way (its nil is `ref.null`).
158
340
        Some(missing_else_type(then_ty))
159
3672
    } else if else_diverges {
160
        compile_for_stack(ctx, &mut else_scratch, symbols, &args[2])?;
161
        None
162
3672
    } else if else_lit {
163
680
        Some(peek_stack_type(&mut symbols.clone(), &args[2])?)
164
    } else {
165
2992
        Some(compile_for_stack(
166
2992
            ctx,
167
2992
            &mut else_scratch,
168
2992
            symbols,
169
2992
            &args[2],
170
        )?)
171
    };
172

            
173
4012
    let result_ty = reconciled_if_type(then_ty, else_ty, then_lit, else_lit);
174

            
175
4012
    emit_if_arm(
176
4012
        ctx,
177
4012
        &mut then_scratch,
178
4012
        symbols,
179
4012
        &args[1],
180
4012
        then_lit,
181
4012
        then_ty,
182
4012
        result_ty,
183
    )?;
184
4012
    if has_else {
185
3672
        emit_if_arm(
186
3672
            ctx,
187
3672
            &mut else_scratch,
188
3672
            symbols,
189
3672
            &args[2],
190
3672
            else_lit,
191
3672
            else_ty,
192
3672
            result_ty,
193
        )?;
194
340
    } else {
195
340
        emit_typed_nil(&mut else_scratch, result_ty);
196
340
    }
197

            
198
4012
    emit.if_block(BlockType::Result(ctx.wasm_val_type(result_ty)));
199
4012
    emit.splice(&then_scratch.take_bytes());
200
4012
    emit.else_block();
201
4012
    emit.splice(&else_scratch.take_bytes());
202
4012
    emit.block_end();
203
4012
    Ok(result_ty)
204
4012
}
205

            
206
/// Emit one IF arm into its `scratch` at the unified `result_ty`. A deferred
207
/// numeric-literal arm is compiled here exactly once: coerced across
208
/// Index↔Scalar when `result_ty` is numeric, else compiled naturally and boxed
209
/// to ride an AnyRef block. A non-literal arm was already compiled into
210
/// `scratch`; it only needs the i32→AnyRef box.
211
7684
fn emit_if_arm(
212
7684
    ctx: &mut CompileContext,
213
7684
    scratch: &mut FunctionEmitter,
214
7684
    symbols: &mut SymbolTable,
215
7684
    arg: &Expr,
216
7684
    is_lit: bool,
217
7684
    arm_ty: Option<WasmType>,
218
7684
    result_ty: WasmType,
219
7684
) -> Result<()> {
220
7684
    if is_lit {
221
1768
        if crosses_to(result_ty) {
222
1768
            compile_for_stack_as(ctx, scratch, symbols, arg, result_ty)?;
223
        } else {
224
            let raw = compile_for_stack(ctx, scratch, symbols, arg)?;
225
            coerce_scratch(scratch, Some(raw), result_ty);
226
        }
227
5916
    } else {
228
5916
        coerce_scratch(scratch, arm_ty, result_ty);
229
5916
    }
230
7684
    Ok(())
231
7684
}
232

            
233
/// Type the `nil` of a runtime IF's missing else so the block stays
234
/// homogeneous with an i32-repr then-branch. A plain `I32` then is a COUNT —
235
/// its missing-else nil must also be `I32` so the result keeps Number
236
/// fidelity (else `unify_if_type` widens I32-vs-Bool to AnyRef and the count
237
/// is lost behind a marker). Every other then-type takes `Bool` (the natural
238
/// truth-typed nil): a `Bool` then stays `Bool` (Nil/Bool fidelity), a
239
/// ref-typed or diverging then widens to AnyRef either way.
240
340
fn missing_else_type(then_ty: Option<WasmType>) -> WasmType {
241
340
    match then_ty {
242
136
        Some(WasmType::I32) => WasmType::I32,
243
204
        _ => WasmType::Bool,
244
    }
245
340
}
246

            
247
/// Unify the two arm types of a runtime IF. `None` is a diverging arm
248
/// (contributes no value). Equal types pass through; genuinely different
249
/// types widen to `AnyRef` — the only common supertype that lets a
250
/// heterogeneous IF (e.g. a no-else `(if t ratio)` whose else is `nil`) ride
251
/// one wasm block result. AnyRef-valued IFs serialize at marker fidelity
252
/// (the documented heterogeneous-value limitation, shared with catch-each
253
/// cells); homogeneous IFs keep full value fidelity.
254
6936
fn unify_if_type(then_ty: Option<WasmType>, else_ty: Option<WasmType>) -> WasmType {
255
6936
    match (then_ty, else_ty) {
256
6732
        (Some(a), Some(b)) if a == b => a,
257
204
        (Some(a), None) | (None, Some(a)) => a,
258
        (None, None) => WasmType::I32,
259
3808
        (Some(_), Some(_)) => WasmType::AnyRef,
260
    }
261
6936
}
262

            
263
/// Index and Scalar are the only strata a numeric literal can cross into; a
264
/// literal can never become Money (it has no currency).
265
7412
fn crosses_to(ty: WasmType) -> bool {
266
7412
    matches!(ty, WasmType::I32 | WasmType::Ratio)
267
7412
}
268

            
269
/// Whether `expr` resolves to a numeric literal (a dimension-flexible token).
270
/// Probes on a clone so it applies no live mutation.
271
13532
fn is_number_literal(symbols: &SymbolTable, expr: &Expr) -> bool {
272
13532
    matches!(eval_value(&mut symbols.clone(), expr), Ok(Expr::Number(_)))
273
13532
}
274

            
275
/// Unify two IF-arm types, reconciling a numeric-LITERAL arm with the other arm
276
/// by coercing the literal across the Index↔Scalar boundary instead of widening
277
/// to `AnyRef` (ADR-0028). `(if c 1 ratio)` is a Ratio IF; `(if c 1 2)` an Index
278
/// IF; `(if c 1 1/2)` joins to Scalar. A literal next to Money (or a non-numeric),
279
/// and two runtime numerics of different strata, still widen to AnyRef (the
280
/// escape hatch) — a literal cannot become Money.
281
6936
fn reconciled_if_type(
282
6936
    then_ty: Option<WasmType>,
283
6936
    else_ty: Option<WasmType>,
284
6936
    then_lit: bool,
285
6936
    else_lit: bool,
286
6936
) -> WasmType {
287
6936
    let base = unify_if_type(then_ty, else_ty);
288
6936
    if base != WasmType::AnyRef {
289
3128
        return base;
290
3808
    }
291
3808
    let (Some(t), Some(e)) = (then_ty, else_ty) else {
292
        return base;
293
    };
294
3808
    if !(crosses_to(t) && crosses_to(e)) {
295
2380
        return base;
296
1428
    }
297
1428
    match (then_lit, else_lit) {
298
1428
        (true, false) => e,
299
        (false, true) => t,
300
        (true, true) => WasmType::Ratio,
301
        (false, false) => base,
302
    }
303
6936
}
304

            
305
/// Box a compiled branch scratch to the unified `target`. A diverging branch
306
/// (`branch_ty == None`) already ended in terminating control flow — its
307
/// stack is polymorphic, so it needs no value. Only i32 → AnyRef needs a
308
/// `ref.i31` box; every other type is already an anyref subtype.
309
5916
fn coerce_scratch(scratch: &mut FunctionEmitter, branch_ty: Option<WasmType>, target: WasmType) {
310
    // The i32-repr value types (I32 / Bool) box into `(ref i31)` to ride an
311
    // AnyRef block; every other type is already an anyref subtype.
312
5916
    if target == WasmType::AnyRef && matches!(branch_ty, Some(WasmType::I32 | WasmType::Bool)) {
313
680
        scratch.ref_i31();
314
5236
    }
315
5916
}
316

            
317
/// Push the `nil` value of a runtime IF's missing else, typed to match the
318
/// unified block result: `ref.null any` for an AnyRef block, i32 `0`
319
/// otherwise (a missing else only widens to AnyRef when the then-branch is a
320
/// non-i32 ref type).
321
340
fn emit_typed_nil(emit: &mut FunctionEmitter, target: WasmType) {
322
340
    match target {
323
136
        WasmType::AnyRef => emit.ref_null_any(),
324
204
        _ => emit.i32_const(0),
325
    }
326
340
}
327

            
328
/// Classify an `Expr` by the `WasmType` it would push if `compile_for_stack`
329
/// emitted it. Mirrors the literal lowerings in
330
/// `crate::compiler::expr::stack::compile_for_stack` — `Number → Ratio`,
331
/// `Bool / Nil → Bool`, `String → StringRef`, runtime placeholders carry
332
/// their declared type — so the IF / COND BlockType peek can be honest
333
/// about what each branch will deposit on the stack. Anything not
334
/// statically classifiable is left to surface as a real codegen error
335
/// downstream rather than masked as I32 here.
336
7616
fn peek_stack_type(symbols: &mut SymbolTable, expr: &Expr) -> Result<WasmType> {
337
7616
    let resolved = eval_value(symbols, expr)?;
338
7616
    Ok(crate::compiler::expr::classify_stack_type(&resolved).unwrap_or(WasmType::I32))
339
7616
}
340

            
341
5508
pub(super) fn if_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
342
5508
    if args.len() < 2 || args.len() > 3 {
343
        return Err(Error::Compile(
344
            "IF requires a test, a then-form, and an optional else-form".to_string(),
345
        ));
346
5508
    }
347
    // Classify on a CLONE — branch peeks below must not apply runtime
348
    // branches' side effects to the live table (they execute at runtime, in
349
    // one branch only). A const test re-evals on the live table once below.
350
5508
    let test = eval_value(&mut symbols.clone(), &args[0])?;
351
5508
    let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
352
5508
    super::reject_non_boolean_runtime_test(&test, test_diverges)?;
353
5508
    if test_diverges {
354
        // The test exits before yielding a condition; its value is whatever
355
        // the diverging form evaluates to (the branches are dead). This is the
356
        // single live application of the test's effects.
357
        return eval_value(symbols, &args[0]);
358
5508
    }
359
5508
    if super::is_runtime_test(&test) {
360
        // A runtime-test IF emits a wasm `if (result T)` block whose T the
361
        // codegen path (`compile_if_for_stack`) derives by unifying both
362
        // branches' *stack* types. Eval-time typing MUST agree, or a binder
363
        // that sizes a local from this eval type (let / defvar / lambda arg)
364
        // mismatches the value codegen actually pushes. Mirror the exact
365
        // unification (divergence-aware, widen-to-AnyRef on disagreement).
366
        // Peek each branch on a CLONE so branch side effects don't leak.
367
2924
        let then_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[1])?;
368
2924
        let has_else = args.len() == 3;
369
2924
        let else_diverges =
370
2924
            has_else && super::block_exits::form_diverges(&mut symbols.clone(), &args[2])?;
371
2924
        let then_ty = if then_diverges {
372
            None
373
        } else {
374
2924
            Some(peek_stack_type(&mut symbols.clone(), &args[1])?)
375
        };
376
2924
        let else_ty = match (has_else, else_diverges) {
377
            (false, _) => Some(missing_else_type(then_ty)), // missing else ≡ nil
378
            (true, true) => None,
379
2924
            (true, false) => Some(peek_stack_type(&mut symbols.clone(), &args[2])?),
380
        };
381
2924
        let then_lit = is_number_literal(symbols, &args[1]);
382
2924
        let else_lit = has_else && is_number_literal(symbols, &args[2]);
383
2924
        return Ok(Expr::WasmRuntime(reconciled_if_type(
384
2924
            then_ty, else_ty, then_lit, else_lit,
385
2924
        )));
386
2584
    }
387
    // Const test: apply its effects to the live table once, then the live arm.
388
2584
    let test = eval_value(symbols, &args[0])?;
389
2584
    if is_truthy(&test) {
390
1768
        eval_value(symbols, &args[1])
391
816
    } else if args.len() == 3 {
392
816
        eval_value(symbols, &args[2])
393
    } else {
394
        Ok(Expr::Nil)
395
    }
396
5508
}