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
1136
fn body_falls_through(symbols: &mut SymbolTable, body: &[Expr]) -> Result<bool> {
34
6177
    for (idx, form) in body.iter().enumerate() {
35
6177
        let is_tail = idx + 1 == body.len();
36
6177
        if form_diverges(symbols, form)? {
37
71
            return Ok(false);
38
6106
        }
39
6106
        if is_tail {
40
994
            return Ok(true);
41
5112
        }
42
    }
43
71
    Ok(true)
44
1136
}
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
144059
pub(super) fn form_diverges(symbols: &mut SymbolTable, form: &Expr) -> Result<bool> {
61
144059
    let Expr::List(items) = form else {
62
83851
        return Ok(false);
63
    };
64
60208
    let head = match items.first() {
65
59995
        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
142
        Some(callee @ (Expr::List(_) | Expr::Lambda(_, _))) => {
75
142
            if form_diverges(symbols, callee)? {
76
                return Ok(true);
77
142
            }
78
142
            for arg in &items[1..] {
79
142
                if form_diverges(symbols, arg)? {
80
142
                    return Ok(true);
81
                }
82
            }
83
            return Ok(false);
84
        }
85
71
        _ => return Ok(false),
86
    };
87

            
88
59995
    if head.eq_ignore_ascii_case(RETURN_FROM) || head.eq_ignore_ascii_case(ERROR) {
89
7029
        return Ok(true);
90
52966
    }
91
52966
    if head.eq_ignore_ascii_case(BEGIN) || head.eq_ignore_ascii_case(PROGN) {
92
994
        return body_falls_through(symbols, &items[1..]).map(|ft| !ft);
93
51972
    }
94
51972
    if head.eq_ignore_ascii_case(IF) {
95
1136
        return if_diverges(symbols, items);
96
50836
    }
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
50836
    if head.eq_ignore_ascii_case(UNWIND_PROTECT) {
105
1420
        let Some((body, cleanup)) = items[1..].split_first() else {
106
            return Ok(false);
107
        };
108
1420
        if form_diverges(symbols, body)? {
109
1278
            return Ok(true);
110
142
        }
111
        // Cleanup runs as a sequence; it diverges iff control can't fall
112
        // through it (some reachable form unconditionally exits).
113
142
        return body_falls_through(symbols, cleanup).map(|ft| !ft);
114
49416
    }
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
49416
    if is_macro(symbols, head) {
119
4828
        return macro_diverges(symbols, head, &items[1..]);
120
44588
    }
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
44588
    if head_is_eager_call(symbols, head) {
129
59995
        for arg in &items[1..] {
130
59995
            if form_diverges(symbols, arg)? {
131
284
                return Ok(true);
132
59711
            }
133
        }
134
9869
    }
135
44304
    Ok(false)
136
144059
}
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
44588
fn head_is_eager_call(symbols: &SymbolTable, head: &str) -> bool {
142
9869
    matches!(
143
44588
        symbols.lookup(head).map(|sym| sym.kind()),
144
        Some(SymbolKind::Native | SymbolKind::Operator | SymbolKind::Function)
145
    )
146
44588
}
147

            
148
49416
fn is_macro(symbols: &SymbolTable, head: &str) -> bool {
149
49416
    matches!(symbols.lookup(head), Some(sym) if sym.kind() == SymbolKind::Macro)
150
49416
}
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
4828
fn macro_diverges(symbols: &mut SymbolTable, head: &str, args: &[Expr]) -> Result<bool> {
158
4828
    let func = match symbols.lookup(head) {
159
4828
        Some(sym) => sym.function().cloned(),
160
        None => return Ok(false),
161
    };
162
4828
    let Some(Expr::Lambda(params, body)) = func else {
163
        return Ok(false);
164
    };
165
4828
    symbols.enter_macro_expansion()?;
166
4828
    let result = expand_macro(symbols, &params, &body, args).and_then(|expansion| {
167
4828
        let code = match expansion {
168
142
            Expr::Quote(inner) => *inner,
169
4686
            other => other,
170
        };
171
4828
        form_diverges(symbols, &code)
172
4828
    });
173
4828
    symbols.exit_macro_expansion();
174
4828
    result
175
4828
}
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
1136
fn if_diverges(symbols: &mut SymbolTable, items: &[Expr]) -> Result<bool> {
183
1136
    if items.len() < 3 || items.len() > 4 {
184
        return Ok(false);
185
1136
    }
186
1136
    if form_diverges(symbols, &items[1])? {
187
142
        return Ok(true);
188
994
    }
189
994
    let test = eval_value(symbols, &items[1])?;
190
994
    if test.is_wasm_runtime() {
191
568
        let then_div = form_diverges(symbols, &items[2])?;
192
568
        let else_div = match items.get(3) {
193
568
            Some(else_branch) => form_diverges(symbols, else_branch)?,
194
            None => false,
195
        };
196
568
        return Ok(then_div && else_div);
197
426
    }
198
426
    let live = if super::is_truthy(&test) {
199
355
        &items[2]
200
    } else {
201
71
        match items.get(3) {
202
71
            Some(else_branch) => else_branch,
203
            None => return Ok(false),
204
        }
205
    };
206
426
    form_diverges(symbols, live)
207
1136
}
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
994
pub(super) fn peek_value_type(symbols: &mut SymbolTable, expr: &Expr) -> Result<WasmType> {
214
994
    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
994
    }
221
994
    let resolved = eval_value(symbols, expr)?;
222
994
    Ok(crate::compiler::expr::classify_stack_type(&resolved).unwrap_or(WasmType::I32))
223
994
}