plum
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 ¶ms { |
| 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 ¶m.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 | ``` |