plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


plum-checker/src/monomorphize.rs
use std::collections::BTreeMap;
use plum_core::ast;
use crate::types::PlumType;

/// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
/// spelling for a generic type parameter — this is how we recognize one, since
/// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
/// generics declaration list.
pub fn isGenericParamName(name: &str) -> bool {
    let mut chars = name.chars();
    match (chars.next(), chars.next()) {
        (Some(c), None) => c.is_ascii_uppercase(),
        _ => false,
    }
}

/// The generic parameter names introduced by a `Class`, in declaration order.
pub fn classGenericParams(c: &ast::Class) -> Vec<String> {
    c.generics.iter().map(|g| g.name.clone()).collect()
}

/// The generic parameter names implicitly introduced by a `Fn` — every distinct
/// single-uppercase-letter type name appearing in its params or return type, in
/// first-appearance order.
pub fn fnGenericParams(f: &ast::Fn) -> Vec<String> {
    let mut names: Vec<String> = Vec::new();
    let mut consider = |n: &str| {
        if isGenericParamName(n) && !names.iter().any(|x| x == n) {
            names.push(n.to_string());
        }
    };
    for p in &f.params {
        match &p.ty {
            ast::ParamType::Type(t) => consider(&t.name),
            ast::ParamType::Variadic(t) => consider(&t.name),
            // TODO: closures/fn-value params don't yet participate in generic
            // parameter inference.
            ast::ParamType::Fn(_, _) => {}
        }
    }
    if let Some(r) = &f.returns {
        consider(&r.name);
    }
    names
}

/// The generic parameter names implicitly introduced by an `Enum` — every distinct
/// single-uppercase-letter variant field type name, in first-appearance order.
pub fn enumGenericParams(e: &ast::Enum) -> Vec<String> {
    let mut names: Vec<String> = Vec::new();
    for v in &e.variants {
        for field_ty in &v.fields {
            if isGenericParamName(field_ty) && !names.iter().any(|x| x == field_ty) {
                names.push(field_ty.clone());
            }
        }
    }
    names
}

/// A resolved binding from a generic item's parameter names to concrete types for
/// one instantiation site, e.g. `{"a": Int}` for `Box(value: 5)`.
#[derive(Debug, Clone)]
pub struct Substitution(pub BTreeMap<String, PlumType>);

impl Substitution {
    fn get(&self, name: &str) -> Option<&PlumType> {
        self.0.get(name)
    }
}

/// Converts a resolved concrete `PlumType` back into the `ast::Type` shape needed
/// to substitute into a declared field/param/return type position. Only ever
/// called with types resolved from a real call-site argument's inferred type, so
/// `TVar`/`TFun` (which never arise from a concrete argument) are an internal-error
/// case rather than something this needs to model.
fn plumTypeToAstType(t: &PlumType) -> ast::Type {
    let name = match t {
        PlumType::TInt => "Int".to_string(),
        PlumType::TFloat => "Float".to_string(),
        PlumType::TBool => "Bool".to_string(),
        PlumType::TStr => "Str".to_string(),
        PlumType::TByte => "Byte".to_string(),
        PlumType::TByteSlice => "[]Byte".to_string(),
        PlumType::TUnit => "Unit".to_string(),
        PlumType::TNamed(n) => n.clone(),
        PlumType::TVar(_) | PlumType::TFun(_, _) | PlumType::TVariadic(_) => t.to_string(),
    };
    ast::Type { name, generics: vec![] }
}

fn substituteType(ty: &ast::Type, subst: &Substitution) -> ast::Type {
    if ty.generics.is_empty() {
        if let Some(concrete) = subst.get(&ty.name) {
            return plumTypeToAstType(concrete);
        }
    }
    ast::Type {
        name: ty.name.clone(),
        generics: ty.generics.iter().map(|g| substituteType(g, subst)).collect(),
    }
}

/// Mangles a generic item's base name and its resolved concrete type arguments
/// (in the item's own generic-parameter declaration order) into the internal name
/// used for its specialized copy, e.g. `Box` + `[Int]` -> `"Box$Int"`.
pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
    let mut out = base.to_string();
    for t in type_args {
        out.push('$');
        out.push_str(&t.to_string());
    }
    out
}

/// Produces a concrete, specialized copy of a generic class under `mangled_name`,
/// substituting every field whose declared type names one of the class's generic
/// parameters with its resolved concrete type. The class's own `generics` list is
/// cleared on the copy (it is now fully concrete).
pub fn specializeClass(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class {
    ast::Class {
        name: mangled_name.to_string(),
        implements: c.implements.clone(),
        generics: vec![],
        fields: c.fields.iter().map(|f| ast::Field {
            name: f.name.clone(),
            ty: substituteType(&f.ty, subst),
        }).collect(),
    }
}

/// Produces a concrete, specialized copy of a generic function (or method) under
/// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
/// whose receiver class was itself specialized (e.g. a method declared on `Box`
/// becomes a method on `Box$Int`); pass the original `f.type_param.clone()`
/// unchanged for a plain free function. The body is left structurally identical
/// here — its own call sites are rewritten separately (Task 2), since expressions
/// don't carry declared-type annotations the way fields/params/return types do.
pub fn specializeFn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_type_param: Option<String>) -> ast::Fn {
    ast::Fn {
        name: mangled_name.to_string(),
        type_param: new_type_param,
        is_extern: f.is_extern,
        params: f.params.iter().map(|p| ast::Param {
            name: p.name.clone(),
            ty: match &p.ty {
                ast::ParamType::Type(t) => ast::ParamType::Type(substituteType(t, subst)),
                ast::ParamType::Variadic(t) => ast::ParamType::Variadic(substituteType(t, subst)),
                ast::ParamType::Fn(params, ret) => ast::ParamType::Fn(
                    params.iter().map(|t| substituteType(t, subst)).collect(),
                    ret.as_ref().map(|r| Box::new(substituteType(r, subst))),
                ),
            },
            default: p.default.clone(),
        }).collect(),
        returns: f.returns.as_ref().map(|r| substituteType(r, subst)),
        body: f.body.clone(),
    }
}

/// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
/// substituting every variant field type name that matches one of the enum's
/// generic parameters with its resolved concrete type's name.
///
/// Variant names are ALSO mangled here, with the same suffix as the enum's own
/// name (e.g. `Some` -> `Some$Int`) — even a payload-free variant like `None`.
/// This is necessary because the runtime `EnumVariants` table (built by
/// `buildGlobalTables`) is keyed by bare variant name globally: without this,
/// two specializations of the same generic enum would both register a variant
/// literally named `Some`, colliding in that flat table.
pub fn specializeEnum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
    let params = enumGenericParams(e);
    let type_args: Vec<PlumType> = params.iter().filter_map(|p| subst.get(p).cloned()).collect();
    ast::Enum {
        name: mangled_name.to_string(),
        params: e.params.clone(),
        variants: e.variants.iter().map(|v| ast::EnumVariant {
            name: mangle(&v.name, &type_args),
            fields: v.fields.iter().map(|f| {
                subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
            }).collect(),
            values: v.values.clone(),
        }).collect(),
    }
}

use std::collections::BTreeSet;
use crate::types::{TypeEnv, TypeScheme};
use crate::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo, EnumParams, CheckCtx};

