1
//! `COND` clause chain. The stack-result variant rewrites the chain
2
//! into a nested `IF` and delegates to `compile_if_for_stack` so the
3
//! resulting wasm block carries a value back to the caller. Effect
4
//! and runtime paths walk the chain directly so a known-true clause
5
//! can stop emission early.
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_body, compile_expr, compile_for_effect, compile_for_stack, compile_nil, eval_value,
14
    serialize_stack_to_output,
15
};
16
use crate::error::{Error, Result};
17
use crate::runtime::SymbolTable;
18

            
19
996
pub(super) fn compile_cond(
20
996
    ctx: &mut CompileContext,
21
996
    emit: &mut FunctionEmitter,
22
996
    symbols: &mut SymbolTable,
23
996
    args: &[Expr],
24
996
) -> Result<()> {
25
1162
    for (i, clause) in args.iter().enumerate() {
26
1162
        let elems = clause.as_list().ok_or_else(|| {
27
            Error::Compile(format!("COND: clause must be a list, got {clause:?}"))
28
        })?;
29
1162
        if elems.is_empty() {
30
            return Err(Error::Compile("COND: empty clause".to_string()));
31
1162
        }
32
        // Classify on a CLONE so the test's compile-time effects reach the
33
        // live table only via the single emit below.
34
1162
        let test = eval_value(&mut symbols.clone(), &elems[0])?;
35
1162
        let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &elems[0])?;
36
1162
        super::reject_non_boolean_runtime_test(&test, test_diverges)?;
37
1162
        if super::is_runtime_test(&test) || test_diverges {
38
            // Runtime (or diverging) test: emit ONE merged `if (result T)`
39
            // chain via the stack path, then serialize the single result.
40
            // Serializing each clause body separately double-advances the
41
            // compile-time output cursor (every clause bakes an entity header,
42
            // but only one runs) — the decoder then reads a garbage entity
43
            // slot. The IF-chain the stack path builds also fires a diverging
44
            // test correctly (vs. const-folding past it). Mirror IF's fix.
45
332
            let ty = compile_cond_for_stack(ctx, emit, symbols, &args[i..])?;
46
332
            return serialize_stack_to_output(ctx, emit, ty);
47
830
        }
48
830
        if elems.len() == 1 && test.is_wasm_runtime() {
49
            return compile_expr(ctx, emit, symbols, &elems[0]);
50
830
        }
51
830
        let test = super::apply_const_test(ctx, emit, symbols, &elems[0], &test)?;
52
830
        if !test.is_nil() {
53
498
            if elems.len() == 1 {
54
83
                return compile_expr(ctx, emit, symbols, &test);
55
415
            }
56
415
            return compile_body(ctx, emit, symbols, &elems[1..]);
57
332
        }
58
    }
59
166
    compile_nil(ctx, emit);
60
166
    Ok(())
61
996
}
62

            
63
415
pub(super) fn compile_cond_for_stack(
64
415
    ctx: &mut CompileContext,
65
415
    emit: &mut FunctionEmitter,
66
415
    symbols: &mut SymbolTable,
67
415
    args: &[Expr],
68
415
) -> Result<WasmType> {
69
415
    let chain = rewrite_cond_to_if_chain(args)?;
70
415
    compile_for_stack(ctx, emit, symbols, &chain)
71
415
}
72

            
73
1245
fn rewrite_cond_to_if_chain(clauses: &[Expr]) -> Result<Expr> {
74
1245
    if clauses.is_empty() {
75
415
        return Ok(Expr::Nil);
76
830
    }
77
830
    let head = clauses[0].as_list().ok_or_else(|| {
78
        Error::Compile(format!("COND: clause must be a list, got {:?}", clauses[0]))
79
    })?;
80
830
    if head.is_empty() {
81
        return Err(Error::Compile("COND: empty clause".to_string()));
82
830
    }
83
830
    let test = head[0].clone();
84
830
    let body = match head.len() {
85
        1 => test.clone(),
86
830
        2 => head[1].clone(),
87
        _ => {
88
            let mut forms = Vec::with_capacity(head.len());
89
            forms.push(Expr::Symbol("BEGIN".to_string()));
90
            forms.extend_from_slice(&head[1..]);
91
            Expr::List(forms)
92
        }
93
    };
94
830
    let else_branch = rewrite_cond_to_if_chain(&clauses[1..])?;
95
830
    Ok(Expr::List(vec![
96
830
        Expr::Symbol("IF".to_string()),
97
830
        test,
98
830
        body,
99
830
        else_branch,
100
830
    ]))
101
1245
}
102

            
103
664
pub(super) fn compile_cond_for_effect(
104
664
    ctx: &mut CompileContext,
105
664
    emit: &mut FunctionEmitter,
106
664
    symbols: &mut SymbolTable,
107
664
    args: &[Expr],
108
664
) -> Result<()> {
109
664
    for (i, clause) in args.iter().enumerate() {
110
664
        let elems = clause.as_list().ok_or_else(|| {
111
            Error::Compile(format!("COND: clause must be a list, got {clause:?}"))
112
        })?;
113
664
        if elems.is_empty() {
114
            return Err(Error::Compile("COND: empty clause".to_string()));
115
664
        }
116
        // Classify on a CLONE so the test's compile-time side effects are not
117
        // applied to the live table during classification — they ride the
118
        // single emit below exactly once.
119
664
        let test = eval_value(&mut symbols.clone(), &elems[0])?;
120
664
        let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &elems[0])?;
121
664
        super::reject_non_boolean_runtime_test(&test, test_diverges)?;
122
664
        if super::is_runtime_test(&test) || test_diverges {
123
664
            return compile_cond_runtime_for_effect(ctx, emit, symbols, &args[i..]);
124
        }
125
        let test = super::apply_const_test(ctx, emit, symbols, &elems[0], &test)?;
126
        if !test.is_nil() {
127
            for expr in &elems[1..] {
128
                compile_for_effect(ctx, emit, symbols, expr)?;
129
            }
130
            return Ok(());
131
        }
132
    }
