plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
docs/superpowers/specs/2026-07-19-wasm-codegen-typechecker-design.md
| 78b9e89 | 1 | # Plum WASM Codegen + Type Checker — Design Spec |
| 78b9e89 | 2 | |
| 78b9e89 | 3 | **Date:** 2026-07-19 |
| 78b9e89 | 4 | **Status:** Approved |
| 78b9e89 | 5 | |
| 78b9e89 | 6 | --- |
| 78b9e89 | 7 | |
| 78b9e89 | 8 | ## Overview |
| 78b9e89 | 9 | |
| 78b9e89 | 10 | Add a strict type checker and WASM code generator to the plum language compiler, following the same architecture as the sibling `hica` compiler (already present in `plum/hica/`). |
| 78b9e89 | 11 | |
| 78b9e89 | 12 | Pipeline: `parse (plum-core) → check (plum-checker) → codegen (plum-wasm-codegen)` |
| 78b9e89 | 13 | |
| 78b9e89 | 14 | --- |
| 78b9e89 | 15 | |
| 78b9e89 | 16 | ## Crate Structure |
| 78b9e89 | 17 | |
| 78b9e89 | 18 | Two new crates added to the workspace in `Cargo.toml`: |
| 78b9e89 | 19 | |
| 78b9e89 | 20 | - `plum-checker` — type inference and checking |
| 78b9e89 | 21 | - `plum-wasm-codegen` — lowers checked AST to `.wasm` binary via `wasm_encoder` |
| 78b9e89 | 22 | |
| 78b9e89 | 23 | Both depend on `plum-core` for the shared AST types. |
| 78b9e89 | 24 | |
| 78b9e89 | 25 | --- |
| 78b9e89 | 26 | |
| 78b9e89 | 27 | ## Type Checker (`plum-checker`) |
| 78b9e89 | 28 | |
| 78b9e89 | 29 | ### Core Types |
| 78b9e89 | 30 | |
| 78b9e89 | 31 | ```rust |
| 78b9e89 | 32 | pub type TypeEnv = BTreeMap<String, TypeScheme>; |
| 78b9e89 | 33 | |
| 78b9e89 | 34 | pub struct TypeScheme { |
| 78b9e89 | 35 | pub vars: Vec<String>, // generic type params |
| 78b9e89 | 36 | pub body: Box<PlumType>, |
| 78b9e89 | 37 | } |
| 78b9e89 | 38 | |
| 78b9e89 | 39 | pub enum PlumType { |
| 78b9e89 | 40 | TInt, |
| 78b9e89 | 41 | TFloat, |
| 78b9e89 | 42 | TBool, |
| 78b9e89 | 43 | TStr, |
| 78b9e89 | 44 | TUnit, |
| 78b9e89 | 45 | TVar(String), // fresh inference variable |
| 78b9e89 | 46 | TFun(Vec<PlumType>, Box<PlumType>), |
| 78b9e89 | 47 | TNamed(String), // user-defined class/enum/trait name (v1: opaque) |
| 78b9e89 | 48 | } |
| 78b9e89 | 49 | |
| 78b9e89 | 50 | pub struct InferState { |
| 78b9e89 | 51 | pub counter: u64, // fresh variable counter |
| 78b9e89 | 52 | } |
| 78b9e89 | 53 | ``` |
| 78b9e89 | 54 | |
| 78b9e89 | 55 | ### Error Reporting |
| 78b9e89 | 56 | |
| 78b9e89 | 57 | ```rust |
| 78b9e89 | 58 | pub struct CheckError { |
| 78b9e89 | 59 | pub message: String, |
| 78b9e89 | 60 | } |
| 78b9e89 | 61 | pub type CheckResult<T> = Result<T, Vec<CheckError>>; |
| 78b9e89 | 62 | ``` |
| 78b9e89 | 63 | |
| 78b9e89 | 64 | A non-empty error list halts compilation entirely (strict mode). Errors accumulate per-function so multiple errors are reported in one pass. |
| 78b9e89 | 65 | |
| 78b9e89 | 66 | ### Primitive Type Mapping |
| 78b9e89 | 67 | |
| 78b9e89 | 68 | | Plum type | Internal | |
| 78b9e89 | 69 | |-----------|-----------| |
| 78b9e89 | 70 | | `Int` | `TInt` | |
| 78b9e89 | 71 | | `Float` | `TFloat` | |
| 78b9e89 | 72 | | `Bool` | `TBool` | |
| 78b9e89 | 73 | | `Str` | `TStr` | |
| 78b9e89 | 74 | | `Unit` | `TUnit` | |
| 78b9e89 | 75 | |
| 78b9e89 | 76 | ### Checks in v1 (minimal core) |
| 78b9e89 | 77 | |
| 78b9e89 | 78 | - **Variable scope**: `Assign` targets added to env; undeclared variable reference → error |
| 78b9e89 | 79 | - **Function signatures**: params added to local env; return expression type must match declared return type |
| 78b9e89 | 80 | - **Binary ops**: both operands must be same numeric type; result type = operand type |
| 78b9e89 | 81 | - **Boolean ops** (`&&`, `||`, `!`): operands must be `TBool` |
| 78b9e89 | 82 | - **Compare ops**: both operands same type; result = `TBool` |
| 78b9e89 | 83 | - **`if`/`else`**: all branches must return the same type (or `TUnit` for statement-style) |
| 78b9e89 | 84 | - **`for` range**: range operands must be `TInt`; body may be `TUnit` |
| 78b9e89 | 85 | - **`while`**: condition must be `TBool` |
| 78b9e89 | 86 | - **`FnCall`**: arity and argument types must match declared function signature |
| 78b9e89 | 87 | - **`return`**: type must match enclosing function's declared return type |
| 78b9e89 | 88 | |
| 78b9e89 | 89 | ### Out of scope for v1 |
| 78b9e89 | 90 | |
| 78b9e89 | 91 | Traits, classes, enums, and generic dispatch are **recognized** in the AST but produce an "unsupported in v1" error if their methods or constructors are invoked in checked code. Top-level `class`/`trait`/`enum` declarations are accepted without deep checking. |
| 78b9e89 | 92 | |
| 78b9e89 | 93 | ### Public API |
| 78b9e89 | 94 | |
| 78b9e89 | 95 | ```rust |
| 78b9e89 | 96 | pub fn check_source(source: &plum_core::ast::Source) -> CheckResult<()>; |
| 78b9e89 | 97 | ``` |
| 78b9e89 | 98 | |
| 78b9e89 | 99 | --- |
| 78b9e89 | 100 | |
| 78b9e89 | 101 | ## WASM Codegen (`plum-wasm-codegen`) |
| 78b9e89 | 102 | |
| 78b9e89 | 103 | ### WasmModule helper |
| 78b9e89 | 104 | |
| 78b9e89 | 105 | Mirrors `hica-wasm-codegen`'s `WasmModule` struct — a thin builder over `wasm_encoder` sections (types, imports, functions, exports, memories, globals, data segments, tables, elements). |
| 78b9e89 | 106 | |
| 78b9e89 | 107 | ### CompileCtx |
| 78b9e89 | 108 | |
| 78b9e89 | 109 | ```rust |
| 78b9e89 | 110 | pub struct CompileCtx { |
| 78b9e89 | 111 | pub module: WasmModule, |
| 78b9e89 | 112 | pub func_ids: HashMap<String, u32>, |
| 78b9e89 | 113 | pub func_sigs: HashMap<String, FuncSig>, |
| 78b9e89 | 114 | pub current_locals: HashMap<String, u32>, |
| 78b9e89 | 115 | pub label_count: u32, |
| 78b9e89 | 116 | pub bump_offset: u32, // linear memory bump allocator for strings |
| 78b9e89 | 117 | control_stack: Vec<ControlFrame>, |
| 78b9e89 | 118 | } |
| 78b9e89 | 119 | ``` |
| 78b9e89 | 120 | |
| 78b9e89 | 121 | ### Type Mapping (Plum → WASM) |
| 78b9e89 | 122 | |
| 78b9e89 | 123 | | Plum type | WASM ValType | |
| 78b9e89 | 124 | |-----------|-------------| |
| 78b9e89 | 125 | | `Int` | `i64` | |
| 78b9e89 | 126 | | `Float` | `f64` | |
| 78b9e89 | 127 | | `Bool` | `i32` | |
| 78b9e89 | 128 | | `Str` | `i32` (ptr) | |
| 78b9e89 | 129 | | `Unit` | (no value) | |
| 78b9e89 | 130 | |
| 78b9e89 | 131 | ### Memory Layout |
| 78b9e89 | 132 | |
| 78b9e89 | 133 | One linear memory (1 page = 64 KiB min). String literals are emitted into a data segment; a bump allocator pointer (global `i32`) tracks the next free byte for runtime string allocation. |
| 78b9e89 | 134 | |
| 78b9e89 | 135 | ### Codegen Scope (v1) |
| 78b9e89 | 136 | |
| 78b9e89 | 137 | | Plum construct | WASM output | |
| 78b9e89 | 138 | |---|---| |
| 78b9e89 | 139 | | Top-level `fn` | `function` with matching type | |
| 78b9e89 | 140 | | `Int` / `Float` / `Bool` literals | `i64.const` / `f64.const` / `i32.const` | |
| 78b9e89 | 141 | | Arithmetic `BinOp` | `i64.add`, `i64.sub`, `f64.mul`, etc. | |
| 78b9e89 | 142 | | `BoolOp` | `i32.and`, `i32.or` | |
| 78b9e89 | 143 | | `CompareOp` | `i64.lt_s`, `f64.eq`, etc. | |
| 78b9e89 | 144 | | `if` / `else if` / `else` | `block` + `if` instructions | |
| 78b9e89 | 145 | | `for` (range `a..b`) | `loop` + `br_if` | |
| 78b9e89 | 146 | | `while` | `loop` + `br_if` | |
| 78b9e89 | 147 | | `Assign` (local) | `local.set` + `local.get` | |
| 78b9e89 | 148 | | `FnCall` | `call` | |
| 78b9e89 | 149 | | `return` | `return` | |
| 78b9e89 | 150 | | `Const` (top-level) | WASM `global` with constant initializer | |
| 78b9e89 | 151 | | `main()` | exported as `"main"` | |
| 78b9e89 | 152 | |
| 78b9e89 | 153 | ### Public API |
| 78b9e89 | 154 | |
| 78b9e89 | 155 | ```rust |
| 78b9e89 | 156 | pub fn compile_source(source: &plum_core::ast::Source) -> Result<Vec<u8>, String>; |
| 78b9e89 | 157 | ``` |
| 78b9e89 | 158 | |
| 78b9e89 | 159 | Returns the raw `.wasm` bytes. |
| 78b9e89 | 160 | |
| 78b9e89 | 161 | --- |
| 78b9e89 | 162 | |
| 78b9e89 | 163 | ## CLI Integration (`plum-cli`) |
| 78b9e89 | 164 | |
| 78b9e89 | 165 | New `compile` subcommand: |
| 78b9e89 | 166 | |
| 78b9e89 | 167 | ``` |
| 78b9e89 | 168 | plum compile <file.plum> [-o output.wasm] |
| 78b9e89 | 169 | ``` |
| 78b9e89 | 170 | |
| 78b9e89 | 171 | Steps: |
| 78b9e89 | 172 | 1. Parse with `plum-core::parse_source` |
| 78b9e89 | 173 | 2. Type-check with `plum-checker::check_source` — errors printed to stderr, exit 1 |
| 78b9e89 | 174 | 3. Codegen with `plum-wasm-codegen::compile_source` |
| 78b9e89 | 175 | 4. Write `.wasm` to output path (default: input path with `.wasm` extension) |
| 78b9e89 | 176 | |
| 78b9e89 | 177 | --- |
| 78b9e89 | 178 | |
| 78b9e89 | 179 | ## Testing |
| 78b9e89 | 180 | |
| 78b9e89 | 181 | ### `plum-checker/tests/` |
| 78b9e89 | 182 | |
| 78b9e89 | 183 | Unit tests per rule: |
| 78b9e89 | 184 | - Undeclared variable → error |
| 78b9e89 | 185 | - Wrong return type → error |
| 78b9e89 | 186 | - Binary op type mismatch → error |
| 78b9e89 | 187 | - Valid function → no errors |
| 78b9e89 | 188 | |
| 78b9e89 | 189 | ### `plum-wasm-codegen/tests/` |
| 78b9e89 | 190 | |
| 78b9e89 | 191 | Integration tests: |
| 78b9e89 | 192 | - Compile `test/add.plum` → validate `.wasm` bytes with `wasmparser` |
| 78b9e89 | 193 | - Compile factorial function → run with `wasmtime` crate, assert result |
| 78b9e89 | 194 | |
| 78b9e89 | 195 | --- |
| 78b9e89 | 196 | |
| 78b9e89 | 197 | ## Reference |
| 78b9e89 | 198 | |
| 78b9e89 | 199 | - hica type checker: `hica/crates/hica-checker/src/lib.rs` |
| 78b9e89 | 200 | - hica WASM codegen: `hica/crates/hica-wasm-codegen/src/lib.rs` |
| 78b9e89 | 201 | - plum AST: `plum-core/src/ast.rs` |