1
//! Runtime-type inference for the let-body's tail value.
2
//!
3
//! `infer_runtime_type_with_body` walks the body for an accumulator
4
//! `(setf <name> (cons V <name>))` and reads the cons car's
5
//! `PairElement` so the promoted runtime type matches what the codegen
6
//! path emits. The walker tracks `DOLIST` / `DO` / `LET` loop-var
7
//! bindings so nested setfs resolve their car symbol against the
8
//! right type. Without this, a wrong-shape fallback surfaces as a
9
//! `ref.cast` mismatch in downstream consumers.
10

            
11
use crate::ast::{Expr, PairElement, WasmType};
12
use crate::compiler::expr::eval_value;
13
use crate::compiler::native::literal_pair_element;
14
use crate::runtime::{Symbol, SymbolKind, SymbolTable};
15

            
16
/// `elems[from..]`, never panicking on a malformed/short binder form: a `(do)`
17
/// / `(dolist)` / `(let)` with fewer than `from` elements yields an empty slice
18
/// rather than an out-of-range slice index. This walk is a best-effort
19
/// accumulator-type inference hint, so a too-short form simply has no body to
20
/// scan; the malformed form is rejected by the real binding-compile path later.
21
5229
fn tail(elems: &[Expr], from: usize) -> &[Expr] {
22
5229
    elems.get(from..).unwrap_or(&[])
23
5229
}
24

            
25
9055
pub(super) fn infer_runtime_type(expr: &Expr) -> WasmType {
26
    // Bytes share StringRef's `i8_array` representation but aren't a
27
    // `compile_for_stack` literal, so they're classified here, not in the
28
    // shared `classify_stack_type`.
29
9055
    if let Expr::Bytes(_) = expr {
30
        return WasmType::StringRef;
31
9055
    }
32
9055
    match crate::compiler::expr::classify_stack_type(expr) {
33
9055
        Some(ty) => ty,
34
        // For any remaining shape (List, Symbol, Quote, etc.) fall
35
        // back to a generic PairRef. PairElement::I32 is the
36
        // least-restrictive guess; downstream consumers refuse it
37
        // explicitly when wrong, so a disagreement surfaces as a
38
        // compile error rather than a runtime trap.
39
        None => WasmType::PairRef(PairElement::I32),
40
    }
41
9055
}
42

            
43
/// Runtime local type for a let-var the body MUTATES via `setf`/`set!`, or
44
/// `None` if the var is never assigned. A const-initialized accumulator
45
/// (`(let ((acc 0)) … (setf acc …))` / `(let ((r nil)) … (setf r (cons …)))`)
46
/// must become a real wasm local so `setf` emits `local.set` — otherwise the
47
/// assignment takes the const eval-rebind path (no runtime wasm) and is lost
48
/// across loop scopes (a DO/dolist accumulator silently stays at its init). The
49
/// element type of a cons accumulator comes from the same setf walk the eval
50
/// path uses; any other assigned var sizes from its init value.
51
12699
pub(super) fn assigned_var_runtime_type(
52
12699
    body: &[Expr],
53
12699
    name: &str,
54
12699
    init_val: &Expr,
55
12699
    symbols: &SymbolTable,
56
12699
) -> Option<WasmType> {
57
13529
    if !body.iter().any(|form| form_assigns_var(form, name)) {
58
3984
        return None;
59
8715
    }
60
8715
    let mut env = symbols.clone();
61
8715
    if let Some(elem) = walk_for_setf_pair_element(body, name, &mut env) {
62
2490
        return Some(WasmType::PairRef(elem));
63
6225
    }
64
    // The rhs isn't a CONS accumulator. Size the local from the FIRST `setf`
65
    // rhs that resolves to a concrete RUNTIME type (e.g. `(setf out
66
    // (get-input-entities))` or `(setf out (a-user-fn))` → PairRef) so it
67
    // matches what the setf stores — a `nil` init / a `(setf out nil)` first
68
    // store would otherwise size it as the falsy `Bool` (i32) and the later ref
69
    // store would fail wasm validation. Each rhs is resolved on a CLONE — the
70
    // same depth-guarded const-fold/inline the compiler uses for type inference
71
    // everywhere, so a user-fn rhs is typed by its body's result type (not
72
    // skipped) and a non-terminating recursion surfaces as an `Err` (skipped via
73
    // `.ok()`), never a hang. Const / nil rhs (no runtime type) are skipped; if
74
    // NO store yields a runtime type we fall back to the init's type. A
75
    // genuinely conflicting second runtime store is left for codegen to reject
76
    // (it owns the single local's type).
77
6225
    collect_setf_rhs(body, name)
78
6225
        .into_iter()
79
6474
        .find_map(|rhs| {
80
6474
            eval_value(&mut env.clone(), rhs)
81
6474
                .ok()
82
6474
                .and_then(|v| v.wasm_type())
83
6474
        })
84
6225
        .or_else(|| Some(infer_runtime_type(init_val)))
85
12699
}
86

            
87
/// Every `(setf <name> rhs)` / `(set! <name> rhs)` rhs anywhere in `body`, in
88
/// source order (recursing into nested forms). Used by
89
/// [`assigned_var_runtime_type`] to size a setf-mutated local from its first
90
/// runtime-typed store when it is not a CONS accumulator.
91
6225
fn collect_setf_rhs<'a>(body: &'a [Expr], name: &str) -> Vec<&'a Expr> {
92
6225
    let mut out = Vec::new();
