1
//! Closure parameter-type inference from body usage.
2
//!
3
//! A value-position `(lambda …)` lifted to a closure needs user-visible param
4
//! types. Defaulting every param to `Ratio` mis-types a list accumulator (a
5
//! param used as a `(cons CAR param)` cdr is a pair, not a scalar), so FOLD /
6
//! MAP over such a closure rejected it. This module infers each required
7
//! param's `WasmType` from how the body uses it; the closure's result type then
8
//! follows from compiling the body under those param types.
9
//!
10
//! Today's single inferred shape: a param used as the CDR of `(cons CAR param)`
11
//! → `PairRef(<car cell element>)`. A param with no such use stays `Ratio`
12
//! (identical to the old default — no existing scalar closure regresses). The
13
//! scan is SCOPE-AWARE: a binder (`lambda`/`let`/`let*`/`do`/`do*`/`dolist`)
14
//! that rebinds the name shadows the param in its BODY, so a cons-cdr use of the
15
//! inner variable must not retype the outer param — but the binder's INIT
16
//! expressions (and a `dolist` list-expr) are in the OUTER scope and ARE
17
//! scanned.
18

            
19
use crate::ast::{Expr, LambdaParams, PairElement, WasmType};
20

            
21
/// Infers each required param's user-visible `WasmType` from `body`.
22
4067
pub(super) fn infer_param_types(params: &LambdaParams, body: &Expr) -> Vec<WasmType> {
23
4067
    params
24
4067
        .required
25
4067
        .iter()
26
5312
        .map(|name| pinned_param_type(body, name).unwrap_or(WasmType::Ratio))
27
4067
        .collect()
28
4067
}
29

            
30
/// The `WasmType` `name` is pinned to by a type-constraining use in `body`,
31
/// anywhere `name` still refers to the searched param (not inside a scope that
32
/// rebinds it). Two pins today:
33
/// - CDR of a `(cons CAR name)` → `PairRef(<car cell element>)` (list accumulator);
34
/// - argument to a string native (`string=`, `upcase-string`) → `StringRef`.
35
///
36
/// `None` if no pinning use is found (caller defaults to `Ratio`).
37
15879
fn pinned_param_type(body: &Expr, name: &str) -> Option<WasmType> {
38
15879
    let Expr::List(elems) = body else {
39
10298
        return None;
40
    };
41
5581
    if let [Expr::Symbol(head), car, Expr::Symbol(cdr)] = elems.as_slice()
42
2248
        && head.eq_ignore_ascii_case("cons")
43
171
        && cdr == name
44
    {
45
87
        return Some(WasmType::PairRef(car_cell_element(car)));
46
5494
    }
47
5494
    if let Some(Expr::Symbol(head)) = elems.first()
48
5493
        && is_string_native(head)
49
167
        && elems[1..]
50
167
            .iter()
51
167
            .any(|a| matches!(a, Expr::Symbol(s) if s == name))
52
    {
53
167
        return Some(WasmType::StringRef);
54
5327
    }
55
5327
    match head_upper(elems).as_deref() {
56
        // Quoted forms are data, not evaluated — a param mentioned inside must
57
        // not pin its type. (The `Expr::Quote`/`Expr::Quasiquote` AST variants
58
        // already return `None` above by not being a `List`; this guards the
59
        // `(quote …)` / `(quasiquote …)` list spellings.)
60
5326
        Some("QUOTE") | Some("QUASIQUOTE") => None,
61
5324
        Some("LET") => scan_let(elems, name, false),
62
5321
        Some("LET*") => scan_let(elems, name, true),
63
5320
        Some("DO") | Some("DO*") => scan_do(elems, name),
64
5317
        Some("DOLIST") => scan_dolist(elems, name),
65
5315
        Some("LAMBDA") => {
66
            // The body is shadowed iff `name` is one of the lambda's params; a
67
            // param list carries no scannable outer-scope subform.
68
1
            if lambda_binds(elems, name) {
69
                None
70
            } else {
71
1
                scan_each(tail(elems, 1), name)
72
            }
73
        }
74
5315
        _ => scan_each(tail(elems, 1), name),
75
    }
76
15879
}
77

            
78
/// Natives that REQUIRE a `StringRef` argument, so a param passed to one is a
79
/// string. (Only these two exist today; extend alongside new string natives.)
80
5493
fn is_string_native(head: &str) -> bool {
81
167
    matches!(
82
5493
        head.to_ascii_uppercase().as_str(),
83
5493
        "STRING=" | "UPCASE-STRING"
84
    )
85
5493
}
86

            
87
/// The `PairElement` a car expression contributes. Literal cars pin their slot;
88
/// a sibling-param / captured / computed car has no statically-known cell type,
89
/// so it widens to `AnyRef` — keeping the cdr param's pair element consistent
90
/// with whatever `compile_cons_to_stack` actually produces for that car.
91
93
fn car_cell_element(car: &Expr) -> PairElement {
92
93
    crate::compiler::native::literal_pair_element(car).unwrap_or(PairElement::AnyRef)
93
93
}
94

            
95
5327
fn head_upper(elems: &[Expr]) -> Option<String> {
96
5327
    match elems.first() {
97
5326
        Some(Expr::Symbol(s)) => Some(s.to_ascii_uppercase()),
98
1
        _ => None,
99
    }
100
5327
}
101

            
102
5322
fn scan_each(forms: &[Expr], name: &str) -> Option<WasmType> {
103
10551
    forms.iter().find_map(|e| pinned_param_type(e, name))
104
5322
}
105

            
106
/// `forms[from..]`, but never panics on a malformed/short form: a list with
107
/// fewer than `from` elements (e.g. a bare `()`, `(let)`, `(do)`) yields an
108
/// empty slice rather than an out-of-range slice index. Param-type inference is
109
/// a best-effort hint, so a too-short form simply contributes no pin; the
110
/// malformed form is rejected (or compiled) by the real path later.
111
5322
fn tail(forms: &[Expr], from: usize) -> &[Expr] {
112
5322
    forms.get(from..).unwrap_or(&[])
113
5322
}
114

            
115
/// `(let ((v init)…) body…)` / `(let* …)`. Binding INITs are in the outer scope
116
/// (for LET* up to the binding that introduces `name`); the body is shadowed
117
/// once any binding (re)binds `name`.
118
4
fn scan_let(elems: &[Expr], name: &str, sequential: bool) -> Option<WasmType> {
119
4
    let mut shadowed = false;
120
4
    if let Some(Expr::List(bindings)) = elems.get(1) {
121
2
        for binding in bindings {
122
2
            let Expr::List(parts) = binding else { continue };
123
2
            let binds_here = matches!(parts.first(), Some(Expr::Symbol(s)) if s == name);
124
            // For LET* a binding shadows `name` for SUBSEQUENT inits; for LET
125
            // every init is in the outer scope regardless.
126
2
            if !(sequential && shadowed)
127
2
                && let Some(init) = parts.get(1)
128
2
                && let Some(ty) = pinned_param_type(init, name)
129
            {
130
1
                return Some(ty);
131
1
            }
132
1
            if binds_here {
133
1
                shadowed = true;
134
1
            }
135
        }
136
2
    }
137
3
    if shadowed {
138
1
        None
139
    } else {
140
2
        scan_each(tail(elems, 2), name)
141
    }
142
4
}
143

            
144
/// `(do ((v init step)…) (end res…) body…)`. Inits are outer scope; steps, the
145
/// end clause, and the body see the loop vars, so they're shadowed once any var
146
/// is `name`.
147
3
fn scan_do(elems: &[Expr], name: &str) -> Option<WasmType> {
148
3
    let mut shadowed = false;
149
3
    if let Some(Expr::List(vars)) = elems.get(1) {
150
        for var in vars {
151
            let Expr::List(parts) = var else { continue };
152
            if let Some(init) = parts.get(1)
153
                && let Some(ty) = pinned_param_type(init, name)
154
            {
155
                return Some(ty);
156
            }
157
            if matches!(parts.first(), Some(Expr::Symbol(s)) if s == name) {
158
                shadowed = true;
159
            }
160
        }
161
3
    }
162
3
    if shadowed {
163
        None
164
    } else {
165
3
        scan_each(tail(elems, 2), name)
166
    }
167
3
}
168

            
169
/// `(dolist (var list-expr [result]) body…)`. The list-expr is outer scope; the
170
/// result form and body see the loop var.
171
2
fn scan_dolist(elems: &[Expr], name: &str) -> Option<WasmType> {
172
2
    let Some(Expr::List(spec)) = elems.get(1) else {
173
1
        return scan_each(tail(elems, 1), name);
174
    };
175
1
    if let Some(list_expr) = spec.get(1)
176
1
        && let Some(ty) = pinned_param_type(list_expr, name)
177
    {
178
1
        return Some(ty);
179
    }
180
    let var_is_name = matches!(spec.first(), Some(Expr::Symbol(s)) if s == name);
181
    if var_is_name {
182
        None
183
    } else {
184
        scan_each(tail(elems, 2), name)
185
    }
186
2
}
187

            
188
1
fn lambda_binds(elems: &[Expr], name: &str) -> bool {
189
1
    matches!(elems.get(1), Some(Expr::List(params))
190
        if params.iter().any(|p| matches!(p, Expr::Symbol(s) if s == name)))
191
1
}
192

            
193
#[cfg(test)]
194
mod tests;