1
//! Control-flow helpers for `BLOCK` emit-time result typing.
2
//!
3
//! The block's result type is discovered while the body is compiled —
4
//! each reachable `(return-from)` records its value type into the block
5
//! frame (see `block.rs`). This module no longer collects exit types by
6
//! syntactic pre-scan; it only answers two narrower, decidable questions
7
//! the emitter still needs:
8
//!
9
//! - [`form_diverges`] — does a single form unconditionally diverge?
10
//!   Used by `IF`'s stack-compile to peek the live branch past a
11
//!   diverging one, and by BLOCK to stop compiling dead sequential forms.
12
//!
13
//! Divergence is conservative: any form we cannot prove diverges is
14
//! treated as falling through. Over-estimating fall-through only costs a
15
//! redundant (harmless) seal `unreachable`; it never drops a live value,
16
//! because actual exit values ride the recorded `return-from` `br` edges.
17

            
18
use crate::ast::{Expr, WasmType};
19
use crate::compiler::expr::{eval_value, expand_macro};
20
use crate::error::Result;
21
use crate::runtime::{SymbolKind, SymbolTable};
22

            
23
const RETURN_FROM: &str = "return-from";
24
const ERROR: &str = "error";
25
const IF: &str = "if";
26
const BEGIN: &str = "begin";
27
const PROGN: &str = "progn";
28
const UNWIND_PROTECT: &str = "unwind-protect";
29

            
30
/// Whether control can fall through `body` to its tail value position.
31
/// False once a form unconditionally diverges — subsequent forms (incl.
32
/// the tail) are dead. Internal helper for `BEGIN`/`PROGN` divergence.
33
1088
fn body_falls_through(symbols: &mut SymbolTable, body: &[Expr]) -> Result<bool> {
34
5916
    for (idx, form) in body.iter().enumerate() {
35
5916
        let is_tail = idx + 1 == body.len();
36
5916
        if form_diverges(symbols, form)? {
37
68
            return Ok(false);
38
5848
        }
39
5848
        if is_tail {
40
952
            return Ok(true);
41
4896
        }
42
    }
43
68
    Ok(true)
44
1088
}
45

            
46
/// Whether `form` unconditionally diverges (cannot fall through to its
47
/// successor): `(error …)`, any `(return-from …)`, a `BEGIN`/`PROGN`
48
/// whose last reachable form diverges, or an `IF` whose test or live
49
/// branch(es) all diverge. Anything else is conservatively treated as
50
/// falling through — code stored as a value (quote / lambda / labels
51
/// bodies) and ordinary calls do not diverge here.
52
///
53
/// CONTRACT: this is a *query* that internally runs `eval_value` (for
54
/// const-fold / macro expansion / IF-test classification), which can apply
55
/// compile-time side effects (`setf`, macro expansion) to `symbols`. Callers
56
/// MUST pass a THROWAWAY CLONE (`&mut symbols.clone()`), never the live table
57
/// — every call site does. Because `symbols` here is already disposable, the
58
/// internal re-evaluations (e.g. `if_diverges` classifying then valuing the
59
/// test) are clone-local and cannot double-apply effects to a live table.
60
138046
pub(super) fn form_diverges(symbols: &mut SymbolTable, form: &Expr) -> Result<bool> {
61
138046
    let Expr::List(items) = form else {
62
80342
        return Ok(false);
63
    };
64
57704
    let head = match items.first() {
65
57500
        Some(Expr::Symbol(s)) => s.as_str(),
66
        // A genuinely-eager non-symbol callee — an inline lambda `((lambda …)
67
        // args)` or a computed function `((f) args)` (head is a `List`/`Lambda`
68
        // node) — evaluates the callee then each arg before the call, so it
69
        // diverges if any of them diverges. Other non-symbol heads (notably a
70
        // quoted symbol `('quote …)`, which the call path may route to the LAZY
71
        // QUOTE special form) are NOT classified eager: over-reporting
72
        // divergence is unsafe (it would drop a live tail), so we conservatively
73
        // fall through to non-diverging for them.
74
136
        Some(callee @ (Expr::List(_) | Expr::Lambda(_, _))) => {
75
136
            if form_diverges(symbols, callee)? {
76
                return Ok(true);
77
136
            }
78
136
            for arg in &items[1..] {
79
136
                if form_diverges(symbols, arg)? {
80
136
                    return Ok(true);
81
                }
82
            }
83
            return Ok(false);
84
        }
85
68
        _ => return Ok(false),
86
    };
87

            
88
57500
    if head.eq_ignore_ascii_case(RETURN_FROM) || head.eq_ignore_ascii_case(ERROR) {
89
6732
        return Ok(true);
90
50768
    }
91
50768
    if head.eq_ignore_ascii_case(BEGIN) || head.eq_ignore_ascii_case(PROGN) {
92
952
        return body_falls_through(symbols, &items[1..]).map(|ft| !ft);
93
49816
    }
94
49816
    if head.eq_ignore_ascii_case(IF) {
95
1088
        return if_diverges(symbols, items);
96
48728
    }
97
    // `(unwind-protect body cleanup...)` diverges if EITHER the protected
98
    // body diverges (the raise/return-from propagates past the form after
99
    // cleanup) OR a cleanup form diverges. Cleanup always runs on the way out,
100
    // so a `(return-from)` / `(error)` / `(go)` in a reachable cleanup
101
    // unconditionally transfers control — the body's value never falls
102
    // through. Misjudging this leaves a dead tail "reachable" and lets BLOCK
103
    // unify a bogus type against the real (cleanup) exit.
104
48728
    if head.eq_ignore_ascii_case(UNWIND_PROTECT) {
105
1360
        let Some((body, cleanup)) = items[1..].split_first() else {
106
            return Ok(false);
107
        };
108
1360
        if form_diverges(symbols, body)? {
109
1224
            return Ok(true);
110
136
        }
111
        // Cleanup runs as a sequence; it diverges iff control can't fall
112
        // through it (some reachable form unconditionally exits).
113
136
        return body_falls_through(symbols, cleanup).map(|ft| !ft);
114
47368
    }
115
    // A macro call whose expansion diverges (e.g. expands to a bare
116
    // `(return-from …)`) is itself diverging. Expand one step and
117
    // re-classify, mirroring the compile path.
118
47368
    if is_macro(symbols, head) {
119
4624
        return macro_diverges(symbols, head, &items[1..]);
120
42744
    }
121
    // An eager call (native / operator / user function) evaluates ALL its
122
    // arguments before the call, so it diverges if any argument diverges — the
123
    // arg's `(return-from …)` / `(error …)` transfers control before the call
124
    // runs. e.g. `(cons (return-from b "x") 2)` exits via the car, leaving the
125
    // block's tail dead. Gated on an eager-call head: special forms (handled
126
    // above) and lazy heads (QUOTE / AND / OR with short-circuit) must NOT
127
    // propagate — over-reporting divergence would wrongly drop a live tail.
128
42744
    if head_is_eager_call(symbols, head) {
129
57464
        for arg in &items[1..] {
130
57464
            if form_diverges(symbols, arg)? {
131
272
                return Ok(true);
132
57192
            }
133
        }
134
9490
    }
135
42472
    Ok(false)
136
138046
}
137

            
138
/// Whether `head` names an eager call — a native, operator, or user function
139
/// that evaluates all its arguments before being applied. Special forms and
140
/// macros (lazy / custom evaluation order) are excluded.
141
42744
fn head_is_eager_call(symbols: &SymbolTable, head: &str) -> bool {
142
9490
    matches!(
143
42744
        symbols.lookup(head).map(|sym| sym.kind()),
144
        Some(SymbolKind::Native | SymbolKind::Operator | SymbolKind::Function)
145
    )
146
42744
}
147

            
148
47368
fn is_macro(symbols: &SymbolTable, head: &str) -> bool {
149
47368
    matches!(symbols.lookup(head), Some(sym) if sym.kind() == SymbolKind::Macro)
150
47368
}
151

            
152
/// Classify divergence of a macro call by one-step expansion + recursion,
153
/// with a depth guard balanced like `expand_macro_then`: enter before
154
/// expanding, exit after the recursive re-classification (including the
155
/// error path) so a self-referential macro is bounded while sibling
156
/// divergence checks on the same throwaway table start from fresh depth.
157
4624
fn macro_diverges(symbols: &mut SymbolTable, head: &str, args: &[Expr]) -> Result<bool> {
158
4624
    let func = match symbols.lookup(head) {
159
4624
        Some(sym) => sym.function().cloned(),
160
        None => return Ok(false),
161
    };
162
4624
    let Some(Expr::Lambda(params, body)) = func else {
163
        return Ok(false);
164
    };
165
4624
    symbols.enter_macro_expansion()?;
166
4624
    let result = expand_macro(symbols, &params, &body, args).and_then(|expansion| {
167
4624
        let code = match expansion {
168
136
            Expr::Quote(inner) => *inner,
169
4488
            other => other,
170
        };
171
4624
        form_diverges(symbols, &code)
172
4624
    });
173
4624
    symbols.exit_macro_expansion();
174
4624
    result
175
4624
}
176

            
177
/// `IF` divergence, mirroring the emitter's const-fold: a diverging *test*
178
/// makes the whole IF diverge (control transfers away before the branches);
179
/// otherwise a constant test keeps only the live branch and a runtime test
180
/// diverges only when *both* branches diverge (a missing else-branch falls
181
/// through).
182
1088
fn if_diverges(symbols: &mut SymbolTable, items: &[Expr]) -> Result<bool> {
183
1088
    if items.len() < 3 || items.len() > 4 {
184
        return Ok(false);
185
1088
    }
186
1088
    if form_diverges(symbols, &items[1])? {
187
136
        return Ok(true);
188
952
    }
189
952
    let test = eval_value(symbols, &items[1])?;
190
952
    if test.is_wasm_runtime() {
191
544
        let then_div = form_diverges(symbols, &items[2])?;
192
544
        let else_div = match items.get(3) {
193
544
            Some(else_branch) => form_diverges(symbols, else_branch)?,
194
            None => false,
195
        };
196
544
        return Ok(then_div && else_div);
197
408
    }
198
408
    let live = if super::is_truthy(&test) {
199
340
        &items[2]
200
    } else {
201
68
        match items.get(3) {
202
68
            Some(else_branch) => else_branch,
203
            None => return Ok(false),
204
        }
205
    };
206
408
    form_diverges(symbols, live)
207
1088
}
208

            
209
/// Static `WasmType` a value-position form would deposit on the stack,
210
/// looking through a `(return-from name V)` to `V`. Mirrors the literal
211
/// lowerings the stack-compile path uses. Used by `eval_return_from` to
212
/// surface a runtime placeholder of the right type.
213
952
pub(super) fn peek_value_type(symbols: &mut SymbolTable, expr: &Expr) -> Result<WasmType> {
214
952
    if let Expr::List(items) = expr
215
        && let Some(Expr::Symbol(head)) = items.first()
216
        && head.eq_ignore_ascii_case(RETURN_FROM)
217
        && items.len() == 3
218
    {
219
        return peek_value_type(symbols, &items[2]);
220
952
    }
221
952
    let resolved = eval_value(symbols, expr)?;
222
952
    Ok(crate::compiler::expr::classify_stack_type(&resolved).unwrap_or(WasmType::I32))
223
952
}