plum

#treesitter#compiler#wasm

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`