enum PendingSpecialization<'a> {
    Class { base: &'a ast::Class, subst: Substitution, mangled: String },
    Fn { base: &'a ast::Fn, subst: Substitution, mangled: String, new_receiver: Option<String> },
    Enum { base: &'a ast::Enum, subst: Substitution, mangled: String },
}

/// True if `pat` binds `name` anywhere within it (a `Name` sub-pattern, at any
/// nesting depth inside a `Class` constructor pattern) — used by
/// `renameVarInStmt`'s `Match` case to recognize when a NESTED case pattern
/// re-shadows the name currently being renamed, in which case that nested
/// case's body refers to a different (shadowing) binding and must be left alone.
fn caseBindsName(pat: &ast::CasePattern, name: &str) -> bool {
    match pat {
        ast::CasePattern::Name(n) => n == name,
        ast::CasePattern::Class { fields, .. } => fields.iter().any(|f| caseBindsName(f, name)),
        _ => false,
    }
}

/// Renames every `Expr::Var(old)` to `Expr::Var(new)` within `block`, used by
/// `Monomorphizer::dedupLocalName` to rename a match-case/for-loop binding
/// (plus every reference to it) once its usage is known to be confined to that
/// one block — see `local_types_by_name`'s doc comment for why this is needed
/// at all. Purely syntactic (no type information needed): stops descending into
/// any NESTED scope that re-binds `old` itself (a nested `for` over the same
/// name, or a nested `match` case whose pattern binds it again), since that
/// inner scope's occurrences of `old` are a different, shadowing variable, not
/// the one being renamed.
fn renameVarInBlock(block: &mut ast::Block, old: &str, new: &str) {
    for stmt in &mut block.stmts {
        renameVarInStmt(stmt, old, new);
    }
}

fn renameVarInStmt(stmt: &mut ast::Stmt, old: &str, new: &str) {
    match stmt {
        ast::Stmt::Assign(a) => {
            for v in &mut a.values {
                renameVarInExpr(v, old, new);
            }
            for t in &mut a.targets {
                match t {
                    ast::AssignTarget::Var(n) => {
                        if n == old {
                            *n = new.to_string();
                        }
                    }
                    ast::AssignTarget::Field(obj, _) => renameVarInExpr(obj, old, new),
                }
            }
        }
        ast::Stmt::Return(Some(e)) => renameVarInExpr(e, old, new),
        ast::Stmt::Return(None) | ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
        ast::Stmt::Assert(e) => renameVarInExpr(e, old, new),
        ast::Stmt::Expr(e) => renameVarInExpr(e, old, new),
        ast::Stmt::If(if_) => {
            renameVarInExpr(&mut if_.condition, old, new);
            renameVarInBlock(&mut if_.body, old, new);
            for ei in &mut if_.else_ifs {
                renameVarInExpr(&mut ei.condition, old, new);
                renameVarInBlock(&mut ei.body, old, new);
            }
            if let Some(else_block) = &mut if_.else_ {
                renameVarInBlock(else_block, old, new);
            }
        }
        ast::Stmt::While(w) => {
            renameVarInExpr(&mut w.condition, old, new);
            renameVarInBlock(&mut w.body, old, new);
        }
        ast::Stmt::For(f) => {
            renameVarInExpr(&mut f.iter, old, new);
            if !f.vars.iter().any(|v| v == old) {
                renameVarInBlock(&mut f.body, old, new);
            }
        }
        ast::Stmt::Match(m) => {
            for s in &mut m.subjects {
                renameVarInExpr(s, old, new);
            }
            for case in &mut m.cases {
                if !case.patterns.iter().any(|p| caseBindsName(p, old)) {
                    renameVarInBlock(&mut case.body, old, new);
                }
            }
        }
    }
}

fn renameVarInExpr(expr: &mut ast::Expr, old: &str, new: &str) {
    match expr {
        ast::Expr::Var(n) => {
            if n == old {
                *n = new.to_string();
            }
        }
        ast::Expr::ClassCall(call) => {
            for fa in &mut call.fields {
                renameVarInExpr(&mut fa.value, old, new);
            }
        }
        ast::Expr::FnCall(call) => {
            for arg in &mut call.args {
                renameVarInArg(arg, old, new);
            }
        }
        ast::Expr::Attribute(attr) => {
            renameVarInExpr(&mut attr.object, old, new);
            if let ast::AttrKind::Method(call) = &mut attr.attr {
                for arg in &mut call.args {
                    renameVarInArg(arg, old, new);
                }
            }
        }
        ast::Expr::Binary(b) => { renameVarInExpr(&mut b.left, old, new); renameVarInExpr(&mut b.right, old, new); }
        ast::Expr::Bool(b) => { renameVarInExpr(&mut b.left, old, new); renameVarInExpr(&mut b.right, old, new); }
        ast::Expr::Compare(c) => { renameVarInExpr(&mut c.left, old, new); renameVarInExpr(&mut c.right, old, new); }
        ast::Expr::Not(inner) => renameVarInExpr(inner, old, new),
        ast::Expr::Unary(u) => renameVarInExpr(&mut u.operand, old, new),
        ast::Expr::Paren(inner) => renameVarInExpr(inner, old, new),
        ast::Expr::Ternary(t) => {
            renameVarInExpr(&mut t.condition, old, new);
            renameVarInExpr(&mut t.then, old, new);
            renameVarInExpr(&mut t.else_, old, new);
        }
        ast::Expr::String(s) => {
            for part in &mut s.parts {
                if let ast::StringPart::Interp(e) = part {
                    renameVarInExpr(e, old, new);
                }
            }
        }
        ast::Expr::Int(_) | ast::Expr::Float(_) | ast::Expr::Self_ | ast::Expr::TypeName(_) => {}
        // Not recursed into — matches `rewriteExpr`'s identical `Closure` case
        // (closure bodies are compiled/free-variable-captured separately and
        // aren't otherwise touched by this pass either). A closure capturing a
        // variable that gets renamed here is a known, narrow residual gap.
        ast::Expr::Closure(_) => {}
    }
}

fn renameVarInArg(arg: &mut ast::Arg, old: &str, new: &str) {
    match arg {
        ast::Arg::Positional(e) => renameVarInExpr(e, old, new),
        ast::Arg::Keyword { value, .. } => renameVarInExpr(value, old, new),
        ast::Arg::Pair { value, .. } => renameVarInExpr(value, old, new),
    }
}

struct Monomorphizer<'a> {
    classes_generic: BTreeMap<String, &'a ast::Class>,
    fns_generic: BTreeMap<String, &'a ast::Fn>,
    methods_generic_on: BTreeMap<String, Vec<&'a ast::Fn>>,
    /// Same as `methods_generic_on`, but for a method declared on a generic ENUM
    /// (e.g. `Result`'s `isOk`/`isErr`) rather than a generic class — a separate map
    /// because the two need separate lookups keyed by their own base-name maps
    /// (`enums_generic_by_name` vs `classes_generic`) at both classification and
    /// specialization time.
    methods_generic_on_enum: BTreeMap<String, Vec<&'a ast::Fn>>,
    /// Bare variant name (e.g. `"Some"`) -> the generic `Enum` it belongs to. Keyed
    /// by variant name because a construction site (`Some(5)`) parses as a `FnCall`
    /// whose `name` is the VARIANT, not the enum's own name.
    enums_generic_by_variant: BTreeMap<String, &'a ast::Enum>,
    /// Mangled enum name -> {original variant name -> mangled variant name}, e.g.
    /// `"Option$Int" -> {"Some": "Some$Int", "None": "None$Int"}`. Populated eagerly
    /// (in `resolveEnumInstantiation`, at the moment an instantiation's concrete
    /// type arguments become known) rather than waiting for the worklist to actually
    /// produce that specialization — so both a construction call site and a later
    /// `match` on the same specialization can rewrite variant names consistently,
    /// regardless of processing order.
    enum_variant_mangling: BTreeMap<String, BTreeMap<String, String>>,
    /// The enum's own bare name -> the generic `Enum` — used to detect a bare
    /// generic-enum-typed function param (e.g. `o: Option`), distinct from
    /// `enums_generic_by_variant` (keyed by VARIANT name, used for construction
    /// sites like `Some(5)`).
    enums_generic_by_name: BTreeMap<String, &'a ast::Enum>,
    /// Free functions that are NOT generic by `fnGenericParams`'s lowercase-letter
    /// convention, but whose param type(s) bare-name a generic class or enum (e.g.
    /// `unwrapOr(o: Option, ...)`) — such a function still needs its own
    /// per-call-site specialization, since its receiver generic class/enum is
    /// dropped from the monomorphized output and the bare name would otherwise
    /// resolve to nothing.
    fns_bare_generic: BTreeMap<String, &'a ast::Fn>,
    global_env: TypeEnv,
    classes: ClassEnv,
    methods: MethodEnv,
    enum_variants: EnumVariants,
    enum_params: EnumParams,
    specialized: BTreeSet<String>,
    enqueued: BTreeSet<String>,
    worklist: Vec<PendingSpecialization<'a>>,
    produced: Vec<ast::Item>,
    /// The declared return type of the function/method currently being rewritten
    /// by `rewriteFnBody` — consulted by `resolveEnumInstantiation` as a fallback
    /// when a single variant construction site (e.g. `Ok(5)`) can't pin down every
    /// one of the enum's generic params by itself (see its doc comment). Reset at
    /// the top of every `rewriteFnBody` call; never needs saving/restoring since
    /// closures aren't recursed into by this pass (see `rewriteExpr`'s `Closure` arm).
    current_return_type: Option<ast::Type>,
    /// (receiver name or `None` for a free function, function/method name) ->
    /// (the generic enum its declared return type names, the concrete type args
    /// it names them with) — for every function/method whose OWN declared return
    /// type is a fully-general instantiation of a known generic enum (e.g.
    /// `-> Result[Int, Str]`). Populated once up front (in `monomorphizeSource`,
    /// alongside `enums_generic_by_name`) from the ORIGINAL, unmodified signatures
    /// — unlike everywhere else in this file, this doesn't need the specialization
    /// to have actually run yet, since the declared signature already says
    /// everything needed. Consulted by `resolveCallReturnType`.
    fn_return_generic_enum: BTreeMap<(Option<String>, String), (&'a ast::Enum, Vec<PlumType>)>,
    /// Every local name's type as first seen in the function currently being
    /// rewritten (reset per `rewriteFnBody` call, like `current_return_type`).
    /// Wasm local slots are allocated once per NAME for the whole function (see
    /// `plum-wasm-codegen`'s `Collector`/`compileFnBody`), not per lexical scope —
    /// so two unrelated bindings that happen to share a name (e.g. `Ok(v)` in two
    /// separate, non-overlapping `match` statements) would silently collide on
    /// one slot if their types ever differ. `dedupLocalName` consults this to
    /// catch that and rename the second, conflicting binding instead.
    local_types_by_name: BTreeMap<String, PlumType>,
    /// Bumped each time `dedupLocalName` needs a fresh name; part of the fresh
    /// name itself, so collisions between two different renames are impossible.
    rename_counter: usize,
}

impl<'a> Monomorphizer<'a> {
    /// Like `infer`, but tries `resolveCallReturnType` first — needed anywhere the
    /// resulting `PlumType` will be used to look up `enum_variant_mangling` (i.e.
    /// wherever a value might need its constructor-pattern names rewritten later:
    /// an assignment's recorded local type, or a `match` subject's type).
    fn inferConcrete(&mut self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
        self.resolveCallReturnType(e, env).unwrap_or_else(|| self.infer(e, env))
    }

    fn infer(&self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
        let ctx = CheckCtx { classes: &self.classes, methods: &self.methods, enum_variants: &self.enum_variants, enum_params: &self.enum_params };
        crate::inferExpr(e, env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
    }

    /// Records `name`'s first-seen type in `local_types_by_name` without
    /// renaming anything — for a binding whose usage isn't cleanly bounded to a
    /// single `&mut ast::Block` this pass has in hand at the binding site (e.g.
    /// a plain `Assign` — its "scope" is however much of the flat function body
    /// follows it, not a nested block). This still lets a LATER, cleanly-bounded
    /// binding (`dedupLocalName`, from a `match` arm or `for` loop) detect a
    /// conflict against it and rename itself accordingly; it just means a
    /// conflict in the other direction (an `Assign` conflicting with an
    /// EARLIER match-bound name) isn't caught. Real but narrower residual gap —
    /// see the `local_types_by_name` doc comment.
    fn seedLocalType(&mut self, name: &str, ty: &PlumType) {
        self.local_types_by_name.entry(name.to_string()).or_insert_with(|| ty.clone());
    }

    /// Ensures `name` can be bound to `ty` here without colliding with a
    /// DIFFERENT type already recorded for that same name elsewhere in the
    /// current function (see `local_types_by_name`'s doc comment for why that's
    /// otherwise unsafe). If there's no conflict, returns `name` unchanged. If
    /// there IS one, mints a fresh name, renames every reference to `name`
    /// within `scope` (a match case body / for-loop body — the full extent this
    /// particular binding's usage can ever reach) to that fresh name, and
    /// returns it for the caller to use as the actual binding name instead.
    fn dedupLocalName(&mut self, name: &str, ty: &PlumType, scope: &mut ast::Block) -> String {
        match self.local_types_by_name.get(name) {
            None => {
                self.local_types_by_name.insert(name.to_string(), ty.clone());
                name.to_string()
            }
            Some(existing) if existing == ty => name.to_string(),
            Some(_) => {
                self.rename_counter += 1;
                let fresh = format!("{}$dup{}", name, self.rename_counter);
                self.local_types_by_name.insert(fresh.clone(), ty.clone());
                renameVarInBlock(scope, name, &fresh);
                fresh
            }
        }
    }

    /// Rewrites a function/method body's generic call sites. When `resolve_return`
    /// is set (for freshly-generated specializations, whose declared return may be
    /// a generic parameter like `a` — which the current grammar can't even parse,
    /// leaving `returns: None` — or a generic class), the declared return type is
    /// re-derived from the concrete inferred type of the body's tail expression.
    /// For an ordinary (non-generic) top-level function we only rewrite the return
    /// annotation when it names a generic class used bare (e.g. `-> Box`), which
    /// the body's construction site has just been specialized to a mangled name.
    fn rewriteFnBody(&mut self, f: &mut ast::Fn, resolve_return: bool) -> Result<(), String> {
        self.current_return_type = f.returns.clone();
        self.local_types_by_name.clear();
        let mut env = self.global_env.clone();
        if let Some(recv) = &f.type_param {
            env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
        }
        for p in &f.params {
            let ty = match &p.ty {
                ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
                ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
                ast::ParamType::Fn(params, ret) => {
                    let param_types = params.iter().map(crate::plumTypeFromAst).collect();
                    let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
                    PlumType::TFun(param_types, Box::new(ret_ty))
                }
            };
            self.local_types_by_name.insert(p.name.clone(), ty.clone());
            env.insert(p.name.clone(), TypeScheme::mono(ty));
        }
        // The function's own return type, resolved for THIS specialization — used
        // below to resolve a bare payload-free variant (`None`) that's the
        // BODY'S TAIL EXPRESSION (implicit return), the same way `Stmt::Return`
        // already does. A tail `Stmt::Expr` never goes through `Stmt::Return`'s
        // own handling, so without this a trailing bare `None` is left
        // unmangled and later fails the real checker's return-type unification.
        let expected_ret = self.current_return_type.clone()
            .map(|rt| self.resolveFieldType(&rt))
            .map(|rt| crate::plumTypeFromAst(&rt));
        let tail: Option<PlumType> = match &mut f.body {
            ast::FnBody::Expr(e) => {
                if let Some(expected) = &expected_ret {
                    self.resolveBareVariantAgainstExpected(e, expected);
                }
                self.rewriteExpr(e, &env)?;
                Some(self.infer(e, &env))
            }
            ast::FnBody::Block(block) => {
                if let Some(expected) = &expected_ret {
                    if let Some(ast::Stmt::Expr(e)) = block.stmts.last_mut() {
                        self.resolveBareVariantAgainstExpected(e, expected);
                    }
                }
                self.rewriteBlock(block, &mut env)?;
                match block.stmts.last() {
                    Some(ast::Stmt::Expr(e)) => Some(self.infer(e, &env)),
                    Some(ast::Stmt::Return(Some(e))) => Some(self.infer(e, &env)),
                    _ => None,
                }
            }
            // No body to rewrite or infer a tail type from.
            ast::FnBody::Extern => None,
        };
        if let Some(t) = tail {
            self.maybeRewriteReturn(f, &t, resolve_return);
        }
        Ok(())
    }

    /// Overwrites `f.returns` with a concrete type derived from the body's tail
    /// type `t` when the currently-declared return type is genuinely generic or
    /// unresolved. Never clobbers a real, concrete declared return type — even for
    /// a specialization (`resolve_return: true`) — so a generic function whose body
    /// is internally inconsistent with its concrete declared return (e.g.
    /// `wrong(x: a) -> Int = "hello"`) is left for the checker's normal
    /// return-type-mismatch logic to REJECT rather than silently rewritten (and
    /// thereby masked). The overwrite fires only when:
    ///   - `f.returns` is `None` — the unparseable `-> a` generic-parameter-return
    ///     case, where the grammar dropped the annotation entirely (this only ever
    ///     happens for a specialization, which is the only path that can supply a
    ///     concrete tail type to fill it in); or
    ///   - the declared return names something still-generic: a generic-parameter
    ///     letter (e.g. `-> a`) or a generic class used bare (e.g. `-> Box`).
    /// For the `Some(rt)` arm this condition is identical whether `resolve_return`
    /// is `true` or `false`; the specialization path differs only in that its tail
    /// is inferred against a resolved substitution, so a generic-parameter-letter
    /// return resolves to the specialization's concrete bound type (which the
    /// ordinary path cannot do). The unparseable-`None` fill-in is gated on
    /// `resolve_return` so an ordinary void function (`returns: None` meaning "no
    /// declared return", not "a generic return the grammar dropped") is never given
    /// a fabricated return type. Never fabricates a return from an un-inferrable
    /// (`TVar`) tail.
    fn maybeRewriteReturn(&self, f: &mut ast::Fn, t: &PlumType, resolve_return: bool) {
        if matches!(t, PlumType::TVar(_) | PlumType::TFun(_, _)) {
            return;
        }
        let needs = match &f.returns {
            None => resolve_return,
            Some(rt) => {
                isGenericParamName(&rt.name)
                    || self.classes_generic.contains_key(&rt.name)
                    || self.enums_generic_by_name.contains_key(&rt.name)
            }
        };
        if needs {
            f.returns = Some(ast::Type { name: t.to_string(), generics: vec![] });
        }
    }

    fn rewriteBlock(&mut self, block: &mut ast::Block, env: &mut TypeEnv) -> Result<(), String> {
        for stmt in &mut block.stmts {
            self.rewriteStmt(stmt, env)?;
        }
        Ok(())
    }

    /// Rewrites `pat` (bare-name/constructor pattern) in place: an uppercase variant
    /// name gets mangled to its specialized form (`Some` -> `Some$Int`) if `mangling`
    /// says this position's subject is a specialized generic enum; a plain binding
    /// name is inserted into `case_env` at `ty`. Recurses into a constructor
    /// pattern's own fields (`Some(Some(v))`), looking up *that* field's own
    /// mangling table from `self.enum_variant_mangling` — a nested sub-pattern can
    /// be a specialization independent of its enclosing pattern's.
    fn manglePattern(
        &mut self,
        pat: &mut ast::CasePattern,
        ty: &PlumType,
        mangling: Option<&BTreeMap<String, String>>,
        case_env: &mut TypeEnv,
        body: &mut ast::Block,
    ) {
        match pat {
            ast::CasePattern::Name(n) => {
                let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
                    && self.enum_variants.contains_key(n.as_str());
                if is_variant {
                    if let Some(table) = mangling {
                        if let Some(mangled_variant) = table.get(n) {
                            *n = mangled_variant.clone();
                        }
                    }
                } else {
                    let bound = self.dedupLocalName(n, ty, body);
                    if bound != *n {
                        *n = bound.clone();
                    }
                    case_env.insert(bound, TypeScheme::mono(ty.clone()));
                }
            }
            ast::CasePattern::Class { name, fields } => {
                if let Some(table) = mangling {
                    if let Some(mangled_variant) = table.get(name) {
                        *name = mangled_variant.clone();
                    }
                }
                if let Some(info) = self.enum_variants.get(name.as_str()) {
                    let field_types = info.field_types.clone();
                    for (f, fty) in fields.iter_mut().zip(field_types.iter()) {
                        let field_mangling: Option<BTreeMap<String, String>> = match fty {
                            PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
                            _ => None,
                        };
                        self.manglePattern(f, fty, field_mangling.as_ref(), case_env, body);
                    }
                }
            }
            _ => {}
        }
    }

    fn rewriteStmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
        match stmt {
            ast::Stmt::Assign(a) => {
                for (target, value) in a.targets.iter_mut().zip(a.values.iter_mut()) {
                    // For a field target (`self.head = None`), resolve the
                    // value against the field's OWN declared type BEFORE the
                    // generic rewrite/inference below — same reasoning as
                    // `ClassCall`'s field values: a bare payload-free variant
                    // has no type of its own, but the field it's being
                    // written into does.
                    if let ast::AssignTarget::Field(object, field_name) = target {
                        if let PlumType::TNamed(class_name) = self.infer(object, env) {
                            if let Some(field_ty) = self.classes.get(&class_name)
                                .and_then(|fields| fields.iter().find(|(n, _)| n == field_name).map(|(_, t)| t.clone()))
                            {
                                self.resolveBareVariantAgainstExpected(value, &field_ty);
                            }
                        }
                    }
                    self.rewriteExpr(value, env)?;
                    let ty = self.inferConcrete(value, env);
                    match target {
                        ast::AssignTarget::Var(name) => {
                            self.seedLocalType(name, &ty);
                            env.insert(name.clone(), TypeScheme::mono(ty));
                        }
                        ast::AssignTarget::Field(object, _) => {
                            self.rewriteExpr(object, env)?;
                        }
                    }
                }
            }
            ast::Stmt::Return(Some(e)) => {
                // Same idea as the field-target case above, but against the
                // enclosing function's own declared return type (`return
                // None` inside a method returning `Option[Int]`).
                if let Some(rt) = self.current_return_type.clone() {
                    let resolved = self.resolveFieldType(&rt);
                    let expected = crate::plumTypeFromAst(&resolved);
                    self.resolveBareVariantAgainstExpected(e, &expected);
                }
                self.rewriteExpr(e, env)?;
            }
            ast::Stmt::Return(None) => {}
            ast::Stmt::If(if_) => {
                self.rewriteExpr(&mut if_.condition, env)?;
                self.rewriteBlock(&mut if_.body, &mut env.clone())?;
                for ei in &mut if_.else_ifs {
                    self.rewriteExpr(&mut ei.condition, env)?;
                    self.rewriteBlock(&mut ei.body, &mut env.clone())?;
                }
                if let Some(else_block) = &mut if_.else_ {
                    self.rewriteBlock(else_block, &mut env.clone())?;
                }
            }
            ast::Stmt::While(w) => {
                self.rewriteExpr(&mut w.condition, env)?;
                self.rewriteBlock(&mut w.body, &mut env.clone())?;
            }
            ast::Stmt::For(f) => {
                self.rewriteExpr(&mut f.iter, env)?;
                let mut inner = env.clone();
                let ast::For { vars, body, .. } = f;
                for v in vars.iter_mut() {
                    let bound = self.dedupLocalName(v, &PlumType::TInt, body);
                    inner.insert(bound.clone(), TypeScheme::mono(PlumType::TInt));
                    *v = bound;
                }
                self.rewriteBlock(body, &mut inner)?;
            }
            ast::Stmt::Expr(e) => self.rewriteExpr(e, env)?,
            ast::Stmt::Assert(e) => self.rewriteExpr(e, env)?,
            ast::Stmt::Match(m) => {
                for s in &mut m.subjects {
                    self.rewriteExpr(s, env)?;
                }
                // One (type, variant-mangling table) pair per subject — `match a, b`
                // needs each position's own generic-enum specialization handled
                // independently, not just the first subject's.
                let subject_types: Vec<PlumType> = m.subjects.iter().map(|s| self.inferConcrete(s, env)).collect();
                // If a subject's concrete type is a specialized generic enum, its
                // variant-name mangling table lets us rewrite that position's patterns
                // (`Some`/`None` -> `Some$Int`/`None$Int`) to reference the correct
                // specialization, so the checker/codegen's unmodified, bare-name-keyed
                // `EnumVariants` lookup still resolves each pattern correctly.
                let variant_manglings: Vec<Option<BTreeMap<String, String>>> = subject_types
                    .iter()
                    .map(|ty| match ty {
                        PlumType::TNamed(n) => self.enum_variant_mangling.get(n).cloned(),
                        _ => None,
                    })
                    .collect();
                for case in &mut m.cases {
                    let mut case_env = env.clone();
                    let ast::Case { patterns, body } = case;
                    for (pat, (subject_ty, variant_mangling)) in
                        patterns.iter_mut().zip(subject_types.iter().zip(variant_manglings.iter()))
                    {
                        self.manglePattern(pat, subject_ty, variant_mangling.as_ref(), &mut case_env, body);
                    }
                    // A case body's own LAST statement, when it's a bare
                    // `Stmt::Expr`, is itself an implicit-return position
                    // whenever this whole `match` is the function's tail
                    // expression (`first`/`last`'s `None =>` arm ending in a
                    // bare `None`, for instance) — same reasoning as
                    // `rewriteFnBody`'s own tail-expression handling, just one
                    // level down through the match. Resolving against the
                    // function's OWN declared return type is a no-op unless
                    // the expression is actually a bare payload-free variant
                    // of that same enum family, so this is harmless even for
                    // a match that ISN'T in tail position.
                    if let Some(rt) = self.current_return_type.clone() {
                        let resolved = self.resolveFieldType(&rt);
                        let expected = crate::plumTypeFromAst(&resolved);
                        if let Some(ast::Stmt::Expr(e)) = body.stmts.last_mut() {
                            self.resolveBareVariantAgainstExpected(e, &expected);
                        }
                    }
                    self.rewriteBlock(body, &mut case_env)?;
                }
            }
            ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
        }
        Ok(())
    }

    fn resolveClassInstantiation(&mut self, call: &mut ast::ClassCall, env: &TypeEnv) -> Result<(), String> {
        let Some(class) = self.classes_generic.get(call.type_name.as_str()).copied() else { return Ok(()) };
        let params = classGenericParams(class);
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
        for gp in &params {
            if let Some(field) = class.fields.iter().find(|f| f.ty.name == *gp) {
                if let Some(fa) = call.fields.iter().find(|fa| fa.name == field.name) {
                    // A bare payload-free variant reference (`None`) carries
                    // no type of its own to bind a generic param FROM — skip
                    // it here; it gets resolved AGAINST the binding (once
                    // known) below instead, same as every other field.
                    let is_bare_variant = matches!(&fa.value, ast::Expr::TypeName(n) if self.enums_generic_by_variant.contains_key(n));
                    if !is_bare_variant {
                        bindings.insert(gp.clone(), self.infer(&fa.value, env));
                    }
                }
            }
        }
        // No field is EVER directly typed as a bare generic param for a class
        // like `List[T]` (its fields are `Option[Node[T]]`/`Int`, never a bare
        // `T`) — and even where one exists, constructing with a payload-free
        // value (`List(head: None, ...)`) gives no VALUE to infer a type from
        // regardless. Fall back to an explicit `List[Int](...)` annotation at
        // the call site when field-value inference alone isn't enough.
        if bindings.len() != params.len() && call.generics.len() == params.len() {
            for (p, gt) in params.iter().zip(call.generics.iter()) {
                bindings.entry(p.clone()).or_insert_with(|| crate::plumTypeFromAst(gt));
            }
        }
        if bindings.len() != params.len() {
            return Err(format!(
                "monomorphize: could not resolve all generic parameters for '{}' at this call site — pass them explicitly, e.g. '{}[Int](...)'",
                call.type_name, call.type_name
            ));
        }
        let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
        let mangled = mangle(&call.type_name, &type_args);

        // Now that every generic param is bound, resolve any bare
        // payload-free-variant field values (`None`) against THIS class's own
        // (about-to-be-specialized) field types — codegen only ever sees the
        // mangled specializations (the generic template is dropped), so a
        // still-bare `None` would be an unresolvable reference by the time it
        // gets there. `specializeClass` is a pure function; calling it here
        // ahead of the worklist actually processing this specialization is
        // fine — the worklist dedups on `mangled` regardless of how many
        // times it's computed.
        let spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
        for fa in &mut call.fields {
            if let Some(field) = spec_class.fields.iter().find(|f| f.name == fa.name) {
                let field_ty = self.resolveFieldType(&field.ty);
                let expected = crate::plumTypeFromAst(&field_ty);
                self.resolveBareVariantAgainstExpected(&mut fa.value, &expected);
            }
        }

        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
            self.enqueued.insert(mangled.clone());
            self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
        }
        // Register this specialization's own field types and its methods'
        // signatures right now — a construction site like this one can appear
        // inside an ORDINARY (non-generic) function, which gets rewritten in
        // the pass BEFORE the worklist above ever runs. Any later statement in
        // that SAME function body (e.g. `l.get(1)` followed by a `match` on
        // its result) needs `self.methods`/`self.classes` to already know
        // about "List$Int" right now, not once the worklist eventually
        // catches up.
        if !self.classes.contains_key(&mangled) {
            let field_types: Vec<(String, PlumType)> = spec_class.fields.iter()
                .map(|f| (f.name.clone(), crate::plumTypeFromAst(&self.resolveFieldType(&f.ty))))
                .collect();
            self.classes.insert(mangled.clone(), field_types);
        }
        self.registerClassMethodSignatures(class, &mangled, &bindings);
        call.type_name = mangled;
        Ok(())
    }

    /// Eagerly computes and registers (into `self.methods`) the `(mangled,
    /// method_name) -> TFun` signature of every method declared on `class`,
    /// for the specialization named `mangled` under `bindings` — without
    /// producing the actual `ast::Fn` items (that still only happens once the
    /// worklist entry for this specialization is popped, avoiding duplicate
    /// emission). Needed so a call site that appears in a function processed
    /// BEFORE the worklist runs (see callers) can still resolve a method call
    /// against this specialization immediately.
    fn registerClassMethodSignatures(&mut self, class: &'a ast::Class, mangled: &str, bindings: &BTreeMap<String, PlumType>) {
        let Some(methods) = self.methods_generic_on.get(class.name.as_str()).cloned() else { return };
        for method in methods {
            let key = (mangled.to_string(), method.name.clone());
            if self.methods.contains_key(&key) {
                continue;
            }
            let mut specialized_method = specializeFn(method, &Substitution(bindings.clone()), &method.name, Some(mangled.to_string()));
            self.resolveFnSignature(&mut specialized_method);
            let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
                ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
                ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
                ast::ParamType::Fn(params, ret) => {
                    let param_types = params.iter().map(crate::plumTypeFromAst).collect();
                    let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
                    PlumType::TFun(param_types, Box::new(ret_ty))
                }
            }).collect();
            let ret = specialized_method.returns.as_ref()
                .map(crate::plumTypeFromAst)
                .unwrap_or(PlumType::TUnit);
            self.methods.insert(key, PlumType::TFun(param_types, Box::new(ret)));
        }
    }

    /// If `expr` is a bare reference to a payload-free variant of a GENERIC
    /// enum (`None`), and `expected` names a SPECIFIC specialization of that
    /// same enum (`Option$Node$Int`), rewrites `expr`'s name to that
    /// specialization's own mangled variant name (`None$Node$Int`) and
    /// ensures that specialization is registered — codegen only ever knows
    /// about specializations (the generic template enum is dropped entirely),
    /// so an un-rewritten bare reference would be unresolvable by the time it
    /// gets there. No-op if `expr` isn't a bare generic-enum variant, or
    /// `expected` doesn't name a specialization of the SAME enum.
    fn resolveBareVariantAgainstExpected(&mut self, expr: &mut ast::Expr, expected: &PlumType) {
        let ast::Expr::TypeName(n) = expr else { return };
        let Some(e) = self.enums_generic_by_variant.get(n.as_str()).copied() else { return };
        let PlumType::TNamed(mangled) = expected else { return };
        if !mangled.starts_with(&format!("{}$", e.name)) {
            return;
        }
        if let Some(table) = self.enum_variant_mangling.get(mangled) {
            if let Some(mangled_variant) = table.get(n) {
                *n = mangled_variant.clone();
            }
        }
    }

    fn resolveFnInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
        let Some(f) = self.fns_generic.get(call.name.as_str()).copied() else { return Ok(()) };
        let params = fnGenericParams(f);
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
        for (param, arg) in f.params.iter().zip(call.args.iter()) {
            let gp = match &param.ty {
                ast::ParamType::Type(t) => t.name.clone(),
                ast::ParamType::Variadic(t) => t.name.clone(),
                // TODO: fn-value params don't yet resolve to a generic parameter.
                ast::ParamType::Fn(_, _) => String::new(),
            };
            if params.contains(&gp) {
                let arg_expr = match arg {
                    ast::Arg::Positional(e) => e,
                    ast::Arg::Keyword { value, .. } => value,
                    ast::Arg::Pair { value, .. } => value,
                };
                bindings.entry(gp).or_insert_with(|| self.infer(arg_expr, env));
            }
        }
        if bindings.len() != params.len() {
            return Err(format!(
                "monomorphize: could not resolve all generic parameters for '{}' at this call site",
                call.name
            ));
        }
        let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
        let mangled = mangle(&call.name, &type_args);
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
            self.enqueued.insert(mangled.clone());
            self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
        }
        call.name = mangled;
        Ok(())
    }

    /// The bare names of any generic class or enum referenced directly (not via a
    /// lowercase-letter generic parameter) in `f`'s param types — e.g. `"Option"` for
    /// `unwrapOr(o: Option, default: Int) -> Int`. See `fns_bare_generic`'s doc
    /// comment for why such a function needs its own specialization.
    fn fnBareGenericRefs(&self, f: &ast::Fn) -> Vec<String> {
        let mut names: Vec<String> = Vec::new();
        for p in &f.params {
            let n = match &p.ty {
                ast::ParamType::Type(t) => &t.name,
                ast::ParamType::Variadic(t) => &t.name,
                // TODO: fn-value params don't yet participate in bare-generic resolution.
                ast::ParamType::Fn(_, _) => continue,
            };
            if (self.classes_generic.contains_key(n.as_str()) || self.enums_generic_by_name.contains_key(n.as_str()))
                && !names.iter().any(|x| x == n)
            {
                names.push(n.clone());
            }
        }
        names
    }

    /// Resolves a call to an otherwise-ordinary function whose param type(s)
    /// bare-name a generic class/enum, specializing it per call site exactly like a
    /// truly-generic function — reusing the same `PendingSpecialization::Fn`
    /// worklist entry and the unmodified `specializeFn`, whose substitution
    /// mechanism already replaces any type whose bare name matches a substitution
    /// key (it doesn't care whether that key came from a lowercase-letter generic
    /// parameter or a bare generic class/enum reference).
    fn resolveBareGenericFnInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
        let Some(f) = self.fns_bare_generic.get(call.name.as_str()).copied() else { return Ok(()) };
        let refs = self.fnBareGenericRefs(f);
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
        for (param, arg) in f.params.iter().zip(call.args.iter()) {
            let n = match &param.ty {
                ast::ParamType::Type(t) => t.name.clone(),
                ast::ParamType::Variadic(t) => t.name.clone(),
                // TODO: fn-value params don't yet resolve to a bare generic reference.
                ast::ParamType::Fn(_, _) => String::new(),
            };
            if refs.contains(&n) {
                let arg_expr = match arg {
                    ast::Arg::Positional(e) => e,
                    ast::Arg::Keyword { value, .. } => value,
                    ast::Arg::Pair { value, .. } => value,
                };
                bindings.entry(n).or_insert_with(|| self.infer(arg_expr, env));
            }
        }
        if bindings.len() != refs.len() {
            return Err(format!(
                "monomorphize: could not resolve all generic parameters for '{}' at this call site",
                call.name
            ));
        }
        let type_args: Vec<PlumType> = refs.iter().map(|p| bindings[p].clone()).collect();
        let mangled = mangle(&call.name, &type_args);
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
            self.enqueued.insert(mangled.clone());
            self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
        }
        call.name = mangled;
        Ok(())
    }

    /// Resolves a construction of a generic enum's variant (e.g. `Some(5)` for
    /// `enum Option = | Some(a) | None`), rewriting `call.name` from the bare
    /// variant name (`Some`) to its mangled form (`Some$Int`) once the enum's own
    /// concrete instantiation is known. Mangling is eager and deterministic — it
    /// doesn't wait for the worklist to actually produce the specialized `ast::Enum`
    /// (see `enum_variant_mangling`'s doc comment).
    ///
    /// A variant that carries no generic fields (e.g. `None`) can't pin down the
    /// enum's type parameters on its own, so such a construction site is left alone
    /// here — some other construction site (e.g. `Some(5)`) is what drives the
    /// specialization. (A bare `None` used as a *value*, not a call, is
    /// `ast::Expr::TypeName` and doesn't go through this function at all — see the
    /// plan's Global Constraints for that narrower, documented residual limitation.)
    fn resolveEnumInstantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
        let Some(e) = self.enums_generic_by_variant.get(call.name.as_str()).copied() else { return Ok(()) };
        let params = enumGenericParams(e);
        let Some(variant) = e.variants.iter().find(|v| v.name == call.name) else { return Ok(()) };
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
        for (field_ty_name, arg) in variant.fields.iter().zip(call.args.iter()) {
            if params.contains(field_ty_name) {
                let arg_expr = match arg {
                    ast::Arg::Positional(e) => e,
                    ast::Arg::Keyword { value, .. } => value,
                    ast::Arg::Pair { value, .. } => value,
                };
                bindings.entry(field_ty_name.clone()).or_insert_with(|| self.infer(arg_expr, env));
            }
        }
        // This single construction site couldn't pin down every generic parameter
        // by itself — e.g. `Ok(5)` only ever supplies `Result`'s `T`, never its `E`
        // (no `Ok` call site can, since `Err`'s payload is a disjoint field). Fall
        // back to the enclosing function's declared return type, if it names this
        // same enum with an explicit, fully-general `[...]` instantiation (e.g.
        // `-> Result[Int, Str]`) — that's the one other place a type this
        // construction site can't see on its own is written down on purpose.
        if bindings.len() != params.len() {
            if let Some(rt) = &self.current_return_type {
                if rt.name == e.name && rt.generics.len() == params.len() {
                    for (p, gt) in params.iter().zip(rt.generics.iter()) {
                        bindings.entry(p.clone()).or_insert_with(|| crate::plumTypeFromAst(gt));
                    }
                }
            }
        }
        // Still couldn't pin down every generic parameter (e.g. a payload-free
        // `None`, or no informative return-type annotation either). Leave it for
        // another site to drive.
        if bindings.len() != params.len() {
            return Ok(());
        }
        let mangled = self.ensureEnumSpecialized(e, &params, bindings);
        call.name = self.enum_variant_mangling[&mangled][&variant.name].clone();
        Ok(())
    }

    /// Registers (if not already registered) the specialization of generic enum
    /// `e` at `bindings` — mangling every variant name, teaching `self.enum_variants`
    /// about each mangled variant (so `self.infer` on an already-rewritten
    /// construction site resolves correctly instead of falling back to an
    /// uninformative `TVar`), and enqueueing the specialization to actually be
    /// produced. Returns the mangled enum name. Shared by `resolveEnumInstantiation`
    /// (bindings inferred from a construction site's own args, falling back to the
    /// enclosing return type) and `resolveCallReturnType` (bindings taken directly
    /// from a callee's *own* declared return type, with no construction site at all
    /// — see its doc comment).
    fn ensureEnumSpecialized(&mut self, e: &'a ast::Enum, params: &[String], bindings: BTreeMap<String, PlumType>) -> String {
        let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
        let mangled = mangle(&e.name, &type_args);
        if !self.enum_variant_mangling.contains_key(&mangled) {
            let mut table = BTreeMap::new();
            for (tag, v) in e.variants.iter().enumerate() {
                let mangled_variant = mangle(&v.name, &type_args);
                table.insert(v.name.clone(), mangled_variant.clone());
                let field_types: Vec<PlumType> = v.fields.iter().map(|f| {
                    bindings.get(f).cloned().unwrap_or_else(|| {
                        crate::plumTypeFromAst(&ast::Type { name: f.clone(), generics: vec![] })
                    })
                }).collect();
                self.enum_variants.insert(mangled_variant, EnumVariantInfo {
                    enum_name: mangled.clone(),
                    tag: tag as i32,
                    field_types,
                    values: v.values.clone(),
                });
            }
            self.enum_variant_mangling.insert(mangled.clone(), table);
        }
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
            self.enqueued.insert(mangled.clone());
            self.worklist.push(PendingSpecialization::Enum { base: e, subst: Substitution(bindings), mangled: mangled.clone() });
        }
        mangled
    }

    /// Fully resolves a FIELD's declared type into something that will actually
    /// exist after monomorphization. `specializeClass`'s own field substitution
    /// only replaces a bare generic-param NAME (`T` -> `Int`) — a field declared
    /// `Option[Node[T]]` becomes `Option[Node[Int]]` this way, which is now
    /// fully CONCRETE but still names the generic TEMPLATES `Option`/`Node`
    /// directly, both of which monomorphization deletes from the output (only
    /// their mangled specializations, e.g. `Node$Int`, survive). Recursively
    /// resolves any nested generic arguments first (so `Node[Int]` inside
    /// `Option[Node[Int]]` becomes `Node$Int` before `Option[...]` itself is
    /// resolved), then — if the type names a known generic class/enum applied
    /// to arguments — mangles it to that specialization's real name and
    /// enqueues the specialization if it hasn't been already (via the same
    /// `ensureEnumSpecialized` used for enum construction sites, for enums; classes
    /// don't have an equivalent shared helper, so that half is inlined here).
    /// A field that's already concrete (no generics), or whose name isn't a
    /// known generic template, is returned unchanged (or with just its nested
    /// generics resolved) — this is ALSO called on every ordinary (non-generic)
    /// class's fields, not just specialized ones, since a plain class can
    /// perfectly well have a field like `items: List[Int]`.
    fn resolveFieldType(&mut self, ty: &ast::Type) -> ast::Type {
        if ty.generics.is_empty() {
            return ty.clone();
        }
        let resolved_args: Vec<ast::Type> = ty.generics.iter().map(|g| self.resolveFieldType(g)).collect();
        // A type argument that's STILL a bare single-uppercase-letter name after
        // resolving means it's a truly free type variable at this point in the
        // pipeline — e.g. `List[U]` inside `List[T]`'s own `map` method, where
        // `U` is `map`'s OWN generic param, not `List`'s `T` (already substituted
        // to a concrete type by the time this runs). Mangling/specializing
        // against a placeholder name would silently manufacture a bogus
        // `List$U` "specialization" baked from the letter U itself, so leave the
        // whole type unresolved instead — see the `map`/method-level-generics
        // gap noted in `libs/std/list.plum`.
        if resolved_args.iter().any(|a| isGenericParamName(&a.name)) {
            return ast::Type { name: ty.name.clone(), generics: resolved_args };
        }
        let type_args: Vec<PlumType> = resolved_args.iter().map(crate::plumTypeFromAst).collect();

        if let Some(class) = self.classes_generic.get(ty.name.as_str()).copied() {
            let params = classGenericParams(class);
            if params.len() == type_args.len() {
                let bindings: BTreeMap<String, PlumType> = params.into_iter().zip(type_args.iter().cloned()).collect();
                let mangled = mangle(&ty.name, &type_args);
                if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
                    self.enqueued.insert(mangled.clone());
                    self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings.clone()), mangled: mangled.clone() });
                }
                // Register this specialization's field types right now, even
                // though its actual `Item::Class`/method output is only pushed
                // to `m.produced` once the worklist entry above is popped —
                // another method being rewritten in THIS SAME worklist round
                // (e.g. a sibling method on the class currently being
                // specialized) may need to resolve a field access against it
                // immediately, well before that later worklist entry runs.
                if !self.classes.contains_key(&mangled) {
                    // Insert a placeholder BEFORE recursing into the fields below —
                    // a self-referential class (`Node[T]`'s own `prev`/`next` fields
                    // point back to `Option[Node[T]]`) would otherwise recurse into
                    // resolving its own not-yet-registered specialization forever.
                    // The recursive re-entry only needs this specialization's
                    // MANGLED NAME to build its own field's type, not its fields —
                    // those get filled in for real below once the recursion unwinds.
                    self.classes.insert(mangled.clone(), vec![]);
                    let mut spec_class = specializeClass(class, &Substitution(bindings.clone()), &mangled);
                    for f in &mut spec_class.fields {
                        f.ty = self.resolveFieldType(&f.ty);
                    }
                    self.classes.insert(
                        mangled.clone(),
                        spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
                    );
                    self.registerClassMethodSignatures(class, &mangled, &bindings);
                }
                return ast::Type { name: mangled, generics: vec![] };
            }
        }
        if let Some(e) = self.enums_generic_by_name.get(ty.name.as_str()).copied() {
            let params = enumGenericParams(e);
            if params.len() == type_args.len() {
                let bindings: BTreeMap<String, PlumType> = params.iter().cloned().zip(type_args.iter().cloned()).collect();
                let mangled = self.ensureEnumSpecialized(e, &params, bindings);
                return ast::Type { name: mangled, generics: vec![] };
            }
        }
        ast::Type { name: ty.name.clone(), generics: resolved_args }
    }

    /// Resolves every generic-with-args type in `f`'s own signature (params,
    /// return) via `resolveFieldType`, mutating `f` in place. Must run before
    /// `f` is pushed to `m.produced` — the real checker rebuilds its tables
    /// FRESH from that final AST (via `buildGlobalTables`, using the plain
    /// generics-dropping `plumTypeFromAst`), so whatever a signature still
    /// says at that point is what the checker sees; a bare unresolved `Node`
    /// or `List[Int]` left in a param would either dangle or get its generics
    /// silently dropped again.
    fn resolveFnSignature(&mut self, f: &mut ast::Fn) {
        self.resolveFnParamTypes(f);
        self.resolveFnReturnType(f);
    }

    /// Just the params half of `resolveFnSignature` — deliberately split out
    /// so callers can run this BEFORE `rewriteFnBody` (env seeding needs
    /// param types already flattened, e.g. `Node[T]` -> `Node$Int`, or a
    /// field access on a param would fail to resolve) while leaving
    /// `f.returns` untouched until AFTER the body's been rewritten. The body
    /// rewrite reads `self.current_return_type` (seeded from `f.returns` as
    /// originally declared, generics and all) to drive
    /// `resolveEnumInstantiation`'s fallback for construction sites that
    /// can't infer every generic param from their own arguments alone (e.g.
    /// `Err("...")` needs `Result`'s OTHER param, `T`, from the function's
    /// own `-> Result[Int, Str]` declaration) — flattening the return type
    /// up front would replace `rt.name` with an already-mangled name that
    /// fallback's own bare-template-name comparison can never match again.
    fn resolveFnParamTypes(&mut self, f: &mut ast::Fn) {
        for p in &mut f.params {
            match &mut p.ty {
                ast::ParamType::Type(t) => *t = self.resolveFieldType(t),
                ast::ParamType::Variadic(t) => *t = self.resolveFieldType(t),
                ast::ParamType::Fn(_, _) => {}
            }
        }
    }

    /// The returns half of `resolveFnSignature` — see `resolveFnParamTypes`'s
    /// doc comment for why this must run AFTER `rewriteFnBody`, not before.
    fn resolveFnReturnType(&mut self, f: &mut ast::Fn) {
        if let Some(r) = &mut f.returns {
            *r = self.resolveFieldType(r);
        }
    }

    /// A construction site (`Ok(5)`) only ever tells us about the TYPE that's being
    /// PRODUCED — it says nothing about code further downstream that CONSUMES an
    /// already-specialized generic-enum value returned from calling some other
    /// (already fully concrete, non-generic) function or method, e.g.
    /// `match parseIt() { Ok(v) => ... }` where `fun parseIt() -> Result[Int, Str]`.
    /// `self.infer` can't help there either: it's built from `buildGlobalTables`,
    /// which (like every other `PlumType` site) drops a declared type's generic
    /// args entirely (`plumTypeFromAst` maps `Result[Int, Str]` to the bare
    /// `TNamed("Result")`) — so it has no way to know this call's result is the
    /// SPECIALIZED `Result$Int$Str`, not the generic template.
    ///
    /// This resolves that one specific, common shape directly from the callee's own
    /// declaration (recorded in `fn_return_generic_enum` during classification) —
    /// bypassing `self.infer` entirely, since the answer is already fully known
    /// from the signature and doesn't depend on this call site's arguments at all.
    /// Returns `None` for anything else (an ordinary call, a call to a function
    /// whose return isn't a generic-enum instantiation, ...), meaning "fall back to
    /// `self.infer` as before."
    fn resolveCallReturnType(&mut self, e: &ast::Expr, env: &TypeEnv) -> Option<PlumType> {
        let (recv, name) = match e {
            ast::Expr::FnCall(call) => (None, call.name.clone()),
            ast::Expr::Attribute(attr) => match &attr.attr {
                ast::AttrKind::Method(call) => {
                    let recv = match crate::methodReceiverName(&self.infer(&attr.object, env)) {
                        Some(r) => r,
                        None => return None,
                    };
                    (Some(recv), call.name.clone())
                }
                ast::AttrKind::Field(_) => return None,
            },
            _ => return None,
        };
        let (enum_ref, type_args) = self.fn_return_generic_enum.get(&(recv, name))?.clone();
        let params = enumGenericParams(enum_ref);
        let bindings: BTreeMap<String, PlumType> = params.iter().cloned().zip(type_args).collect();
        Some(PlumType::TNamed(self.ensureEnumSpecialized(enum_ref, &params, bindings)))
    }

    fn rewriteExpr(&mut self, expr: &mut ast::Expr, env: &TypeEnv) -> Result<(), String> {
        match expr {
            ast::Expr::ClassCall(call) => {
                // Runs FIRST (before the generic per-field rewrite below): a
                // bare payload-free variant field value (`Node(..., next:
                // None)`) carries no type of its own to infer a generic
                // param from, and needs the class's OWN (about-to-be-
                // specialized) field type to resolve which specialization it
                // actually means — `resolveClassInstantiation` handles that
                // internally once it knows the full binding set.
                self.resolveClassInstantiation(call, env)?;
                for fa in &mut call.fields {
                    self.rewriteExpr(&mut fa.value, env)?;
                }
            }
            ast::Expr::FnCall(call) => {
                for arg in &mut call.args {
                    let e = match arg {
                        ast::Arg::Positional(e) => e,
                        ast::Arg::Keyword { value, .. } => value,
                        ast::Arg::Pair { value, .. } => value,
                    };
                    self.rewriteExpr(e, env)?;
                }
                // A `FnCall` may name either a generic free function or a generic
                // enum's variant; the two name spaces don't overlap (variants are
                // capitalized). Enum resolution runs first and rewrites `call.name`
                // to its mangled form when it resolves — `fns_generic` is keyed by
                // the ORIGINAL unmangled free-function names, so a rewritten variant
                // name can never accidentally match it afterward.
                self.resolveEnumInstantiation(call, env)?;
                self.resolveFnInstantiation(call, env)?;
                self.resolveBareGenericFnInstantiation(call, env)?;
            }
            ast::Expr::Attribute(attr) => {
                self.rewriteExpr(&mut attr.object, env)?;
                if let ast::AttrKind::Method(call) = &mut attr.attr {
                    for arg in &mut call.args {
                        let e = match arg {
                            ast::Arg::Positional(e) => e,
                            ast::Arg::Keyword { value, .. } => value,
                            ast::Arg::Pair { value, .. } => value,
                        };
                        self.rewriteExpr(e, env)?;
                    }
                    // Method dispatch on a specialized receiver needs no rewrite here:
                    // once the receiver's construction site is rewritten to its mangled
                    // class name, the receiver's inferred static type IS that mangled
                    // name, and the specialized methods were registered under exactly
                    // that (mangled receiver, method name) key when their class was
                    // specialized (see the `PendingSpecialization::Class` arm below).
                }
            }
            ast::Expr::Binary(b) => { self.rewriteExpr(&mut b.left, env)?; self.rewriteExpr(&mut b.right, env)?; }
            ast::Expr::Bool(b) => { self.rewriteExpr(&mut b.left, env)?; self.rewriteExpr(&mut b.right, env)?; }
            ast::Expr::Compare(c) => {
                self.rewriteExpr(&mut c.left, env)?;
                self.rewriteExpr(&mut c.right, env)?;
                // `current != None`: `current`'s side may already be a SPECIFIC
                // enum specialization (e.g. `Option$Node$Int`) while `None`
                // itself is still bare (it carries no type of its own) —
                // resolve each side against the OTHER's type; a no-op unless
                // that side is actually a bare generic-enum variant.
                let lt = self.infer(&c.left, env);
                let rt = self.infer(&c.right, env);
                self.resolveBareVariantAgainstExpected(&mut c.left, &rt);
                self.resolveBareVariantAgainstExpected(&mut c.right, &lt);
            }
            ast::Expr::Not(inner) => self.rewriteExpr(inner, env)?,
            ast::Expr::Unary(u) => self.rewriteExpr(&mut u.operand, env)?,
            ast::Expr::Paren(inner) => self.rewriteExpr(inner, env)?,
            ast::Expr::Ternary(t) => {
                self.rewriteExpr(&mut t.condition, env)?;
                self.rewriteExpr(&mut t.then, env)?;
                self.rewriteExpr(&mut t.else_, env)?;
            }
            // String interpolation can embed arbitrary expressions (including generic
            // call sites), so recurse into its interpolated parts.
            ast::Expr::String(s) => {
                for part in &mut s.parts {
                    if let ast::StringPart::Interp(e) = part {
                        self.rewriteExpr(e, env)?;
                    }
                }
            }
            ast::Expr::Int(_) | ast::Expr::Float(_)
            | ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
            // TODO: closure bodies don't yet get rewritten for generic call sites.
            ast::Expr::Closure(_) => {}
        }
        Ok(())
    }
}

