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
# Plum WASM Codegen + Type Checker — Design Spec
**Date:** 2026-07-19
**Status:** Approved
---
## Overview
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/`).
Pipeline: `parse (plum-core) → check (plum-checker) → codegen (plum-wasm-codegen)`
---
## Crate Structure
Two new crates added to the workspace in `Cargo.toml`:
- `plum-checker` — type inference and checking
- `plum-wasm-codegen` — lowers checked AST to `.wasm` binary via `wasm_encoder`
Both depend on `plum-core` for the shared AST types.
---
## Type Checker (`plum-checker`)
### Core Types
```rust
pub type TypeEnv = BTreeMap<String, TypeScheme>;
pub struct TypeScheme {
pub vars: Vec<String>, // generic type params
pub body: Box<PlumType>,
}
pub enum PlumType {
TInt,
TFloat,
TBool,
TStr,
TUnit,
TVar(String), // fresh inference variable
TFun(Vec<PlumType>, Box<PlumType>),
TNamed(String), // user-defined class/enum/trait name (v1: opaque)
}
pub struct InferState {
pub counter: u64, // fresh variable counter
}
```
### Error Reporting
```rust
pub struct CheckError {
pub message: String,
}
pub type CheckResult<T> = Result<T, Vec<CheckError>>;
```
A non-empty error list halts compilation entirely (strict mode). Errors accumulate per-function so multiple errors are reported in one pass.
### Primitive Type Mapping
| Plum type | Internal |
|-----------|-----------|
| `Int` | `TInt` |
| `Float` | `TFloat` |
| `Bool` | `TBool` |
| `Str` | `TStr` |
| `Unit` | `TUnit` |
### Checks in v1 (minimal core)
- **Variable scope**: `Assign` targets added to env; undeclared variable reference → error
- **Function signatures**: params added to local env; return expression type must match declared return type
- **Binary ops**: both operands must be same numeric type; result type = operand type
- **Boolean ops** (`&&`, `||`, `!`): operands must be `TBool`
- **Compare ops**: both operands same type; result = `TBool`
- **`if`/`else`**: all branches must return the same type (or `TUnit` for statement-style)
- **`for` range**: range operands must be `TInt`; body may be `TUnit`
- **`while`**: condition must be `TBool`
- **`FnCall`**: arity and argument types must match declared function signature
- **`return`**: type must match enclosing function's declared return type
### Out of scope for v1
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.
### Public API
```rust
pub fn check_source(source: &plum_core::ast::Source) -> CheckResult<()>;
```
---
## WASM Codegen (`plum-wasm-codegen`)
### WasmModule helper
Mirrors `hica-wasm-codegen`'s `WasmModule` struct — a thin builder over `wasm_encoder` sections (types, imports, functions, exports, memories, globals, data segments, tables, elements).
### CompileCtx
```rust
pub struct CompileCtx {
pub module: WasmModule,
pub func_ids: HashMap<String, u32>,
pub func_sigs: HashMap<String, FuncSig>,
pub current_locals: HashMap<String, u32>,
pub label_count: u32,
pub bump_offset: u32, // linear memory bump allocator for strings
control_stack: Vec<ControlFrame>,
}
```
### Type Mapping (Plum → WASM)
| Plum type | WASM ValType |
|-----------|-------------|
| `Int` | `i64` |
| `Float` | `f64` |
| `Bool` | `i32` |
| `Str` | `i32` (ptr) |
| `Unit` | (no value) |
### Memory Layout
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.
### Codegen Scope (v1)
| Plum construct | WASM output |
|---|---|
| Top-level `fn` | `function` with matching type |
| `Int` / `Float` / `Bool` literals | `i64.const` / `f64.const` / `i32.const` |
| Arithmetic `BinOp` | `i64.add`, `i64.sub`, `f64.mul`, etc. |
| `BoolOp` | `i32.and`, `i32.or` |
| `CompareOp` | `i64.lt_s`, `f64.eq`, etc. |
| `if` / `else if` / `else` | `block` + `if` instructions |
| `for` (range `a..b`) | `loop` + `br_if` |
| `while` | `loop` + `br_if` |
| `Assign` (local) | `local.set` + `local.get` |
| `FnCall` | `call` |
| `return` | `return` |
| `Const` (top-level) | WASM `global` with constant initializer |
| `main()` | exported as `"main"` |
### Public API
```rust
pub fn compile_source(source: &plum_core::ast::Source) -> Result<Vec<u8>, String>;
```
Returns the raw `.wasm` bytes.
---
## CLI Integration (`plum-cli`)
New `compile` subcommand:
```
plum compile <file.plum> [-o output.wasm]
```
Steps:
1. Parse with `plum-core::parse_source`
2. Type-check with `plum-checker::check_source` — errors printed to stderr, exit 1
3. Codegen with `plum-wasm-codegen::compile_source`
4. Write `.wasm` to output path (default: input path with `.wasm` extension)
---
## Testing
### `plum-checker/tests/`
Unit tests per rule:
- Undeclared variable → error
- Wrong return type → error
- Binary op type mismatch → error
- Valid function → no errors
### `plum-wasm-codegen/tests/`
Integration tests:
- Compile `test/add.plum` → validate `.wasm` bytes with `wasmparser`
- Compile factorial function → run with `wasmtime` crate, assert result
---
## Reference
- hica type checker: `hica/crates/hica-checker/src/lib.rs`
- hica WASM codegen: `hica/crates/hica-wasm-codegen/src/lib.rs`
- plum AST: `plum-core/src/ast.rs`