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_nil, eval_value, serialize_stack_to_output,
14
};
15
use crate::error::{Error, Result};
16
use crate::runtime::SymbolTable;
17

            
18
use super::arms::{
19
    Arm, OnConflict, compile_arm, emit_typed_nil, eval_arm, finish_arm, reconcile_arms,
20
};
21

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

            
67
8217
pub(super) fn compile_if_for_stack(
68
8217
    ctx: &mut CompileContext,
69
8217
    emit: &mut FunctionEmitter,
70
8217
    symbols: &mut SymbolTable,
71
8217
    args: &[Expr],
72
8217
) -> Result<WasmType> {
73
8217
    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
8217
    }
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
8217
    let test = eval_value(&mut symbols.clone(), &args[0])?;
81
8217
    let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
82
8217
    super::reject_non_boolean_runtime_test(&test, test_diverges)?;
83
8217
    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
83
        compile_for_stack(ctx, emit, symbols, &args[0])?;
89
83
        return Ok(WasmType::I32);
90
8134
    }
91
8134
    if super::is_runtime_test(&test) {
92
7221
        compile_for_stack(ctx, emit, symbols, &args[0])?;
93
7221
        return compile_runtime_if(ctx, emit, symbols, args);
94
913
    }
95
913
    let test = super::apply_const_test(ctx, emit, symbols, &args[0], &test)?;
96
913
    if !test.is_nil() {
97
664
        compile_for_stack(ctx, emit, symbols, &args[1])
98
249
    } else if args.len() == 3 {
99
249
        compile_for_stack(ctx, emit, symbols, &args[2])
100
    } else {
101
        // Const-false IF with no else ≡ nil — falsy i31, typed `Bool` so it
102
        // serializes as Nil and matches the eval mirror's `Expr::Nil`.
103
        emit.i32_const(0);
104
        Ok(WasmType::Bool)
105
    }
106
8217
}
107

            
108
/// Emit the runtime `if (result T)` block, with the i32 condition already on
109
/// the stack. Each branch compiles into its own scratch seeded at the
110
/// branch's wasm depth (`parent + 1`) — `compile_for_stack` lowers a
111
/// recursive self-call to `call $idx` and terminates, where an `eval_value`
112
/// peek would inline the body and recurse forever. The real branch types are
113
/// then unified (divergence-aware, widen-to-AnyRef on disagreement), each
114
/// scratch is coerced to `T`, and both are spliced into the live block — so
115
/// each branch is compiled exactly once.
116
7221
fn compile_runtime_if(
117
7221
    ctx: &mut CompileContext,
118
7221
    emit: &mut FunctionEmitter,
119
7221
    symbols: &mut SymbolTable,
120
7221
    args: &[Expr],
121
7221
) -> Result<WasmType> {
122
7221
    let branch_depth = emit.block_depth() + 1;
123
7221
    let mut then_scratch = FunctionEmitter::new_seeded(branch_depth);
124
7221
    let then_arm = compile_arm(ctx, &mut then_scratch, symbols, &args[1])?;
125
7221
    let mut else_scratch = FunctionEmitter::new_seeded(branch_depth);
126
7221
    let else_arm = match args.get(2) {
127
6640
        Some(else_form) => compile_arm(ctx, &mut else_scratch, symbols, else_form)?,
128
581
        None => Some(Arm::Nil),
129
    };
130
7221
    let arms: Vec<Arm> = [then_arm, else_arm].into_iter().flatten().collect();
131
7221
    let result_ty = reconcile_arms(&arms, OnConflict::Widen)?;
132

            
133
7221
    if let Some(arm) = then_arm {
134
6972
        finish_arm(ctx, &mut then_scratch, symbols, &args[1], arm, result_ty)?;
135
249
    }
136
7221
    match (args.get(2), else_arm) {
137
6640
        (Some(else_form), Some(arm)) => {
138
6640
            finish_arm(ctx, &mut else_scratch, symbols, else_form, arm, result_ty)?;
139
        }
140
581
        (None, _) => emit_typed_nil(ctx, &mut else_scratch, result_ty)?,
141
        (Some(_), None) => {}
142
    }
143

            
144
7221
    emit.if_block(BlockType::Result(ctx.wasm_val_type(result_ty)));
145
7221
    emit.splice(&then_scratch.take_bytes());
146
7221
    emit.else_block();
147
7221
    emit.splice(&else_scratch.take_bytes());
148
7221
    emit.block_end();
149
7221
    Ok(result_ty)
150
7221
}
151

            
152
8466
pub(super) fn if_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
153
8466
    if args.len() < 2 || args.len() > 3 {
154
        return Err(Error::Compile(
155
            "IF requires a test, a then-form, and an optional else-form".to_string(),
156
        ));
157
8466
    }
158
    // Classify on a CLONE — branch peeks below must not apply runtime
159
    // branches' side effects to the live table (they execute at runtime, in
160
    // one branch only). A const test re-evals on the live table once below.
161
8466
    let test = eval_value(&mut symbols.clone(), &args[0])?;
162
8466
    let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &args[0])?;
163
8466
    super::reject_non_boolean_runtime_test(&test, test_diverges)?;
164
8466
    if test_diverges {
165
        // The test exits before yielding a condition; its value is whatever
166
        // the diverging form evaluates to (the branches are dead). This is the
167
        // single live application of the test's effects.
168
        return eval_value(symbols, &args[0]);
169
8466
    }
170
8466
    if super::is_runtime_test(&test) {
171
        // A runtime-test IF emits a wasm `if (result T)` block whose T the
172
        // codegen path (`compile_if_for_stack`) reconciles from both arms.
173
        // Eval-time typing MUST agree, or a binder that sizes a local from this
174
        // eval type (let / defvar / lambda arg) mismatches the value codegen
175
        // pushes — so both sides reconcile the same arms.
176
5229
        let then_arm = eval_arm(symbols, &args[1])?;
177
5229
        let else_arm = match args.get(2) {
178
5063
            Some(else_form) => eval_arm(symbols, else_form)?,
179
166
            None => Some(Arm::Nil),
180
        };
181
5229
        let arms: Vec<Arm> = [then_arm, else_arm].into_iter().flatten().collect();
182
5229
        return Ok(Expr::WasmRuntime(reconcile_arms(&arms, OnConflict::Widen)?));
183
3237
    }
184
    // Const test: apply its effects to the live table once, then the live arm.
185
3237
    let test = eval_value(symbols, &args[0])?;
186
3237
    if !test.is_nil() {
187
2158
        eval_value(symbols, &args[1])
188
1079
    } else if args.len() == 3 {
189
1079
        eval_value(symbols, &args[2])
190
    } else {
191
        Ok(Expr::Nil)
192
    }
193
8466
}