plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
docs/superpowers/plans/2026-07-20-tail-position-and-grammar-gaps.md
| f1e33d7 | 1 | # Tail-Position Value Propagation and Grammar Gap Fixes Implementation Plan |
| f1e33d7 | 2 | |
| f1e33d7 | 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. |
| f1e33d7 | 4 | |
| f1e33d7 | 5 | **Goal:** Fix two pre-existing, unrelated-to-enums defects discovered during general enum support: (1) a tree-sitter-plum grammar limitation where a multi-line body's trailing statement can only be `$.primary_expression`, not a full `$.expression`; (2) a `plum-wasm-codegen` gap where a function's tail `match`/`if` (without explicit `return` in every arm) silently drops its value instead of returning it, producing wasm that fails validation. |
| f1e33d7 | 6 | |
| f1e33d7 | 7 | **Architecture:** Fix 1 is a one-line grammar change (`$.primary_expression` → `$.expression` in `_statement`) plus corpus tests. Fix 2 threads an `Option<ValType>` "value position" parameter through the match/if compilation functions (`compile_match`, `compile_match_arms`, `compile_variant_eq_arm`, `compile_variant_constructor_arm`, and a new `compile_if`), with two new small recursive helpers (`compile_stmt_in_value_position`, `compile_block_in_value_position`) that decide, for a statement/block that must produce the function's return value, whether to recurse further (nested `if`/`match`), leave a bare expression's value on the stack, pass through a `return`/`todo` unchanged (both are stack-polymorphic in wasm), or emit a clear compile error for any other shape. |
| f1e33d7 | 8 | |
| f1e33d7 | 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. |
| f1e33d7 | 10 | |
| f1e33d7 | 11 | ## Global Constraints |
| f1e33d7 | 12 | |
| f1e33d7 | 13 | - Out of scope: generics monomorphization, multi-subject `match`, nested constructor patterns, a full "does every path return a value" static analysis in `plum-checker` — only make value-position `if`/`match` either compile correctly or fail with a clear `codegen:`-prefixed error, matching this file's existing error-message convention. |
| f1e33d7 | 14 | - No changes needed in `plum-checker` for either fix — re-run its full suite (including `examples_test.rs`) to confirm no regression, but don't touch its source. |
| f1e33d7 | 15 | - Follow existing code style: terse one-line "why" comments only where non-obvious; error messages use the existing `"codegen: ..."` prefix convention already used throughout `plum-wasm-codegen/src/lib.rs`. |
| f1e33d7 | 16 | - 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. |
| f1e33d7 | 17 | |
| f1e33d7 | 18 | --- |
| f1e33d7 | 19 | |
| f1e33d7 | 20 | ### Task 1: Grammar — accept a full expression as a body's trailing statement |
| f1e33d7 | 21 | |
| f1e33d7 | 22 | **Files:** |
| f1e33d7 | 23 | - Modify: `tooling/tree-sitter-plum/grammar.js:166-179` (`_statement` rule) |
| f1e33d7 | 24 | - Test: `tooling/tree-sitter-plum/test/corpus/` (new cases) |
| f1e33d7 | 25 | |
| f1e33d7 | 26 | **Interfaces:** |
| f1e33d7 | 27 | - Consumes: nothing from other tasks. |
| f1e33d7 | 28 | - Produces: `_statement` accepts any `$.expression` (comparison, boolean-op, ternary, or the existing `$.primary_expression` alternatives), not just `$.primary_expression`. `plum-core`'s parser needs no change — `parse_case`/block-statement parsing already dispatches on node kind, and every new node kind reachable through `expression` (`comparison_operator`, `boolean_operator`, `ternary_expression`) is already handled by `AstParser::parse_expression` (used for expression-context nodes elsewhere). |
| f1e33d7 | 29 | |
| f1e33d7 | 30 | - [ ] **Step 1: Make the grammar change** |
| f1e33d7 | 31 | |
| f1e33d7 | 32 | In `tooling/tree-sitter-plum/grammar.js`, change: |
| f1e33d7 | 33 | |
| f1e33d7 | 34 | ```js |
| f1e33d7 | 35 | _statement: ($) => |
| f1e33d7 | 36 | choice( |
| f1e33d7 | 37 | $.assign, |
| f1e33d7 | 38 | $.break, |
| f1e33d7 | 39 | $.continue, |
| f1e33d7 | 40 | $.assert, |
| f1e33d7 | 41 | $.for, |
| f1e33d7 | 42 | $.while, |
| f1e33d7 | 43 | $.if, |
| f1e33d7 | 44 | $.match, |
| f1e33d7 | 45 | $.return, |
| f1e33d7 | 46 | $.todo, |
| f1e33d7 | 47 | $.primary_expression |
| f1e33d7 | 48 | ), |
| f1e33d7 | 49 | ``` |
| f1e33d7 | 50 | |
| f1e33d7 | 51 | to: |
| f1e33d7 | 52 | |
| f1e33d7 | 53 | ```js |
| f1e33d7 | 54 | _statement: ($) => |
| f1e33d7 | 55 | choice( |
| f1e33d7 | 56 | $.assign, |
| f1e33d7 | 57 | $.break, |
| f1e33d7 | 58 | $.continue, |
| f1e33d7 | 59 | $.assert, |
| f1e33d7 | 60 | $.for, |
| f1e33d7 | 61 | $.while, |
| f1e33d7 | 62 | $.if, |
| f1e33d7 | 63 | $.match, |
| f1e33d7 | 64 | $.return, |
| f1e33d7 | 65 | $.todo, |
| f1e33d7 | 66 | $.expression |
| f1e33d7 | 67 | ), |
| f1e33d7 | 68 | ``` |
| f1e33d7 | 69 | |
| f1e33d7 | 70 | - [ ] **Step 2: Regenerate and run the existing corpus suite** |
| f1e33d7 | 71 | |
| f1e33d7 | 72 | ```bash |
| f1e33d7 | 73 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate && npx --yes tree-sitter-cli test |
| f1e33d7 | 74 | ``` |
| f1e33d7 | 75 | |
| f1e33d7 | 76 | Expected: generation succeeds with no unresolved-conflict errors, and all pre-existing corpus cases still pass (every statement previously reachable via `primary_expression` remains reachable, since `expression`'s own last alternative is `primary_expression` — see `grammar.js`'s `expression` rule). |
| f1e33d7 | 77 | |
| f1e33d7 | 78 | - [ ] **Step 3: Add corpus cases for the newly-parseable statement forms** |
| f1e33d7 | 79 | |
| f1e33d7 | 80 | Append to `tooling/tree-sitter-plum/test/corpus/function.txt` three new cases (input half only — the next step fills in the expected tree): |
| f1e33d7 | 81 | |
| f1e33d7 | 82 | ``` |
| f1e33d7 | 83 | ================================================================================ |
| f1e33d7 | 84 | function - bare comparison as body's trailing statement |
| f1e33d7 | 85 | ================================================================================ |
| f1e33d7 | 86 | |
| f1e33d7 | 87 | isNone(o: Option) -> Bool = |
| f1e33d7 | 88 | o == None |
| f1e33d7 | 89 | |
| f1e33d7 | 90 | -------------------------------------------------------------------------------- |
| f1e33d7 | 91 | ================================================================================ |
| f1e33d7 | 92 | function - bare boolean-operator as body's trailing statement |
| f1e33d7 | 93 | ================================================================================ |
| f1e33d7 | 94 | |
| f1e33d7 | 95 | bothTrue(a: Bool, b: Bool) -> Bool = |
| f1e33d7 | 96 | a && b |
| f1e33d7 | 97 | |
| f1e33d7 | 98 | -------------------------------------------------------------------------------- |
| f1e33d7 | 99 | ================================================================================ |
| f1e33d7 | 100 | function - bare ternary as body's trailing statement |
| f1e33d7 | 101 | ================================================================================ |
| f1e33d7 | 102 | |
| f1e33d7 | 103 | pick(cond: Bool, a: Int, b: Int) -> Int = |
| f1e33d7 | 104 | cond ? a : b |
| f1e33d7 | 105 | |
| f1e33d7 | 106 | -------------------------------------------------------------------------------- |
| f1e33d7 | 107 | ``` |
| f1e33d7 | 108 | |
| f1e33d7 | 109 | - [ ] **Step 4: Generate the expected trees and verify them** |
| f1e33d7 | 110 | |
| f1e33d7 | 111 | ```bash |
| f1e33d7 | 112 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test -u -f "bare comparison as body" && npx --yes tree-sitter-cli test -u -f "bare boolean-operator as body" && npx --yes tree-sitter-cli test -u -f "bare ternary as body" |
| f1e33d7 | 113 | ``` |
| f1e33d7 | 114 | |
| f1e33d7 | 115 | Open `test/corpus/function.txt` and confirm each of the three new cases' generated tree has **no** `ERROR`/`MISSING` node — the comparison/boolean-op/ternary node must appear as a single, complete node directly inside the function's `body`, not split into two separate statements. |
| f1e33d7 | 116 | |
| f1e33d7 | 117 | - [ ] **Step 5: Run the full corpus suite** |
| f1e33d7 | 118 | |
| f1e33d7 | 119 | ```bash |
| f1e33d7 | 120 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test |
| f1e33d7 | 121 | ``` |
| f1e33d7 | 122 | |
| f1e33d7 | 123 | Expected: all cases pass, old and new. |
| f1e33d7 | 124 | |
| f1e33d7 | 125 | - [ ] **Step 6: Run the full Rust workspace suite** |
| f1e33d7 | 126 | |
| f1e33d7 | 127 | ```bash |
| f1e33d7 | 128 | cargo test --workspace |
| f1e33d7 | 129 | ``` |
| f1e33d7 | 130 | |
| f1e33d7 | 131 | Expected: green (the grammar change doesn't remove any previously-valid parse, so nothing downstream should regress; `plum-checker`/`plum-wasm-codegen` tests exercise the parser transitively via their own `parse()` helpers). |
| f1e33d7 | 132 | |
| f1e33d7 | 133 | - [ ] **Step 7: Commit** |
| f1e33d7 | 134 | |
| f1e33d7 | 135 | ```bash |
| f1e33d7 | 136 | git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/test/corpus/function.txt |
| f1e33d7 | 137 | git add tooling/tree-sitter-plum/src # generated parser.c etc, only if tracked — check `git status` first |
| f1e33d7 | 138 | git commit -m "fix(tree-sitter-plum): allow a full expression as a body's trailing statement" |
| f1e33d7 | 139 | ``` |
| f1e33d7 | 140 | |
| f1e33d7 | 141 | --- |
| f1e33d7 | 142 | |
| f1e33d7 | 143 | ### Task 2: Codegen — recursive value-position propagation for tail `if`/`match` |
| f1e33d7 | 144 | |
| f1e33d7 | 145 | **Files:** |
| f1e33d7 | 146 | - Modify: `plum-wasm-codegen/src/lib.rs` (see exact line ranges in each step below — line numbers assume Task 1 has already landed and did not touch this file, so they should still be accurate; verify by reading the current file before editing) |
| f1e33d7 | 147 | - Test: `plum-wasm-codegen/tests/codegen_tests.rs` |
| f1e33d7 | 148 | |
| f1e33d7 | 149 | **Interfaces:** |
| f1e33d7 | 150 | - Consumes: nothing new from Task 1 (Task 1 only touched the grammar; this task's AST shapes — `ast::Stmt::If`, `ast::Stmt::Match`, `ast::Stmt::Expr`, `ast::Stmt::Return`, `ast::Stmt::Todo` — are unchanged). |
| f1e33d7 | 151 | - Produces: `compile_block_as_fn_body`'s signature changes from `(..., has_return_value: bool)` to `(..., result_vt: Option<ValType>)` — its one call site (in `compile_fn_body`) is part of this task. `compile_match`'s signature gains a trailing `result_vt: Option<ValType>` parameter; so do `compile_match_arms`, `compile_variant_eq_arm`, `compile_variant_constructor_arm`. Three new functions: `block_type_for(Option<ValType>) -> BlockType`, `compile_case_body(&ast::Block, Option<ValType>, ...) -> Result<(), String>`, `compile_if(&ast::If, Option<ValType>, ...) -> Result<(), String>`, `compile_block_in_value_position(&ast::Block, ValType, ...) -> Result<(), String>`, `compile_stmt_in_value_position(&ast::Stmt, ValType, ...) -> Result<(), String>`. Task 3 does not depend on any of these names directly — it only exercises the feature through `.plum` source and `compile_source`. |
| f1e33d7 | 152 | |
| f1e33d7 | 153 | - [ ] **Step 1: Write failing tests** |
| f1e33d7 | 154 | |
| f1e33d7 | 155 | Append to `plum-wasm-codegen/tests/codegen_tests.rs`: |
| f1e33d7 | 156 | |
| f1e33d7 | 157 | ```rust |
| f1e33d7 | 158 | #[test] |
| f1e33d7 | 159 | fn tail_match_without_return_runs_correctly() { |
| f1e33d7 | 160 | let src = "\ |
| f1e33d7 | 161 | bindExample(n: Int) -> Int = |
| f1e33d7 | 162 | match n |
| f1e33d7 | 163 | x => |
| f1e33d7 | 164 | x |
| f1e33d7 | 165 | |
| f1e33d7 | 166 | main() -> Int = |
| f1e33d7 | 167 | bindExample(5) |
| f1e33d7 | 168 | "; |
| f1e33d7 | 169 | let source = parse(src); |
| f1e33d7 | 170 | let bytes = compile_source(&source).expect("compile failed"); |
| f1e33d7 | 171 | assert_eq!(run_main(&bytes), 5); |
| f1e33d7 | 172 | } |
| f1e33d7 | 173 | |
| f1e33d7 | 174 | #[test] |
| f1e33d7 | 175 | fn tail_if_without_return_runs_correctly() { |
| f1e33d7 | 176 | let src = "\ |
| f1e33d7 | 177 | abs(n: Int) -> Int = |
| f1e33d7 | 178 | if n < 0 |
| f1e33d7 | 179 | -n |
| f1e33d7 | 180 | else |
| f1e33d7 | 181 | n |
| f1e33d7 | 182 | |
| f1e33d7 | 183 | main() -> Int = |
| f1e33d7 | 184 | abs(-7) |
| f1e33d7 | 185 | "; |
| f1e33d7 | 186 | let source = parse(src); |
| f1e33d7 | 187 | let bytes = compile_source(&source).expect("compile failed"); |
| f1e33d7 | 188 | assert_eq!(run_main(&bytes), 7); |
| f1e33d7 | 189 | } |
| f1e33d7 | 190 | |
| f1e33d7 | 191 | #[test] |
| f1e33d7 | 192 | fn tail_if_nested_inside_match_arm_without_return_runs_correctly() { |
| f1e33d7 | 193 | let src = "\ |
| f1e33d7 | 194 | classify(n: Int) -> Int = |
| f1e33d7 | 195 | match n |
| f1e33d7 | 196 | 0 => |
| f1e33d7 | 197 | 1 |
| f1e33d7 | 198 | x => |
| f1e33d7 | 199 | if x < 0 |
| f1e33d7 | 200 | -1 |
| f1e33d7 | 201 | else |
| f1e33d7 | 202 | 2 |
| f1e33d7 | 203 | |
| f1e33d7 | 204 | main() -> Int = |
| f1e33d7 | 205 | classify(-5) |
| f1e33d7 | 206 | "; |
| f1e33d7 | 207 | let source = parse(src); |
| f1e33d7 | 208 | let bytes = compile_source(&source).expect("compile failed"); |
| f1e33d7 | 209 | assert_eq!(run_main(&bytes), -1); |
| f1e33d7 | 210 | } |
| f1e33d7 | 211 | |
| f1e33d7 | 212 | #[test] |
| f1e33d7 | 213 | fn tail_match_mixing_return_and_bare_expr_arms_runs_correctly() { |
| f1e33d7 | 214 | let src = "\ |
| f1e33d7 | 215 | describe(n: Int) -> Int = |
| f1e33d7 | 216 | match n |
| f1e33d7 | 217 | 0 => |
| f1e33d7 | 218 | return 100 |
| f1e33d7 | 219 | x => |
| f1e33d7 | 220 | x * 2 |
| f1e33d7 | 221 | |
| f1e33d7 | 222 | main() -> Int = |
| f1e33d7 | 223 | describe(21) |
| f1e33d7 | 224 | "; |
| f1e33d7 | 225 | let source = parse(src); |
| f1e33d7 | 226 | let bytes = compile_source(&source).expect("compile failed"); |
| f1e33d7 | 227 | assert_eq!(run_main(&bytes), 42); |
| f1e33d7 | 228 | } |
| f1e33d7 | 229 | |
| f1e33d7 | 230 | #[test] |
| f1e33d7 | 231 | fn tail_enum_match_without_return_runs_correctly() { |
| f1e33d7 | 232 | let src = "\ |
| f1e33d7 | 233 | enum Option = |
| f1e33d7 | 234 | | Some(Int) |
| f1e33d7 | 235 | | None |
| f1e33d7 | 236 | |
| f1e33d7 | 237 | unwrapOr(o: Option, default: Int) -> Int = |
| f1e33d7 | 238 | match o |
| f1e33d7 | 239 | Some(v) => |
| f1e33d7 | 240 | v |
| f1e33d7 | 241 | None => |
| f1e33d7 | 242 | default |
| f1e33d7 | 243 | |
| f1e33d7 | 244 | main() -> Int = |
| f1e33d7 | 245 | unwrapOr(Some(9), 0) |
| f1e33d7 | 246 | "; |
| f1e33d7 | 247 | let source = parse(src); |
| f1e33d7 | 248 | let bytes = compile_source(&source).expect("compile failed"); |
| f1e33d7 | 249 | assert_eq!(run_main(&bytes), 9); |
| f1e33d7 | 250 | } |
| f1e33d7 | 251 | |
| f1e33d7 | 252 | #[test] |
| f1e33d7 | 253 | fn tail_if_without_else_is_a_clear_error() { |
| f1e33d7 | 254 | let src = "\ |
| f1e33d7 | 255 | bad(n: Int) -> Int = |
| f1e33d7 | 256 | if n < 0 |
| f1e33d7 | 257 | return 1 |
| f1e33d7 | 258 | "; |
| f1e33d7 | 259 | let source = parse(src); |
| f1e33d7 | 260 | let err = compile_source(&source).expect_err("if without else in value position must be a clear error, not invalid wasm"); |
| f1e33d7 | 261 | assert!(err.contains("doesn't produce a return value"), "got: {}", err); |
| f1e33d7 | 262 | } |
| f1e33d7 | 263 | |
| f1e33d7 | 264 | #[test] |
| f1e33d7 | 265 | fn tail_match_non_exhaustive_is_a_clear_error() { |
| f1e33d7 | 266 | let src = "\ |
| f1e33d7 | 267 | bad(n: Int) -> Int = |
| f1e33d7 | 268 | match n |
| f1e33d7 | 269 | 0 => |
| f1e33d7 | 270 | 1 |
| f1e33d7 | 271 | "; |
| f1e33d7 | 272 | let source = parse(src); |
| f1e33d7 | 273 | let err = compile_source(&source).expect_err("non-exhaustive match in value position must be a clear error, not invalid wasm"); |
| f1e33d7 | 274 | assert!(err.contains("doesn't produce a return value"), "got: {}", err); |
| f1e33d7 | 275 | } |
| f1e33d7 | 276 | |
| f1e33d7 | 277 | #[test] |
| f1e33d7 | 278 | fn tail_match_arm_ending_in_non_value_statement_is_a_clear_error() { |
| f1e33d7 | 279 | let src = "\ |
| f1e33d7 | 280 | bad(n: Int) -> Int = |
| f1e33d7 | 281 | match n |
| f1e33d7 | 282 | x => |
| f1e33d7 | 283 | y = x |
| f1e33d7 | 284 | "; |
| f1e33d7 | 285 | let source = parse(src); |
| f1e33d7 | 286 | let err = compile_source(&source).expect_err("a match arm ending in a non-value statement must be a clear error, not invalid wasm"); |
| f1e33d7 | 287 | assert!(err.contains("doesn't produce a return value"), "got: {}", err); |
| f1e33d7 | 288 | } |
| f1e33d7 | 289 | ``` |
| f1e33d7 | 290 | |
| f1e33d7 | 291 | - [ ] **Step 2: Run to see them fail** |
| f1e33d7 | 292 | |
| f1e33d7 | 293 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests tail_` |
| f1e33d7 | 294 | Expected: the first five tests fail with `wasmparser::validate` errors surfacing as `compile failed` panics (the value is dropped, producing invalid wasm) or wrong runtime results; the last two "clear error" tests fail because `compile_source` currently returns `Ok` (or panics) instead of the expected `Err`. |
| f1e33d7 | 295 | |
| f1e33d7 | 296 | - [ ] **Step 3: Read the current file to confirm line numbers, then make the edits** |
| f1e33d7 | 297 | |
| f1e33d7 | 298 | Read `plum-wasm-codegen/src/lib.rs` around the ranges below before editing — Task 1 doesn't touch this file, so these should still be accurate, but verify. |
| f1e33d7 | 299 | |
| f1e33d7 | 300 | **3a. Add `block_type_for` near the other small helpers** (e.g. right after `plum_type_to_valtype`, around line 249-256): |
| f1e33d7 | 301 | |
| f1e33d7 | 302 | ```rust |
| f1e33d7 | 303 | fn block_type_for(result_vt: Option<ValType>) -> BlockType { |
| f1e33d7 | 304 | result_vt.map(BlockType::Result).unwrap_or(BlockType::Empty) |
| f1e33d7 | 305 | } |
| f1e33d7 | 306 | ``` |
| f1e33d7 | 307 | |
| f1e33d7 | 308 | **3b. Remove `stmt_always_diverges` and `block_always_diverges`** (currently lines 642-662) — they become dead code once `compile_block_as_fn_body` no longer needs them (Step 3d). Delete this whole block: |
| f1e33d7 | 309 | |
| f1e33d7 | 310 | ```rust |
| f1e33d7 | 311 | /// True if control can never fall through past this statement — every reachable path |
| f1e33d7 | 312 | /// ends in a `return`. Used to decide whether a tail-position If/Match needs a |
| f1e33d7 | 313 | /// trailing `unreachable` to satisfy wasm's per-block (not whole-function) validation |
| f1e33d7 | 314 | /// when the function declares a non-Unit return type. |
| f1e33d7 | 315 | fn stmt_always_diverges(stmt: &ast::Stmt) -> bool { |
| f1e33d7 | 316 | match stmt { |
| f1e33d7 | 317 | ast::Stmt::Return(_) | ast::Stmt::Todo => true, |
| f1e33d7 | 318 | ast::Stmt::If(if_) => { |
| f1e33d7 | 319 | if_.else_.is_some() |
| f1e33d7 | 320 | && block_always_diverges(&if_.body) |
| f1e33d7 | 321 | && if_.else_ifs.iter().all(|ei| block_always_diverges(&ei.body)) |
| f1e33d7 | 322 | && if_.else_.as_ref().is_some_and(block_always_diverges) |
| f1e33d7 | 323 | } |
| f1e33d7 | 324 | ast::Stmt::Match(m) => !m.cases.is_empty() && m.cases.iter().all(|c| block_always_diverges(&c.body)), |
| f1e33d7 | 325 | _ => false, |
| f1e33d7 | 326 | } |
| f1e33d7 | 327 | } |
| f1e33d7 | 328 | |
| f1e33d7 | 329 | fn block_always_diverges(block: &ast::Block) -> bool { |
| f1e33d7 | 330 | block.stmts.last().map(stmt_always_diverges).unwrap_or(false) |
| f1e33d7 | 331 | } |
| f1e33d7 | 332 | ``` |
| f1e33d7 | 333 | |
| f1e33d7 | 334 | **3c. Add the new value-position helpers**, right after `compile_block` (currently lines 635-640) and before where `stmt_always_diverges` used to be: |
| f1e33d7 | 335 | |
| f1e33d7 | 336 | ```rust |
| f1e33d7 | 337 | /// Compiles a case/branch body either as an ordinary statement block (`result_vt: None`) |
| f1e33d7 | 338 | /// or, when in value position, via `compile_block_in_value_position` so its own tail |
| f1e33d7 | 339 | /// statement propagates a value instead of being dropped. |
| f1e33d7 | 340 | fn compile_case_body( |
| f1e33d7 | 341 | block: &ast::Block, |
| f1e33d7 | 342 | result_vt: Option<ValType>, |
| f1e33d7 | 343 | body: &mut Vec<u8>, |
| f1e33d7 | 344 | ctx: &LocalCtx, |
| f1e33d7 | 345 | state: &mut ModuleState, |
| f1e33d7 | 346 | ) -> Result<(), String> { |
| f1e33d7 | 347 | match result_vt { |
| f1e33d7 | 348 | Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state), |
| f1e33d7 | 349 | None => compile_block(block, body, ctx, state), |
| f1e33d7 | 350 | } |
| f1e33d7 | 351 | } |
| f1e33d7 | 352 | |
| f1e33d7 | 353 | /// Compiles a block whose value must be produced when control reaches its end — every |
| f1e33d7 | 354 | /// statement except the last compiles normally; the last is compiled via |
| f1e33d7 | 355 | /// `compile_stmt_in_value_position`. |
| f1e33d7 | 356 | fn compile_block_in_value_position( |
| f1e33d7 | 357 | block: &ast::Block, |
| f1e33d7 | 358 | result_vt: ValType, |
| f1e33d7 | 359 | body: &mut Vec<u8>, |
| f1e33d7 | 360 | ctx: &LocalCtx, |
| f1e33d7 | 361 | state: &mut ModuleState, |
| f1e33d7 | 362 | ) -> Result<(), String> { |
| f1e33d7 | 363 | let (last, rest) = block.stmts.split_last().ok_or_else(|| { |
| f1e33d7 | 364 | "codegen: function has a control-flow path that doesn't produce a return value (empty branch)".to_string() |
| f1e33d7 | 365 | })?; |
| f1e33d7 | 366 | for stmt in rest { |
| f1e33d7 | 367 | compile_stmt(stmt, body, ctx, state)?; |
| f1e33d7 | 368 | } |
| f1e33d7 | 369 | compile_stmt_in_value_position(last, result_vt, body, ctx, state) |
| f1e33d7 | 370 | } |
| f1e33d7 | 371 | |
| f1e33d7 | 372 | /// Compiles a single statement in value position: a bare expression is left on the stack |
| f1e33d7 | 373 | /// (not dropped); `return`/`todo` compile normally (both are stack-polymorphic in wasm — |
| f1e33d7 | 374 | /// control never falls through past them, so no value is needed on this path); `if`/`match` |
| f1e33d7 | 375 | /// recurse so every arm/branch resolves the same way. Any other statement kind can't |
| f1e33d7 | 376 | /// produce a value, so this returns a clear error instead of ever emitting wasm that |
| f1e33d7 | 377 | /// would fail validation. |
| f1e33d7 | 378 | fn compile_stmt_in_value_position( |
| f1e33d7 | 379 | stmt: &ast::Stmt, |
| f1e33d7 | 380 | result_vt: ValType, |
| f1e33d7 | 381 | body: &mut Vec<u8>, |
| f1e33d7 | 382 | ctx: &LocalCtx, |
| f1e33d7 | 383 | state: &mut ModuleState, |
| f1e33d7 | 384 | ) -> Result<(), String> { |
| f1e33d7 | 385 | match stmt { |
| f1e33d7 | 386 | ast::Stmt::Expr(e) => compile_expr(e, body, ctx, state), |
| f1e33d7 | 387 | ast::Stmt::Return(_) | ast::Stmt::Todo => compile_stmt(stmt, body, ctx, state), |
| f1e33d7 | 388 | ast::Stmt::If(if_) => compile_if(if_, Some(result_vt), body, ctx, state), |
| f1e33d7 | 389 | ast::Stmt::Match(m) => compile_match(m, body, ctx, state, Some(result_vt)), |
| f1e33d7 | 390 | _ => Err( |
| f1e33d7 | 391 | "codegen: function has a control-flow path that doesn't produce a return value".to_string(), |
| f1e33d7 | 392 | ), |
| f1e33d7 | 393 | } |
| f1e33d7 | 394 | } |
| f1e33d7 | 395 | |
| f1e33d7 | 396 | /// Compiles an `if`/`else if`/`else` chain. `result_vt` is `None` for an ordinary statement |
| f1e33d7 | 397 | /// (each branch is `BlockType::Empty`, nothing left on the stack) or `Some(vt)` when this |
| f1e33d7 | 398 | /// `if` is in value position — every branch must then leave a `vt` value on the stack, which |
| f1e33d7 | 399 | /// requires an `else` (a value can't be produced on a path that doesn't exist). |
| f1e33d7 | 400 | fn compile_if( |
| f1e33d7 | 401 | if_: &ast::If, |
| f1e33d7 | 402 | result_vt: Option<ValType>, |
| f1e33d7 | 403 | body: &mut Vec<u8>, |
| f1e33d7 | 404 | ctx: &LocalCtx, |
| f1e33d7 | 405 | state: &mut ModuleState, |
| f1e33d7 | 406 | ) -> Result<(), String> { |
| f1e33d7 | 407 | if result_vt.is_some() && if_.else_.is_none() { |
| f1e33d7 | 408 | return Err( |
| f1e33d7 | 409 | "codegen: function has a control-flow path that doesn't produce a return value (if without else)".to_string(), |
| f1e33d7 | 410 | ); |
| f1e33d7 | 411 | } |
| f1e33d7 | 412 | let bt = block_type_for(result_vt); |
| f1e33d7 | 413 | compile_expr(&if_.condition, body, ctx, state)?; |
| f1e33d7 | 414 | Instruction::If(bt).encode(body); |
| f1e33d7 | 415 | compile_case_body(&if_.body, result_vt, body, ctx, state)?; |
| f1e33d7 | 416 | if !if_.else_ifs.is_empty() || if_.else_.is_some() { |
| f1e33d7 | 417 | Instruction::Else.encode(body); |
| f1e33d7 | 418 | for ei in &if_.else_ifs { |
| f1e33d7 | 419 | compile_expr(&ei.condition, body, ctx, state)?; |
| f1e33d7 | 420 | Instruction::If(bt).encode(body); |
| f1e33d7 | 421 | compile_case_body(&ei.body, result_vt, body, ctx, state)?; |
| f1e33d7 | 422 | Instruction::Else.encode(body); |
| f1e33d7 | 423 | } |
| f1e33d7 | 424 | if let Some(else_block) = &if_.else_ { |
| f1e33d7 | 425 | compile_case_body(else_block, result_vt, body, ctx, state)?; |
| f1e33d7 | 426 | } |
| f1e33d7 | 427 | for _ in &if_.else_ifs { |
| f1e33d7 | 428 | Instruction::End.encode(body); |
| f1e33d7 | 429 | } |
| f1e33d7 | 430 | } |
| f1e33d7 | 431 | Instruction::End.encode(body); |
| f1e33d7 | 432 | Ok(()) |
| f1e33d7 | 433 | } |
| f1e33d7 | 434 | ``` |
| f1e33d7 | 435 | |
| f1e33d7 | 436 | **3d. Simplify `compile_block_as_fn_body`** (currently lines 664-706) — replace the whole function: |
| f1e33d7 | 437 | |
| f1e33d7 | 438 | ```rust |
| f1e33d7 | 439 | /// Compiles a block that is the body of a function. If the function returns a value, |
| f1e33d7 | 440 | /// its tail statement is compiled in value position (see `compile_stmt_in_value_position`) |
| f1e33d7 | 441 | /// so a bare expression, or an `if`/`match` whose arms resolve to one, propagates that |
| f1e33d7 | 442 | /// value instead of being dropped. |
| f1e33d7 | 443 | fn compile_block_as_fn_body( |
| f1e33d7 | 444 | block: &ast::Block, |
| f1e33d7 | 445 | body: &mut Vec<u8>, |
| f1e33d7 | 446 | ctx: &LocalCtx, |
| f1e33d7 | 447 | state: &mut ModuleState, |
| f1e33d7 | 448 | result_vt: Option<ValType>, |
| f1e33d7 | 449 | ) -> Result<(), String> { |
| f1e33d7 | 450 | match result_vt { |
| f1e33d7 | 451 | Some(vt) => compile_block_in_value_position(block, vt, body, ctx, state), |
| f1e33d7 | 452 | None => compile_block(block, body, ctx, state), |
| f1e33d7 | 453 | } |
| f1e33d7 | 454 | } |
| f1e33d7 | 455 | ``` |
| f1e33d7 | 456 | |
| f1e33d7 | 457 | **3e. Update `compile_fn_body`'s call site** (currently around lines 620 and 627): |
| f1e33d7 | 458 | |
| f1e33d7 | 459 | Replace: |
| f1e33d7 | 460 | |
| f1e33d7 | 461 | ```rust |
| f1e33d7 | 462 | let has_return_value = f.returns.as_ref().map(|r| r.name != "Unit").unwrap_or(false); |
| f1e33d7 | 463 | |
| f1e33d7 | 464 | match &f.body { |
| f1e33d7 | 465 | ast::FnBody::Expr(e) => { |
| f1e33d7 | 466 | compile_expr(e, &mut body, &local_ctx, state)?; |
| f1e33d7 | 467 | } |
| f1e33d7 | 468 | ast::FnBody::Block(block) => { |
| f1e33d7 | 469 | compile_block_as_fn_body(block, &mut body, &local_ctx, state, has_return_value)?; |
| f1e33d7 | 470 | } |
| f1e33d7 | 471 | } |
| f1e33d7 | 472 | ``` |
| f1e33d7 | 473 | |
| f1e33d7 | 474 | with: |
| f1e33d7 | 475 | |
| f1e33d7 | 476 | ```rust |
| f1e33d7 | 477 | let result_vt = ret_type_to_wasm(f.returns.as_ref()); |
| f1e33d7 | 478 | |
| f1e33d7 | 479 | match &f.body { |
| f1e33d7 | 480 | ast::FnBody::Expr(e) => { |
| f1e33d7 | 481 | compile_expr(e, &mut body, &local_ctx, state)?; |
| f1e33d7 | 482 | } |
| f1e33d7 | 483 | ast::FnBody::Block(block) => { |
| f1e33d7 | 484 | compile_block_as_fn_body(block, &mut body, &local_ctx, state, result_vt)?; |
| f1e33d7 | 485 | } |
| f1e33d7 | 486 | } |
| f1e33d7 | 487 | ``` |
| f1e33d7 | 488 | |
| f1e33d7 | 489 | **3f. Replace `compile_stmt`'s inline `If` arm** (currently lines 730-750) with a call to the new `compile_if`: |
| f1e33d7 | 490 | |
| f1e33d7 | 491 | Replace: |
| f1e33d7 | 492 | |
| f1e33d7 | 493 | ```rust |
| f1e33d7 | 494 | ast::Stmt::If(if_) => { |
| f1e33d7 | 495 | compile_expr(&if_.condition, body, ctx, state)?; |
| f1e33d7 | 496 | Instruction::If(BlockType::Empty).encode(body); |
| f1e33d7 | 497 | compile_block(&if_.body, body, ctx, state)?; |
| f1e33d7 | 498 | if !if_.else_ifs.is_empty() || if_.else_.is_some() { |
| f1e33d7 | 499 | Instruction::Else.encode(body); |
| f1e33d7 | 500 | for ei in &if_.else_ifs { |
| f1e33d7 | 501 | compile_expr(&ei.condition, body, ctx, state)?; |
| f1e33d7 | 502 | Instruction::If(BlockType::Empty).encode(body); |
| f1e33d7 | 503 | compile_block(&ei.body, body, ctx, state)?; |
| f1e33d7 | 504 | Instruction::Else.encode(body); |
| f1e33d7 | 505 | } |
| f1e33d7 | 506 | if let Some(else_block) = &if_.else_ { |
| f1e33d7 | 507 | compile_block(else_block, body, ctx, state)?; |
| f1e33d7 | 508 | } |
| f1e33d7 | 509 | for _ in &if_.else_ifs { |
| f1e33d7 | 510 | Instruction::End.encode(body); |
| f1e33d7 | 511 | } |
| f1e33d7 | 512 | } |
| f1e33d7 | 513 | Instruction::End.encode(body); |
| f1e33d7 | 514 | } |
| f1e33d7 | 515 | ``` |
| f1e33d7 | 516 | |
| f1e33d7 | 517 | with: |
| f1e33d7 | 518 | |
| f1e33d7 | 519 | ```rust |
| f1e33d7 | 520 | ast::Stmt::If(if_) => { |
| f1e33d7 | 521 | compile_if(if_, None, body, ctx, state)?; |
| f1e33d7 | 522 | } |
| f1e33d7 | 523 | ``` |
| f1e33d7 | 524 | |
| f1e33d7 | 525 | **3g. Update `compile_stmt`'s `Match` arm** (currently `compile_match(m, body, ctx, state)?;`) to pass `None`: |
| f1e33d7 | 526 | |
| f1e33d7 | 527 | ```rust |
| f1e33d7 | 528 | ast::Stmt::Match(m) => { |
| f1e33d7 | 529 | compile_match(m, body, ctx, state, None)?; |
| f1e33d7 | 530 | } |
| f1e33d7 | 531 | ``` |
| f1e33d7 | 532 | |
| f1e33d7 | 533 | **3h. Update `compile_match`'s signature and body** (currently lines 844-863): |
| f1e33d7 | 534 | |
| f1e33d7 | 535 | Replace: |
| f1e33d7 | 536 | |
| f1e33d7 | 537 | ```rust |
| f1e33d7 | 538 | fn compile_match(m: &ast::Match, body: &mut Vec<u8>, ctx: &LocalCtx, state: &mut ModuleState) -> Result<(), String> { |
| f1e33d7 | 539 | if m.subjects.len() != 1 { |
| f1e33d7 | 540 | return Err("codegen: multi-subject match is not yet supported".to_string()); |
| f1e33d7 | 541 | } |
| f1e33d7 | 542 | let subject = &m.subjects[0]; |
| f1e33d7 | 543 | let subject_ty = infer_local_type(subject, ctx); |
| f1e33d7 | 544 | let subject_vt = plum_type_to_valtype(&subject_ty); |
| f1e33d7 | 545 | |
| f1e33d7 | 546 | let key = m as *const ast::Match as usize; |
| f1e33d7 | 547 | let slot = *ctx |
| f1e33d7 | 548 | .match_scratch_index |
| f1e33d7 | 549 | .get(&key) |
| f1e33d7 | 550 | .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?; |
| f1e33d7 | 551 | let scratch_local = ctx.match_scratch_base + slot; |
| f1e33d7 | 552 | |
| f1e33d7 | 553 | compile_expr(subject, body, ctx, state)?; |
| f1e33d7 | 554 | Instruction::LocalSet(scratch_local).encode(body); |
| f1e33d7 | 555 | |
| f1e33d7 | 556 | compile_match_arms(&m.cases, subject_vt, scratch_local, body, ctx, state) |
| f1e33d7 | 557 | } |
| f1e33d7 | 558 | ``` |
| f1e33d7 | 559 | |
| f1e33d7 | 560 | with: |
| f1e33d7 | 561 | |
| f1e33d7 | 562 | ```rust |
| f1e33d7 | 563 | fn compile_match( |
| f1e33d7 | 564 | m: &ast::Match, |
| f1e33d7 | 565 | body: &mut Vec<u8>, |
| f1e33d7 | 566 | ctx: &LocalCtx, |
| f1e33d7 | 567 | state: &mut ModuleState, |
| f1e33d7 | 568 | result_vt: Option<ValType>, |
| f1e33d7 | 569 | ) -> Result<(), String> { |
| f1e33d7 | 570 | if m.subjects.len() != 1 { |
| f1e33d7 | 571 | return Err("codegen: multi-subject match is not yet supported".to_string()); |
| f1e33d7 | 572 | } |
| f1e33d7 | 573 | let subject = &m.subjects[0]; |
| f1e33d7 | 574 | let subject_ty = infer_local_type(subject, ctx); |
| f1e33d7 | 575 | let subject_vt = plum_type_to_valtype(&subject_ty); |
| f1e33d7 | 576 | |
| f1e33d7 | 577 | let key = m as *const ast::Match as usize; |
| f1e33d7 | 578 | let slot = *ctx |
| f1e33d7 | 579 | .match_scratch_index |
| f1e33d7 | 580 | .get(&key) |
| f1e33d7 | 581 | .ok_or_else(|| "internal codegen error: missing match scratch slot".to_string())?; |
| f1e33d7 | 582 | let scratch_local = ctx.match_scratch_base + slot; |
| f1e33d7 | 583 | |
| f1e33d7 | 584 | compile_expr(subject, body, ctx, state)?; |
| f1e33d7 | 585 | Instruction::LocalSet(scratch_local).encode(body); |
| f1e33d7 | 586 | |
| f1e33d7 | 587 | compile_match_arms(&m.cases, subject_vt, scratch_local, result_vt, body, ctx, state) |
| f1e33d7 | 588 | } |
| f1e33d7 | 589 | ``` |
| f1e33d7 | 590 | |
| f1e33d7 | 591 | **3i. Update `compile_match_arms`** (currently lines 865-921): |
| f1e33d7 | 592 | |
| f1e33d7 | 593 | Replace the whole function: |
| f1e33d7 | 594 | |
| f1e33d7 | 595 | ```rust |
| f1e33d7 | 596 | fn compile_match_arms( |
| f1e33d7 | 597 | cases: &[ast::Case], |
| f1e33d7 | 598 | subject_vt: ValType, |
| f1e33d7 | 599 | scratch_local: u32, |
| f1e33d7 | 600 | result_vt: Option<ValType>, |
| f1e33d7 | 601 | body: &mut Vec<u8>, |
| f1e33d7 | 602 | ctx: &LocalCtx, |
| f1e33d7 | 603 | state: &mut ModuleState, |
| f1e33d7 | 604 | ) -> Result<(), String> { |
| f1e33d7 | 605 | let (case, rest) = match cases.split_first() { |
| f1e33d7 | 606 | None => { |
| f1e33d7 | 607 | return match result_vt { |
| f1e33d7 | 608 | Some(_) => Err( |
| f1e33d7 | 609 | "codegen: function has a control-flow path that doesn't produce a return value (non-exhaustive match)".to_string(), |
| f1e33d7 | 610 | ), |
| f1e33d7 | 611 | None => Ok(()), |
| f1e33d7 | 612 | }; |
| f1e33d7 | 613 | } |
| f1e33d7 | 614 | Some(pair) => pair, |
| f1e33d7 | 615 | }; |
| f1e33d7 | 616 | let pat = case.patterns.first().ok_or_else(|| "codegen: match case has no pattern".to_string())?; |
| f1e33d7 | 617 | match pat { |
| f1e33d7 | 618 | ast::CasePattern::Wildcard => { |
| f1e33d7 | 619 | // Any cases after a wildcard are unreachable, matching real match semantics. |
| f1e33d7 | 620 | compile_case_body(&case.body, result_vt, body, ctx, state) |
| f1e33d7 | 621 | } |
| f1e33d7 | 622 | ast::CasePattern::Name(n) => { |
| f1e33d7 | 623 | let is_variant = n.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) |
| f1e33d7 | 624 | && ctx.enum_variants.contains_key(n); |
| f1e33d7 | 625 | if is_variant { |
| f1e33d7 | 626 | compile_variant_eq_arm(n, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state) |
| f1e33d7 | 627 | } else { |
| f1e33d7 | 628 | let idx = ctx |
| f1e33d7 | 629 | .locals |
| f1e33d7 | 630 | .get(n) |
| f1e33d7 | 631 | .copied() |
| f1e33d7 | 632 | .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?; |
| f1e33d7 | 633 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 634 | Instruction::LocalSet(idx).encode(body); |
| f1e33d7 | 635 | ctx.type_env.borrow_mut().insert(n.clone(), TypeScheme::mono(plum_type_from_valtype_hint(subject_vt))); |
| f1e33d7 | 636 | compile_case_body(&case.body, result_vt, body, ctx, state) |
| f1e33d7 | 637 | // A binding arm always matches — any following cases are unreachable. |
| f1e33d7 | 638 | } |
| f1e33d7 | 639 | } |
| f1e33d7 | 640 | ast::CasePattern::Int(n) => { |
| f1e33d7 | 641 | if subject_vt != ValType::I64 { |
| f1e33d7 | 642 | return Err("codegen: integer match pattern against a non-Int subject".to_string()); |
| f1e33d7 | 643 | } |
| f1e33d7 | 644 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 645 | Instruction::I64Const(*n).encode(body); |
| f1e33d7 | 646 | Instruction::I64Eq.encode(body); |
| f1e33d7 | 647 | Instruction::If(block_type_for(result_vt)).encode(body); |
| f1e33d7 | 648 | compile_case_body(&case.body, result_vt, body, ctx, state)?; |
| f1e33d7 | 649 | Instruction::Else.encode(body); |
| f1e33d7 | 650 | compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?; |
| f1e33d7 | 651 | Instruction::End.encode(body); |
| f1e33d7 | 652 | Ok(()) |
| f1e33d7 | 653 | } |
| f1e33d7 | 654 | ast::CasePattern::String(_) => Err("codegen: string match patterns are not yet supported".to_string()), |
| f1e33d7 | 655 | ast::CasePattern::Float(_) => Err("codegen: float match patterns are not yet supported".to_string()), |
| f1e33d7 | 656 | ast::CasePattern::Class { name, fields } => { |
| f1e33d7 | 657 | compile_variant_constructor_arm(name, fields, subject_vt, scratch_local, result_vt, case, rest, body, ctx, state) |
| f1e33d7 | 658 | } |
| f1e33d7 | 659 | } |
| f1e33d7 | 660 | } |
| f1e33d7 | 661 | ``` |
| f1e33d7 | 662 | |
| f1e33d7 | 663 | **3j. Update `compile_variant_eq_arm`** (currently lines 923-951): |
| f1e33d7 | 664 | |
| f1e33d7 | 665 | Replace the whole function: |
| f1e33d7 | 666 | |
| f1e33d7 | 667 | ```rust |
| f1e33d7 | 668 | #[allow(clippy::too_many_arguments)] |
| f1e33d7 | 669 | fn compile_variant_eq_arm( |
| f1e33d7 | 670 | name: &str, |
| f1e33d7 | 671 | subject_vt: ValType, |
| f1e33d7 | 672 | scratch_local: u32, |
| f1e33d7 | 673 | result_vt: Option<ValType>, |
| f1e33d7 | 674 | case: &ast::Case, |
| f1e33d7 | 675 | rest: &[ast::Case], |
| f1e33d7 | 676 | body: &mut Vec<u8>, |
| f1e33d7 | 677 | ctx: &LocalCtx, |
| f1e33d7 | 678 | state: &mut ModuleState, |
| f1e33d7 | 679 | ) -> Result<(), String> { |
| f1e33d7 | 680 | let info = ctx |
| f1e33d7 | 681 | .enum_variants |
| f1e33d7 | 682 | .get(name) |
| f1e33d7 | 683 | .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?; |
| f1e33d7 | 684 | if subject_vt != ValType::I32 { |
| f1e33d7 | 685 | return Err(format!("codegen: enum tag pattern '{}' against a non-enum subject", name)); |
| f1e33d7 | 686 | } |
| f1e33d7 | 687 | let tag = info.tag; |
| f1e33d7 | 688 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 689 | Instruction::I32Const(tag).encode(body); |
| f1e33d7 | 690 | Instruction::I32Eq.encode(body); |
| f1e33d7 | 691 | Instruction::If(block_type_for(result_vt)).encode(body); |
| f1e33d7 | 692 | compile_case_body(&case.body, result_vt, body, ctx, state)?; |
| f1e33d7 | 693 | Instruction::Else.encode(body); |
| f1e33d7 | 694 | compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?; |
| f1e33d7 | 695 | Instruction::End.encode(body); |
| f1e33d7 | 696 | Ok(()) |
| f1e33d7 | 697 | } |
| f1e33d7 | 698 | ``` |
| f1e33d7 | 699 | |
| f1e33d7 | 700 | **3k. Update `compile_variant_constructor_arm`** (currently lines 953-1026): |
| f1e33d7 | 701 | |
| f1e33d7 | 702 | Replace the whole function: |
| f1e33d7 | 703 | |
| f1e33d7 | 704 | ```rust |
| f1e33d7 | 705 | #[allow(clippy::too_many_arguments)] |
| f1e33d7 | 706 | fn compile_variant_constructor_arm( |
| f1e33d7 | 707 | name: &str, |
| f1e33d7 | 708 | fields: &[ast::CasePattern], |
| f1e33d7 | 709 | subject_vt: ValType, |
| f1e33d7 | 710 | scratch_local: u32, |
| f1e33d7 | 711 | result_vt: Option<ValType>, |
| f1e33d7 | 712 | case: &ast::Case, |
| f1e33d7 | 713 | rest: &[ast::Case], |
| f1e33d7 | 714 | body: &mut Vec<u8>, |
| f1e33d7 | 715 | ctx: &LocalCtx, |
| f1e33d7 | 716 | state: &mut ModuleState, |
| f1e33d7 | 717 | ) -> Result<(), String> { |
| f1e33d7 | 718 | let info = ctx |
| f1e33d7 | 719 | .enum_variants |
| f1e33d7 | 720 | .get(name) |
| f1e33d7 | 721 | .ok_or_else(|| format!("codegen: unknown enum variant '{}'", name))?; |
| f1e33d7 | 722 | if subject_vt != ValType::I32 { |
| f1e33d7 | 723 | return Err(format!("codegen: constructor pattern '{}' against a non-enum subject", name)); |
| f1e33d7 | 724 | } |
| f1e33d7 | 725 | if fields.len() != info.field_types.len() { |
| f1e33d7 | 726 | return Err(format!( |
| f1e33d7 | 727 | "codegen: constructor pattern '{}' expects {} field(s), got {}", |
| f1e33d7 | 728 | name, info.field_types.len(), fields.len() |
| f1e33d7 | 729 | )); |
| f1e33d7 | 730 | } |
| f1e33d7 | 731 | let tag = info.tag; |
| f1e33d7 | 732 | let field_types = info.field_types.clone(); |
| f1e33d7 | 733 | |
| f1e33d7 | 734 | // A constructor pattern can only match if the runtime subject is actually |
| f1e33d7 | 735 | // a heap pointer (payload variants are always >= HEAP_BASE); a |
| f1e33d7 | 736 | // payload-free sibling variant is a small int tag, and loading i32 from |
| f1e33d7 | 737 | // that address would read unrelated/zeroed memory instead of a real tag. |
| f1e33d7 | 738 | // Guard with a range check before doing the I32Load. |
| f1e33d7 | 739 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 740 | Instruction::I32Const(HEAP_BASE as i32).encode(body); |
| f1e33d7 | 741 | Instruction::I32GeU.encode(body); |
| f1e33d7 | 742 | Instruction::If(BlockType::Result(ValType::I32)).encode(body); |
| f1e33d7 | 743 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 744 | Instruction::I32Load(MemArg { offset: 0, align: 2, memory_index: 0 }).encode(body); |
| f1e33d7 | 745 | Instruction::I32Const(tag).encode(body); |
| f1e33d7 | 746 | Instruction::I32Eq.encode(body); |
| f1e33d7 | 747 | Instruction::Else.encode(body); |
| f1e33d7 | 748 | Instruction::I32Const(0).encode(body); |
| f1e33d7 | 749 | Instruction::End.encode(body); |
| f1e33d7 | 750 | Instruction::If(block_type_for(result_vt)).encode(body); |
| f1e33d7 | 751 | for (i, (pat, field_ty)) in fields.iter().zip(field_types.iter()).enumerate() { |
| f1e33d7 | 752 | let bind_name = match pat { |
| f1e33d7 | 753 | ast::CasePattern::Name(n) => Some(n.as_str()), |
| f1e33d7 | 754 | ast::CasePattern::Wildcard => None, |
| f1e33d7 | 755 | _ => return Err("codegen: only bare bindings or '_' are supported inside a constructor pattern".to_string()), |
| f1e33d7 | 756 | }; |
| f1e33d7 | 757 | if let Some(n) = bind_name { |
| f1e33d7 | 758 | let idx = ctx |
| f1e33d7 | 759 | .locals |
| f1e33d7 | 760 | .get(n) |
| f1e33d7 | 761 | .copied() |
| f1e33d7 | 762 | .ok_or_else(|| format!("internal codegen error: missing binding local '{}'", n))?; |
| f1e33d7 | 763 | Instruction::LocalGet(scratch_local).encode(body); |
| f1e33d7 | 764 | let offset = ((i + 1) as u64) * 8; |
| f1e33d7 | 765 | match plum_type_to_valtype(field_ty) { |
| f1e33d7 | 766 | ValType::I64 => Instruction::I64Load(MemArg { offset, align: 3, memory_index: 0 }), |
| f1e33d7 | 767 | ValType::F64 => Instruction::F64Load(MemArg { offset, align: 3, memory_index: 0 }), |
| f1e33d7 | 768 | _ => Instruction::I32Load(MemArg { offset, align: 2, memory_index: 0 }), |
| f1e33d7 | 769 | }.encode(body); |
| f1e33d7 | 770 | Instruction::LocalSet(idx).encode(body); |
| f1e33d7 | 771 | ctx.type_env.borrow_mut().insert(n.to_string(), TypeScheme::mono(field_ty.clone())); |
| f1e33d7 | 772 | } |
| f1e33d7 | 773 | } |
| f1e33d7 | 774 | compile_case_body(&case.body, result_vt, body, ctx, state)?; |
| f1e33d7 | 775 | Instruction::Else.encode(body); |
| f1e33d7 | 776 | compile_match_arms(rest, subject_vt, scratch_local, result_vt, body, ctx, state)?; |
| f1e33d7 | 777 | Instruction::End.encode(body); |
| f1e33d7 | 778 | Ok(()) |
| f1e33d7 | 779 | } |
| f1e33d7 | 780 | ``` |
| f1e33d7 | 781 | |
| f1e33d7 | 782 | - [ ] **Step 4: Run the codegen test suite** |
| f1e33d7 | 783 | |
| f1e33d7 | 784 | Run: `cargo test -p plum-wasm-codegen --test codegen_tests` |
| f1e33d7 | 785 | Expected: every test passes, including all 8 new ones from Step 1 and every pre-existing test in the file (in particular, every test that already uses explicit `return` in match/if arms must still pass unchanged — `result_vt: None` for ordinary statement position and value-position `return` handling are both untouched by this refactor). |
| f1e33d7 | 786 | |
| f1e33d7 | 787 | - [ ] **Step 5: Run the full workspace and tree-sitter suites** |
| f1e33d7 | 788 | |
| f1e33d7 | 789 | ```bash |
| f1e33d7 | 790 | cargo test --workspace |
| f1e33d7 | 791 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test |
| f1e33d7 | 792 | ``` |
| f1e33d7 | 793 | |
| f1e33d7 | 794 | Expected: fully green. |
| f1e33d7 | 795 | |
| f1e33d7 | 796 | - [ ] **Step 6: Commit** |
| f1e33d7 | 797 | |
| f1e33d7 | 798 | ```bash |
| f1e33d7 | 799 | git add plum-wasm-codegen/src/lib.rs plum-wasm-codegen/tests/codegen_tests.rs |
| f1e33d7 | 800 | git commit -m "fix(plum-wasm-codegen): propagate a value through tail-position match/if without explicit return" |
| f1e33d7 | 801 | ``` |
| f1e33d7 | 802 | |
| f1e33d7 | 803 | --- |
| f1e33d7 | 804 | |
| f1e33d7 | 805 | ### Task 3: Restore `examples/match.plum` to idiomatic bare-tail style, update docs |
| f1e33d7 | 806 | |
| f1e33d7 | 807 | **Files:** |
| f1e33d7 | 808 | - Modify: `examples/match.plum` |
| f1e33d7 | 809 | - Modify: `README.md` |
| f1e33d7 | 810 | - Test: `plum-wasm-codegen/tests/examples_test.rs` (no changes expected, just re-run) |
| f1e33d7 | 811 | |
| f1e33d7 | 812 | **Interfaces:** |
| f1e33d7 | 813 | - Consumes: Task 2's fix (a function whose tail statement is `match`/`if` with bare-expression arms now compiles and runs correctly). |
| f1e33d7 | 814 | - Produces: nothing further downstream — this is the final integration/documentation task. |
| f1e33d7 | 815 | |
| f1e33d7 | 816 | - [ ] **Step 1: Revert `examples/match.plum`'s five functions to their natural bare-tail-expression form** |
| f1e33d7 | 817 | |
| f1e33d7 | 818 | Replace the whole file `examples/match.plum` with: |
| f1e33d7 | 819 | |
| f1e33d7 | 820 | ```plum |
| f1e33d7 | 821 | enum Color = |
| f1e33d7 | 822 | | Red |
| f1e33d7 | 823 | | Green |
| f1e33d7 | 824 | | Blue |
| f1e33d7 | 825 | |
| f1e33d7 | 826 | enum Option = |
| f1e33d7 | 827 | | Some(Int) |
| f1e33d7 | 828 | | None |
| f1e33d7 | 829 | |
| f1e33d7 | 830 | describeNumber(n: Int) -> Str = |
| f1e33d7 | 831 | match n |
| f1e33d7 | 832 | 0 => |
| f1e33d7 | 833 | "zero" |
| f1e33d7 | 834 | 1 => |
| f1e33d7 | 835 | "one" |
| f1e33d7 | 836 | _ => |
| f1e33d7 | 837 | "many" |
| f1e33d7 | 838 | |
| f1e33d7 | 839 | describeBool(b: Bool) -> Int = |
| f1e33d7 | 840 | match b |
| f1e33d7 | 841 | True => |
| f1e33d7 | 842 | 1 |
| f1e33d7 | 843 | False => |
| f1e33d7 | 844 | 0 |
| f1e33d7 | 845 | |
| f1e33d7 | 846 | bindExample(n: Int) -> Int = |
| f1e33d7 | 847 | match n |
| f1e33d7 | 848 | x => |
| f1e33d7 | 849 | x |
| f1e33d7 | 850 | |
| f1e33d7 | 851 | describeColor(c: Color) -> Str = |
| f1e33d7 | 852 | match c |
| f1e33d7 | 853 | Red => |
| f1e33d7 | 854 | "red" |
| f1e33d7 | 855 | Green => |
| f1e33d7 | 856 | "green" |
| f1e33d7 | 857 | Blue => |
| f1e33d7 | 858 | "blue" |
| f1e33d7 | 859 | |
| f1e33d7 | 860 | describeOption(opt: Option) -> Int = |
| f1e33d7 | 861 | match opt |
| f1e33d7 | 862 | Some(v) => |
| f1e33d7 | 863 | v |
| f1e33d7 | 864 | None => |
| f1e33d7 | 865 | 0 |
| f1e33d7 | 866 | |
| f1e33d7 | 867 | main() -> Int = |
| f1e33d7 | 868 | describeOption(Some(5)) |
| f1e33d7 | 869 | ``` |
| f1e33d7 | 870 | |
| f1e33d7 | 871 | (This is identical to the file's content before the `return`-adding workaround, restoring the originally-intended idiomatic style now that Task 2 makes it actually compile.) |
| f1e33d7 | 872 | |
| f1e33d7 | 873 | - [ ] **Step 2: Run the examples test suite** |
| f1e33d7 | 874 | |
| f1e33d7 | 875 | Run: `cargo test -p plum-wasm-codegen --test examples_test` |
| f1e33d7 | 876 | Expected: `match_example_compiles_and_runs_correctly` (already asserting `describeOption(Some(5)) == 5`) still passes — now genuinely exercising bare-tail-expression match arms instead of explicit `return`. |
| f1e33d7 | 877 | |
| f1e33d7 | 878 | - [ ] **Step 3: Remove the now-fixed bullet from README's Known Gaps** |
| f1e33d7 | 879 | |
| f1e33d7 | 880 | In `README.md`, remove this line (currently line 327): |
| f1e33d7 | 881 | |
| f1e33d7 | 882 | ```markdown |
| f1e33d7 | 883 | - a function body's final statement being a `match`/`if` whose arms don't all use explicit `return` — the arm values are silently dropped instead of returned, producing invalid wasm rather than a clear error (workaround: always `return` from match/if arms in tail position) |
| f1e33d7 | 884 | ``` |
| f1e33d7 | 885 | |
| f1e33d7 | 886 | so the Known Gaps list reads (only the remaining, still-true items): |
| f1e33d7 | 887 | |
| f1e33d7 | 888 | ```markdown |
| f1e33d7 | 889 | - string interpolation (plain, non-interpolated string literals do compile) |
| f1e33d7 | 890 | - multi-subject `match` (`match a, b`) |
| f1e33d7 | 891 | - 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 |
| f1e33d7 | 892 | - nested constructor patterns inside `match` (`Some(Some(v))`) — a constructor pattern's own sub-patterns must be a bare binding or `_` |
| f1e33d7 | 893 | ``` |
| f1e33d7 | 894 | |
| f1e33d7 | 895 | - [ ] **Step 4: Run the full workspace and tree-sitter suites one final time** |
| f1e33d7 | 896 | |
| f1e33d7 | 897 | ```bash |
| f1e33d7 | 898 | cargo test --workspace |
| f1e33d7 | 899 | cd tooling/tree-sitter-plum && npx --yes tree-sitter-cli test |
| f1e33d7 | 900 | ``` |
| f1e33d7 | 901 | |
| f1e33d7 | 902 | Expected: fully green, zero known failures. |
| f1e33d7 | 903 | |
| f1e33d7 | 904 | - [ ] **Step 5: Commit** |
| f1e33d7 | 905 | |
| f1e33d7 | 906 | ```bash |
| f1e33d7 | 907 | git add examples/match.plum README.md |
| f1e33d7 | 908 | git commit -m "docs+test: restore examples/match.plum to idiomatic bare-tail style; tail-position gap fixed" |
| f1e33d7 | 909 | ``` |