1
//! `BLOCK` / `RETURN-FROM` — Common-Lisp lexical labelled-exit
2
//! primitive. `(block name body...)` opens a wasm `block` whose label
3
//! is bound to `name` for the body's lexical extent;
4
//! `(return-from name value)` walks the BLOCK label stack and emits
5
//! `<value>` followed by `br <relative-depth>` to that block's exit.
6
//!
7
//! The block's wasm result type is discovered at **emit time**: the body
8
//! is compiled into a scratch buffer first; each reachable
9
//! `(return-from name v)` records `v`'s type into the block frame, and
10
//! the fall-through tail (if control can reach it) contributes its type
11
//! too. Unifying those gives the result type `T`; then `block (result T)`
12
//! is opened in the parent and the scratch bytes spliced in. Because only
13
//! *compiled* (reachable) exits record, code stored as a value
14
//! (quote / lambda / labels bodies, discarded macro args) contributes
15
//! nothing — no syntactic pre-scan and no per-form blocklist. The scratch
16
//! emitter is seeded at the depth the block frame will occupy, so every
17
//! relative `br` (incl. RETURN-FROM to an outer block) stays correct
18
//! after splicing.
19
//!
20
//! Lexical-only: a RETURN-FROM whose name doesn't resolve to a BLOCK
21
//! frame in scope is a structured compile error. Dynamic non-local
22
//! exits are Tier 3 territory (try_table + throw).
23

            
24
use wasm_encoder::BlockType;
25

            
26
use crate::ast::{Expr, WasmType};
27
use crate::compiler::context::CompileContext;
28
use crate::compiler::emit::FunctionEmitter;
29
use crate::compiler::expr::{compile_for_effect, compile_for_stack, serialize_stack_to_output};
30
use crate::error::{Error, Result};
31
use crate::runtime::SymbolTable;
32

            
33
use super::block_exits::form_diverges;
34

            
35
const BLOCK: &str = "block";
36
const RETURN_FROM: &str = "return-from";
37

            
38
pub(super) fn eval_block(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
39
    let (_, body) = parse_block(args)?;
40
    if body.is_empty() {
41
        return Ok(Expr::Nil);
42
    }
43
    super::super::binding::eval_body(symbols, body)
44
}
45

            
46
952
pub(super) fn eval_return_from(symbols: &mut SymbolTable, args: &[Expr]) -> Result<Expr> {
47
952
    let (_name, value) = parse_return_from(args)?;
48
952
    let value_ty = super::block_exits::peek_value_type(symbols, value)?;
49
952
    Ok(Expr::WasmRuntime(value_ty))
50
952
}
51

            
52
3264
pub(super) fn compile_block(
53
3264
    ctx: &mut CompileContext,
54
3264
    emit: &mut FunctionEmitter,
55
3264
    symbols: &mut SymbolTable,
56
3264
    args: &[Expr],
57
3264
) -> Result<()> {
58
3264
    let ty = compile_block_for_stack(ctx, emit, symbols, args)?;
59
3128
    serialize_stack_to_output(ctx, emit, ty)
60
3264
}
61

            
62
408
pub(super) fn compile_block_for_effect(
63
408
    ctx: &mut CompileContext,
64
408
    emit: &mut FunctionEmitter,
65
408
    symbols: &mut SymbolTable,
66
408
    args: &[Expr],
67
408
) -> Result<()> {
68
408
    let ty = compile_block_for_stack(ctx, emit, symbols, args)?;
69
408
    let _ = ty;
70
408
    emit.drop_value();
71
408
    Ok(())
72
408
}
73

            
74
3740
pub(super) fn compile_block_for_stack(
75
3740
    ctx: &mut CompileContext,
76
3740
    emit: &mut FunctionEmitter,
77
3740
    symbols: &mut SymbolTable,
78
3740
    args: &[Expr],
79
3740
) -> Result<WasmType> {
80
3740
    let (name, body) = parse_block(args)?;
81
3740
    if body.is_empty() {
82
        // Empty `(block n)` ≡ nil — falsy i31, typed `Bool` to serialize as
83
        // Nil and match the eval mirror (`eval_block` → `Expr::Nil`).
84
        emit.i32_const(0);
85
        return Ok(WasmType::Bool);
86
3740
    }
87

            
88
    // The block frame sits one structured-control level below the parent's
89
    // current depth. Compile the body into a scratch emitter seeded there,
90
    // so relative `br` targets resolve as if the frame were already open;
91
    // splicing preserves them.
92
3740
    let frame_depth = emit.block_depth() + 1;
93
3740
    let mut scratch = FunctionEmitter::new_seeded(frame_depth);
94
3740
    ctx.push_block_label(name, frame_depth);
95
3740
    let body_result = compile_block_body(ctx, &mut scratch, symbols, body);
96
3740
    let recorded_exits = ctx.pop_block_label(name)?;
97
3740
    let outcome = body_result?;
98

            
99
3672
    let tail_ty = outcome.falls_through.then_some(outcome.tail_ty);
100
3672
    let result_ty = unify_block_type(name, &recorded_exits, tail_ty)?;
101

            
102
3604
    if !outcome.falls_through {
103
2108
        // The body's tail is dead (an earlier form diverged); its concrete
104
2108
        // value would clash with the block result type, so a trailing
105
2108
        // `unreachable` resets the validator to a polymorphic stack. Live
106
2108
        // values leave only via the recorded `return-from` `br` edges.
107
2108
        scratch.unreachable();
108
2108
    }
109

            
110
3604
    emit.block_start_typed(BlockType::Result(ctx.wasm_val_type(result_ty)));
111
3604
    emit.splice(&scratch.take_bytes());
112
3604
    emit.block_end();
113
3604
    Ok(result_ty)
114
3740
}
115

            
116
/// Body-compile result: the tail value's type and whether control can
117
/// reach it.
118
struct BodyOutcome {
119
    tail_ty: WasmType,
120
    falls_through: bool,
121
}
122

            
123
/// Compile a BLOCK body into `emit`, stopping at the first form that
124
/// unconditionally diverges — sequential forms after it are dead and must
125
/// not be emitted (their `(return-from)`s would record bogus exits, and
126
/// their bytes are unreachable). Returns the last *compiled* form's type
127
/// and whether control falls through to it.
128
3740
fn compile_block_body(
129
3740
    ctx: &mut CompileContext,
130
3740
    emit: &mut FunctionEmitter,
131
3740
    symbols: &mut SymbolTable,
132
3740
    body: &[Expr],
133
3740
) -> Result<BodyOutcome> {
134
3740
    let last = body.len() - 1;
135
4964
    for (idx, form) in body.iter().enumerate() {
136
        // Classify divergence on a CLONE: `form_diverges` runs `eval_value`
137
        // (const-folding, macro expansion) which can mutate the symbol
138
        // table via `setf`/`defparameter`. The form is compiled for real
139
        // right after, so letting the classification mutate `symbols` would
140
        // double-apply those effects. The clone keeps the query pure.
141
4964
        let diverges = form_diverges(&mut symbols.clone(), form)?;
142
4964
        if idx == last {
143
2312
            let tail_ty = compile_for_stack(ctx, emit, symbols, form)?;
144
2244
            return Ok(BodyOutcome {
145
2244
                tail_ty,
146
2244
                falls_through: !diverges,
147
2244
            });
148
2652
        }
149
2652
        if diverges {
150
            // This non-tail form exits the block; the rest is dead. Compile
151
            // it for effect so its `return-from` records and its `br` is
152
            // emitted, then stop — the tail value comes from the recorded
153
            // exits, sealed by the caller's `unreachable`.
154
1428
            compile_for_effect(ctx, emit, symbols, form)?;
155
1428
            return Ok(BodyOutcome {
156
1428
                tail_ty: WasmType::I32,
157
1428
                falls_through: false,
158
1428
            });
159
1224
        }
160
1224
        compile_for_effect(ctx, emit, symbols, form)?;
161
    }
162
    // Unreachable: an empty body is handled by the caller.
163
    Ok(BodyOutcome {
164
        tail_ty: WasmType::I32,
165
        falls_through: true,
166
    })
167
3740
}
168

            
169
/// Unify the reachable exit types into the block's wasm result type. Every
170
/// recorded `(return-from)` exit and the fall-through tail (when reachable)
171
/// must agree; a disagreement among reachable exits is a real type error.
172
/// No reachable value-yielding exit at all (every path diverged) degrades
173
/// to `I32` — wasm stack-polymorphism past the divergence accepts it.
174
3672
fn unify_block_type(
175
3672
    name: &str,
176
3672
    recorded_exits: &[WasmType],
177
3672
    tail_ty: Option<WasmType>,
178
3672
) -> Result<WasmType> {
179
3672
    let mut chosen = tail_ty;
180
3672
    for &ty in recorded_exits {
181
952
        match chosen {
182
952
            Some(existing) if existing != ty => {
183
68
                return Err(Error::Compile(format!(
184
68
                    "BLOCK '{name}': conflicting exit types {existing} and {ty}"
185
68
                )));
186
            }
187
884
            Some(_) => {}
188
1836
            None => chosen = Some(ty),
189
        }
190
    }
191
3604
    Ok(chosen.unwrap_or(WasmType::I32))
192
3672
}
193

            
194
1564
pub(super) fn compile_return_from(
195
1564
    ctx: &mut CompileContext,
196
1564
    emit: &mut FunctionEmitter,
197
1564
    symbols: &mut SymbolTable,
198
1564
    args: &[Expr],
199
1564
) -> Result<()> {
200
1564
    compile_return_from_for_stack(ctx, emit, symbols, args)?;
201
1496
    Ok(())
202
1564
}
203

            
204
2924
pub(super) fn compile_return_from_for_stack(
205
2924
    ctx: &mut CompileContext,
206
2924
    emit: &mut FunctionEmitter,
207
2924
    symbols: &mut SymbolTable,
208
2924
    args: &[Expr],
209
2924
) -> Result<WasmType> {
210
2924
    let (name, value) = parse_return_from(args)?;
211
2924
    let target_depth = match ctx.lookup_block_label(name) {
212
2788
        Some(frame) => frame.wasm_depth,
213
        None => {
214
136
            return Err(Error::Compile(format!(
215
136
                "RETURN-FROM '{name}': no enclosing BLOCK with that name"
216
136
            )));
217
        }
218
    };
219
2788
    let value_ty = compile_for_stack(ctx, emit, symbols, value)?;
220
    // Emit-time discovery: record this reachable exit's type so the
221
    // enclosing block can unify its result type from exactly the exits
222
    // that compile. Consistency is enforced by `unify_block_type`.
223
2788
    ctx.record_block_exit(name, value_ty);
224
    // CL unwind semantics: before branching out, run the cleanup of every
225
    // `(unwind-protect)` this exit crosses (innermost-first). The value is
226
    // already on the stack; cleanups compile for effect (stack-neutral) so
227
    // they don't disturb it. `br` then carries the value to the target block.
228
2788
    super::unwind_protect::emit_crossing_cleanups(ctx, emit, symbols, target_depth)?;
229
2788
    let current = emit.block_depth();
230
2788
    let relative = current.checked_sub(target_depth).ok_or_else(|| {
231
        Error::Compile(format!(
232
            "RETURN-FROM '{name}': inconsistent block-depth tracking ({current} < {target_depth})"
233
        ))
234
    })?;
235
2788
    emit.br(relative);
236
    // After a `br`, control never reaches here. wasm's stack polymorphism
237
    // past `unreachable` lets the validator infer any surrounding-block
238
    // result type, so we don't materialize a typed value.
239
2788
    emit.unreachable();
240
2788
    Ok(value_ty)
241
2924
}
242

            
243
3740
fn parse_block(args: &[Expr]) -> Result<(&str, &[Expr])> {
244
3740
    if args.is_empty() {
245
        return Err(Error::Arity {
246
            name: BLOCK.to_string(),
247
            expected: 1,
248
            actual: 0,
249
        });
250
3740
    }
251
3740
    let name = match &args[0] {
252
3740
        Expr::Symbol(s) => s.as_str(),
253
        other => {
254
            return Err(Error::Type {
255
                expected: format!("symbol for BLOCK label, got {other:?}"),
256
                actual: format!("{other:?}"),
257
            });
258
        }
259
    };
260
3740
    Ok((name, &args[1..]))
261
3740
}
262

            
263
3876
fn parse_return_from(args: &[Expr]) -> Result<(&str, &Expr)> {
264
3876
    if args.len() != 2 {
265
        return Err(Error::Arity {
266
            name: RETURN_FROM.to_string(),
267
            expected: 2,
268
            actual: args.len(),
269
        });
270
3876
    }
271
3876
    let name = match &args[0] {
272
3876
        Expr::Symbol(s) => s.as_str(),
273
        other => {
274
            return Err(Error::Type {
275
                expected: format!("symbol for RETURN-FROM target, got {other:?}"),
276
                actual: format!("{other:?}"),
277
            });
278
        }
279
    };
280
3876
    Ok((name, &args[1]))
281
3876
}