Lines
83.81 %
Functions
48.89 %
Branches
100 %
//! Closure parameter-type inference from body usage.
//!
//! A value-position `(lambda …)` lifted to a closure needs user-visible param
//! types. Defaulting every param to `Ratio` mis-types a list accumulator (a
//! param used as a `(cons CAR param)` cdr is a pair, not a scalar), so FOLD /
//! MAP over such a closure rejected it. This module infers each required
//! param's `WasmType` from how the body uses it; the closure's result type then
//! follows from compiling the body under those param types.
//! Today's single inferred shape: a param used as the CDR of `(cons CAR param)`
//! → `PairRef(<car cell element>)`. A param with no such use stays `Ratio`
//! (identical to the old default — no existing scalar closure regresses). The
//! scan is SCOPE-AWARE: a binder (`lambda`/`let`/`let*`/`do`/`do*`/`dolist`)
//! that rebinds the name shadows the param in its BODY, so a cons-cdr use of the
//! inner variable must not retype the outer param — but the binder's INIT
//! expressions (and a `dolist` list-expr) are in the OUTER scope and ARE
//! scanned.
use crate::ast::{Expr, LambdaParams, PairElement, WasmType};
/// Infers each required param's user-visible `WasmType` from `body`.
pub(super) fn infer_param_types(params: &LambdaParams, body: &Expr) -> Vec<WasmType> {
params
.required
.iter()
.map(|name| pinned_param_type(body, name).unwrap_or(WasmType::Ratio))
.collect()
}
/// The `WasmType` `name` is pinned to by a type-constraining use in `body`,
/// anywhere `name` still refers to the searched param (not inside a scope that
/// rebinds it). Two pins today:
/// - CDR of a `(cons CAR name)` → `PairRef(<car cell element>)` (list accumulator);
/// - argument to a string native (`string=`, `upcase-string`) → `StringRef`.
///
/// `None` if no pinning use is found (caller defaults to `Ratio`).
fn pinned_param_type(body: &Expr, name: &str) -> Option<WasmType> {
let Expr::List(elems) = body else {
return None;
};
if let [Expr::Symbol(head), car, Expr::Symbol(cdr)] = elems.as_slice()
&& head.eq_ignore_ascii_case("cons")
&& cdr == name
{
return Some(WasmType::PairRef(car_cell_element(car)));
if let Some(Expr::Symbol(head)) = elems.first()
&& is_string_native(head)
&& elems[1..]
.any(|a| matches!(a, Expr::Symbol(s) if s == name))
return Some(WasmType::StringRef);
match head_upper(elems).as_deref() {
// Quoted forms are data, not evaluated — a param mentioned inside must
// not pin its type. (The `Expr::Quote`/`Expr::Quasiquote` AST variants
// already return `None` above by not being a `List`; this guards the
// `(quote …)` / `(quasiquote …)` list spellings.)
Some("QUOTE") | Some("QUASIQUOTE") => None,
Some("LET") => scan_let(elems, name, false),
Some("LET*") => scan_let(elems, name, true),
Some("DO") | Some("DO*") => scan_do(elems, name),
Some("DOLIST") => scan_dolist(elems, name),
Some("LAMBDA") => {
// The body is shadowed iff `name` is one of the lambda's params; a
// param list carries no scannable outer-scope subform.
if lambda_binds(elems, name) {
None
} else {
scan_each(tail(elems, 1), name)
_ => scan_each(tail(elems, 1), name),
/// Natives that REQUIRE a `StringRef` argument, so a param passed to one is a
/// string. (Only these two exist today; extend alongside new string natives.)
fn is_string_native(head: &str) -> bool {
matches!(
head.to_ascii_uppercase().as_str(),
"STRING=" | "UPCASE-STRING"
)
/// The `PairElement` a car expression contributes. Literal cars pin their slot;
/// a sibling-param / captured / computed car has no statically-known cell type,
/// so it widens to `AnyRef` — keeping the cdr param's pair element consistent
/// with whatever `compile_cons_to_stack` actually produces for that car.
fn car_cell_element(car: &Expr) -> PairElement {
crate::compiler::native::literal_pair_element(car).unwrap_or(PairElement::AnyRef)
fn head_upper(elems: &[Expr]) -> Option<String> {
match elems.first() {
Some(Expr::Symbol(s)) => Some(s.to_ascii_uppercase()),
_ => None,
fn scan_each(forms: &[Expr], name: &str) -> Option<WasmType> {
forms.iter().find_map(|e| pinned_param_type(e, name))
/// `forms[from..]`, but never panics on a malformed/short form: a list with
/// fewer than `from` elements (e.g. a bare `()`, `(let)`, `(do)`) yields an
/// empty slice rather than an out-of-range slice index. Param-type inference is
/// a best-effort hint, so a too-short form simply contributes no pin; the
/// malformed form is rejected (or compiled) by the real path later.
fn tail(forms: &[Expr], from: usize) -> &[Expr] {
forms.get(from..).unwrap_or(&[])
/// `(let ((v init)…) body…)` / `(let* …)`. Binding INITs are in the outer scope
/// (for LET* up to the binding that introduces `name`); the body is shadowed
/// once any binding (re)binds `name`.
fn scan_let(elems: &[Expr], name: &str, sequential: bool) -> Option<WasmType> {
let mut shadowed = false;
if let Some(Expr::List(bindings)) = elems.get(1) {
for binding in bindings {
let Expr::List(parts) = binding else { continue };
let binds_here = matches!(parts.first(), Some(Expr::Symbol(s)) if s == name);
// For LET* a binding shadows `name` for SUBSEQUENT inits; for LET
// every init is in the outer scope regardless.
if !(sequential && shadowed)
&& let Some(init) = parts.get(1)
&& let Some(ty) = pinned_param_type(init, name)
return Some(ty);
if binds_here {
shadowed = true;
if shadowed {
scan_each(tail(elems, 2), name)
/// `(do ((v init step)…) (end res…) body…)`. Inits are outer scope; steps, the
/// end clause, and the body see the loop vars, so they're shadowed once any var
/// is `name`.
fn scan_do(elems: &[Expr], name: &str) -> Option<WasmType> {
if let Some(Expr::List(vars)) = elems.get(1) {
for var in vars {
let Expr::List(parts) = var else { continue };
if let Some(init) = parts.get(1)
if matches!(parts.first(), Some(Expr::Symbol(s)) if s == name) {
/// `(dolist (var list-expr [result]) body…)`. The list-expr is outer scope; the
/// result form and body see the loop var.
fn scan_dolist(elems: &[Expr], name: &str) -> Option<WasmType> {
let Some(Expr::List(spec)) = elems.get(1) else {
return scan_each(tail(elems, 1), name);
if let Some(list_expr) = spec.get(1)
&& let Some(ty) = pinned_param_type(list_expr, name)
let var_is_name = matches!(spec.first(), Some(Expr::Symbol(s)) if s == name);
if var_is_name {
fn lambda_binds(elems: &[Expr], name: &str) -> bool {
matches!(elems.get(1), Some(Expr::List(params))
if params.iter().any(|p| matches!(p, Expr::Symbol(s) if s == name)))
#[cfg(test)]
mod tests;