93
6225
    collect_setf_rhs_into(body, name, &mut out);
94
6225
    out
95
6225
}
96

            
97
49966
fn collect_setf_rhs_into<'a>(body: &'a [Expr], name: &str, out: &mut Vec<&'a Expr>) {
98
133713
    for form in body {
99
133713
        let Expr::List(elems) = form else { continue };
100
43741
        if let Some(Expr::Symbol(head)) = elems.first()
101
41583
            && (head == "SETF" || head == "SET!")
102
        {
103
7968
            for pair in elems[1..].chunks(2) {
104
7968
                if let (Some(Expr::Symbol(place)), Some(rhs)) = (pair.first(), pair.get(1))
105
7968
                    && place == name
106
6640
                {
107
6640
                    out.push(rhs);
108
6640
                }
109
            }
110
35856
        }
111
        // Don't descend into a binder that rebinds `name` — its body's setfs
112
        // target a DIFFERENT (shadowing) variable.
113
43741
        if !binder_rebinds(elems, name) {
114
43741
            collect_setf_rhs_into(elems, name, out);
115
43741
        }
116
    }
117
49966
}
118

            
119
/// Recursively detects a `(setf <name> …)` / `(set! <name> …)` targeting
120
/// `name` anywhere in `form`, NOT descending into a sub-form that rebinds
121
/// `name` (its inner setfs target a shadowing variable).
122
159941
fn form_assigns_var(form: &Expr, name: &str) -> bool {
123
159941
    let Expr::List(elems) = form else {
124
94703
        return false;
125
    };
126
65238
    if let Some(Expr::Symbol(head)) = elems.first()
127
59511
        && (head == "SETF" || head == "SET!")
128
    {
129
9296
        for pair in elems[1..].chunks(2) {
130
9296
            if let Some(Expr::Symbol(place)) = pair.first()
131
9296
                && place == name
132
            {
133
8715
                return true;
134
581
            }
135
        }
136
55942
    }
137
56523
    if binder_rebinds(elems, name) {
138
249
        return false;
139
56274
    }
140
146412
    elems.iter().any(|e| form_assigns_var(e, name))
141
159941
}
142

            
143
/// Whether the list form headed by `elems` binds `name` (a lambda param, or a
144
/// `let`/`let*`/`do`/`do*`/`dolist` variable), shadowing an enclosing binding.
145
/// Used to keep the setf-promotion scans from attributing a shadowed inner
146
/// assignment to the outer binding. Mirrors `lambda::param_infer`'s sibling.
147
151973
fn binder_rebinds(elems: &[Expr], name: &str) -> bool {
148
151973
    let Some(Expr::Symbol(head)) = elems.first() else {
149
8217
        return false;
150
    };
151
143756
    let in_specs = |spec: Option<&Expr>| {
152
7885
        matches!(spec, Some(Expr::List(items)) if items.iter().any(|b| match b {
153
664
            Expr::Symbol(s) => s == name,
154
7221
            Expr::List(parts) => matches!(parts.first(), Some(Expr::Symbol(s)) if s == name),
155
            _ => false,
156
7885
        }))
157
7553
    };
158
143756
    match head.to_ascii_uppercase().as_str() {
159
143756
        "LAMBDA" | "LET" | "LET*" | "DO" | "DO*" => in_specs(elems.get(1)),
160
136203
        "DOLIST" => matches!(
161
6723
            elems.get(1),
162
6723
            Some(Expr::List(spec)) if matches!(spec.first(), Some(Expr::Symbol(s)) if s == name)
163
        ),
164
129480
        _ => false,
165
    }
166
151973
}
167

            
168
/// When the body's tail returns a Symbol whose value flows from a
169
/// `(setf <name> (cons V <name>))` accumulator earlier in the body,
170
/// resolve the inferred runtime type by walking the body for that
171
/// setf and reading the cons car's PairElement. The walker tracks
172
/// dolist/do loop-var bindings so a setf nested inside a dolist
173
/// resolves the loop var's element type (e.g. `tag` inside `(dolist
174
/// (tag (entities-for ...)) ...)` resolves via the dolist's list
175
/// expression's PairElement). Without this, the cons car's symbol
176
/// would be unbound in the eval-time symbol table.
177
830
pub(super) fn infer_runtime_type_with_body(
178
830
    val: &Expr,
179
830
    result_expr: &Expr,
180
830
    body: &[Expr],
181
830
    symbols: &SymbolTable,
182
830
) -> WasmType {
183
830
    if let Expr::Symbol(name) = result_expr {
184
166
        let mut env = symbols.clone();
185
166
        if let Some(elem) = walk_for_setf_pair_element(body, name, &mut env) {
186
            return WasmType::PairRef(elem);
187
166
        }
188
664
    }
189
830
    infer_runtime_type(val)
190
830
}
191

            
192
/// Walks `body` for `(setf <name> <rhs>)` and infers the matching
193
/// PairElement from the cons car of the rhs. Tracks DOLIST/DO/LET
194
/// loop-var bindings into `env` so nested setfs resolve their car
195
/// symbol against the right type.
196
8881
fn walk_for_setf_pair_element(
197
8881
    body: &[Expr],
198
8881
    target: &str,
199
8881
    env: &mut SymbolTable,
200
8881
) -> Option<PairElement> {
201
14359
    for form in body {
202
14359
        if let Some(elem) = walk_form(form, target, env) {
203
2490
            return Some(elem);
204
11869
        }
205
    }
206
6391
    None
207
8881
}
208

            
209
136286
fn walk_form(form: &Expr, target: &str, env: &mut SymbolTable) -> Option<PairElement> {
210
136286
    let Expr::List(elems) = form else {
211
84577
        return None;
212
    };
213
    // A binder that rebinds `target` shadows it: any `(setf target …)` inside
214
    // refers to the inner variable, not the accumulator we're typing.
215
51709
    if binder_rebinds(elems, target) {
216
        return None;
217
51709
    }
218
51709
    let head = elems.first().and_then(|e| match e {
219
51377
        Expr::Symbol(s) => Some(s.as_str()),
220
332
        _ => None,
221
51709
    });
222
51709
    match head {
223
51377
        Some("SETF") => {
224
10458
            for pair in elems[1..].chunks(2) {
225
10458
                if let Some(Expr::Symbol(var)) = pair.first()
226
10458
                    && var == target
227
9130
                    && let Some(rhs) = pair.get(1)
228
9130
                    && let Some(elem) = step_pair_element(rhs, env)
229
                {
230
2490
                    return Some(elem);
231
7968
                }
232
            }
233
15936
            for e in &elems[1..] {
234
15936
                if let Some(found) = walk_form(e, target, env) {
235
                    return Some(found);
236
15936
                }
237
            }
238
7885
            None
239
        }
240
41002
        Some("DOLIST") => {
241
2324
            if let Some(Expr::List(var_spec)) = elems.get(1)
242
2324
                && let Some(Expr::Symbol(var_name)) = var_spec.first()
243
2324
                && let Some(list_expr) = var_spec.get(1)
244
            {
245
2324
                let mut probe = env.clone();
246
2324
                if let Ok(list_val) = eval_value(&mut probe, list_expr)
247
2324
                    && let Some(WasmType::PairRef(elem)) = list_val.wasm_type()
248
1660
                {
249
1660
                    env.define(
250
1660
                        Symbol::new(var_name, SymbolKind::Variable)
251
1660
                            .with_value(Expr::WasmRuntime(elem.as_wasm_type())),
252
1660
                    );
253
1660
                }
254
            }
255
2490
            for e in tail(elems, 2) {
256
2490
                if let Some(found) = walk_form(e, target, env) {
257
249
                    return Some(found);
258
2241
                }
259
            }
260
2075
            None
261
        }
262
38678
        Some("DO") | Some("DO*") => {
263
2656
            if let Some(Expr::List(vars)) = elems.get(1) {
264
2656
                for v in vars {
265
2656
                    if let Expr::List(vparts) = v
266
2656
                        && let Some(Expr::Symbol(vname)) = vparts.first()
267
2656
                        && let Some(init) = vparts.get(1)
268
                    {
269
2656
                        let mut probe = env.clone();
270
2656
                        if let Ok(init_val) = eval_value(&mut probe, init) {
271
2656
                            // A DO var is a RUNTIME value of the init's stack
272
2656
                            // type, not the const init itself: an integer init
273
2656
                            // (`(i 0 …)`) makes `i` a runtime Index (I32), so
274
2656
                            // `(cons i …)` is an I32 cell. Binding the const `0`
275
2656
                            // would resolve its pair element as Ratio (the
276
2656
                            // numeric-literal cell slot) and mis-size the
277
2656
                            // accumulator → a CAR ref.cast trap.
278
2656
                            env.define(
279
2656
                                Symbol::new(vname, SymbolKind::Variable)
280
2656
                                    .with_value(Expr::WasmRuntime(infer_runtime_type(&init_val))),
281
2656
                            );
282
2656
                        }
283
                    }
284
                }
285
            }
286
2656
            for e in tail(elems, 3) {
287
2656
                if let Some(found) = walk_form(e, target, env) {
288
1826
                    return Some(found);
289
830
                }
290
            }
291
830
            None
292
        }
293
36022
        Some("LET") | Some("LET*") => {
294
249
            if let Some(Expr::List(bindings)) = elems.get(1) {
295
249
                for b in bindings {
296
249
                    if let Expr::List(parts) = b
297
249
                        && let Some(Expr::Symbol(bname)) = parts.first()
298
                    {
299
249
                        let init_val = match parts.get(1) {
300
249
                            Some(init) => {
301
249
                                let mut probe = env.clone();
302
249
                                eval_value(&mut probe, init).unwrap_or(Expr::Nil)
303
                            }
304
                            None => Expr::Nil,
305
                        };
306
249
                        env.define(Symbol::new(bname, SymbolKind::Variable).with_value(init_val));
307
                    }
308
                }
309
            }
310
249
            for e in tail(elems, 2) {
311
249
                if let Some(found) = walk_form(e, target, env) {
312
                    return Some(found);
313
249
                }
314
            }
315
249
            None
316
        }
317
        _ => {
318
100596
            for e in elems {
319
100596
                if let Some(found) = walk_form(e, target, env) {
320
2158
                    return Some(found);
321
98438
                }
322
            }
323
33947
            None
324
        }
325
    }
326
136286
}
327

            
328
/// Inline copy of `iteration::common::step_pair_element` to avoid a
329
/// cyclic `pub(super)` exposure. Same semantics: peeks at a CONS
330
/// expression's car and resolves the corresponding PairElement.
331
23904
fn step_pair_element(expr: &Expr, symbols: &SymbolTable) -> Option<PairElement> {
332
23904
    let Expr::List(elems) = expr else {
333
16268
        return None;
334
    };
335
7636
    if let Some(Expr::Symbol(name)) = elems.first()
336
7636
        && name == "CONS"
337
2490
        && elems.len() == 3
338
2490
        && let Some(elem) = resolve_pair_element(&elems[1], symbols)
339
    {
340
2490
        return Some(elem);
341
5146
    }
342
14774
    elems.iter().find_map(|e| step_pair_element(e, symbols))
343
23904
}
344

            
345
4648
fn resolve_pair_element(expr: &Expr, symbols: &SymbolTable) -> Option<PairElement> {
346
83
    match expr {
347
2158
        Expr::WasmRuntime(ty) | Expr::WasmLocal(_, ty) => PairElement::from_wasm_type(*ty),
348
332
        Expr::Number(_) => literal_pair_element(expr),
349
        Expr::T => Some(PairElement::Bool),
350
2075
        Expr::Symbol(name) => {
351
2075
            let sym = symbols.lookup(name)?;
352
2075
            let value = sym.value()?;
353
2075
            resolve_pair_element(value, symbols)
354
        }
355
        // A host-fn / form car hasn't reduced to a runtime placeholder yet;
356
        // eval on a CLONE so the accumulator element matches codegen. Admitted
357
        // only when the whole car subtree is pure-native (no user-callable
358
        // anywhere), so inference can't recurse into a (possibly
359
        // non-terminating) user lambda — native handlers eval their args, so a
360
        // top-level-head check alone wouldn't be enough.
361
83
        Expr::List(_) if is_pure_native_expr(expr, symbols) => {
362
83
            match eval_value(&mut symbols.clone(), expr).ok()? {
363
                Expr::List(_) => None,
364
83
                other => resolve_pair_element(&other, symbols),
365
            }
366
        }
367
        _ => None,
368
    }
369
4648
}
370

            
371
/// Whether `expr` is safe to `eval_value` purely for type inference: it
372
/// contains NO user-callable anywhere. Native eval handlers recurse into their
373
/// args, and higher-order natives (MAP/FILTER/FOLD) invoke a function-valued
374
/// argument, so the check spans the whole tree — every call head must be a
375
/// native/operator (no user shadow) and every operand pure-native, and a bare
376
/// symbol is admitted only when it doesn't name a user function/lambda value.
377
/// Conservative: any non-inert shape is rejected. Mirrors
378
/// `iteration::common::is_pure_native_expr`.
379
249
fn is_pure_native_expr(expr: &Expr, symbols: &SymbolTable) -> bool {
380
249
    match expr {
381
        Expr::Number(_)
382
        | Expr::T
383
        | Expr::Nil
384
        | Expr::String(_)
385
        | Expr::Bytes(_)
386
        | Expr::Keyword(_)
387
        | Expr::Quote(_)
388
        | Expr::WasmRuntime(_)
389
83
        | Expr::WasmLocal(_, _) => true,
390
        Expr::Symbol(name) => !names_user_callable(name, symbols),
391
166
        Expr::List(elems) => match elems.first() {
392
166
            Some(Expr::Symbol(head)) => {
393
166
                head_is_native(head, symbols)
394
166
                    && elems[1..].iter().all(|e| is_pure_native_expr(e, symbols))
395
            }
396
            _ => false,
397
        },
398
        _ => false,
399
    }
400
249
}
401

            
402
166
fn head_is_native(head: &str, symbols: &SymbolTable) -> bool {
403
166
    match symbols.lookup(head) {
404
166
        Some(sym) => {
405
166
            sym.function().is_none()
406
166
                && matches!(sym.kind(), SymbolKind::Native | SymbolKind::Operator)
407
        }
408
        None => false,
409
    }
410
166
}
411

            
412
/// Whether `name` resolves to a user function / lambda value — passed to a
413
/// higher-order native it would be dereferenced and called during
414
/// `eval_value`, so it is NOT inert. Mirrors `iteration::common`'s sibling.
415
fn names_user_callable(name: &str, symbols: &SymbolTable) -> bool {
416
    match symbols.lookup(name) {
417
        Some(sym) => {
418
            sym.function().is_some()
419
                || matches!(sym.value(), Some(Expr::Lambda(_, _)))
420
                || matches!(sym.kind(), SymbolKind::Function | SymbolKind::Macro)
421
        }
422
        None => false,
423
    }
424
}