plum

#treesitter#compiler#wasm

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

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


docs/superpowers/plans/2026-07-20-generics-monomorphization.md
34f159a 1
# Generics Monomorphization Implementation Plan
34f159a 2
34f159a 3
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
34f159a 4
34f159a 5
**Goal:** Make user-declared generics (classes, free functions, methods on generic classes, and enums, using the currently-documented single-lowercase-letter generic syntax) actually monomorphize and compile to wasm, by specializing a mangled, fully-concrete copy of each generic item per unique concrete-type-argument combination actually used in the program, then running each specialized copy through the existing, unmodified non-generic checker/codegen pipeline.
34f159a 6
34f159a 7
**Architecture:** A new pass, `plum_checker::monomorphize::monomorphize_source`, runs on the raw `ast::Source` *before* both `check_source` and `compile_source` do anything else. It: (1) classifies every top-level item as generic-template (dropped from the output) or concrete (passed through, with its body rewritten in place); (2) walks every concrete function/method body with a lightweight, type-inference-driven expression rewriter that resolves each generic-construction/generic-call site's concrete type arguments (reusing `plum_checker`'s existing `infer_expr`/`unify`), mangles a specialization name, rewrites the call site to reference it, and enqueues that specialization if not already produced; (3) drains that worklist to a fixed point (a freshly-specialized body can itself introduce further, now-fully-concrete instantiation sites), guarded against runaway recursion. The output is a plain, fully-concrete `ast::Source` with zero generic syntax left in it — `plum-checker`'s and `plum-wasm-codegen`'s existing pipelines need **no changes** to consume it.
34f159a 8
34f159a 9
**Tech Stack:** Rust (workspace: `plum-core`, `plum-checker`, `plum-wasm-codegen`).
34f159a 10
34f159a 11
## Global Constraints
34f159a 12
34f159a 13
- **Scope of "generic" for this pass:** `Class`/`Trait` generics come from their explicit `generics: Vec<GenericParam>` list. `Fn` and `Enum` have no such list in the AST — a generic parameter is detected *implicitly*: any single-lowercase-letter type name (`a`, `b`, `c`, `d`, ...) appearing in a `Fn`'s param/return types, or an `Enum` variant's field type name, is treated as an implicit generic parameter.
34f159a 14
- **In scope:** generic classes and their declared methods (specialized together, via the class's own substitution — a method's receiver becomes the class's mangled name); generic free functions; generic enums.
34f159a 15
- **Out of scope (do not attempt):** a method introducing its *own* additional generic parameter beyond its receiver class's (e.g. a method-scoped `b` on top of a class-scoped `a`) — such a method is left exactly as unspecialized/permissive as it is today, matching existing behavior for a shape this pass doesn't support; trait-bound enforcement (`Comparable(a: Ord)`'s `Ord` bound) — stays permissive, unchanged; generic instantiation sites inside string-interpolation parts (string interpolation itself is a separate, already-known, unrelated codegen gap); making `libs/std/list.plum`/`map.plum` compile as-is (they need closures, `Nil`/optional-chaining, decorators, and other unimplemented syntax first — this plan does not touch the grammar or those features at all).
34f159a 16
- Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use a `"monomorphize: ..."` prefix, mirroring the existing `"codegen: ..."` convention.
34f159a 17
- This is new algorithmic code, not a modification of existing tested logic — validate it primarily through the tests each task specifies (TDD), not by assuming any single line is exactly right on first write.
34f159a 18
- Every task must leave `cargo test --workspace` and `npx --yes tree-sitter-cli test` (from `tooling/tree-sitter-plum/`) green before moving to the next task.
34f159a 19
34f159a 20
---
34f159a 21
34f159a 22
### Task 1: Core specialization primitives
34f159a 23
34f159a 24
**Files:**
34f159a 25
- Create: `plum-checker/src/monomorphize.rs`
34f159a 26
- Modify: `plum-checker/src/lib.rs` (add `pub mod monomorphize;`)
34f159a 27
- Test: `plum-checker/tests/monomorphize_tests.rs` (new file)
34f159a 28
34f159a 29
**Interfaces:**
34f159a 30
- Consumes: `plum_core::ast::{Class, Fn, Enum, Field, Param, ParamType, ReturnType, Type, GenericParam, EnumVariant}` (all already `Clone`); `plum_checker::types::PlumType` (already `Display`-implemented, giving `"Int"`/`"Float"`/`"Bool"`/`"Str"`/`"Unit"`/a class name for `TNamed`).
34f159a 31
- Produces (all `pub` in `plum_checker::monomorphize`, used by Task 2 in the same file and by nothing outside this crate yet):
34f159a 32
  ```rust
34f159a 33
  pub fn is_generic_param_name(name: &str) -> bool
34f159a 34
  pub fn class_generic_params(c: &ast::Class) -> Vec<String>
34f159a 35
  pub fn fn_generic_params(f: &ast::Fn) -> Vec<String>
34f159a 36
  pub fn enum_generic_params(e: &ast::Enum) -> Vec<String>
34f159a 37
  pub struct Substitution(pub std::collections::BTreeMap<String, PlumType>)
34f159a 38
  pub fn mangle(base: &str, type_args: &[PlumType]) -> String
34f159a 39
  pub fn specialize_class(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class
34f159a 40
  pub fn specialize_fn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_type_param: Option<String>) -> ast::Fn
34f159a 41
  pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum
34f159a 42
  ```
34f159a 43
34f159a 44
- [ ] **Step 1: Write failing tests**
34f159a 45
34f159a 46
Create `plum-checker/tests/monomorphize_tests.rs`:
34f159a 47
34f159a 48
```rust
34f159a 49
use plum_checker::monomorphize::*;
34f159a 50
use plum_checker::types::PlumType;
34f159a 51
use plum_core::ast;
34f159a 52
34f159a 53
#[test]
34f159a 54
fn is_generic_param_name_accepts_single_lowercase_letters_only() {
34f159a 55
    assert!(is_generic_param_name("a"));
34f159a 56
    assert!(is_generic_param_name("d"));
34f159a 57
    assert!(!is_generic_param_name("Int"));
34f159a 58
    assert!(!is_generic_param_name("ab"));
34f159a 59
    assert!(!is_generic_param_name("A"));
34f159a 60
    assert!(!is_generic_param_name(""));
34f159a 61
}
34f159a 62
34f159a 63
#[test]
34f159a 64
fn class_generic_params_reads_declared_generics_list() {
34f159a 65
    let c = ast::Class {
34f159a 66
        name: "Box".to_string(),
34f159a 67
        implements: vec![],
34f159a 68
        generics: vec![ast::GenericParam { name: "a".to_string(), bounds: vec![] }],
34f159a 69
        fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "a".to_string(), generics: vec![] } }],
34f159a 70
    };
34f159a 71
    assert_eq!(class_generic_params(&c), vec!["a".to_string()]);
34f159a 72
}
34f159a 73
34f159a 74
#[test]
34f159a 75
fn fn_generic_params_detects_implicit_lowercase_letter_types_in_order() {
34f159a 76
    let f = ast::Fn {
34f159a 77
        name: "pair".to_string(),
34f159a 78
        type_param: None,
34f159a 79
        params: vec![
34f159a 80
            ast::Param { name: "first".to_string(), ty: ast::ParamType::Type(ast::Type { name: "a".to_string(), generics: vec![] }), default: None },
34f159a 81
            ast::Param { name: "second".to_string(), ty: ast::ParamType::Type(ast::Type { name: "b".to_string(), generics: vec![] }), default: None },
34f159a 82
        ],
34f159a 83
        returns: Some(ast::ReturnType { name: "Bool".to_string(), generics: vec![] }),
34f159a 84
        body: ast::FnBody::Block(ast::Block { stmts: vec![] }),
34f159a 85
    };
34f159a 86
    assert_eq!(fn_generic_params(&f), vec!["a".to_string(), "b".to_string()]);
34f159a 87
}
34f159a 88
34f159a 89
#[test]
34f159a 90
fn enum_generic_params_detects_implicit_lowercase_letter_variant_fields() {
34f159a 91
    let e = ast::Enum {
34f159a 92
        name: "Option".to_string(),
34f159a 93
        variants: vec![
34f159a 94
            ast::EnumVariant { name: "Some".to_string(), fields: vec!["a".to_string()] },
34f159a 95
            ast::EnumVariant { name: "None".to_string(), fields: vec![] },
34f159a 96
        ],
34f159a 97
    };
34f159a 98
    assert_eq!(enum_generic_params(&e), vec!["a".to_string()]);
34f159a 99
}
34f159a 100
34f159a 101
#[test]
34f159a 102
fn mangle_joins_base_name_and_type_args() {
34f159a 103
    assert_eq!(mangle("Box", &[PlumType::TInt]), "Box$Int");
34f159a 104
    assert_eq!(mangle("Pair", &[PlumType::TInt, PlumType::TStr]), "Pair$Int$Str");
34f159a 105
    assert_eq!(mangle("Green", &[]), "Green");
34f159a 106
}
34f159a 107
34f159a 108
#[test]
34f159a 109
fn specialize_class_substitutes_generic_field_types_and_clears_generics_list() {
34f159a 110
    let c = ast::Class {
34f159a 111
        name: "Box".to_string(),
34f159a 112
        implements: vec![],
34f159a 113
        generics: vec![ast::GenericParam { name: "a".to_string(), bounds: vec![] }],
34f159a 114
        fields: vec![ast::Field { name: "value".to_string(), ty: ast::Type { name: "a".to_string(), generics: vec![] } }],
34f159a 115
    };
34f159a 116
    let mut bindings = std::collections::BTreeMap::new();
34f159a 117
    bindings.insert("a".to_string(), PlumType::TInt);
34f159a 118
    let specialized = specialize_class(&c, &Substitution(bindings), "Box$Int");
34f159a 119
    assert_eq!(specialized.name, "Box$Int");
34f159a 120
    assert!(specialized.generics.is_empty());
34f159a 121
    assert_eq!(specialized.fields[0].ty.name, "Int");
34f159a 122
}
34f159a 123
34f159a 124
#[test]
34f159a 125
fn specialize_fn_substitutes_generic_param_and_return_types() {
34f159a 126
    let f = ast::Fn {
34f159a 127
        name: "wrap".to_string(),
34f159a 128
        type_param: None,
34f159a 129
        params: vec![ast::Param { name: "value".to_string(), ty: ast::ParamType::Type(ast::Type { name: "a".to_string(), generics: vec![] }), default: None }],
34f159a 130
        returns: Some(ast::ReturnType { name: "a".to_string(), generics: vec![] }),
34f159a 131
        body: ast::FnBody::Expr(ast::Expr::Var("value".to_string())),
34f159a 132
    };
34f159a 133
    let mut bindings = std::collections::BTreeMap::new();
34f159a 134
    bindings.insert("a".to_string(), PlumType::TInt);
34f159a 135
    let specialized = specialize_fn(&f, &Substitution(bindings), "wrap$Int", None);
34f159a 136
    assert_eq!(specialized.name, "wrap$Int");
34f159a 137
    match &specialized.params[0].ty {
34f159a 138
        ast::ParamType::Type(t) => assert_eq!(t.name, "Int"),
34f159a 139
        _ => panic!("expected ParamType::Type"),
34f159a 140
    }
34f159a 141
    assert_eq!(specialized.returns.unwrap().name, "Int");
34f159a 142
}
34f159a 143
34f159a 144
#[test]
34f159a 145
fn specialize_fn_sets_new_receiver_for_a_method() {
34f159a 146
    let f = ast::Fn {
34f159a 147
        name: "getValue".to_string(),
34f159a 148
        type_param: Some("Box".to_string()),
34f159a 149
        params: vec![],
34f159a 150
        returns: Some(ast::ReturnType { name: "a".to_string(), generics: vec![] }),
34f159a 151
        body: ast::FnBody::Expr(ast::Expr::Self_),
34f159a 152
    };
34f159a 153
    let mut bindings = std::collections::BTreeMap::new();
34f159a 154
    bindings.insert("a".to_string(), PlumType::TStr);
34f159a 155
    let specialized = specialize_fn(&f, &Substitution(bindings), "getValue", Some("Box$Str".to_string()));
34f159a 156
    assert_eq!(specialized.name, "getValue");
34f159a 157
    assert_eq!(specialized.type_param, Some("Box$Str".to_string()));
34f159a 158
    assert_eq!(specialized.returns.unwrap().name, "Str");
34f159a 159
}
34f159a 160
34f159a 161
#[test]
34f159a 162
fn specialize_enum_substitutes_generic_variant_field_names() {
34f159a 163
    let e = ast::Enum {
34f159a 164
        name: "Option".to_string(),
34f159a 165
        variants: vec![
34f159a 166
            ast::EnumVariant { name: "Some".to_string(), fields: vec!["a".to_string()] },
34f159a 167
            ast::EnumVariant { name: "None".to_string(), fields: vec![] },
34f159a 168
        ],
34f159a 169
    };
34f159a 170
    let mut bindings = std::collections::BTreeMap::new();
34f159a 171
    bindings.insert("a".to_string(), PlumType::TInt);
34f159a 172
    let specialized = specialize_enum(&e, &Substitution(bindings), "Option$Int");
34f159a 173
    assert_eq!(specialized.name, "Option$Int");
34f159a 174
    assert_eq!(specialized.variants[0].fields, vec!["Int".to_string()]);
34f159a 175
    assert!(specialized.variants[1].fields.is_empty());
34f159a 176
}
34f159a 177
```
34f159a 178
34f159a 179
- [ ] **Step 2: Run to see them fail**
34f159a 180
34f159a 181
Run: `cargo test -p plum-checker --test monomorphize_tests`
34f159a 182
Expected: fails to compile — `plum_checker::monomorphize` doesn't exist yet.
34f159a 183
34f159a 184
- [ ] **Step 3: Create `plum-checker/src/monomorphize.rs`**
34f159a 185
34f159a 186
```rust
34f159a 187
use std::collections::BTreeMap;
34f159a 188
use plum_core::ast;
34f159a 189
use crate::types::PlumType;
34f159a 190
34f159a 191
/// A single lowercase letter (`a`, `b`, `c`, `d`, ...) is the grammar's only legal
34f159a 192
/// spelling for a generic type parameter — this is how we recognize one, since
34f159a 193
/// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
34f159a 194
/// generics declaration list.
34f159a 195
pub fn is_generic_param_name(name: &str) -> bool {
34f159a 196
    let mut chars = name.chars();
34f159a 197
    match (chars.next(), chars.next()) {
34f159a 198
        (Some(c), None) => c.is_ascii_lowercase(),
34f159a 199
        _ => false,
34f159a 200
    }
34f159a 201
}
34f159a 202
34f159a 203
/// The generic parameter names introduced by a `Class`, in declaration order.
34f159a 204
pub fn class_generic_params(c: &ast::Class) -> Vec<String> {
34f159a 205
    c.generics.iter().map(|g| g.name.clone()).collect()
34f159a 206
}
34f159a 207
34f159a 208
/// The generic parameter names implicitly introduced by a `Fn` — every distinct
34f159a 209
/// single-lowercase-letter type name appearing in its params or return type, in
34f159a 210
/// first-appearance order.
34f159a 211
pub fn fn_generic_params(f: &ast::Fn) -> Vec<String> {
34f159a 212
    let mut names: Vec<String> = Vec::new();
34f159a 213
    let mut consider = |n: &str| {
34f159a 214
        if is_generic_param_name(n) && !names.iter().any(|x| x == n) {
34f159a 215
            names.push(n.to_string());
34f159a 216
        }
34f159a 217
    };
34f159a 218
    for p in &f.params {
34f159a 219
        match &p.ty {
34f159a 220
            ast::ParamType::Type(t) => consider(&t.name),
34f159a 221
            ast::ParamType::Variadic(t) => consider(&t.name),
34f159a 222
        }
34f159a 223
    }
34f159a 224
    if let Some(r) = &f.returns {
34f159a 225
        consider(&r.name);
34f159a 226
    }
34f159a 227
    names
34f159a 228
}
34f159a 229
34f159a 230
/// The generic parameter names implicitly introduced by an `Enum` — every distinct
34f159a 231
/// single-lowercase-letter variant field type name, in first-appearance order.
34f159a 232
pub fn enum_generic_params(e: &ast::Enum) -> Vec<String> {
34f159a 233
    let mut names: Vec<String> = Vec::new();
34f159a 234
    for v in &e.variants {
34f159a 235
        for field_ty in &v.fields {
34f159a 236
            if is_generic_param_name(field_ty) && !names.iter().any(|x| x == field_ty) {
34f159a 237
                names.push(field_ty.clone());
34f159a 238
            }
34f159a 239
        }
34f159a 240
    }
34f159a 241
    names
34f159a 242
}
34f159a 243
34f159a 244
/// A resolved binding from a generic item's parameter names to concrete types for
34f159a 245
/// one instantiation site, e.g. `{"a": Int}` for `Box(value: 5)`.
34f159a 246
#[derive(Debug, Clone)]
34f159a 247
pub struct Substitution(pub BTreeMap<String, PlumType>);
34f159a 248
34f159a 249
impl Substitution {
34f159a 250
    fn get(&self, name: &str) -> Option<&PlumType> {
34f159a 251
        self.0.get(name)
34f159a 252
    }
34f159a 253
}
34f159a 254
34f159a 255
/// Converts a resolved concrete `PlumType` back into the `ast::Type` shape needed
34f159a 256
/// to substitute into a declared field/param/return type position. Only ever
34f159a 257
/// called with types resolved from a real call-site argument's inferred type, so
34f159a 258
/// `TVar`/`TFun` (which never arise from a concrete argument) are an internal-error
34f159a 259
/// case rather than something this needs to model.
34f159a 260
fn plum_type_to_ast_type(t: &PlumType) -> ast::Type {
34f159a 261
    let name = match t {
34f159a 262
        PlumType::TInt => "Int".to_string(),
34f159a 263
        PlumType::TFloat => "Float".to_string(),
34f159a 264
        PlumType::TBool => "Bool".to_string(),
34f159a 265
        PlumType::TStr => "Str".to_string(),
34f159a 266
        PlumType::TUnit => "Unit".to_string(),
34f159a 267
        PlumType::TNamed(n) => n.clone(),
34f159a 268
        PlumType::TVar(_) | PlumType::TFun(_, _) => t.to_string(),
34f159a 269
    };
34f159a 270
    ast::Type { name, generics: vec![] }
34f159a 271
}
34f159a 272
34f159a 273
fn substitute_type(ty: &ast::Type, subst: &Substitution) -> ast::Type {
34f159a 274
    if ty.generics.is_empty() {
34f159a 275
        if let Some(concrete) = subst.get(&ty.name) {
34f159a 276
            return plum_type_to_ast_type(concrete);
34f159a 277
        }
34f159a 278
    }
34f159a 279
    ast::Type {
34f159a 280
        name: ty.name.clone(),
34f159a 281
        generics: ty.generics.iter().map(|g| substitute_type(g, subst)).collect(),
34f159a 282
    }
34f159a 283
}
34f159a 284
34f159a 285
/// Mangles a generic item's base name and its resolved concrete type arguments
34f159a 286
/// (in the item's own generic-parameter declaration order) into the internal name
34f159a 287
/// used for its specialized copy, e.g. `Box` + `[Int]` -> `"Box$Int"`.
34f159a 288
pub fn mangle(base: &str, type_args: &[PlumType]) -> String {
34f159a 289
    let mut out = base.to_string();
34f159a 290
    for t in type_args {
34f159a 291
        out.push('$');
34f159a 292
        out.push_str(&t.to_string());
34f159a 293
    }
34f159a 294
    out
34f159a 295
}
34f159a 296
34f159a 297
/// Produces a concrete, specialized copy of a generic class under `mangled_name`,
34f159a 298
/// substituting every field whose declared type names one of the class's generic
34f159a 299
/// parameters with its resolved concrete type. The class's own `generics` list is
34f159a 300
/// cleared on the copy (it is now fully concrete).
34f159a 301
pub fn specialize_class(c: &ast::Class, subst: &Substitution, mangled_name: &str) -> ast::Class {
34f159a 302
    ast::Class {
34f159a 303
        name: mangled_name.to_string(),
34f159a 304
        implements: c.implements.clone(),
34f159a 305
        generics: vec![],
34f159a 306
        fields: c.fields.iter().map(|f| ast::Field {
34f159a 307
            name: f.name.clone(),
34f159a 308
            ty: substitute_type(&f.ty, subst),
34f159a 309
        }).collect(),
34f159a 310
    }
34f159a 311
}
34f159a 312
34f159a 313
/// Produces a concrete, specialized copy of a generic function (or method) under
34f159a 314
/// `mangled_name`. `new_type_param` overrides the receiver-type name for a method
34f159a 315
/// whose receiver class was itself specialized (e.g. a method declared on `Box`
34f159a 316
/// becomes a method on `Box$Int`); pass the original `f.type_param.clone()`
34f159a 317
/// unchanged for a plain free function. The body is left structurally identical
34f159a 318
/// here — its own call sites are rewritten separately (Task 2), since expressions
34f159a 319
/// don't carry declared-type annotations the way fields/params/return types do.
34f159a 320
pub fn specialize_fn(f: &ast::Fn, subst: &Substitution, mangled_name: &str, new_type_param: Option<String>) -> ast::Fn {
34f159a 321
    ast::Fn {
34f159a 322
        name: mangled_name.to_string(),
34f159a 323
        type_param: new_type_param,
34f159a 324
        params: f.params.iter().map(|p| ast::Param {
34f159a 325
            name: p.name.clone(),
34f159a 326
            ty: match &p.ty {
34f159a 327
                ast::ParamType::Type(t) => ast::ParamType::Type(substitute_type(t, subst)),
34f159a 328
                ast::ParamType::Variadic(t) => ast::ParamType::Variadic(substitute_type(t, subst)),
34f159a 329
            },
34f159a 330
            default: p.default.clone(),
34f159a 331
        }).collect(),
34f159a 332
        returns: f.returns.as_ref().map(|r| {
34f159a 333
            let substituted = substitute_type(&ast::Type { name: r.name.clone(), generics: vec![] }, subst);
34f159a 334
            ast::ReturnType { name: substituted.name, generics: vec![] }
34f159a 335
        }),
34f159a 336
        body: f.body.clone(),
34f159a 337
    }
34f159a 338
}
34f159a 339
34f159a 340
/// Produces a concrete, specialized copy of a generic enum under `mangled_name`,
34f159a 341
/// substituting every variant field type name that matches one of the enum's
34f159a 342
/// generic parameters with its resolved concrete type's name.
34f159a 343
pub fn specialize_enum(e: &ast::Enum, subst: &Substitution, mangled_name: &str) -> ast::Enum {
34f159a 344
    ast::Enum {
34f159a 345
        name: mangled_name.to_string(),
34f159a 346
        variants: e.variants.iter().map(|v| ast::EnumVariant {
34f159a 347
            name: v.name.clone(),
34f159a 348
            fields: v.fields.iter().map(|f| {
34f159a 349
                subst.get(f).map(|t| t.to_string()).unwrap_or_else(|| f.clone())
34f159a 350
            }).collect(),
34f159a 351
        }).collect(),
34f159a 352
    }
34f159a 353
}
34f159a 354
```
34f159a 355
34f159a 356
- [ ] **Step 4: Register the module**
34f159a 357
34f159a 358
In `plum-checker/src/lib.rs`, add near the top (alongside the existing `pub mod types;`):
34f159a 359
34f159a 360
```rust
34f159a 361
pub mod monomorphize;
34f159a 362
```
34f159a 363
34f159a 364
- [ ] **Step 5: Run the tests**
34f159a 365
34f159a 366
Run: `cargo test -p plum-checker --test monomorphize_tests`
34f159a 367
Expected: all 9 tests pass.
34f159a 368
34f159a 369
- [ ] **Step 6: Run the full checker crate suite**
34f159a 370
34f159a 371
Run: `cargo test -p plum-checker`
34f159a 372
Expected: green (this new module isn't wired into `check_source` yet, so no existing behavior is affected).
34f159a 373
34f159a 374
- [ ] **Step 7: Commit**
34f159a 375
34f159a 376
```bash
34f159a 377
git add plum-checker/src/monomorphize.rs plum-checker/src/lib.rs plum-checker/tests/monomorphize_tests.rs
34f159a 378
git commit -m "feat(plum-checker): core generics specialization primitives"
34f159a 379
```
34f159a 380
34f159a 381
---
34f159a 382
34f159a 383
### Task 2: Instantiation-site collection, worklist driver, and pipeline integration
34f159a 384
34f159a 385
**Files:**
34f159a 386
- Modify: `plum-checker/src/monomorphize.rs` (append to the file from Task 1)
34f159a 387
- Modify: `plum-checker/src/lib.rs` (`check_source`)
34f159a 388
- Modify: `plum-wasm-codegen/src/lib.rs` (`compile_source`)
34f159a 389
- Test: `plum-checker/tests/checker_tests.rs`
34f159a 390
34f159a 391
**Interfaces:**
34f159a 392
- Consumes: Task 1's `class_generic_params`/`fn_generic_params`/`enum_generic_params`/`Substitution`/`mangle`/`specialize_class`/`specialize_fn`/`specialize_enum`; `plum_checker`'s existing `build_global_tables`, `infer_expr`, `unify`, `CheckCtx`, `plum_type_from_ast`, `types::{TypeEnv, TypeScheme, PlumType}`.
34f159a 393
- Produces: `pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String>` — the single entry point Tasks 3 and both pipeline integrations depend on. Its output is a plain `ast::Source` containing zero generic syntax (every generic `Class`/`Fn`/`Enum` template is replaced by zero or more mangled concrete specializations, and every non-generic item's body has its call sites rewritten to reference those mangled names where needed).
34f159a 394
34f159a 395
- [ ] **Step 1: Write failing checker tests for the end-to-end behavior**
34f159a 396
34f159a 397
Append to `plum-checker/tests/checker_tests.rs`:
34f159a 398
34f159a 399
```rust
34f159a 400
#[test]
34f159a 401
fn generic_class_instantiated_at_two_concrete_types_type_checks() {
34f159a 402
    let src = "\
34f159a 403
type Box(a) =
34f159a 404
  value: a
34f159a 405
34f159a 406
makeIntBox() -> Box =
34f159a 407
  Box(value: 5)
34f159a 408
34f159a 409
makeStrBox() -> Box =
34f159a 410
  Box(value: \"x\")
34f159a 411
";
34f159a 412
    let source = parse(src);
34f159a 413
    let result = check_source(&source);
34f159a 414
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
34f159a 415
}
34f159a 416
34f159a 417
#[test]
34f159a 418
fn generic_function_called_with_different_concrete_types_per_site_type_checks() {
34f159a 419
    let src = "\
34f159a 420
wrap(value: a) -> Bool =
34f159a 421
  True
34f159a 422
34f159a 423
useInt() -> Bool =
34f159a 424
  wrap(5)
34f159a 425
34f159a 426
useStr() -> Bool =
34f159a 427
  wrap(\"x\")
34f159a 428
";
34f159a 429
    let source = parse(src);
34f159a 430
    let result = check_source(&source);
34f159a 431
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
34f159a 432
}
34f159a 433
34f159a 434
#[test]
34f159a 435
fn generic_function_with_two_independent_type_params_type_checks() {
34f159a 436
    let src = "\
34f159a 437
pair(first: a, second: b) -> Bool =
34f159a 438
  True
34f159a 439
34f159a 440
use() -> Bool =
34f159a 441
  pair(1, \"x\")
34f159a 442
";
34f159a 443
    let source = parse(src);
34f159a 444
    let result = check_source(&source);
34f159a 445
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
34f159a 446
}
34f159a 447
34f159a 448
#[test]
34f159a 449
fn generic_method_on_generic_class_type_checks() {
34f159a 450
    let src = "\
34f159a 451
type Box(a) =
34f159a 452
  value: a
34f159a 453
34f159a 454
getValue<Box>() -> a =
34f159a 455
  self.value
34f159a 456
34f159a 457
use() -> Int =
34f159a 458
  b = Box(value: 5)
34f159a 459
  b.getValue()
34f159a 460
";
34f159a 461
    let source = parse(src);
34f159a 462
    let result = check_source(&source);
34f159a 463
    assert!(result.is_ok(), "expected Ok, got {:?}", result.err());
34f159a 464
}
34f159a 465
```
34f159a 466
34f159a 467
- [ ] **Step 2: Run to see them fail**
34f159a 468
34f159a 469
Run: `cargo test -p plum-checker --test checker_tests generic_`
34f159a 470
Expected: all four fail or error, since `check_source` doesn't call `monomorphize_source` yet (a generic field/param type name like `a` is currently just permissively accepted as an unmodeled type name everywhere — these specific tests may currently happen to pass permissively too; if any already pass, note that in your report, but proceed with the implementation regardless since the goal is *correct* resolution, not merely "doesn't error").
34f159a 471
34f159a 472
- [ ] **Step 3: Append the `Monomorphizer` driver to `plum-checker/src/monomorphize.rs`**
34f159a 473
34f159a 474
```rust
34f159a 475
use std::collections::BTreeSet;
34f159a 476
use crate::types::{TypeEnv, TypeScheme};
34f159a 477
use crate::{ClassEnv, MethodEnv, EnumVariants, CheckCtx};
34f159a 478
34f159a 479
enum PendingSpecialization<'a> {
34f159a 480
    Class { base: &'a ast::Class, subst: Substitution, mangled: String },
34f159a 481
    Fn { base: &'a ast::Fn, subst: Substitution, mangled: String, new_receiver: Option<String> },
34f159a 482
    Enum { base: &'a ast::Enum, subst: Substitution, mangled: String },
34f159a 483
}
34f159a 484
34f159a 485
struct Monomorphizer<'a> {
34f159a 486
    classes_generic: BTreeMap<String, &'a ast::Class>,
34f159a 487
    fns_generic: BTreeMap<String, &'a ast::Fn>,
34f159a 488
    methods_generic_on: BTreeMap<String, Vec<&'a ast::Fn>>,
34f159a 489
    global_env: TypeEnv,
34f159a 490
    classes: ClassEnv,
34f159a 491
    methods: MethodEnv,
34f159a 492
    enum_variants: EnumVariants,
34f159a 493
    specialized: BTreeSet<String>,
34f159a 494
    enqueued: BTreeSet<String>,
34f159a 495
    worklist: Vec<PendingSpecialization<'a>>,
34f159a 496
    produced: Vec<ast::Item>,
34f159a 497
}
34f159a 498
34f159a 499
impl<'a> Monomorphizer<'a> {
34f159a 500
    fn infer(&self, e: &ast::Expr, env: &TypeEnv) -> PlumType {
34f159a 501
        let ctx = CheckCtx { classes: &self.classes, methods: &self.methods, enum_variants: &self.enum_variants };
34f159a 502
        crate::infer_expr(e, env, &ctx).unwrap_or(PlumType::TVar("_".to_string()))
34f159a 503
    }
34f159a 504
34f159a 505
    fn rewrite_fn_body(&mut self, f: &mut ast::Fn) -> Result<(), String> {
34f159a 506
        let mut env = self.global_env.clone();
34f159a 507
        if let Some(recv) = &f.type_param {
34f159a 508
            env.insert("self".to_string(), TypeScheme::mono(PlumType::TNamed(recv.clone())));
34f159a 509
        }
34f159a 510
        for p in &f.params {
34f159a 511
            let ty = match &p.ty {
34f159a 512
                ast::ParamType::Type(t) => crate::plum_type_from_ast(t),
34f159a 513
                ast::ParamType::Variadic(t) => crate::plum_type_from_ast(t),
34f159a 514
            };
34f159a 515
            env.insert(p.name.clone(), TypeScheme::mono(ty));
34f159a 516
        }
34f159a 517
        match &mut f.body {
34f159a 518
            ast::FnBody::Expr(e) => self.rewrite_expr(e, &env)?,
34f159a 519
            ast::FnBody::Block(block) => self.rewrite_block(block, &mut env)?,
34f159a 520
        }
34f159a 521
        Ok(())
34f159a 522
    }
34f159a 523
34f159a 524
    fn rewrite_block(&mut self, block: &mut ast::Block, env: &mut TypeEnv) -> Result<(), String> {
34f159a 525
        for stmt in &mut block.stmts {
34f159a 526
            self.rewrite_stmt(stmt, env)?;
34f159a 527
        }
34f159a 528
        Ok(())
34f159a 529
    }
34f159a 530
34f159a 531
    fn rewrite_stmt(&mut self, stmt: &mut ast::Stmt, env: &mut TypeEnv) -> Result<(), String> {
34f159a 532
        match stmt {
34f159a 533
            ast::Stmt::Assign(a) => {
34f159a 534
                for (target, value) in a.targets.iter().zip(a.values.iter_mut()) {
34f159a 535
                    self.rewrite_expr(value, env)?;
34f159a 536
                    let ty = self.infer(value, env);
34f159a 537
                    env.insert(target.clone(), TypeScheme::mono(ty));
34f159a 538
                }
34f159a 539
            }
34f159a 540
            ast::Stmt::Return(Some(e)) => self.rewrite_expr(e, env)?,
34f159a 541
            ast::Stmt::Return(None) => {}
34f159a 542
            ast::Stmt::If(if_) => {
34f159a 543
                self.rewrite_expr(&mut if_.condition, env)?;
34f159a 544
                self.rewrite_block(&mut if_.body, &mut env.clone())?;
34f159a 545
                for ei in &mut if_.else_ifs {
34f159a 546
                    self.rewrite_expr(&mut ei.condition, env)?;
34f159a 547
                    self.rewrite_block(&mut ei.body, &mut env.clone())?;
34f159a 548
                }
34f159a 549
                if let Some(else_block) = &mut if_.else_ {
34f159a 550
                    self.rewrite_block(else_block, &mut env.clone())?;
34f159a 551
                }
34f159a 552
            }
34f159a 553
            ast::Stmt::While(w) => {
34f159a 554
                self.rewrite_expr(&mut w.condition, env)?;
34f159a 555
                self.rewrite_block(&mut w.body, &mut env.clone())?;
34f159a 556
            }
34f159a 557
            ast::Stmt::For(f) => {
34f159a 558
                self.rewrite_expr(&mut f.iter, env)?;
34f159a 559
                let mut inner = env.clone();
34f159a 560
                for v in &f.vars {
34f159a 561
                    inner.insert(v.clone(), TypeScheme::mono(PlumType::TInt));
34f159a 562
                }
34f159a 563
                self.rewrite_block(&mut f.body, &mut inner)?;
34f159a 564
            }
34f159a 565
            ast::Stmt::Expr(e) => self.rewrite_expr(e, env)?,
34f159a 566
            ast::Stmt::Assert(e) => self.rewrite_expr(e, env)?,
34f159a 567
            ast::Stmt::Match(m) => {
34f159a 568
                for s in &mut m.subjects {
34f159a 569
                    self.rewrite_expr(s, env)?;
34f159a 570
                }
34f159a 571
                let subject_ty = m.subjects.first().map(|s| self.infer(s, env)).unwrap_or(PlumType::TInt);
34f159a 572
                for case in &mut m.cases {
34f159a 573
                    let mut case_env = env.clone();
34f159a 574
                    if let Some(ast::CasePattern::Name(n)) = case.patterns.first() {
34f159a 575
                        let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
34f159a 576
                            && self.enum_variants.contains_key(n);
34f159a 577
                        if !is_variant {
34f159a 578
                            case_env.insert(n.clone(), TypeScheme::mono(subject_ty.clone()));
34f159a 579
                        }
34f159a 580
                    }
34f159a 581
                    self.rewrite_block(&mut case.body, &mut case_env)?;
34f159a 582
                }
34f159a 583
            }
34f159a 584
            ast::Stmt::Break | ast::Stmt::Continue | ast::Stmt::Todo => {}
34f159a 585
        }
34f159a 586
        Ok(())
34f159a 587
    }
34f159a 588
34f159a 589
    fn resolve_class_instantiation(&mut self, call: &mut ast::ClassCall, env: &TypeEnv) -> Result<(), String> {
34f159a 590
        let Some(class) = self.classes_generic.get(call.type_name.as_str()).copied() else { return Ok(()) };
34f159a 591
        let params = class_generic_params(class);
34f159a 592
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
34f159a 593
        for gp in &params {
34f159a 594
            if let Some(field) = class.fields.iter().find(|f| f.ty.name == *gp) {
34f159a 595
                if let Some(fa) = call.fields.iter().find(|fa| fa.name == field.name) {
34f159a 596
                    bindings.insert(gp.clone(), self.infer(&fa.value, env));
34f159a 597
                }
34f159a 598
            }
34f159a 599
        }
34f159a 600
        if bindings.len() != params.len() {
34f159a 601
            return Err(format!(
34f159a 602
                "monomorphize: could not resolve all generic parameters for '{}' at this call site",
34f159a 603
                call.type_name
34f159a 604
            ));
34f159a 605
        }
34f159a 606
        let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
34f159a 607
        let mangled = mangle(&call.type_name, &type_args);
34f159a 608
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
34f159a 609
            self.enqueued.insert(mangled.clone());
34f159a 610
            self.worklist.push(PendingSpecialization::Class { base: class, subst: Substitution(bindings), mangled: mangled.clone() });
34f159a 611
        }
34f159a 612
        call.type_name = mangled;
34f159a 613
        Ok(())
34f159a 614
    }
34f159a 615
34f159a 616
    fn resolve_fn_instantiation(&mut self, call: &mut ast::FnCall, env: &TypeEnv) -> Result<(), String> {
34f159a 617
        let Some(f) = self.fns_generic.get(call.name.as_str()).copied() else { return Ok(()) };
34f159a 618
        let params = fn_generic_params(f);
34f159a 619
        let mut bindings: BTreeMap<String, PlumType> = BTreeMap::new();
34f159a 620
        for (param, arg) in f.params.iter().zip(call.args.iter()) {
34f159a 621
            let gp = match &param.ty {
34f159a 622
                ast::ParamType::Type(t) => t.name.clone(),
34f159a 623
                ast::ParamType::Variadic(t) => t.name.clone(),
34f159a 624
            };
34f159a 625
            if params.contains(&gp) {
34f159a 626
                let arg_expr = match arg {
34f159a 627
                    ast::Arg::Positional(e) => e,
34f159a 628
                    ast::Arg::Keyword { value, .. } => value,
34f159a 629
                    ast::Arg::Pair { value, .. } => value,
34f159a 630
                };
34f159a 631
                bindings.entry(gp).or_insert_with(|| self.infer(arg_expr, env));
34f159a 632
            }
34f159a 633
        }
34f159a 634
        if bindings.len() != params.len() {
34f159a 635
            return Err(format!(
34f159a 636
                "monomorphize: could not resolve all generic parameters for '{}' at this call site",
34f159a 637
                call.name
34f159a 638
            ));
34f159a 639
        }
34f159a 640
        let type_args: Vec<PlumType> = params.iter().map(|p| bindings[p].clone()).collect();
34f159a 641
        let mangled = mangle(&call.name, &type_args);
34f159a 642
        if !self.specialized.contains(&mangled) && !self.enqueued.contains(&mangled) {
34f159a 643
            self.enqueued.insert(mangled.clone());
34f159a 644
            self.worklist.push(PendingSpecialization::Fn { base: f, subst: Substitution(bindings), mangled: mangled.clone(), new_receiver: None });
34f159a 645
        }
34f159a 646
        call.name = mangled;
34f159a 647
        Ok(())
34f159a 648
    }
34f159a 649
34f159a 650
    fn rewrite_expr(&mut self, expr: &mut ast::Expr, env: &TypeEnv) -> Result<(), String> {
34f159a 651
        match expr {
34f159a 652
            ast::Expr::ClassCall(call) => {
34f159a 653
                for fa in &mut call.fields {
34f159a 654
                    self.rewrite_expr(&mut fa.value, env)?;
34f159a 655
                }
34f159a 656
                self.resolve_class_instantiation(call, env)?;
34f159a 657
            }
34f159a 658
            ast::Expr::FnCall(call) => {
34f159a 659
                for arg in &mut call.args {
34f159a 660
                    let e = match arg {
34f159a 661
                        ast::Arg::Positional(e) => e,
34f159a 662
                        ast::Arg::Keyword { value, .. } => value,
34f159a 663
                        ast::Arg::Pair { value, .. } => value,
34f159a 664
                    };
34f159a 665
                    self.rewrite_expr(e, env)?;
34f159a 666
                }
34f159a 667
                self.resolve_fn_instantiation(call, env)?;
34f159a 668
            }
34f159a 669
            ast::Expr::Attribute(attr) => {
34f159a 670
                self.rewrite_expr(&mut attr.object, env)?;
34f159a 671
                if let ast::AttrKind::Method(call) = &mut attr.attr {
34f159a 672
                    for arg in &mut call.args {
34f159a 673
                        let e = match arg {
34f159a 674
                            ast::Arg::Positional(e) => e,
34f159a 675
                            ast::Arg::Keyword { value, .. } => value,
34f159a 676
                            ast::Arg::Pair { value, .. } => value,
34f159a 677
                        };
34f159a 678
                        self.rewrite_expr(e, env)?;
34f159a 679
                    }
34f159a 680
                    // Method dispatch on a specialized receiver needs no rewrite here:
34f159a 681
                    // once the receiver's construction site is rewritten to its mangled
34f159a 682
                    // class name, the receiver's inferred static type IS that mangled
34f159a 683
                    // name, and the specialized methods were registered under exactly
34f159a 684
                    // that (mangled receiver, method name) key when their class was
34f159a 685
                    // specialized (see the `PendingSpecialization::Class` arm below).
34f159a 686
                }
34f159a 687
            }
34f159a 688
            ast::Expr::Binary(b) => { self.rewrite_expr(&mut b.left, env)?; self.rewrite_expr(&mut b.right, env)?; }
34f159a 689
            ast::Expr::Bool(b) => { self.rewrite_expr(&mut b.left, env)?; self.rewrite_expr(&mut b.right, env)?; }
34f159a 690
            ast::Expr::Compare(c) => { self.rewrite_expr(&mut c.left, env)?; self.rewrite_expr(&mut c.right, env)?; }
34f159a 691
            ast::Expr::Not(inner) => self.rewrite_expr(inner, env)?,
34f159a 692
            ast::Expr::Unary(u) => self.rewrite_expr(&mut u.operand, env)?,
34f159a 693
            ast::Expr::Paren(inner) => self.rewrite_expr(inner, env)?,
34f159a 694
            ast::Expr::Ternary(t) => {
34f159a 695
                self.rewrite_expr(&mut t.condition, env)?;
34f159a 696
                self.rewrite_expr(&mut t.then, env)?;
34f159a 697
                self.rewrite_expr(&mut t.else_, env)?;
34f159a 698
            }
34f159a 699
            ast::Expr::Int(_) | ast::Expr::Float(_) | ast::Expr::String(_)
34f159a 700
            | ast::Expr::Self_ | ast::Expr::Var(_) | ast::Expr::TypeName(_) => {}
34f159a 701
        }
34f159a 702
        Ok(())
34f159a 703
    }
34f159a 704
}
34f159a 705
34f159a 706
/// Runs the whole generics-monomorphization pass over `source`, producing a plain,
34f159a 707
/// fully-concrete `ast::Source` with every generic `Class`/`Fn`/`Enum` template
34f159a 708
/// replaced by zero or more mangled concrete specializations, and every remaining
34f159a 709
/// item's body rewritten so its call sites reference those mangled names. The
34f159a 710
/// result has no generic syntax left in it — `check_source`/`compile_source` run
34f159a 711
/// on it completely unmodified.
34f159a 712
pub fn monomorphize_source(source: &ast::Source) -> Result<ast::Source, String> {
34f159a 713
    let (global_env, classes, methods, enum_variants) = crate::build_global_tables(source);
34f159a 714
34f159a 715
    let mut m = Monomorphizer {
34f159a 716
        classes_generic: BTreeMap::new(),
34f159a 717
        fns_generic: BTreeMap::new(),
34f159a 718
        methods_generic_on: BTreeMap::new(),
34f159a 719
        global_env,
34f159a 720
        classes,
34f159a 721
        methods,
34f159a 722
        enum_variants,
34f159a 723
        specialized: BTreeSet::new(),
34f159a 724
        enqueued: BTreeSet::new(),
34f159a 725
        worklist: Vec::new(),
34f159a 726
        produced: Vec::new(),
34f159a 727
    };
34f159a 728
34f159a 729
    for item in &source.items {
34f159a 730
        match item {
34f159a 731
            ast::Item::Class(c) if !c.generics.is_empty() => { m.classes_generic.insert(c.name.clone(), c); }
34f159a 732
            _ => {}
34f159a 733
        }
34f159a 734
    }
34f159a 735
    for item in &source.items {
34f159a 736
        if let ast::Item::Fn(f) = item {
34f159a 737
            let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
34f159a 738
            if receiver_is_generic {
34f159a 739
                m.methods_generic_on.entry(f.type_param.clone().unwrap()).or_default().push(f);
34f159a 740
            } else if f.type_param.is_none() && !fn_generic_params(f).is_empty() {
34f159a 741
                m.fns_generic.insert(f.name.clone(), f);
34f159a 742
            }
34f159a 743
            // A method whose receiver is NOT generic is left as a regular method below,
34f159a 744
            // even if its own params/return happen to use a bare lowercase-letter type
34f159a 745
            // name — that shape (a method introducing its own extra generic parameter)
34f159a 746
            // is out of scope for this pass; see the plan's Global Constraints.
34f159a 747
        }
34f159a 748
    }
34f159a 749
34f159a 750
    for item in &source.items {
34f159a 751
        match item {
34f159a 752
            ast::Item::Class(c) if c.generics.is_empty() => m.produced.push(ast::Item::Class(c.clone())),
34f159a 753
            ast::Item::Enum(e) if enum_generic_params(e).is_empty() => m.produced.push(ast::Item::Enum(e.clone())),
34f159a 754
            ast::Item::Const(c) => m.produced.push(ast::Item::Const(c.clone())),
34f159a 755
            ast::Item::Trait(t) => m.produced.push(ast::Item::Trait(t.clone())),
34f159a 756
            ast::Item::Fn(f) => {
34f159a 757
                let receiver_is_generic = f.type_param.as_deref().map(|r| m.classes_generic.contains_key(r)).unwrap_or(false);
34f159a 758
                let is_generic_fn = f.type_param.is_none() && !fn_generic_params(f).is_empty();
34f159a 759
                if !receiver_is_generic && !is_generic_fn {
34f159a 760
                    let mut f2 = f.clone();
34f159a 761
                    m.rewrite_fn_body(&mut f2)?;
34f159a 762
                    m.produced.push(ast::Item::Fn(f2));
34f159a 763
                }
34f159a 764
            }
34f159a 765
            _ => {} // generic Class/Enum declarations dropped here — templates only
34f159a 766
        }
34f159a 767
    }
34f159a 768
34f159a 769
    let mut guard = 0usize;
34f159a 770
    while let Some(pending) = m.worklist.pop() {
34f159a 771
        guard += 1;
34f159a 772
        if guard > 10_000 {
34f159a 773
            return Err("monomorphize: exceeded specialization limit (possible unbounded generic recursion)".to_string());
34f159a 774
        }
34f159a 775
        match pending {
34f159a 776
            PendingSpecialization::Class { base, subst, mangled } => {
34f159a 777
                if !m.specialized.insert(mangled.clone()) { continue; }
34f159a 778
                m.produced.push(ast::Item::Class(specialize_class(base, &subst, &mangled)));
34f159a 779
                if let Some(methods) = m.methods_generic_on.get(base.name.as_str()).cloned() {
34f159a 780
                    for method in methods {
34f159a 781
                        let mut specialized_method = specialize_fn(method, &subst, &method.name, Some(mangled.clone()));
34f159a 782
                        m.rewrite_fn_body(&mut specialized_method)?;
34f159a 783
                        m.produced.push(ast::Item::Fn(specialized_method));
34f159a 784
                    }
34f159a 785
                }
34f159a 786
            }
34f159a 787
            PendingSpecialization::Fn { base, subst, mangled, new_receiver } => {
34f159a 788
                if !m.specialized.insert(mangled.clone()) { continue; }
34f159a 789
                let mut specialized_fn = specialize_fn(base, &subst, &mangled, new_receiver);
34f159a 790
                m.rewrite_fn_body(&mut specialized_fn)?;
34f159a 791
                m.produced.push(ast::Item::Fn(specialized_fn));
34f159a 792
            }
34f159a 793
            PendingSpecialization::Enum { base, subst, mangled } => {
34f159a 794
                if !m.specialized.insert(mangled.clone()) { continue; }
34f159a 795
                m.produced.push(ast::Item::Enum(specialize_enum(base, &subst, &mangled)));
34f159a 796
            }
34f159a 797
        }
34f159a 798
    }
34f159a 799
34f159a 800
    Ok(ast::Source { module: source.module.clone(), imports: source.imports.clone(), items: m.produced })
34f159a 801
}
34f159a 802
```
34f159a 803
34f159a 804
- [ ] **Step 4: Wire the pass into `check_source`**
34f159a 805
34f159a 806
In `plum-checker/src/lib.rs`, replace:
34f159a 807
34f159a 808
```rust
34f159a 809
pub fn check_source(source: &ast::Source) -> CheckResult<()> {
34f159a 810
    let mut errors: Vec<CheckError> = Vec::new();
34f159a 811
    let (global_env, classes, methods, enum_variants) = build_global_tables(source);
34f159a 812
    let ctx = CheckCtx { classes: &classes, methods: &methods, enum_variants: &enum_variants };
34f159a 813
34f159a 814
    for item in &source.items {
34f159a 815
        if let ast::Item::Fn(f) = item {
34f159a 816
            let mut local_errors = check_fn(f, &global_env, &ctx);
34f159a 817
            errors.append(&mut local_errors);
34f159a 818
        }
34f159a 819
    }
34f159a 820
34f159a 821
    if errors.is_empty() { Ok(()) } else { Err(errors) }
34f159a 822
}
34f159a 823
```
34f159a 824
34f159a 825
with:
34f159a 826
34f159a 827
```rust
34f159a 828
pub fn check_source(source: &ast::Source) -> CheckResult<()> {
34f159a 829
    let source = monomorphize::monomorphize_source(source).map_err(|e| vec![CheckError { message: e }])?;
34f159a 830
    let mut errors: Vec<CheckError> = Vec::new();
34f159a 831
    let (global_env, classes, methods, enum_variants) = build_global_tables(&source);
34f159a 832
    let ctx = CheckCtx { classes: &classes, methods: &methods, enum_variants: &enum_variants };
34f159a 833
34f159a 834
    for item in &source.items {
34f159a 835
        if let ast::Item::Fn(f) = item {
34f159a 836
            let mut local_errors = check_fn(f, &global_env, &ctx);
34f159a 837
            errors.append(&mut local_errors);
34f159a 838
        }
34f159a 839
    }
34f159a 840
34f159a 841
    if errors.is_empty() { Ok(()) } else { Err(errors) }
34f159a 842
}
34f159a 843
```
34f159a 844
34f159a 845
- [ ] **Step 5: Wire the pass into `compile_source`**
34f159a 846
34f159a 847
In `plum-wasm-codegen/src/lib.rs`, find `pub fn compile_source(source: &ast::Source) -> Result<Vec<u8>, String> {` and add, as its very first line:
34f159a 848
34f159a 849
```rust
34f159a 850
    let source = &plum_checker::monomorphize::monomorphize_source(source)?;
34f159a 851
```
34f159a 852
34f159a 853
(The rest of the function already only ever reads `source.items`/`&source` by reference, so rebinding the parameter name to this new owned-then-reborrowed value requires no other changes in the function body.)
34f159a 854
34f159a 855
- [ ] **Step 6: Run the new checker tests**
34f159a 856
34f159a 857
Run: `cargo test -p plum-checker --test checker_tests generic_`
34f159a 858
Expected: all four pass.
34f159a 859
34f159a 860
- [ ] **Step 7: Run the full workspace suite**
34f159a 861
34f159a 862
Run: `cargo test --workspace`
34f159a 863
Expected: green — in particular, confirm no existing test that uses a generic-looking type name in a way this pass now treats differently regresses (e.g. `examples/types.plum`'s `Box(a)`/`Comparable(a: Ord)` declarations, which have zero instantiation sites anywhere in that file, should simply be dropped from the monomorphized output with no error, since `examples_test.rs` only requires the file to type-check, not that every declared item survives).
34f159a 864
34f159a 865
- [ ] **Step 8: Commit**
34f159a 866
34f159a 867
```bash
34f159a 868
git add plum-checker/src/monomorphize.rs plum-checker/src/lib.rs plum-wasm-codegen/src/lib.rs plum-checker/tests/checker_tests.rs
34f159a 869
git commit -m "feat(plum-checker): monomorphize generic classes/functions/methods/enums before checking and codegen"
34f159a 870
```
34f159a 871
34f159a 872
---
34f159a 873
34f159a 874
### Task 3: Codegen tests, pathological-recursion guard test, examples, and docs
34f159a 875
34f159a 876
**Files:**
34f159a 877
- Test: `plum-wasm-codegen/tests/codegen_tests.rs`
34f159a 878
- Test: `plum-checker/tests/checker_tests.rs` (one more test)
34f159a 879
- Modify: `examples/types.plum`, `examples/functions.plum` (or a new example file — see Step 3)
34f159a 880
- Modify: `README.md`
34f159a 881
34f159a 882
**Interfaces:**
34f159a 883
- Consumes: Task 2's `monomorphize_source`, wired transparently into `compile_source`.
34f159a 884
- Produces: nothing further downstream — this is the final integration/documentation task.
34f159a 885
34f159a 886
- [ ] **Step 1: Write codegen tests proving the feature end-to-end**
34f159a 887
34f159a 888
Append to `plum-wasm-codegen/tests/codegen_tests.rs`:
34f159a 889
34f159a 890
```rust
34f159a 891
#[test]
34f159a 892
fn generic_class_specialized_at_two_types_does_not_alias() {
34f159a 893
    let src = "\
34f159a 894
type Box(a) =
34f159a 895
  value: a
34f159a 896
34f159a 897
getIntValue<Box>() -> Int =
34f159a 898
  self.value
34f159a 899
34f159a 900
useInt() -> Int =
34f159a 901
  b = Box(value: 7)
34f159a 902
  b.getIntValue()
34f159a 903
34f159a 904
main() -> Int =
34f159a 905
  useInt()
34f159a 906
";
34f159a 907
    let source = parse(src);
34f159a 908
    let bytes = compile_source(&source).expect("compile failed");
34f159a 909
    assert_eq!(run_main(&bytes), 7);
34f159a 910
}
34f159a 911
34f159a 912
#[test]
34f159a 913
fn generic_function_called_at_multiple_concrete_types_runs_correctly() {
34f159a 914
    let src = "\
34f159a 915
identity(value: a) -> a =
34f159a 916
  value
34f159a 917
34f159a 918
main() -> Int =
34f159a 919
  identity(5) + identity(37)
34f159a 920
";
34f159a 921
    let source = parse(src);
34f159a 922
    let bytes = compile_source(&source).expect("compile failed");
34f159a 923
    assert_eq!(run_main(&bytes), 42);
34f159a 924
}
34f159a 925
34f159a 926
#[test]
34f159a 927
fn generic_method_on_generic_class_runs_correctly() {
34f159a 928
    let src = "\
34f159a 929
type Box(a) =
34f159a 930
  value: a
34f159a 931
34f159a 932
getValue<Box>() -> Int =
34f159a 933
  self.value
34f159a 934
34f159a 935
main() -> Int =
34f159a 936
  b = Box(value: 9)
34f159a 937
  b.getValue()
34f159a 938
";
34f159a 939
    let source = parse(src);
34f159a 940
    let bytes = compile_source(&source).expect("compile failed");
34f159a 941
    assert_eq!(run_main(&bytes), 9);
34f159a 942
}
34f159a 943
34f159a 944
#[test]
34f159a 945
fn transitively_generic_call_chain_runs_correctly() {
34f159a 946
    let src = "\
34f159a 947
identity(value: a) -> a =
34f159a 948
  value
34f159a 949
34f159a 950
doubled(value: a) -> Int =
34f159a 951
  identity(value) + identity(value)
34f159a 952
34f159a 953
main() -> Int =
34f159a 954
  doubled(21)
34f159a 955
";
34f159a 956
    let source = parse(src);
34f159a 957
    let bytes = compile_source(&source).expect("compile failed");
34f159a 958
    assert_eq!(run_main(&bytes), 42);
34f159a 959
}
34f159a 960
34f159a 961
#[test]
34f159a 962
fn generic_enum_specialized_and_matched_runs_correctly() {
34f159a 963
    let src = "\
34f159a 964
enum Option =
34f159a 965
  | Some(a)
34f159a 966
  | None
34f159a 967
34f159a 968
unwrapOr(o: Option, default: Int) -> Int =
34f159a 969
  match o
34f159a 970
    Some(v) =>
34f159a 971
      v
34f159a 972
    None =>
34f159a 973
      default
34f159a 974
34f159a 975
main() -> Int =
34f159a 976
  unwrapOr(Some(13), 0)
34f159a 977
";
34f159a 978
    let source = parse(src);
34f159a 979
    let bytes = compile_source(&source).expect("compile failed");
34f159a 980
    assert_eq!(run_main(&bytes), 13);
34f159a 981
}
34f159a 982
```
34f159a 983
34f159a 984
- [ ] **Step 2: Write a checker test for the runaway-recursion guard**
34f159a 985
34f159a 986
Append to `plum-checker/tests/checker_tests.rs`:
34f159a 987
34f159a 988
```rust
34f159a 989
#[test]
34f159a 990
fn unbounded_recursive_generic_instantiation_is_a_clear_error() {
34f159a 991
    // `Wrap(a)`'s own field is `Wrap(Box(a))` — every instantiation of `Wrap` at some
34f159a 992
    // concrete `a` requires instantiating it again at `Box(a)`, a strictly larger type,
34f159a 993
    // forever. This must fail with a clear, bounded error rather than hang or panic.
34f159a 994
    let src = "\
34f159a 995
type Box(a) =
34f159a 996
  value: a
34f159a 997
34f159a 998
type Wrap(a) =
34f159a 999
  inner: Wrap(Box(a))
34f159a 1000
34f159a 1001
use() -> Int =
34f159a 1002
  w = Wrap(inner: 5)
34f159a 1003
  1
34f159a 1004
";
34f159a 1005
    let source = parse(src);
34f159a 1006
    let result = check_source(&source);
34f159a 1007
    assert!(result.is_err());
34f159a 1008
    let errs = result.unwrap_err();
34f159a 1009
    assert!(errs[0].message.contains("monomorphize"), "got: {:?}", errs);
34f159a 1010
}
34f159a 1011
```
34f159a 1012
34f159a 1013
Run: `cargo test -p plum-checker --test checker_tests unbounded_recursive`
34f159a 1014
Expected: passes — the `guard > 10_000` cap in `monomorphize_source` (Task 2) returns a clear error well before any real resource exhaustion. If this test instead hangs or the guard doesn't trip because this particular shape doesn't actually reach the worklist the way you expect (e.g. because `Wrap`'s own field type is never a *call site* — it's just a declared, uninstantiated field type on a template that's never itself constructed with a concrete `a` anywhere reachable) — investigate and adjust the repro source until it genuinely exercises an unbounded worklist growth, since the guard's job is to bound runaway *specialization*, not merely to reject any nonsensical declaration.
34f159a 1015
34f159a 1016
- [ ] **Step 3: Extend the examples with real instantiations**
34f159a 1017
34f159a 1018
`examples/types.plum` and `examples/functions.plum` currently only *declare* generic items (no instantiation sites, so the monomorphization pass now correctly drops them with no error, but they're not actually exercised end-to-end). Append to `examples/functions.plum`:
34f159a 1019
34f159a 1020
```plum
34f159a 1021
34f159a 1022
useWrap() -> Bool =
34f159a 1023
  wrap(5)
34f159a 1024
34f159a 1025
usePair() -> Bool =
34f159a 1026
  pair(1, "x")
34f159a 1027
```
34f159a 1028
34f159a 1029
Append to `examples/types.plum`:
34f159a 1030
34f159a 1031
```plum
34f159a 1032
34f159a 1033
makeIntBox() -> Box =
34f159a 1034
  Box(value: 5)
34f159a 1035
34f159a 1036
makeStrBox() -> Box =
34f159a 1037
  Box(value: "x")
34f159a 1038
```
34f159a 1039
34f159a 1040
Run: `cargo test -p plum-checker --test examples_test` and `cargo test -p plum-wasm-codegen --test examples_test`
34f159a 1041
Expected: both pass — `types.plum` and `functions.plum` now exercise real generic instantiation, type-check, and compile.
34f159a 1042
34f159a 1043
- [ ] **Step 4: Update README's Known Gaps**
34f159a 1044
34f159a 1045
In `README.md`, remove this line from the Known Gaps list:
34f159a 1046
34f159a 1047
```markdown
34f159a 1048
- user-defined generics (they type-check but aren't monomorphized) — this also blocks `libs/std`'s actual `Option`/`Result`/`List`/`Map`, which are declared generically
34f159a 1049
```
34f159a 1050
34f159a 1051
and add, in its place:
34f159a 1052
34f159a 1053
```markdown
34f159a 1054
- `libs/std`'s actual `List`/`Map`/`Option`/`Result` still don't compile — they use several other unimplemented features (closures, `Nil`/optional chaining, decorators, colon-arrow return syntax) unrelated to generics, which are themselves now monomorphized and compiled correctly for the currently-documented generic syntax
34f159a 1055
```
34f159a 1056
34f159a 1057
Also update the "Generics" section's prose (currently ends with "There's no monomorphization/codegen for user-defined generics yet — they type-check permissively but don't compile to wasm.") to instead say generics are monomorphized and compiled, and reference the new examples.
34f159a 1058
34f159a 1059
- [ ] **Step 5: Run the full workspace and tree-sitter suites**
34f159a 1060
34f159a 1061
```bash
34f159a 1062
cargo test --workspace
34f159a 1063
cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test
34f159a 1064
```
34f159a 1065
34f159a 1066
Expected: fully green, zero known failures.
34f159a 1067
34f159a 1068
- [ ] **Step 6: Commit**
34f159a 1069
34f159a 1070
```bash
34f159a 1071
git add plum-wasm-codegen/tests/codegen_tests.rs plum-checker/tests/checker_tests.rs examples/types.plum examples/functions.plum README.md
34f159a 1072
git commit -m "test+docs: generics monomorphization complete; extend examples, update known gaps"
34f159a 1073
```