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