/// Runs the whole generics-monomorphization pass over `source`, producing a plain,
/// fully-concrete `ast::Source` with every generic `Class`/`Fn`/`Enum` template
/// replaced by zero or more mangled concrete specializations, and every remaining
/// item's body rewritten so its call sites reference those mangled names. The
/// result has no generic syntax left in it — `checkSource`/`compileSource` run
/// on it completely unmodified.
pub fn monomorphizeSource(source: &ast::Source) -> Result<ast::Source, String> {
    let (global_env, classes, methods, enum_variants, enum_params) = crate::buildGlobalTables(source);

    let mut m = Monomorphizer {
        classes_generic: BTreeMap::new(),
        fns_generic: BTreeMap::new(),
        methods_generic_on: BTreeMap::new(),
        methods_generic_on_enum: BTreeMap::new(),
        enums_generic_by_variant: BTreeMap::new(),
        enums_generic_by_name: BTreeMap::new(),
        enum_variant_mangling: BTreeMap::new(),
        fns_bare_generic: BTreeMap::new(),
        global_env,
        classes,
        methods,
        enum_variants,
        enum_params,
        specialized: BTreeSet::new(),
        enqueued: BTreeSet::new(),
        worklist: Vec::new(),
        produced: Vec::new(),
        current_return_type: None,
        fn_return_generic_enum: BTreeMap::new(),
        local_types_by_name: BTreeMap::new(),
        rename_counter: 0,
    };

    for item in &source.items {
        match item {
            ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
            ast::Item::Enum(e) if !enumGenericParams(e).is_empty() => {
                m.enums_generic_by_name.insert(e.name.clone(), e);
                for v in &e.variants {
                    m.enums_generic_by_variant.insert(v.name.clone(), e);
                }
            }
            _ => {}
        }
    }
    // Any function/method (generic or not — this doesn't care either way) whose
    // OWN declared return type fully instantiates a known generic enum. Must run
    // after the loop above (needs `enums_generic_by_name` filled) but is otherwise
    // independent of every other classification pass here.
    for item in &source.items {
        if let ast::Item::Fn(f) = item {
            if let Some(rt) = &f.returns {
                if let Some(e) = m.enums_generic_by_name.get(rt.name.as_str()).copied() {
                    let params = enumGenericParams(e);
                    if !rt.generics.is_empty() && rt.generics.len() == params.len() {
                        let type_args: Vec<PlumType> = rt.generics.iter().map(crate::plumTypeFromAst).collect();
                        m.fn_return_generic_enum.insert((f.type_param.clone(), f.name.clone()), (e, type_args));
                    }
                }
            }
        }
    }

    for item in &source.items {
        if let ast::Item::Fn(f) = item {
            let receiver_is_generic_class = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
            let receiver_is_generic_enum = f.type_param.as_deref().map(|r| m.enums_generic_by_name.contains_key(r)).unwrap_or(false);
            if receiver_is_generic_class {
                m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
            } else if receiver_is_generic_enum {
                m.methods_generic_on_enum.entry(f.type_param.clone().unwrap()).or_default().push(f);
            } else if f.type_param.is_none() && !fnGenericParams(f).is_empty() {
                m.fns_generic.insert(f.name.clone(), f);
            } else if f.type_param.is_none() && !m.fnBareGenericRefs(f).is_empty() {
                m.fns_bare_generic.insert(f.name.clone(), f);
            }
            // A method whose receiver is NOT generic is left as a regular method below,
            // even if its own params/return happen to use a bare lowercase-letter type
            // name, or bare-name a generic class/enum — those shapes are out of scope
            // for this pass; see the plan's Global Constraints.
        }
    }

    for item in &source.items {
        match item {
            ast::Item::Class(c) if c.generics.is_empty() => {
                let mut c2 = c.clone();
                for f in &mut c2.fields {
                    f.ty = m.resolveFieldType(&f.ty);
                }
                m.produced.push(ast::Item::Class(c2));
            }
            ast::Item::Enum(e) if enumGenericParams(e).is_empty() => m.produced.push(ast::Item::Enum(e.clone())),
            ast::Item::Const(c) => m.produced.push(ast::Item::Const(c.clone())),
            ast::Item::Trait(t) => m.produced.push(ast::Item::Trait(t.clone())),
            ast::Item::Fn(f) => {
                let receiver_is_generic = f.type_param.as_deref()
                    .map(|r| m.classes_generic.contains_key(r) || m.enums_generic_by_name.contains_key(r))
                    .unwrap_or(false);
                let is_generic_fn = f.type_param.is_none() && !fnGenericParams(f).is_empty();
                let is_bare_generic_fn = f.type_param.is_none() && m.fns_bare_generic.contains_key(f.name.as_str());
                if !receiver_is_generic && !is_generic_fn && !is_bare_generic_fn {
                    let mut f2 = f.clone();
                    m.resolveFnParamTypes(&mut f2);
                    m.rewriteFnBody(&mut f2, false)?;
                    m.resolveFnReturnType(&mut f2);
                    m.produced.push(ast::Item::Fn(f2));
                }
            }
            _ => {} // generic Class/Enum declarations dropped here — templates only
        }
    }

    let mut guard = 0usize;
    while let Some(pending) = m.worklist.pop() {
        guard += 1;
        if guard > 10_000 {
            return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
        }
        match pending {
            PendingSpecialization::Class { base, subst, mangled } => {
                if !m.specialized.insert(mangled.clone()) { continue; }
                let mut spec_class = specializeClass(base, &subst, &mangled);
                // `specializeClass` only substitutes a field's bare generic-param
                // NAME (`T` -> `Int`) — a field like `Option[Node[T]]` becomes
                // `Option[Node[Int]]`, still a generic instantiation, not yet a
                // real (mangled) type. Resolve those the rest of the way now.
                for f in &mut spec_class.fields {
                    f.ty = m.resolveFieldType(&f.ty);
                }
                // Register the specialized class's fields so inference inside its
                // own (and other items') bodies can resolve `receiver.field` on the
                // mangled type — `self.classes` was built from the ORIGINAL source
                // and would otherwise not know this freshly-minted class.
                m.classes.insert(
                    mangled.clone(),
                    spec_class.fields.iter().map(|f| (f.name.clone(), crate::plumTypeFromAst(&f.ty))).collect(),
                );
                m.produced.push(ast::Item::Class(spec_class));
                if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
                    for method in methods {
                        let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
                        // Params (not returns — see `resolveFnParamTypes`'s doc
                        // comment) must run before `rewriteFnBody`/registration
                        // below: both read the signature's types directly off
                        // this AST, and the real checker later rebuilds its own
                        // tables from this exact (post-monomorphize) AST too.
                        m.resolveFnParamTypes(&mut specialized_method);
                        m.rewriteFnBody(&mut specialized_method, true)?;
                        m.resolveFnReturnType(&mut specialized_method);
                        // Register the specialized method's signature under its
                        // (mangled receiver, method name) key so any later body that
                        // dispatches to it can resolve its concrete return type.
                        let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
                            ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
                            ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
                            ast::ParamType::Fn(params, ret) => {
                                let param_types = params.iter().map(crate::plumTypeFromAst).collect();
                                let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
                                PlumType::TFun(param_types, Box::new(ret_ty))
                            }
                        }).collect();
                        let ret = specialized_method.returns.as_ref()
                            .map(crate::plumTypeFromAst)
                            .unwrap_or(PlumType::TUnit);
                        m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
                        m.produced.push(ast::Item::Fn(specialized_method));
                    }
                }
            }
            PendingSpecialization::Fn { base, subst, mangled, new_receiver } => {
                if !m.specialized.insert(mangled.clone()) { continue; }
                let mut specialized_fn = specializeFn(base, &subst, &mangled, new_receiver);
                m.resolveFnParamTypes(&mut specialized_fn);
                m.rewriteFnBody(&mut specialized_fn, true)?;
                m.resolveFnReturnType(&mut specialized_fn);
                // Register the specialized free function's signature so later bodies
                // can resolve calls to it during inference.
                let param_types: Vec<PlumType> = specialized_fn.params.iter().map(|p| match &p.ty {
                    ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
                    ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
                    ast::ParamType::Fn(params, ret) => {
                        let param_types = params.iter().map(crate::plumTypeFromAst).collect();
                        let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
                        PlumType::TFun(param_types, Box::new(ret_ty))
                    }
                }).collect();
                let ret = specialized_fn.returns.as_ref()
                    .map(crate::plumTypeFromAst)
                    .unwrap_or(PlumType::TUnit);
                m.global_env.insert(specialized_fn.name.clone(), TypeScheme::mono(PlumType::TFun(param_types, Box::new(ret))));
                m.produced.push(ast::Item::Fn(specialized_fn));
            }
            PendingSpecialization::Enum { base, subst, mangled } => {
                if !m.specialized.insert(mangled.clone()) { continue; }
                let spec_enum = specializeEnum(base, &subst, &mangled);
                m.produced.push(ast::Item::Enum(spec_enum));
                if let Some(methods) = m.methods_generic_on_enum.get(base.name.as_str()).cloned() {
                    for method in methods {
                        let mut specialized_method = specializeFn(method, &subst, &method.name, Some(mangled.clone()));
                        m.resolveFnParamTypes(&mut specialized_method);
                        m.rewriteFnBody(&mut specialized_method, true)?;
                        m.resolveFnReturnType(&mut specialized_method);
                        let param_types: Vec<PlumType> = specialized_method.params.iter().map(|p| match &p.ty {
                            ast::ParamType::Type(t) => crate::plumTypeFromAst(t),
                            ast::ParamType::Variadic(t) => PlumType::TVariadic(Box::new(crate::plumTypeFromAst(t))),
                            ast::ParamType::Fn(params, ret) => {
                                let param_types = params.iter().map(crate::plumTypeFromAst).collect();
                                let ret_ty = ret.as_ref().map(|r| crate::plumTypeFromAst(r)).unwrap_or(PlumType::TUnit);
                                PlumType::TFun(param_types, Box::new(ret_ty))
                            }
                        }).collect();
                        let ret = specialized_method.returns.as_ref()
                            .map(crate::plumTypeFromAst)
                            .unwrap_or(PlumType::TUnit);
                        m.methods.insert((mangled.clone(), specialized_method.name.clone()), PlumType::TFun(param_types, Box::new(ret)));
                        m.produced.push(ast::Item::Fn(specialized_method));
                    }
                }
            }
        }
    }

    Ok(ast::Source { module: source.module.clone(), imports: source.imports.clone(), items: m.produced })
}