plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
docs/superpowers/plans/2026-07-19-general-enum-support.md
| 2cee306 | 1 | # General Enum Support Implementation Plan |
| 2cee306 | 2 | |
| 2cee306 | 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. |
| 2cee306 | 4 | |
| 2cee306 | 5 | **Goal:** Make arbitrary concrete (non-generic) user-declared `enum`s — both payload-free (`Color = Red | Green | Blue`) and payload-carrying (`Option = Some(Int) | None`, multi-field `Rect(Float, Float)`) — type-check *precisely* and compile to working wasm, for both construction and `match`, generalizing the Bool-only special-casing that exists today. |
| 2cee306 | 6 | |
| 2cee306 | 7 | **Architecture:** Every enum value is a single `i32`: a payload-free variant is its small integer tag directly; a payload variant is a bump-heap pointer to `[tag: i32][field0][field1]...` (same bump-alloc/store/load mechanism already used for class instances). `plum-checker`'s `EnumVariants` table gains a tag number and field types per variant so both the checker and `plum-wasm-codegen` can validate/construct/destructure without re-deriving that data from the AST. A small grammar fix is needed first: today's grammar only lets a capitalized callee use *named*-field syntax (`Cat(name: "x")`), so positional variant calls like `Some(v)` don't actually parse as an expression yet. |
| 2cee306 | 8 | |
| 2cee306 | 9 | **Tech Stack:** Rust (workspace: `plum-core`, `plum-checker`, `plum-wasm-codegen`), tree-sitter grammar (`tooling/tree-sitter-plum`, JS), `wasm-encoder`/`wasmparser`/`wasmtime` for codegen tests. |
| 2cee306 | 10 | |
| 2cee306 | 11 | ## Global Constraints |
| 2cee306 | 12 | |
| 2cee306 | 13 | - Out of scope: generics monomorphization, multi-subject `match`, any pattern kind beyond bare tag / constructor (these remain the documented "Known gaps"). Do not attempt them here. |
| 2cee306 | 14 | - Enum variant sub-patterns inside a constructor pattern (`Some(v)`, `Pair(a, b)`) are flat bindings or `_` wildcards only — no nested constructor patterns. This matches the README's existing description ("binding its argument") and current `libs/std`/`examples` usage. |
| 2cee306 | 15 | - Follow existing code style exactly: this codebase has no doc-comment scaffolding beyond one-line "why" comments: mirror the terse style already in `plum-checker/src/lib.rs` and `plum-wasm-codegen/src/lib.rs`. |
| 2cee306 | 16 | - Every task must leave `cargo test --workspace` and (where grammar changed) `npx --yes tree-sitter-cli test` green before moving to the next task. |
| 2cee306 | 17 | |
| 2cee306 | 18 | --- |
| 2cee306 | 19 | |
| 2cee306 | 20 | ### Task 1: Grammar — positional variant-construction calls (`Some(v)`, `Pair(a, b)`) |
| 2cee306 | 21 | |
| 2cee306 | 22 | **Files:** |
| 2cee306 | 23 | - Modify: `tooling/tree-sitter-plum/grammar.js:55` (conflicts array), `tooling/tree-sitter-plum/grammar.js:387-394` (`fn_call` rule) |
| 2cee306 | 24 | - Test: `tooling/tree-sitter-plum/test/corpus/function.txt` (append a new case) |
| 2cee306 | 25 | |
| 2cee306 | 26 | **Interfaces:** |
| 2cee306 | 27 | - Consumes: nothing from other tasks. |
| 2cee306 | 28 | - Produces: `fn_call` nodes whose `function` field can be a `type_identifier` (not just `var_identifier`). `plum-core`'s `parser.rs::parse_fn_call` (unchanged — it already reads the callee via `self.text(n)` regardless of node kind) will keep parsing these into `ast::FnCall { name, args }` exactly like any other call. |
| 2cee306 | 29 | |
| 2cee306 | 30 | Today, `class_call`'s `class_argument_list` only accepts `name: value` pairs (see `type.txt` corpus: `Cat(name: name, age: 0)`), so a positional capitalized call like `Some(v)` or `Ok(x)` does not currently parse as an expression at all (it only exists as a *pattern*, via `class_pattern`). This task adds that missing expression-level syntax by letting `fn_call`'s callee be either a `var_identifier` or a `type_identifier` — the argument-list grammars already disambiguate cleanly (`class_argument_list` requires `name:`, `fn_argument_list` never does), so this doesn't change parsing of any existing `Cat(name: "x")`-style call. |
| 2cee306 | 31 | |
| 2cee306 | 32 | - [ ] **Step 1: Edit `fn_call` to accept a capitalized callee** |
| 2cee306 | 33 | |
| 2cee306 | 34 | In `tooling/tree-sitter-plum/grammar.js`, change: |
| 2cee306 | 35 | |
| 2cee306 | 36 | ```js |
| 2cee306 | 37 | fn_call: ($) => |
| 2cee306 | 38 | prec(PREC.call, seq( |
| 2cee306 | 39 | field("function", $.var_identifier), |
| 2cee306 | 40 | field( |
| 2cee306 | 41 | "arguments", |
| 2cee306 | 42 | $.fn_argument_list, |
| 2cee306 | 43 | ), |
| 2cee306 | 44 | )), |
| 2cee306 | 45 | ``` |
| 2cee306 | 46 | |
| 2cee306 | 47 | to: |
| 2cee306 | 48 | |
| 2cee306 | 49 | ```js |
| 2cee306 | 50 | fn_call: ($) => |
| 2cee306 | 51 | prec(PREC.call, seq( |
| 2cee306 | 52 | field("function", choice($.var_identifier, $.type_identifier)), |
| 2cee306 | 53 | field( |
| 2cee306 | 54 | "arguments", |
| 2cee306 | 55 | $.fn_argument_list, |
| 2cee306 | 56 | ), |
| 2cee306 | 57 | )), |
| 2cee306 | 58 | ``` |
| 2cee306 | 59 | |
| 2cee306 | 60 | - [ ] **Step 2: Declare the `fn_call`/`class_call` conflict and regenerate** |
| 2cee306 | 61 | |
| 2cee306 | 62 | In the same file, change line 55 from: |
| 2cee306 | 63 | |
| 2cee306 | 64 | ```js |
| 2cee306 | 65 | conflicts: ($) => [], |
| 2cee306 | 66 | ``` |
| 2cee306 | 67 | |
| 2cee306 | 68 | to: |
| 2cee306 | 69 | |
| 2cee306 | 70 | ```js |
| 2cee306 | 71 | conflicts: ($) => [[$.fn_call, $.class_call]], |
| 2cee306 | 72 | ``` |
| 2cee306 | 73 | |
| 2cee306 | 74 | Run: |
| 2cee306 | 75 | |
| 2cee306 | 76 | ```bash |
| 2cee306 | 77 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate |
| 2cee306 | 78 | ``` |
| 2cee306 | 79 | |
| 2cee306 | 80 | Expected: completes with no "Unresolved conflict" errors. (If it reports one anyway, that means the two rules are ambiguous on some input neither of us anticipated — read the reported example carefully before changing anything further; don't just silence it.) |
| 2cee306 | 81 | |
| 2cee306 | 82 | - [ ] **Step 3: Add the input half of a new corpus case** |
| 2cee306 | 83 | |
| 2cee306 | 84 | Append to `tooling/tree-sitter-plum/test/corpus/function.txt`: |
| 2cee306 | 85 | |
| 2cee306 | 86 | ``` |
| 2cee306 | 87 | ================================================================================ |
| 2cee306 | 88 | function - variant construction call (positional args on a capitalized name) |
| 2cee306 | 89 | ================================================================================ |
| 2cee306 | 90 | |
| 2cee306 | 91 | makeSome(v: Int) -> Option = |
| 2cee306 | 92 | Some(v) |
| 2cee306 | 93 | |
| 2cee306 | 94 | -------------------------------------------------------------------------------- |
| 2cee306 | 95 | ``` |
| 2cee306 | 96 | |
| 2cee306 | 97 | (Leave the expected-tree section empty for now — the next step generates it.) |
| 2cee306 | 98 | |
| 2cee306 | 99 | - [ ] **Step 4: Generate the expected tree and verify it** |
| 2cee306 | 100 | |
| 2cee306 | 101 | Run: |
| 2cee306 | 102 | |
| 2cee306 | 103 | ```bash |
| 2cee306 | 104 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test -u -f "variant construction call" |
| 2cee306 | 105 | ``` |
| 2cee306 | 106 | |
| 2cee306 | 107 | Expected: the tool fills in the tree under the separator. Open `test/corpus/function.txt` and confirm the generated tree is `(source (fn (fn_identifier) (param (var_identifier) (type (type_identifier))) (return_type (type_identifier)) (body (primary_expression (fn_call (type_identifier) (fn_argument_list (expression (primary_expression (var_identifier)))))))))` (formatted one-node-per-line as the rest of the corpus already is) — i.e. the callee shows up as `(type_identifier)` inside `fn_call`, not `class_call`, and there is no `ERROR`/`MISSING` node anywhere. |
| 2cee306 | 108 | |
| 2cee306 | 109 | - [ ] **Step 5: Run the full corpus suite** |
| 2cee306 | 110 | |
| 2cee306 | 111 | Run: `cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test` |
| 2cee306 | 112 | Expected: all cases pass, including every pre-existing `class_call` case in `type.txt` (proving the grammar change didn't regress named-field construction). |
| 2cee306 | 113 | |
| 2cee306 | 114 | - [ ] **Step 6: Commit** |
| 2cee306 | 115 | |
| 2cee306 | 116 | ```bash |
| 2cee306 | 117 | git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/test/corpus/function.txt |
| 2cee306 | 118 | git add tooling/tree-sitter-plum/src # generated parser.c etc, if tracked |
| 2cee306 | 119 | git commit -m "feat(tree-sitter-plum): allow positional variant-construction calls" |
| 2cee306 | 120 | ``` |
| 2cee306 | 121 | |
| 2cee306 | 122 | (If `tooling/tree-sitter-plum/src/parser.c` and friends are `.gitignore`d, drop that second `git add` — check `git status` first.) |
| 2cee306 | 123 | |
| 2cee306 | 124 | --- |
| 2cee306 | 125 | |
| 2cee306 | 126 | ### Task 2: Checker — per-variant tag + field types, and a real construction/pattern type check |
| 2cee306 | 127 | |
| 2cee306 | 128 | **Files:** |
| 2cee306 | 129 | - Modify: `plum-checker/src/lib.rs:37-492` |
| 2cee306 | 130 | - Test: `plum-checker/tests/checker_tests.rs` |
| 2cee306 | 131 | |
| 2cee306 | 132 | **Interfaces:** |
| 2cee306 | 133 | - Consumes: nothing from Task 1 at the type level (checker doesn't care about *how* an expression parsed, only its `ast::Expr` shape) — but Task 1 is what makes `Some(v)`-as-`FnCall` reach the checker at all. |
| 2cee306 | 134 | - Produces: |
| 2cee306 | 135 | ```rust |
| 2cee306 | 136 | pub struct EnumVariantInfo { |
| 2cee306 | 137 | pub enum_name: String, |
| 2cee306 | 138 | pub tag: i32, |
| 2cee306 | 139 | pub field_types: Vec<PlumType>, |
| 2cee306 | 140 | } |
| 2cee306 | 141 | pub type EnumVariants = BTreeMap<String, EnumVariantInfo>; |
| 2cee306 | 142 | ``` |
| 2cee306 | 143 | used by Tasks 3 and 4 (`plum-wasm-codegen`) via `ctx.enum_variants.get(name)`. |
| 2cee306 | 144 | |
| 2cee306 | 145 | - [ ] **Step 1: Write failing checker tests for the new behavior** |
| 2cee306 | 146 | |
| 2cee306 | 147 | Append to `plum-checker/tests/checker_tests.rs`: |
| 2cee306 | 148 | |
| 2cee306 | 149 | ```rust |
| 2cee306 | 150 | #[test] |
| 2cee306 | 151 | fn bare_enum_tag_unifies_with_owning_enum_type() { |
| 2cee306 | 152 | // Regression: a bare non-Bool tag like `None` used to type as `TNamed("None")` |
| 2cee306 | 153 | // (itself, not its enum), so comparing it against an `Option` value would wrongly |
| 2cee306 | 154 | // fail with a type mismatch. |
| 2cee306 | 155 | let src = "\ |
| 2cee306 | 156 | enum Option = |
| 2cee306 | 157 | | Some(Int) |
| 2cee306 | 158 | | None |
| 2cee306 | 159 | |
| 2cee306 | 160 | isNone(o: Option) -> Bool = |
| 2cee306 | 161 | o == None |
| 2cee306 | 162 | "; |
| 2cee306 | 163 | let source = parse(src); |
| 2cee306 | 164 | let result = check_source(&source); |
| 2cee306 | 165 | assert!(result.is_ok(), "expected Ok, got {:?}", result.err()); |
| 2cee306 | 166 | } |
| 2cee306 | 167 | |
| 2cee306 | 168 | #[test] |
| 2cee306 | 169 | fn variant_construction_checks_arg_count_and_types() { |
| 2cee306 | 170 | let src = "\ |
| 2cee306 | 171 | enum Option = |
| 2cee306 | 172 | | Some(Int) |
| 2cee306 | 173 | | None |
| 2cee306 | 174 | |
| 2cee306 | 175 | makeSome(v: Int) -> Option = |
| 2cee306 | 176 | Some(v) |
| 2cee306 | 177 | "; |
| 2cee306 | 178 | let source = parse(src); |
| 2cee306 | 179 | let result = check_source(&source); |
| 2cee306 | 180 | assert!(result.is_ok(), "expected Ok, got {:?}", result.err()); |
| 2cee306 | 181 | } |
| 2cee306 | 182 | |
| 2cee306 | 183 | #[test] |
| 2cee306 | 184 | fn variant_construction_wrong_arg_type_is_error() { |
| 2cee306 | 185 | let src = "\ |
| 2cee306 | 186 | enum Option = |
| 2cee306 | 187 | | Some(Int) |
| 2cee306 | 188 | | None |
| 2cee306 | 189 | |
| 2cee306 | 190 | bad() -> Option = |
| 2cee306 | 191 | Some(\"x\") |
| 2cee306 | 192 | "; |
| 2cee306 | 193 | let source = parse(src); |
| 2cee306 | 194 | let result = check_source(&source); |
| 2cee306 | 195 | assert!(result.is_err()); |
| 2cee306 | 196 | } |
| 2cee306 | 197 | |
| 2cee306 | 198 | #[test] |
| 2cee306 | 199 | fn variant_construction_wrong_arg_count_is_error() { |
| 2cee306 | 200 | let src = "\ |
| 2cee306 | 201 | enum Shape = |
| 2cee306 | 202 | | Rect(Float, Float) |
| 2cee306 | 203 | | Circle(Float) |
| 2cee306 | 204 | |
| 2cee306 | 205 | bad() -> Shape = |
| 2cee306 | 206 | Rect(1.0) |
| 2cee306 | 207 | "; |
| 2cee306 | 208 | let source = parse(src); |
| 2cee306 | 209 | let result = check_source(&source); |
| 2cee306 | 210 | assert!(result.is_err()); |
| 2cee306 | 211 | } |
| 2cee306 | 212 | |
| 2cee306 | 213 | #[test] |
| 2cee306 | 214 | fn constructor_pattern_binds_fields_to_declared_types() { |
| 2cee306 | 215 | let src = "\ |
| 2cee306 | 216 | enum Shape = |
| 2cee306 | 217 | | Rect(Float, Float) |
| 2cee306 | 218 | | Circle(Float) |
| 2cee306 | 219 | |
| 2cee306 | 220 | area(s: Shape) -> Float = |
| 2cee306 | 221 | match s |
| 2cee306 | 222 | Rect(w, h) => |
| 2cee306 | 223 | w * h |
| 2cee306 | 224 | Circle(r) => |
| 2cee306 | 225 | r * r |
| 2cee306 | 226 | "; |
| 2cee306 | 227 | let source = parse(src); |
| 2cee306 | 228 | let result = check_source(&source); |
| 2cee306 | 229 | assert!(result.is_ok(), "expected Ok, got {:?}", result.err()); |
| 2cee306 | 230 | } |
| 2cee306 | 231 | |
| 2cee306 | 232 | #[test] |
| 2cee306 | 233 | fn constructor_pattern_wrong_field_count_is_error() { |
| 2cee306 | 234 | let src = "\ |
| 2cee306 | 235 | enum Shape = |
| 2cee306 | 236 | | Rect(Float, Float) |
| 2cee306 | 237 | | Circle(Float) |
| 2cee306 | 238 | |
| 2cee306 | 239 | bad(s: Shape) -> Float = |
| 2cee306 | 240 | match s |
| 2cee306 | 241 | Rect(w) => |
| 2cee306 | 242 | w |
| 2cee306 | 243 | _ => |
| 2cee306 | 244 | 0.0 |
| 2cee306 | 245 | "; |
| 2cee306 | 246 | let source = parse(src); |
| 2cee306 | 247 | let result = check_source(&source); |
| 2cee306 | 248 | assert!(result.is_err()); |
| 2cee306 | 249 | } |
| 2cee306 | 250 | ``` |
| 2cee306 | 251 | |
| 2cee306 | 252 | - [ ] **Step 2: Run the tests to see them fail** |
| 2cee306 | 253 | |
| 2cee306 | 254 | Run: `cargo test -p plum-checker --test checker_tests` |
| 2cee306 | 255 | Expected: `bare_enum_tag_unifies_with_owning_enum_type` fails (type mismatch: `Option` vs `None`); `variant_construction_wrong_arg_count_is_error` and `constructor_pattern_wrong_field_count_is_error` fail (currently these type-check permissively as `Ok`, since arg/field counts aren't validated yet); the other three currently happen to pass already (permissive fallback) — that's fine, they'll keep passing once implemented properly. |
| 2cee306 | 256 | |
| 2cee306 | 257 | - [ ] **Step 3: Add `EnumVariantInfo` and rebuild `EnumVariants`** |
| 2cee306 | 258 | |
| 2cee306 | 259 | In `plum-checker/src/lib.rs`, replace line 47-48: |
| 2cee306 | 260 | |
| 2cee306 | 261 | ```rust |
| 2cee306 | 262 | /// Enum variant name -> owning enum name, e.g. `"True" -> "Bool"`. |
| 2cee306 | 263 | pub type EnumVariants = BTreeMap<String, String>; |
| 2cee306 | 264 | ``` |
| 2cee306 | 265 | |
| 2cee306 | 266 | with: |
| 2cee306 | 267 | |
| 2cee306 | 268 | ```rust |
| 2cee306 | 269 | /// Info about one `enum` variant: which enum it belongs to, its 0-based runtime tag |
| 2cee306 | 270 | /// (numbering is shared across all of that enum's variants), and its payload field |
| 2cee306 | 271 | /// types (empty for a payload-free variant like `Red` or `None`). |
| 2cee306 | 272 | #[derive(Debug, Clone, PartialEq)] |
| 2cee306 | 273 | pub struct EnumVariantInfo { |
| 2cee306 | 274 | pub enum_name: String, |
| 2cee306 | 275 | pub tag: i32, |
| 2cee306 | 276 | pub field_types: Vec<PlumType>, |
| 2cee306 | 277 | } |
| 2cee306 | 278 | /// Enum variant name -> its info, e.g. `"True" -> { enum_name: "Bool", tag: 1, field_types: [] }`. |
| 2cee306 | 279 | pub type EnumVariants = BTreeMap<String, EnumVariantInfo>; |
| 2cee306 | 280 | ``` |
| 2cee306 | 281 | |
| 2cee306 | 282 | Replace lines 66-70: |
| 2cee306 | 283 | |
| 2cee306 | 284 | ```rust |
| 2cee306 | 285 | let mut enum_variants: EnumVariants = BTreeMap::new(); |
| 2cee306 | 286 | // `Bool`'s variants are built in (see `infer_expr`'s TypeName handling) rather |
| 2cee306 | 287 | // than requiring every source file to redeclare `enum Bool = | True | False`. |
| 2cee306 | 288 | enum_variants.insert("True".to_string(), "Bool".to_string()); |
| 2cee306 | 289 | enum_variants.insert("False".to_string(), "Bool".to_string()); |
| 2cee306 | 290 | ``` |
| 2cee306 | 291 | |
| 2cee306 | 292 | with: |
| 2cee306 | 293 | |
| 2cee306 | 294 | ```rust |
| 2cee306 | 295 | let mut enum_variants: EnumVariants = BTreeMap::new(); |
| 2cee306 | 296 | // `Bool`'s variants are built in (see `infer_expr`'s TypeName handling) rather |
| 2cee306 | 297 | // than requiring every source file to redeclare `enum Bool = | True | False`. |
| 2cee306 | 298 | enum_variants.insert("True".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 1, field_types: vec![] }); |
| 2cee306 | 299 | enum_variants.insert("False".to_string(), EnumVariantInfo { enum_name: "Bool".to_string(), tag: 0, field_types: vec![] }); |
| 2cee306 | 300 | ``` |
| 2cee306 | 301 | |
| 2cee306 | 302 | Replace lines 82-86: |
| 2cee306 | 303 | |
| 2cee306 | 304 | ```rust |
| 2cee306 | 305 | ast::Item::Enum(e) => { |
| 2cee306 | 306 | for v in &e.variants { |
| 2cee306 | 307 | enum_variants.insert(v.name.clone(), e.name.clone()); |
| 2cee306 | 308 | } |
| 2cee306 | 309 | } |
| 2cee306 | 310 | ``` |
| 2cee306 | 311 | |
| 2cee306 | 312 | with: |
| 2cee306 | 313 | |
| 2cee306 | 314 | ```rust |
| 2cee306 | 315 | ast::Item::Enum(e) => { |
| 2cee306 | 316 | for (tag, v) in e.variants.iter().enumerate() { |
| 2cee306 | 317 | let field_types = v.fields.iter() |
| 2cee306 | 318 | .map(|f| plum_type_from_ast(&ast::Type { name: f.clone(), generics: vec![] })) |
| 2cee306 | 319 | .collect(); |
| 2cee306 | 320 | enum_variants.insert(v.name.clone(), EnumVariantInfo { |
| 2cee306 | 321 | enum_name: e.name.clone(), |
| 2cee306 | 322 | tag: tag as i32, |
| 2cee306 | 323 | field_types, |
| 2cee306 | 324 | }); |
| 2cee306 | 325 | } |
| 2cee306 | 326 | } |
| 2cee306 | 327 | ``` |
| 2cee306 | 328 | |
| 2cee306 | 329 | - [ ] **Step 4: Fix `TypeName` to type as the owning enum, not itself** |
| 2cee306 | 330 | |
| 2cee306 | 331 | Replace lines 368-371: |
| 2cee306 | 332 | |
| 2cee306 | 333 | ```rust |
| 2cee306 | 334 | ast::Expr::TypeName(n) => match n.as_str() { |
| 2cee306 | 335 | "True" | "False" => Ok(PlumType::TBool), |
| 2cee306 | 336 | other => Ok(PlumType::TNamed(other.to_string())), |
| 2cee306 | 337 | }, |
| 2cee306 | 338 | ``` |
| 2cee306 | 339 | |
| 2cee306 | 340 | with: |
| 2cee306 | 341 | |
| 2cee306 | 342 | ```rust |
| 2cee306 | 343 | ast::Expr::TypeName(n) => match n.as_str() { |
| 2cee306 | 344 | "True" | "False" => Ok(PlumType::TBool), |
| 2cee306 | 345 | _ => match ctx.enum_variants.get(n) { |
| 2cee306 | 346 | Some(info) => Ok(PlumType::TNamed(info.enum_name.clone())), |
| 2cee306 | 347 | // Unmodeled/builtin type name: allow, codegen will catch. |
| 2cee306 | 348 | None => Ok(PlumType::TNamed(n.to_string())), |
| 2cee306 | 349 | }, |
| 2cee306 | 350 | }, |
| 2cee306 | 351 | ``` |
| 2cee306 | 352 | |
| 2cee306 | 353 | - [ ] **Step 5: Type-check variant-construction `FnCall`s properly** |
| 2cee306 | 354 | |
| 2cee306 | 355 | Replace lines 409-429: |
| 2cee306 | 356 | |
| 2cee306 | 357 | ```rust |
| 2cee306 | 358 | ast::Expr::FnCall(call) => { |
| 2cee306 | 359 | match lookup(env, &call.name) { |
| 2cee306 | 360 | Ok(PlumType::TFun(param_types, ret)) => { |
| 2cee306 | 361 | if call.args.len() != param_types.len() { |
| 2cee306 | 362 | return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len())); |
| 2cee306 | 363 | } |
| 2cee306 | 364 | for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() { |
| 2cee306 | 365 | let arg_expr = match arg { |
| 2cee306 | 366 | ast::Arg::Positional(e) => e, |
| 2cee306 | 367 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 368 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 369 | }; |
| 2cee306 | 370 | let actual = infer_expr(arg_expr, env, ctx)?; |
| 2cee306 | 371 | unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?; |
| 2cee306 | 372 | } |
| 2cee306 | 373 | Ok(*ret) |
| 2cee306 | 374 | } |
| 2cee306 | 375 | Ok(_) => Err(format!("'{}' is not a function", call.name)), |
| 2cee306 | 376 | Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch |
| 2cee306 | 377 | } |
| 2cee306 | 378 | } |
| 2cee306 | 379 | ``` |
| 2cee306 | 380 | |
| 2cee306 | 381 | with: |
| 2cee306 | 382 | |
| 2cee306 | 383 | ```rust |
| 2cee306 | 384 | ast::Expr::FnCall(call) => { |
| 2cee306 | 385 | if let Some(info) = ctx.enum_variants.get(&call.name) { |
| 2cee306 | 386 | if call.args.len() != info.field_types.len() { |
| 2cee306 | 387 | return Err(format!( |
| 2cee306 | 388 | "variant '{}': expected {} arg(s), got {}", |
| 2cee306 | 389 | call.name, info.field_types.len(), call.args.len() |
| 2cee306 | 390 | )); |
| 2cee306 | 391 | } |
| 2cee306 | 392 | for (i, (arg, expected)) in call.args.iter().zip(info.field_types.iter()).enumerate() { |
| 2cee306 | 393 | let arg_expr = match arg { |
| 2cee306 | 394 | ast::Arg::Positional(e) => e, |
| 2cee306 | 395 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 396 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 397 | }; |
| 2cee306 | 398 | let actual = infer_expr(arg_expr, env, ctx)?; |
| 2cee306 | 399 | unify(expected, &actual).map_err(|e| format!("variant '{}' arg {}: {}", call.name, i, e))?; |
| 2cee306 | 400 | } |
| 2cee306 | 401 | return Ok(PlumType::TNamed(info.enum_name.clone())); |
| 2cee306 | 402 | } |
| 2cee306 | 403 | match lookup(env, &call.name) { |
| 2cee306 | 404 | Ok(PlumType::TFun(param_types, ret)) => { |
| 2cee306 | 405 | if call.args.len() != param_types.len() { |
| 2cee306 | 406 | return Err(format!("call '{}': expected {} args, got {}", call.name, param_types.len(), call.args.len())); |
| 2cee306 | 407 | } |
| 2cee306 | 408 | for (i, (arg, expected)) in call.args.iter().zip(param_types.iter()).enumerate() { |
| 2cee306 | 409 | let arg_expr = match arg { |
| 2cee306 | 410 | ast::Arg::Positional(e) => e, |
| 2cee306 | 411 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 412 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 413 | }; |
| 2cee306 | 414 | let actual = infer_expr(arg_expr, env, ctx)?; |
| 2cee306 | 415 | unify(expected, &actual).map_err(|e| format!("call '{}' arg {}: {}", call.name, i, e))?; |
| 2cee306 | 416 | } |
| 2cee306 | 417 | Ok(*ret) |
| 2cee306 | 418 | } |
| 2cee306 | 419 | Ok(_) => Err(format!("'{}' is not a function", call.name)), |
| 2cee306 | 420 | Err(_) => Ok(PlumType::TVar("_".to_string())), // unknown fn: allow, codegen will catch |
| 2cee306 | 421 | } |
| 2cee306 | 422 | } |
| 2cee306 | 423 | ``` |
| 2cee306 | 424 | |
| 2cee306 | 425 | - [ ] **Step 6: Type-check constructor patterns against real field types** |
| 2cee306 | 426 | |
| 2cee306 | 427 | Replace the doc comment and body at lines 333-359: |
| 2cee306 | 428 | |
| 2cee306 | 429 | ```rust |
| 2cee306 | 430 | /// Checks a single case pattern against the type of the subject it matches, binding any |
| 2cee306 | 431 | /// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind |
| 2cee306 | 432 | /// against an unconstrained type since enum variants don't carry per-field type info (v1.5). |
| 2cee306 | 433 | fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> { |
| 2cee306 | 434 | match pat { |
| 2cee306 | 435 | ast::CasePattern::Wildcard => Ok(()), |
| 2cee306 | 436 | ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt), |
| 2cee306 | 437 | ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat), |
| 2cee306 | 438 | ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr), |
| 2cee306 | 439 | ast::CasePattern::Name(n) => { |
| 2cee306 | 440 | let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| 2cee306 | 441 | && ctx.enum_variants.contains_key(n); |
| 2cee306 | 442 | if is_known_variant { |
| 2cee306 | 443 | Ok(()) // equality check against a known enum tag, e.g. `True` |
| 2cee306 | 444 | } else { |
| 2cee306 | 445 | env.insert(n.clone(), TypeScheme::mono(subject_ty.clone())); |
| 2cee306 | 446 | Ok(()) |
| 2cee306 | 447 | } |
| 2cee306 | 448 | } |
| 2cee306 | 449 | ast::CasePattern::Class { name: _, fields } => { |
| 2cee306 | 450 | for f in fields { |
| 2cee306 | 451 | check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?; |
| 2cee306 | 452 | } |
| 2cee306 | 453 | Ok(()) |
| 2cee306 | 454 | } |
| 2cee306 | 455 | } |
| 2cee306 | 456 | } |
| 2cee306 | 457 | ``` |
| 2cee306 | 458 | |
| 2cee306 | 459 | with: |
| 2cee306 | 460 | |
| 2cee306 | 461 | ```rust |
| 2cee306 | 462 | /// Checks a single case pattern against the type of the subject it matches, binding any |
| 2cee306 | 463 | /// new names it introduces into `env`. Constructor-payload sub-patterns (`Some(x)`) bind |
| 2cee306 | 464 | /// against that variant's declared field types (see `EnumVariantInfo::field_types`). |
| 2cee306 | 465 | fn check_pattern(pat: &ast::CasePattern, subject_ty: &PlumType, env: &mut TypeEnv, ctx: &CheckCtx) -> Result<(), String> { |
| 2cee306 | 466 | match pat { |
| 2cee306 | 467 | ast::CasePattern::Wildcard => Ok(()), |
| 2cee306 | 468 | ast::CasePattern::Int(_) => unify(subject_ty, &PlumType::TInt), |
| 2cee306 | 469 | ast::CasePattern::Float(_) => unify(subject_ty, &PlumType::TFloat), |
| 2cee306 | 470 | ast::CasePattern::String(_) => unify(subject_ty, &PlumType::TStr), |
| 2cee306 | 471 | ast::CasePattern::Name(n) => { |
| 2cee306 | 472 | let is_known_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| 2cee306 | 473 | && ctx.enum_variants.contains_key(n); |
| 2cee306 | 474 | if is_known_variant { |
| 2cee306 | 475 | Ok(()) // equality check against a known enum tag, e.g. `True` |
| 2cee306 | 476 | } else { |
| 2cee306 | 477 | env.insert(n.clone(), TypeScheme::mono(subject_ty.clone())); |
| 2cee306 | 478 | Ok(()) |
| 2cee306 | 479 | } |
| 2cee306 | 480 | } |
| 2cee306 | 481 | ast::CasePattern::Class { name, fields } => match ctx.enum_variants.get(name) { |
| 2cee306 | 482 | Some(info) => { |
| 2cee306 | 483 | if fields.len() != info.field_types.len() { |
| 2cee306 | 484 | return Err(format!( |
| 2cee306 | 485 | "constructor pattern '{}' expects {} field(s), got {}", |
| 2cee306 | 486 | name, info.field_types.len(), fields.len() |
| 2cee306 | 487 | )); |
| 2cee306 | 488 | } |
| 2cee306 | 489 | for (f, fty) in fields.iter().zip(info.field_types.iter()) { |
| 2cee306 | 490 | check_pattern(f, fty, env, ctx)?; |
| 2cee306 | 491 | } |
| 2cee306 | 492 | Ok(()) |
| 2cee306 | 493 | } |
| 2cee306 | 494 | // Unmodeled/builtin variant: allow, codegen will catch. |
| 2cee306 | 495 | None => { |
| 2cee306 | 496 | for f in fields { |
| 2cee306 | 497 | check_pattern(f, &PlumType::TVar("_".to_string()), env, ctx)?; |
| 2cee306 | 498 | } |
| 2cee306 | 499 | Ok(()) |
| 2cee306 | 500 | } |
| 2cee306 | 501 | }, |
| 2cee306 | 502 | } |
| 2cee306 | 503 | } |
| 2cee306 | 504 | ``` |
| 2cee306 | 505 | |
| 2cee306 | 506 | - [ ] **Step 7: Run checker tests** |
| 2cee306 | 507 | |
| 2cee306 | 508 | Run: `cargo test -p plum-checker --test checker_tests` |
| 2cee306 | 509 | Expected: all pass, including the six new tests. |
| 2cee306 | 510 | |
| 2cee306 | 511 | - [ ] **Step 8: Run the full checker crate + examples test** |
| 2cee306 | 512 | |
| 2cee306 | 513 | Run: `cargo test -p plum-checker` |
| 2cee306 | 514 | Expected: all pass (this includes `examples_test.rs`, which type-checks every file under `examples/` — `types.plum` and `match.plum` both already declare a concrete `Option`/`Color` enum, so this proves the fix doesn't regress them). |
| 2cee306 | 515 | |
| 2cee306 | 516 | - [ ] **Step 9: Commit** |
| 2cee306 | 517 | |
| 2cee306 | 518 | ```bash |
| 2cee306 | 519 | git add plum-checker/src/lib.rs plum-checker/tests/checker_tests.rs |
| 2cee306 | 520 | git commit -m "feat(plum-checker): general enum variant tags, field types, and construction checks" |
| 2cee306 | 521 | ``` |
| 2cee306 | 522 | |
| 2cee306 | 523 | --- |
| 2cee306 | 524 | |
| 2cee306 | 525 | ### Task 3: Codegen — variant construction (`Some(v)`, `Pair(a, b)` compile to a tagged heap struct) |
| 2cee306 | 526 | |
| 2cee306 | 527 | **Files:** |
| 2cee306 | 528 | - Modify: `plum-wasm-codegen/src/lib.rs:1-10` (import), `:450-500` (`Collector::walk_expr`), `:1040-1054` (`compile_expr`'s `FnCall` arm) |
| 2cee306 | 529 | - Test: `plum-wasm-codegen/tests/codegen_tests.rs` |
| 2cee306 | 530 | |
| 2cee306 | 531 | **Interfaces:** |
| 2cee306 | 532 | - Consumes: `plum_checker::EnumVariantInfo` (Task 2) via `ctx.enum_variants.get(&call.name)` — `EnumVariantInfo { enum_name: String, tag: i32, field_types: Vec<PlumType> }`. |
| 2cee306 | 533 | - Produces: a payload-free variant call/bare value compiles to `i32.const <tag>`; a payload variant call compiles to a bump-allocated `[tag][field0][field1]...` struct (8-byte stride per slot, same convention as class fields) with its base pointer left on the stack. Task 4 (match lowering) reads this same layout back out. |
| 2cee306 | 534 | |
| 2cee306 | 535 | - [ ] **Step 1: Write failing codegen tests** |
| 2cee306 | 536 | |
| 2cee306 | 537 | Append to `plum-wasm-codegen/tests/codegen_tests.rs`: |
| 2cee306 | 538 | |
| 2cee306 | 539 | ```rust |
| 2cee306 | 540 | #[test] |
| 2cee306 | 541 | fn payload_free_variant_construction_compiles() { |
| 2cee306 | 542 | let src = "\ |
| 2cee306 | 543 | enum Color = |
| 2cee306 | 544 | | Red |
| 2cee306 | 545 | | Green |
| 2cee306 | 546 | | Blue |
| 2cee306 | 547 | |
| 2cee306 | 548 | main() -> Int =\n x = Green\n 0\n"; |
| 2cee306 | 549 | assert_valid(src); |
| 2cee306 | 550 | } |
| 2cee306 | 551 | |
| 2cee306 | 552 | #[test] |
| 2cee306 | 553 | fn payload_variant_construction_compiles_and_runs() { |
| 2cee306 | 554 | let src = "\ |
| 2cee306 | 555 | enum Option = |
| 2cee306 | 556 | | Some(Int) |
| 2cee306 | 557 | | None |
| 2cee306 | 558 | |
| 2cee306 | 559 | unwrapOr(o: Option, default: Int) -> Int = |
| 2cee306 | 560 | match o |
| 2cee306 | 561 | Some(v) => |
| 2cee306 | 562 | return v |
| 2cee306 | 563 | None => |
| 2cee306 | 564 | return default |
| 2cee306 | 565 | |
| 2cee306 | 566 | main() -> Int = |
| 2cee306 | 567 | unwrapOr(Some(7), 0) |
| 2cee306 | 568 | "; |
| 2cee306 | 569 | let source = parse(src); |
| 2cee306 | 570 | let bytes = compile_source(&source).expect("compile failed"); |
| 2cee306 | 571 | assert_eq!(run_main(&bytes), 7); |
| 2cee306 | 572 | } |
| 2cee306 | 573 | |
| 2cee306 | 574 | #[test] |
| 2cee306 | 575 | fn multi_field_variant_construction_compiles_and_runs() { |
| 2cee306 | 576 | let src = "\ |
| 2cee306 | 577 | enum Shape = |
| 2cee306 | 578 | | Rect(Int, Int) |
| 2cee306 | 579 | | Circle(Int) |
| 2cee306 | 580 | |
| 2cee306 | 581 | area(s: Shape) -> Int = |
| 2cee306 | 582 | match s |
| 2cee306 | 583 | Rect(w, h) => |
| 2cee306 | 584 | return w * h |
| 2cee306 | 585 | Circle(r) => |
| 2cee306 | 586 | return r * r |
| 2cee306 | 587 | |
| 2cee306 | 588 | main() -> Int = |
| 2cee306 | 589 | area(Rect(3, 4)) |
| 2cee306 | 590 | "; |
| 2cee306 | 591 | let source = parse(src); |
| 2cee306 | 592 | let bytes = compile_source(&source).expect("compile failed"); |
| 2cee306 | 593 | assert_eq!(run_main(&bytes), 12); |
| 2cee306 | 594 | } |
| 2cee306 | 595 | ``` |
| 2cee306 | 596 | |
| 2cee306 | 597 | (These also exercise Task 4's match lowering — that's expected; construction and destructuring are tested together since one is useless to test without the other. Task 4 will make the `Some`/`None`/`Rect`/`Circle` match arms actually compile.) |
| 2cee306 | 598 | |
| 2cee306 | 599 | - [ ] **Step 2: Run to see them fail** |
| 2cee306 | 600 | |
| 2cee306 | 601 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests` |
| 2cee306 | 602 | Expected: `payload_free_variant_construction_compiles` fails with `codegen: type name 'Green' is not yet supported as a value` (bare non-Bool `TypeName` isn't handled yet — that's fixed in Step 4 below); the other two fail with `codegen: enum variant pattern '...' is not yet supported (only True/False)` (Task 4's job) — confirm both failure modes appear, then proceed. |
| 2cee306 | 603 | |
| 2cee306 | 604 | - [ ] **Step 3: Import `EnumVariantInfo`** |
| 2cee306 | 605 | |
| 2cee306 | 606 | In `plum-wasm-codegen/src/lib.rs`, change line 6: |
| 2cee306 | 607 | |
| 2cee306 | 608 | ```rust |
| 2cee306 | 609 | use plum_checker::{ClassEnv, MethodEnv, EnumVariants}; |
| 2cee306 | 610 | ``` |
| 2cee306 | 611 | |
| 2cee306 | 612 | to: |
| 2cee306 | 613 | |
| 2cee306 | 614 | ```rust |
| 2cee306 | 615 | use plum_checker::{ClassEnv, MethodEnv, EnumVariants, EnumVariantInfo}; |
| 2cee306 | 616 | ``` |
| 2cee306 | 617 | |
| 2cee306 | 618 | - [ ] **Step 4: Make bare payload-free variants compile as their tag** |
| 2cee306 | 619 | |
| 2cee306 | 620 | Replace lines 1063-1067: |
| 2cee306 | 621 | |
| 2cee306 | 622 | ```rust |
| 2cee306 | 623 | ast::Expr::TypeName(n) => match n.as_str() { |
| 2cee306 | 624 | "True" => Instruction::I32Const(1).encode(body), |
| 2cee306 | 625 | "False" => Instruction::I32Const(0).encode(body), |
| 2cee306 | 626 | other => return Err(format!("codegen: type name '{}' is not yet supported as a value", other)), |
| 2cee306 | 627 | }, |
| 2cee306 | 628 | ``` |
| 2cee306 | 629 | |
| 2cee306 | 630 | with: |
| 2cee306 | 631 | |
| 2cee306 | 632 | ```rust |
| 2cee306 | 633 | ast::Expr::TypeName(n) => match ctx.enum_variants.get(n) { |
| 2cee306 | 634 | Some(info) if info.field_types.is_empty() => { |
| 2cee306 | 635 | Instruction::I32Const(info.tag).encode(body); |
| 2cee306 | 636 | } |
| 2cee306 | 637 | Some(_) => return Err(format!("codegen: '{}' carries a payload — construct it with '{}(...)'", n, n)), |
| 2cee306 | 638 | None => return Err(format!("codegen: type name '{}' is not yet supported as a value", n)), |
| 2cee306 | 639 | }, |
| 2cee306 | 640 | ``` |
| 2cee306 | 641 | |
| 2cee306 | 642 | - [ ] **Step 5: Allocate a scratch slot for payload-variant construction in `Collector`** |
| 2cee306 | 643 | |
| 2cee306 | 644 | In `Collector::walk_expr`, replace lines 480-484: |
| 2cee306 | 645 | |
| 2cee306 | 646 | ```rust |
| 2cee306 | 647 | ast::Expr::FnCall(call) => { |
| 2cee306 | 648 | for arg in &call.args { |
| 2cee306 | 649 | self.walk_arg(arg); |
| 2cee306 | 650 | } |
| 2cee306 | 651 | } |
| 2cee306 | 652 | ``` |
| 2cee306 | 653 | |
| 2cee306 | 654 | with: |
| 2cee306 | 655 | |
| 2cee306 | 656 | ```rust |
| 2cee306 | 657 | ast::Expr::FnCall(call) => { |
| 2cee306 | 658 | let carries_payload = self.cctx.enum_variants.get(&call.name) |
| 2cee306 | 659 | .map(|info| !info.field_types.is_empty()) |
| 2cee306 | 660 | .unwrap_or(false); |
| 2cee306 | 661 | if carries_payload { |
| 2cee306 | 662 | let idx = self.next_classcall_slot; |
| 2cee306 | 663 | self.next_classcall_slot += 1; |
| 2cee306 | 664 | self.classcall_scratch.insert(expr as *const ast::Expr as usize, idx); |
| 2cee306 | 665 | } |
| 2cee306 | 666 | for arg in &call.args { |
| 2cee306 | 667 | self.walk_arg(arg); |
| 2cee306 | 668 | } |
| 2cee306 | 669 | } |
| 2cee306 | 670 | ``` |
| 2cee306 | 671 | |
| 2cee306 | 672 | (`classcall_scratch`/`next_classcall_slot` are the same pool `ClassCall` already uses — a scratch local slot per allocation-then-store expression. Both kinds of expression have distinct pointer identities, so sharing the pool is safe: no key collisions.) |
| 2cee306 | 673 | |
| 2cee306 | 674 | - [ ] **Step 6: Compile payload-variant construction in `compile_expr`** |
| 2cee306 | 675 | |
| 2cee306 | 676 | Replace lines 1040-1054: |
| 2cee306 | 677 | |
| 2cee306 | 678 | ```rust |
| 2cee306 | 679 | ast::Expr::FnCall(call) => { |
| 2cee306 | 680 | for arg in &call.args { |
| 2cee306 | 681 | let arg_expr = match arg { |
| 2cee306 | 682 | ast::Arg::Positional(e) => e, |
| 2cee306 | 683 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 684 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 685 | }; |
| 2cee306 | 686 | compile_expr(arg_expr, body, ctx, state)?; |
| 2cee306 | 687 | } |
| 2cee306 | 688 | let func_idx = ctx |
| 2cee306 | 689 | .func_ids |
| 2cee306 | 690 | .get(&call.name) |
| 2cee306 | 691 | .ok_or_else(|| format!("unknown function '{}'", call.name))?; |
| 2cee306 | 692 | Instruction::Call(*func_idx).encode(body); |
| 2cee306 | 693 | } |
| 2cee306 | 694 | ``` |
| 2cee306 | 695 | |
| 2cee306 | 696 | with: |
| 2cee306 | 697 | |
| 2cee306 | 698 | ```rust |
| 2cee306 | 699 | ast::Expr::FnCall(call) => { |
| 2cee306 | 700 | if let Some(info) = ctx.enum_variants.get(&call.name) { |
| 2cee306 | 701 | compile_variant_construction(info, call, expr, body, ctx, state)?; |
| 2cee306 | 702 | } else { |
| 2cee306 | 703 | for arg in &call.args { |
| 2cee306 | 704 | let arg_expr = match arg { |
| 2cee306 | 705 | ast::Arg::Positional(e) => e, |
| 2cee306 | 706 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 707 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 708 | }; |
| 2cee306 | 709 | compile_expr(arg_expr, body, ctx, state)?; |
| 2cee306 | 710 | } |
| 2cee306 | 711 | let func_idx = ctx |
| 2cee306 | 712 | .func_ids |
| 2cee306 | 713 | .get(&call.name) |
| 2cee306 | 714 | .ok_or_else(|| format!("unknown function '{}'", call.name))?; |
| 2cee306 | 715 | Instruction::Call(*func_idx).encode(body); |
| 2cee306 | 716 | } |
| 2cee306 | 717 | } |
| 2cee306 | 718 | ``` |
| 2cee306 | 719 | |
| 2cee306 | 720 | Then add this new function right after `compile_expr`'s closing brace (after line ~1178, before `plum_type_from_valtype_hint` or anywhere else at module scope): |
| 2cee306 | 721 | |
| 2cee306 | 722 | ```rust |
| 2cee306 | 723 | /// Compiles a variant-construction call. A payload-free variant (`None`, called as |
| 2cee306 | 724 | /// `None()` rather than used bare) is just its tag. A payload variant bump-allocates |
| 2cee306 | 725 | /// `[tag: i32][field0][field1]...` (8-byte stride per slot, matching class field |
| 2cee306 | 726 | /// layout) and leaves the base pointer on the stack. |
| 2cee306 | 727 | fn compile_variant_construction( |
| 2cee306 | 728 | info: &EnumVariantInfo, |
| 2cee306 | 729 | call: &ast::FnCall, |
| 2cee306 | 730 | expr: &ast::Expr, |
| 2cee306 | 731 | body: &mut Vec<u8>, |
| 2cee306 | 732 | ctx: &LocalCtx, |
| 2cee306 | 733 | state: &mut ModuleState, |
| 2cee306 | 734 | ) -> Result<(), String> { |
| 2cee306 | 735 | if call.args.len() != info.field_types.len() { |
| 2cee306 | 736 | return Err(format!( |
| 2cee306 | 737 | "codegen: variant '{}' expects {} arg(s), got {}", |
| 2cee306 | 738 | call.name, info.field_types.len(), call.args.len() |
| 2cee306 | 739 | )); |
| 2cee306 | 740 | } |
| 2cee306 | 741 | if info.field_types.is_empty() { |
| 2cee306 | 742 | Instruction::I32Const(info.tag).encode(body); |
| 2cee306 | 743 | return Ok(()); |
| 2cee306 | 744 | } |
| 2cee306 | 745 | |
| 2cee306 | 746 | let size = (1 + info.field_types.len() as i32) * 8; |
| 2cee306 | 747 | let scratch_key = expr as *const ast::Expr as usize; |
| 2cee306 | 748 | let scratch_idx = *ctx |
| 2cee306 | 749 | .classcall_scratch |
| 2cee306 | 750 | .get(&scratch_key) |
| 2cee306 | 751 | .ok_or_else(|| "internal codegen error: missing variant-call scratch slot".to_string())?; |
| 2cee306 | 752 | let scratch_local = ctx.classcall_scratch_base + scratch_idx; |
| 2cee306 | 753 | |
| 2cee306 | 754 | Instruction::GlobalGet(ctx.bump_global).encode(body); |
| 2cee306 | 755 | Instruction::LocalSet(scratch_local).encode(body); |
| 2cee306 | 756 | Instruction::GlobalGet(ctx.bump_global).encode(body); |
| 2cee306 | 757 | Instruction::I32Const(size).encode(body); |
| 2cee306 | 758 | Instruction::I32Add.encode(body); |
| 2cee306 | 759 | Instruction::GlobalSet(ctx.bump_global).encode(body); |
| 2cee306 | 760 | |
| 2cee306 | 761 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 762 | Instruction::I32Const(info.tag).encode(body); |
| 2cee306 | 763 | Instruction::I32Store(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body); |
| 2cee306 | 764 | |
| 2cee306 | 765 | for (i, (arg, field_ty)) in call.args.iter().zip(info.field_types.iter()).enumerate() { |
| 2cee306 | 766 | let arg_expr = match arg { |
| 2cee306 | 767 | ast::Arg::Positional(e) => e, |
| 2cee306 | 768 | ast::Arg::Keyword { value, .. } => value, |
| 2cee306 | 769 | ast::Arg::Pair { value, .. } => value, |
| 2cee306 | 770 | }; |
| 2cee306 | 771 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 772 | compile_expr(arg_expr, body, ctx, state)?; |
| 2cee306 | 773 | let offset = ((i + 1) as u64) * 8; |
| 2cee306 | 774 | match plum_type_to_valtype(field_ty) { |
| 2cee306 | 775 | ValType::I64 => Instruction::I64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body), |
| 2cee306 | 776 | ValType::F64 => Instruction::F64Store(MemArg { offset, align: 3, memory_index: 0 }).encode(body), |
| 2cee306 | 777 | _ => Instruction::I32Store(MemArg { offset, align: 2, memory_index: 0 }).encode(body), |
| 2cee306 | 778 | }; |
| 2cee306 | 779 | } |
| 2cee306 | 780 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 781 | Ok(()) |
| 2cee306 | 782 | } |
| 2cee306 | 783 | ``` |
| 2cee306 | 784 | |
| 2cee306 | 785 | - [ ] **Step 7: Run codegen tests** |
| 2cee306 | 786 | |
| 2cee306 | 787 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests` |
| 2cee306 | 788 | Expected: `payload_free_variant_construction_compiles` now passes. The other two new tests still fail (match lowering isn't done yet — that's Task 4); confirm they now fail specifically inside `match`, not during construction (temporarily comment out their `match` bodies and replace with a literal return if you want to isolate-verify construction alone; then restore). |
| 2cee306 | 789 | |
| 2cee306 | 790 | - [ ] **Step 8: Commit** |
| 2cee306 | 791 | |
| 2cee306 | 792 | ```bash |
| 2cee306 | 793 | git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs |
| 2cee306 | 794 | git commit -m "feat(plum-wasm-codegen): compile general enum variant construction" |
| 2cee306 | 795 | ``` |
| 2cee306 | 796 | |
| 2cee306 | 797 | --- |
| 2cee306 | 798 | |
| 2cee306 | 799 | ### Task 4: Codegen — match lowering for general enum tags and constructor patterns |
| 2cee306 | 800 | |
| 2cee306 | 801 | **Files:** |
| 2cee306 | 802 | - Modify: `plum-wasm-codegen/src/lib.rs:420-445` (`Collector::walk_stmt`'s `Match` arm), `:844-929` (`compile_match_arms` / `compile_variant_eq_arm`) |
| 2cee306 | 803 | - Test: `plum-wasm-codegen/tests/codegen_tests.rs` (Task 3's `payload_variant_construction_compiles_and_runs` and `multi_field_variant_construction_compiles_and_runs` will pass once this lands) |
| 2cee306 | 804 | |
| 2cee306 | 805 | **Interfaces:** |
| 2cee306 | 806 | - Consumes: `EnumVariantInfo` (Task 2), the bump-heap `[tag][field...]` layout (Task 3). |
| 2cee306 | 807 | - Produces: nothing further downstream — this is the last piece of the feature. |
| 2cee306 | 808 | |
| 2cee306 | 809 | - [ ] **Step 1: Confirm Task 3's two match-dependent tests still fail, for the right reason** |
| 2cee306 | 810 | |
| 2cee306 | 811 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests payload_variant_construction_compiles_and_runs multi_field_variant_construction_compiles_and_runs` |
| 2cee306 | 812 | Expected: both fail with `codegen: enum variant pattern '...' is not yet supported (only True/False)` or `codegen: constructor match patterns are not yet supported`. |
| 2cee306 | 813 | |
| 2cee306 | 814 | - [ ] **Step 2: Bind constructor-pattern fields to their real types in `Collector`** |
| 2cee306 | 815 | |
| 2cee306 | 816 | Replace lines 431-444: |
| 2cee306 | 817 | |
| 2cee306 | 818 | ```rust |
| 2cee306 | 819 | for case in &m.cases { |
| 2cee306 | 820 | let saved = self.env.clone(); |
| 2cee306 | 821 | if m.subjects.len() == 1 { |
| 2cee306 | 822 | if let Some(ast::CasePattern::Name(n)) = case.patterns.first() { |
| 2cee306 | 823 | let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| 2cee306 | 824 | && self.cctx.enum_variants.contains_key(n); |
| 2cee306 | 825 | if !is_variant { |
| 2cee306 | 826 | self.bind(n, subject_ty.clone()); |
| 2cee306 | 827 | } |
| 2cee306 | 828 | } |
| 2cee306 | 829 | } |
| 2cee306 | 830 | self.walk_block(&case.body); |
| 2cee306 | 831 | self.env = saved; |
| 2cee306 | 832 | } |
| 2cee306 | 833 | ``` |
| 2cee306 | 834 | |
| 2cee306 | 835 | with: |
| 2cee306 | 836 | |
| 2cee306 | 837 | ```rust |
| 2cee306 | 838 | for case in &m.cases { |
| 2cee306 | 839 | let saved = self.env.clone(); |
| 2cee306 | 840 | if m.subjects.len() == 1 { |
| 2cee306 | 841 | match case.patterns.first() { |
| 2cee306 | 842 | Some(ast::CasePattern::Name(n)) => { |
| 2cee306 | 843 | let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| 2cee306 | 844 | && self.cctx.enum_variants.contains_key(n); |
| 2cee306 | 845 | if !is_variant { |
| 2cee306 | 846 | self.bind(n, subject_ty.clone()); |
| 2cee306 | 847 | } |
| 2cee306 | 848 | } |
| 2cee306 | 849 | Some(ast::CasePattern::Class { name, fields }) => { |
| 2cee306 | 850 | if let Some(info) = self.cctx.enum_variants.get(name) { |
| 2cee306 | 851 | let field_types = info.field_types.clone(); |
| 2cee306 | 852 | for (f, fty) in fields.iter().zip(field_types.iter()) { |
| 2cee306 | 853 | if let ast::CasePattern::Name(n) = f { |
| 2cee306 | 854 | self.bind(n, fty.clone()); |
| 2cee306 | 855 | } |
| 2cee306 | 856 | } |
| 2cee306 | 857 | } |
| 2cee306 | 858 | } |
| 2cee306 | 859 | _ => {} |
| 2cee306 | 860 | } |
| 2cee306 | 861 | } |
| 2cee306 | 862 | self.walk_block(&case.body); |
| 2cee306 | 863 | self.env = saved; |
| 2cee306 | 864 | } |
| 2cee306 | 865 | ``` |
| 2cee306 | 866 | |
| 2cee306 | 867 | - [ ] **Step 3: Generalize the bare-tag match arm beyond True/False** |
| 2cee306 | 868 | |
| 2cee306 | 869 | Replace lines 900-929 (`compile_variant_eq_arm`): |
| 2cee306 | 870 | |
| 2cee306 | 871 | ```rust |
| 2cee306 | 872 | #[allow(clippy::too_many_arguments)] |
| 2cee306 | 873 | fn compile_variant_eq_arm( |
| 2cee306 | 874 | name: &str, |
| 2cee306 | 875 | subject_vt: ValType, |
| 2cee306 | 876 | scratch_local: u32, |
| 2cee306 | 877 | case: &ast::Case, |
| 2cee306 | 878 | rest: &[ast::Case], |
| 2cee306 | 879 | body: &mut Vec<u8>, |
| 2cee306 | 880 | ctx: &LocalCtx, |
| 2cee306 | 881 | state: &mut ModuleState, |
| 2cee306 | 882 | ) -> Result<(), String> { |
| 2cee306 | 883 | // Only Bool's own variants have a concrete runtime representation in v1.5. |
| 2cee306 | 884 | let tag = match name { |
| 2cee306 | 885 | "True" => 1i32, |
| 2cee306 | 886 | "False" => 0i32, |
| 2cee306 | 887 | other => return Err(format!("codegen: enum variant pattern '{}' is not yet supported (only True/False)", other)), |
| 2cee306 | 888 | }; |
| 2cee306 | 889 | if subject_vt != ValType::I32 { |
| 2cee306 | 890 | return Err("codegen: Bool match pattern against a non-Bool subject".to_string()); |
| 2cee306 | 891 | } |
| 2cee306 | 892 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 893 | Instruction::I32Const(tag).encode(body); |
| 2cee306 | 894 | Instruction::I32Eq.encode(body); |
| 2cee306 | 895 | Instruction::If(BlockType::Empty).encode(body); |
| 2cee306 | 896 | compile_block(&case.body, body, ctx, state)?; |
| 2cee306 | 897 | Instruction::Else.encode(body); |
| 2cee306 | 898 | compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?; |
| 2cee306 | 899 | Instruction::End.encode(body); |
| 2cee306 | 900 | Ok(()) |
| 2cee306 | 901 | } |
| 2cee306 | 902 | ``` |
| 2cee306 | 903 | |
| 2cee306 | 904 | with: |
| 2cee306 | 905 | |
| 2cee306 | 906 | ```rust |
| 2cee306 | 907 | #[allow(clippy::too_many_arguments)] |
| 2cee306 | 908 | fn compile_variant_eq_arm( |
| 2cee306 | 909 | name: &str, |
| 2cee306 | 910 | subject_vt: ValType, |
| 2cee306 | 911 | scratch_local: u32, |
| 2cee306 | 912 | case: &ast::Case, |
| 2cee306 | 913 | rest: &[ast::Case], |
| 2cee306 | 914 | body: &mut Vec<u8>, |
| 2cee306 | 915 | ctx: &LocalCtx, |
| 2cee306 | 916 | state: &mut ModuleState, |
| 2cee306 | 917 | ) -> Result<(), String> { |
| 2cee306 | 918 | let info = ctx |
| 2cee306 | 919 | .enum_variants |
| 2cee306 | 920 | .get(name) |
| 2cee306 | 921 | .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?; |
| 2cee306 | 922 | if subject_vt != ValType::I32 { |
| 2cee306 | 923 | return Err(format!("codegen: enum tag pattern '{}' against a non-enum subject", name)); |
| 2cee306 | 924 | } |
| 2cee306 | 925 | let tag = info.tag; |
| 2cee306 | 926 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 927 | Instruction::I32Const(tag).encode(body); |
| 2cee306 | 928 | Instruction::I32Eq.encode(body); |
| 2cee306 | 929 | Instruction::If(BlockType::Empty).encode(body); |
| 2cee306 | 930 | compile_block(&case.body, body, ctx, state)?; |
| 2cee306 | 931 | Instruction::Else.encode(body); |
| 2cee306 | 932 | compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?; |
| 2cee306 | 933 | Instruction::End.encode(body); |
| 2cee306 | 934 | Ok(()) |
| 2cee306 | 935 | } |
| 2cee306 | 936 | ``` |
| 2cee306 | 937 | |
| 2cee306 | 938 | - [ ] **Step 4: Implement the constructor-pattern match arm** |
| 2cee306 | 939 | |
| 2cee306 | 940 | In `compile_match_arms`, replace line 896: |
| 2cee306 | 941 | |
| 2cee306 | 942 | ```rust |
| 2cee306 | 943 | ast::CasePattern::Class { .. } => Err("codegen: constructor match patterns are not yet supported".to_string()), |
| 2cee306 | 944 | ``` |
| 2cee306 | 945 | |
| 2cee306 | 946 | with: |
| 2cee306 | 947 | |
| 2cee306 | 948 | ```rust |
| 2cee306 | 949 | ast::CasePattern::Class { name, fields } => { |
| 2cee306 | 950 | compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, case, rest, body, ctx, state) |
| 2cee306 | 951 | } |
| 2cee306 | 952 | ``` |
| 2cee306 | 953 | |
| 2cee306 | 954 | Then add this new function right after `compile_variant_eq_arm`: |
| 2cee306 | 955 | |
| 2cee306 | 956 | ```rust |
| 2cee306 | 957 | #[allow(clippy::too_many_arguments)] |
| 2cee306 | 958 | fn compile_variant_constructor_arm( |
| 2cee306 | 959 | name: &str, |
| 2cee306 | 960 | fields: &[ast::CasePattern], |
| 2cee306 | 961 | subject_vt: ValType, |
| 2cee306 | 962 | scratch_local: u32, |
| 2cee306 | 963 | case: &ast::Case, |
| 2cee306 | 964 | rest: &[ast::Case], |
| 2cee306 | 965 | body: &mut Vec<u8>, |
| 2cee306 | 966 | ctx: &LocalCtx, |
| 2cee306 | 967 | state: &mut ModuleState, |
| 2cee306 | 968 | ) -> Result<(), String> { |
| 2cee306 | 969 | let info = ctx |
| 2cee306 | 970 | .enum_variants |
| 2cee306 | 971 | .get(name) |
| 2cee306 | 972 | .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?; |
| 2cee306 | 973 | if subject_vt != ValType::I32 { |
| 2cee306 | 974 | return Err(format!("codegen: constructor pattern '{}' against a non-enum subject", name)); |
| 2cee306 | 975 | } |
| 2cee306 | 976 | if fields.len() != info.field_types.len() { |
| 2cee306 | 977 | return Err(format!( |
| 2cee306 | 978 | "codegen: constructor pattern '{}' expects {} field(s), got {}", |
| 2cee306 | 979 | name, info.field_types.len(), fields.len() |
| 2cee306 | 980 | )); |
| 2cee306 | 981 | } |
| 2cee306 | 982 | let tag = info.tag; |
| 2cee306 | 983 | let field_types = info.field_types.clone(); |
| 2cee306 | 984 | |
| 2cee306 | 985 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 986 | Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body); |
| 2cee306 | 987 | Instruction::I32Const(tag).encode(body); |
| 2cee306 | 988 | Instruction::I32Eq.encode(body); |
| 2cee306 | 989 | Instruction::If(BlockType::Empty).encode(body); |
| 2cee306 | 990 | for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() { |
| 2cee306 | 991 | let bind_name = match pat { |
| 2cee306 | 992 | ast::CasePattern::Name(n) => Some(n.as_str()), |
| 2cee306 | 993 | ast::CasePattern::Wildcard => None, |
| 2cee306 | 994 | _ => return Err("codegen: only bare bindings or '_' are supported inside a constructor pattern".to_string()), |
| 2cee306 | 995 | }; |
| 2cee306 | 996 | if let Some(n) = bind_name { |
| 2cee306 | 997 | let idx = ctx |
| 2cee306 | 998 | .locals |
| 2cee306 | 999 | .get(n) |
| 2cee306 | 1000 | .copied() |
| 2cee306 | 1001 | .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?; |
| 2cee306 | 1002 | Instruction::LocalGet(scratch_local).encode(body); |
| 2cee306 | 1003 | let offset = ((i + 1) as u64) * 8; |
| 2cee306 | 1004 | match plum_type_to_valtype(field_ty) { |
| 2cee306 | 1005 | ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }), |
| 2cee306 | 1006 | ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }), |
| 2cee306 | 1007 | _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }), |
| 2cee306 | 1008 | }.encode(body); |
| 2cee306 | 1009 | Instruction::LocalSet(idx).encode(body); |
| 2cee306 | 1010 | ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone())); |
| 2cee306 | 1011 | } |
| 2cee306 | 1012 | } |
| 2cee306 | 1013 | compile_block(&case.body, body, ctx, state)?; |
| 2cee306 | 1014 | Instruction::Else.encode(body); |
| 2cee306 | 1015 | compile_match_arms(rest, subject_vt, scratch_local, body, ctx, state)?; |
| 2cee306 | 1016 | Instruction::End.encode(body); |
| 2cee306 | 1017 | Ok(()) |
| 2cee306 | 1018 | } |
| 2cee306 | 1019 | ``` |
| 2cee306 | 1020 | |
| 2cee306 | 1021 | - [ ] **Step 5: Run codegen tests** |
| 2cee306 | 1022 | |
| 2cee306 | 1023 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests` |
| 2cee306 | 1024 | Expected: all pass, including Task 3's `payload_variant_construction_compiles_and_runs` (returns 7) and `multi_field_variant_construction_compiles_and_runs` (returns 12). |
| 2cee306 | 1025 | |
| 2cee306 | 1026 | - [ ] **Step 6: Add a dedicated test for a payload-free variant tag pattern beyond Bool, and one for `_`-wildcard inside a constructor pattern** |
| 2cee306 | 1027 | |
| 2cee306 | 1028 | Append to `plum-wasm-codegen/tests/codegen_tests.rs`: |
| 2cee306 | 1029 | |
| 2cee306 | 1030 | ```rust |
| 2cee306 | 1031 | #[test] |
| 2cee306 | 1032 | fn non_bool_bare_tag_pattern_runs_correctly() { |
| 2cee306 | 1033 | let src = "\ |
| 2cee306 | 1034 | enum Color = |
| 2cee306 | 1035 | | Red |
| 2cee306 | 1036 | | Green |
| 2cee306 | 1037 | | Blue |
| 2cee306 | 1038 | |
| 2cee306 | 1039 | code(c: Color) -> Int = |
| 2cee306 | 1040 | match c |
| 2cee306 | 1041 | Red => |
| 2cee306 | 1042 | return 1 |
| 2cee306 | 1043 | Green => |
| 2cee306 | 1044 | return 2 |
| 2cee306 | 1045 | Blue => |
| 2cee306 | 1046 | return 3 |
| 2cee306 | 1047 | |
| 2cee306 | 1048 | main() -> Int = |
| 2cee306 | 1049 | code(Green) |
| 2cee306 | 1050 | "; |
| 2cee306 | 1051 | let source = parse(src); |
| 2cee306 | 1052 | let bytes = compile_source(&source).expect("compile failed"); |
| 2cee306 | 1053 | assert_eq!(run_main(&bytes), 2); |
| 2cee306 | 1054 | } |
| 2cee306 | 1055 | |
| 2cee306 | 1056 | #[test] |
| 2cee306 | 1057 | fn constructor_pattern_wildcard_field_runs_correctly() { |
| 2cee306 | 1058 | let src = "\ |
| 2cee306 | 1059 | enum Option = |
| 2cee306 | 1060 | | Some(Int) |
| 2cee306 | 1061 | | None |
| 2cee306 | 1062 | |
| 2cee306 | 1063 | isSome(o: Option) -> Int = |
| 2cee306 | 1064 | match o |
| 2cee306 | 1065 | Some(_) => |
| 2cee306 | 1066 | return 1 |
| 2cee306 | 1067 | None => |
| 2cee306 | 1068 | return 0 |
| 2cee306 | 1069 | |
| 2cee306 | 1070 | main() -> Int = |
| 2cee306 | 1071 | isSome(Some(99)) |
| 2cee306 | 1072 | "; |
| 2cee306 | 1073 | let source = parse(src); |
| 2cee306 | 1074 | let bytes = compile_source(&source).expect("compile failed"); |
| 2cee306 | 1075 | assert_eq!(run_main(&bytes), 1); |
| 2cee306 | 1076 | } |
| 2cee306 | 1077 | ``` |
| 2cee306 | 1078 | |
| 2cee306 | 1079 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests` |
| 2cee306 | 1080 | Expected: both pass. |
| 2cee306 | 1081 | |
| 2cee306 | 1082 | - [ ] **Step 7: Run the full workspace test suite** |
| 2cee306 | 1083 | |
| 2cee306 | 1084 | Run: `cargo test --workspace` |
| 2cee306 | 1085 | Expected: all green, including `plum-wasm-codegen`'s `examples_test.rs` (see Task 5 — its `match_example_reports_clear_unsupported_pattern_errors` test will now fail because `match.plum` compiles successfully; that's expected and fixed in the next task, not this one). |
| 2cee306 | 1086 | |
| 2cee306 | 1087 | - [ ] **Step 8: Commit** |
| 2cee306 | 1088 | |
| 2cee306 | 1089 | ```bash |
| 2cee306 | 1090 | git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs |
| 2cee306 | 1091 | git commit -m "feat(plum-wasm-codegen): compile general enum match patterns (tags + constructors)" |
| 2cee306 | 1092 | ``` |
| 2cee306 | 1093 | |
| 2cee306 | 1094 | --- |
| 2cee306 | 1095 | |
| 2cee306 | 1096 | ### Task 5: Examples, outdated test expectations, and docs |
| 2cee306 | 1097 | |
| 2cee306 | 1098 | **Files:** |
| 2cee306 | 1099 | - Modify: `plum-wasm-codegen/tests/examples_test.rs:69-78` |
| 2cee306 | 1100 | - Modify: `examples/match.plum` (add a `main` exercising construction, so the example is executed, not just compiled) |
| 2cee306 | 1101 | - Modify: `README.md` (Known gaps section) |
| 2cee306 | 1102 | - Test: same files above |
| 2cee306 | 1103 | |
| 2cee306 | 1104 | **Interfaces:** |
| 2cee306 | 1105 | - Consumes: everything from Tasks 1-4. |
| 2cee306 | 1106 | - Produces: nothing further — this is the final integration/documentation task. |
| 2cee306 | 1107 | |
| 2cee306 | 1108 | - [ ] **Step 1: Fix the now-outdated "expect error" example test** |
| 2cee306 | 1109 | |
| 2cee306 | 1110 | `match.plum` no longer fails to compile — the `match_example_reports_clear_unsupported_pattern_errors` test in `plum-wasm-codegen/tests/examples_test.rs` currently asserts it does. In `plum-wasm-codegen/tests/examples_test.rs`, replace lines 69-78: |
| 2cee306 | 1111 | |
| 2cee306 | 1112 | ```rust |
| 2cee306 | 1113 | /// match.plum and strings.plum intentionally exercise syntax beyond what codegen |
| 2cee306 | 1114 | /// currently lowers (non-Bool enum-tag/constructor match patterns, string |
| 2cee306 | 1115 | /// interpolation) — they must fail loudly with a clear message, not silently |
| 2cee306 | 1116 | /// produce wrong wasm. |
| 2cee306 | 1117 | #[test] |
| 2cee306 | 1118 | fn match_example_reports_clear_unsupported_pattern_errors() { |
| 2cee306 | 1119 | let source = parse_file("match.plum"); |
| 2cee306 | 1120 | let err = compile_source(&source).expect_err("non-Bool enum-tag patterns are not yet supported"); |
| 2cee306 | 1121 | assert!(err.contains("enum variant pattern"), "got: {}", err); |
| 2cee306 | 1122 | } |
| 2cee306 | 1123 | ``` |
| 2cee306 | 1124 | |
| 2cee306 | 1125 | with: |
| 2cee306 | 1126 | |
| 2cee306 | 1127 | ```rust |
| 2cee306 | 1128 | /// match.plum now exercises fully-supported syntax (general enum tag and |
| 2cee306 | 1129 | /// constructor patterns) and must compile and run correctly end to end. |
| 2cee306 | 1130 | #[test] |
| 2cee306 | 1131 | fn match_example_compiles_and_runs_correctly() { |
| 2cee306 | 1132 | let bytes = assert_compiles("match.plum"); |
| 2cee306 | 1133 | let engine = wasmtime::Engine::default(); |
| 2cee306 | 1134 | let module = wasmtime::Module::new(&engine, &bytes).expect("module should be loadable"); |
| 2cee306 | 1135 | let mut store = wasmtime::Store::new(&engine, ()); |
| 2cee306 | 1136 | let instance = wasmtime::Instance::new(&mut store, &module, &[]).expect("module should instantiate"); |
| 2cee306 | 1137 | let main = instance |
| 2cee306 | 1138 | .get_typed_func::<(), i64>(&mut store, "main") |
| 2cee306 | 1139 | .expect("main should have signature () -> i64"); |
| 2cee306 | 1140 | let result = main.call(&mut store, ()).expect("main should not trap"); |
| 2cee306 | 1141 | // describeOption(Some(5)) = 5 |
| 2cee306 | 1142 | assert_eq!(result, 5); |
| 2cee306 | 1143 | } |
| 2cee306 | 1144 | |
| 2cee306 | 1145 | /// strings.plum still exercises string interpolation, which remains unimplemented. |
| 2cee306 | 1146 | #[test] |
| 2cee306 | 1147 | fn strings_example_reports_clear_interpolation_error() { |
| 2cee306 | 1148 | let source = parse_file("strings.plum"); |
| 2cee306 | 1149 | let err = compile_source(&source).expect_err("string interpolation is not yet supported"); |
| 2cee306 | 1150 | assert!(err.contains("interpolation"), "got: {}", err); |
| 2cee306 | 1151 | } |
| 2cee306 | 1152 | ``` |
| 2cee306 | 1153 | |
| 2cee306 | 1154 | Then delete the now-duplicate copy of the same test that already exists lower in the file (originally at lines 80-85, now shifted down by the edit above) — find and remove this exact block, leaving only the one just added above: |
| 2cee306 | 1155 | |
| 2cee306 | 1156 | ```rust |
| 2cee306 | 1157 | #[test] |
| 2cee306 | 1158 | fn strings_example_reports_clear_interpolation_error() { |
| 2cee306 | 1159 | let source = parse_file("strings.plum"); |
| 2cee306 | 1160 | let err = compile_source(&source).expect_err("string interpolation is not yet supported"); |
| 2cee306 | 1161 | assert!(err.contains("interpolation"), "got: {}", err); |
| 2cee306 | 1162 | } |
| 2cee306 | 1163 | ``` |
| 2cee306 | 1164 | |
| 2cee306 | 1165 | (There must be exactly one `strings_example_reports_clear_interpolation_error` function left in the file — `cargo test` will fail to compile with a "duplicate definition" error if both remain.) |
| 2cee306 | 1166 | |
| 2cee306 | 1167 | - [ ] **Step 2: Add a `main` to `examples/match.plum` so it's actually executed, not just compiled** |
| 2cee306 | 1168 | |
| 2cee306 | 1169 | Append to `examples/match.plum`: |
| 2cee306 | 1170 | |
| 2cee306 | 1171 | ```plum |
| 2cee306 | 1172 | |
| 2cee306 | 1173 | main() -> Int = |
| 2cee306 | 1174 | describeOption(Some(5)) |
| 2cee306 | 1175 | ``` |
| 2cee306 | 1176 | |
| 2cee306 | 1177 | - [ ] **Step 3: Run codegen tests** |
| 2cee306 | 1178 | |
| 2cee306 | 1179 | Run: `cargo test -p plum-wasm-codegen --test examples_test` |
| 2cee306 | 1180 | Expected: `match_example_compiles_and_runs_correctly` passes (returns 5), `strings_example_reports_clear_interpolation_error` still passes. |
| 2cee306 | 1181 | |
| 2cee306 | 1182 | - [ ] **Step 4: Update the README's Known Gaps section** |
| 2cee306 | 1183 | |
| 2cee306 | 1184 | In `README.md`, find (around line 319-327): |
| 2cee306 | 1185 | |
| 2cee306 | 1186 | ```markdown |
| 2cee306 | 1187 | ### Known gaps |
| 2cee306 | 1188 | |
| 2cee306 | 1189 | Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code: |
| 2cee306 | 1190 | |
| 2cee306 | 1191 | - string interpolation (plain, non-interpolated string literals do compile) |
| 2cee306 | 1192 | - `match` patterns other than integer literals, bindings, wildcard, and `True`/`False`; non-Bool enum-tag and constructor (`Some(v)`) patterns aren't lowered yet |
| 2cee306 | 1193 | - multi-subject `match` (`match a, b`) |
| 2cee306 | 1194 | - user-defined generics (they type-check but aren't monomorphized) |
| 2cee306 | 1195 | ``` |
| 2cee306 | 1196 | |
| 2cee306 | 1197 | Replace with: |
| 2cee306 | 1198 | |
| 2cee306 | 1199 | ```markdown |
| 2cee306 | 1200 | ### Known gaps |
| 2cee306 | 1201 | |
| 2cee306 | 1202 | Some things parse and type-check but don't compile to wasm yet — `plum-wasm-codegen` reports a clear error rather than silently producing wrong code: |
| 2cee306 | 1203 | |
| 2cee306 | 1204 | - string interpolation (plain, non-interpolated string literals do compile) |
| 2cee306 | 1205 | - multi-subject `match` (`match a, b`) |
| 2cee306 | 1206 | - 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 |
| 2cee306 | 1207 | - nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_` |
| 2cee306 | 1208 | ``` |
| 2cee306 | 1209 | |
| 2cee306 | 1210 | Also update the `match` section's prose just above it (around line 304, "Multiple comma-separated subjects/patterns are accepted by the grammar but not yet lowered by codegen.") — check whether it still needs the caveat about non-Bool enum tags; if that sentence mentions the now-fixed gap, trim it to talk only about multi-subject match remaining unsupported. |
| 2cee306 | 1211 | |
| 2cee306 | 1212 | - [ ] **Step 5: Run the full test suite one more time** |
| 2cee306 | 1213 | |
| 2cee306 | 1214 | Run: |
| 2cee306 | 1215 | ```bash |
| 2cee306 | 1216 | cargo test --workspace |
| 2cee306 | 1217 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test |
| 2cee306 | 1218 | ``` |
| 2cee306 | 1219 | Expected: everything green. |
| 2cee306 | 1220 | |
| 2cee306 | 1221 | - [ ] **Step 6: Commit** |
| 2cee306 | 1222 | |
| 2cee306 | 1223 | ```bash |
| 2cee306 | 1224 | git add plum-wasm-codegen/tests/examples_test.rs examples/match.plum README.md |
| 2cee306 | 1225 | git commit -m "docs+test: general enum support is complete; update known gaps and example" |
| 2cee306 | 1226 | ``` |