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
# 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`