133
    Ok(())
134
664
}
135

            
136
/// Effect-position codegen for a COND suffix whose first clause has a runtime
137
/// (or diverging) test. Each runtime clause opens a guard `if` and (when not
138
/// last) an `else`; clause bodies compile for effect — so effect-only forms
139
/// (`DOLIST`, etc.) that have no stack lowering still work, unlike a
140
/// stack+drop rewrite. Tracks the ACTUAL number of open guard blocks (not the
141
/// clause index, which over-counts when const-false clauses sit between
142
/// runtime ones) so the closing `block_end`s match exactly.
143
664
fn compile_cond_runtime_for_effect(
144
664
    ctx: &mut CompileContext,
145
664
    emit: &mut FunctionEmitter,
146
664
    symbols: &mut SymbolTable,
147
664
    args: &[Expr],
148
664
) -> Result<()> {
149
664
    let last = args.len() - 1;
150
664
    let mut open_guards = 0u32;
151
1411
    for (i, clause) in args.iter().enumerate() {
152
1411
        let elems = clause.as_list().ok_or_else(|| {
153
            Error::Compile(format!("COND: clause must be a list, got {clause:?}"))
154
        })?;
155
1411
        if elems.is_empty() {
156
            return Err(Error::Compile("COND: empty clause".to_string()));
157
1411
        }
158
1411
        let test = eval_value(&mut symbols.clone(), &elems[0])?;
159
1411
        let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &elems[0])?;
160
1411
        super::reject_non_boolean_runtime_test(&test, test_diverges)?;
161
1411
        if test_diverges {
162
            // Compile the test for effect so its exit fires; the remaining
163
            // chain is dead. Close exactly the guards opened so far.
164
249
            compile_for_effect(ctx, emit, symbols, &elems[0])?;
165
249
            for _ in 0..open_guards {
166
                emit.block_end();
167
            }
168
249
            return Ok(());
169
1162
        }
170
1162
        if super::is_runtime_test(&test) {
171
1079
            compile_for_stack(ctx, emit, symbols, &elems[0])?;
172
1079
            emit.if_block(BlockType::Empty);
173
            // Each runtime clause opens exactly one `if` block (one `end`),
174
            // whether or not an `else` arm follows — count it.
175
1079
            open_guards += 1;
176
1079
            for expr in &elems[1..] {
177
1079
                compile_for_effect(ctx, emit, symbols, expr)?;
178
            }
179
1079
            if i != last {
180
664
                emit.else_block();
181
664
            }
182
        } else {
183
83
            let test = super::apply_const_test(ctx, emit, symbols, &elems[0], &test)?;
184
83
            if !test.is_nil() {
185
                for expr in &elems[1..] {
186
                    compile_for_effect(ctx, emit, symbols, expr)?;
187
                }
188
                for _ in 0..open_guards {
189
                    emit.block_end();
190
                }
191
                return Ok(());
192
83
            }
193
        }
194
    }
195
1079
    for _ in 0..open_guards {
196
1079
        emit.block_end();
197
1079
    }
198
415
    Ok(())
199
664
}
200

            
201
166
pub(super) fn cond_form(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
202
332
    for (i, clause) in args.iter().enumerate() {
203
332
        let elems = clause.as_list().ok_or_else(|| {
204
            Error::Compile(format!("COND: clause must be a list, got {clause:?}"))
205
        })?;
206
332
        if elems.is_empty() {
207
            return Err(Error::Compile("COND: empty clause".to_string()));
208
332
        }
209
        // Classify the test on a CLONE so its compile-time side effects
210
        // (setf / macro expansion) are NOT applied to the live table here:
211
        // a const-false test re-evals once on `symbols` below, and a
212
        // runtime/diverging test's effects ride the single `eval_value` of
213
        // the synthesized chain — never twice.
214
332
        let test = eval_value(&mut symbols.clone(), &elems[0])?;
215
332
        let test_diverges = super::block_exits::form_diverges(&mut symbols.clone(), &elems[0])?;
216
332
        super::reject_non_boolean_runtime_test(&test, test_diverges)?;
217
332
        if super::is_runtime_test(&test) || test_diverges {
218
            // First runtime (or diverging) test: the result type is the
219
            // unified type of the remaining IF-chain (the same chain
220
            // `compile_cond_for_stack` emits), NOT a blanket I32 — a binder
221
            // sizing a local from this type must match the codegen. This is
222
            // the SINGLE place the suffix's side effects reach `symbols`.
223
            let chain = rewrite_cond_to_if_chain(&args[i..])?;
224
            return eval_value(symbols, &chain);
225
332
        }
226
        // Const test: apply its effects to the live table exactly once.
227
332
        let test = eval_value(symbols, &elems[0])?;
228
332
        if !test.is_nil() {
229
166
            if elems.len() == 1 {
230
                return Ok(test);
231
166
            }
232
166
            return super::super::binding::eval_body(symbols, &elems[1..]);
233
166
        }
234
    }
235
    Ok(Expr::Nil)
236
166
}