plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
docs/superpowers/specs/2026-07-19-general-enum-support-design.md
# General enum support (checker + codegen)
## Problem
Today only `Bool`'s built-in `True`/`False` variants have a runtime representation
(`i32` 1/0). Any user-declared `enum` — payload-free (`Color = Red | Green | Blue`)
or payload-carrying (`Option = Some(a) | None`) — type-checks permissively but
fails to compile: `plum-wasm-codegen` hits the hardcoded
`"codegen: enum variant pattern '{}' is not yet supported (only True/False)"` error
(`plum-wasm-codegen/src/lib.rs:915`) for any non-Bool tag or constructor pattern in
`match`, and there's no construction path for payload variants like `Some(v)` at all.
This is scoped as a standalone, tractable slice of the larger "implement the
standard library" ambition. **It does not attempt generics monomorphization.**
`libs/std/option.plum` and `result.plum` use generic type params (`a`, `b`) and
will remain blocked on the separate, larger generics-monomorphization gap. This
work targets *concrete* user-declared enums only (e.g. a non-generic `Option`-shaped
enum over a specific type, or `Color`, or `Pair(Int, Str)`).
## Runtime representation
Every enum value is a single `i32`, matching the existing `Bool` scheme:
- **Payload-free variant** (`Red`, `None`, `True`, `False`): the `i32` is the
variant's tag number directly — a small int.
- **Payload variant** (`Some(a)`, `Ok(a)`, `Pair(a, b)`): the `i32` is a pointer
into the bump heap (which starts at address 65536, the second 64KiB page) to a
struct `[tag: i32][field0][field1]...`, using the exact bump-alloc/store/load
mechanism already used for class instance fields.
Tags are assigned per-enum, 0-based in declaration order across *all* variants
(payload and non-payload share one numbering) — e.g. `Option`: `Some`=0, `None`=1.
Because heap pointers are always ≥ 65536 and per-enum tags are small ints, a
payload-free tag value can never collide with a payload variant's pointer value,
so no separate discriminant bit is needed — matching on a bare tag pattern is a
direct `i32` comparison regardless of whether the enum has other payload variants.
## Checker changes (`plum-checker`)
`EnumVariants` (currently `BTreeMap<variant_name, enum_name>`) is extended to also
carry, per variant: its 0-based tag index and its field type names (parallel to
`EnumVariant.fields: Vec<String>` in the AST). This is purely additive — existing
lookups (`contains_key`, enum-name-of) are unaffected, and no currently-accepted or
currently-rejected program changes behavior. Type-checking of enum construction and
matching already works today per the README; this just carries enough data through
for codegen to compute struct layouts and payload types without re-deriving them
from the AST.
## Codegen — construction
A capitalized call expression (`Some(v)`, `Pair(a, b)`) currently only has meaning
for class instantiation. It gets a parallel variant-construction path, chosen by
looking up the callee name in the (extended) enum variants table:
- **No-payload variant used as a bare value** (`None`, `Red`, and `True`/`False`
unchanged): compiles to `i32.const <tag>`.
- **Payload variant call**: bump-allocate `4 * (1 + field_count)` bytes, store the
tag at offset 0, store each argument at its field offset using the same
width-dependent store logic (i32/i64/f64) already used for class fields, leave
the base pointer on the stack as the resulting value.
## Codegen — match lowering
Replace the current hardcoded-to-Bool error path
(`plum-wasm-codegen/src/lib.rs:901-918`, `compile_variant_eq_arm`) with a general
variant-tag test:
- **Bare tag pattern** (`Red`, `None`, `True`, `False`): `subject == i32.const <tag>`.
- **Constructor pattern** (`Some(v)`, `Pair(a, b)`): test that `subject`'s stored
tag word (load `i32` at offset 0 of the pointer) equals the variant's tag, then
for each pattern-bound name, load the corresponding field offset from the
pointer and bind it as a local before evaluating the arm body.
Multi-subject `match` and non-constructor/non-tag patterns (already-known gaps)
remain out of scope.
## Testing plan
- **Checker tests** (`plum-checker/tests/checker_tests.rs`): payload-free enum
(`Color`), payload enum (concrete, non-generic `Option`-shaped enum), and
constructor-pattern matches — confirm the extended `EnumVariants` table changes
no pass/fail outcome versus today.
- **Codegen tests** (`plum-wasm-codegen/tests/codegen_tests.rs`, compiled and run
via wasmtime matching existing style): construct + match a payload-free variant;
construct + destructure a payload variant in `match`; a multi-field variant
(`Pair(a, b)`); a variant used as a class field/method to confirm interop with
existing class codegen.
- **Examples**: extend `examples/match.plum` (or add a new example) with a
concrete user-declared enum exercised end-to-end; update the README's Known
Gaps section to drop the now-solved bullet (non-Bool enum-tag and constructor
patterns), while keeping the user-defined-generics bullet as-is.
## Out of scope
- Generics monomorphization (separate, larger effort — blocks `libs/std`'s actual
`Option`/`Result`/`List`/`Map`).
- Multi-subject `match`.
- Any pattern kind beyond bare tag / constructor (literal, binding, wildcard
already work and are untouched).