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
| 6a8cfe6 | 1 | # General enum support (checker + codegen) |
| 6a8cfe6 | 2 | |
| 6a8cfe6 | 3 | ## Problem |
| 6a8cfe6 | 4 | |
| 6a8cfe6 | 5 | Today only `Bool`'s built-in `True`/`False` variants have a runtime representation |
| 6a8cfe6 | 6 | (`i32` 1/0). Any user-declared `enum` — payload-free (`Color = Red | Green | Blue`) |
| 6a8cfe6 | 7 | or payload-carrying (`Option = Some(a) | None`) — type-checks permissively but |
| 6a8cfe6 | 8 | fails to compile: `plum-wasm-codegen` hits the hardcoded |
| 6a8cfe6 | 9 | `"codegen: enum variant pattern '{}' is not yet supported (only True/False)"` error |
| 6a8cfe6 | 10 | (`plum-wasm-codegen/src/lib.rs:915`) for any non-Bool tag or constructor pattern in |
| 6a8cfe6 | 11 | `match`, and there's no construction path for payload variants like `Some(v)` at all. |
| 6a8cfe6 | 12 | |
| 6a8cfe6 | 13 | This is scoped as a standalone, tractable slice of the larger "implement the |
| 6a8cfe6 | 14 | standard library" ambition. **It does not attempt generics monomorphization.** |
| 6a8cfe6 | 15 | `libs/std/option.plum` and `result.plum` use generic type params (`a`, `b`) and |
| 6a8cfe6 | 16 | will remain blocked on the separate, larger generics-monomorphization gap. This |
| 6a8cfe6 | 17 | work targets *concrete* user-declared enums only (e.g. a non-generic `Option`-shaped |
| 6a8cfe6 | 18 | enum over a specific type, or `Color`, or `Pair(Int, Str)`). |
| 6a8cfe6 | 19 | |
| 6a8cfe6 | 20 | ## Runtime representation |
| 6a8cfe6 | 21 | |
| 6a8cfe6 | 22 | Every enum value is a single `i32`, matching the existing `Bool` scheme: |
| 6a8cfe6 | 23 | |
| 6a8cfe6 | 24 | - **Payload-free variant** (`Red`, `None`, `True`, `False`): the `i32` is the |
| 6a8cfe6 | 25 | variant's tag number directly — a small int. |
| 6a8cfe6 | 26 | - **Payload variant** (`Some(a)`, `Ok(a)`, `Pair(a, b)`): the `i32` is a pointer |
| 6a8cfe6 | 27 | into the bump heap (which starts at address 65536, the second 64KiB page) to a |
| 6a8cfe6 | 28 | struct `[tag: i32][field0][field1]...`, using the exact bump-alloc/store/load |
| 6a8cfe6 | 29 | mechanism already used for class instance fields. |
| 6a8cfe6 | 30 | |
| 6a8cfe6 | 31 | Tags are assigned per-enum, 0-based in declaration order across *all* variants |
| 6a8cfe6 | 32 | (payload and non-payload share one numbering) — e.g. `Option`: `Some`=0, `None`=1. |
| 6a8cfe6 | 33 | |
| 6a8cfe6 | 34 | Because heap pointers are always ≥ 65536 and per-enum tags are small ints, a |
| 6a8cfe6 | 35 | payload-free tag value can never collide with a payload variant's pointer value, |
| 6a8cfe6 | 36 | so no separate discriminant bit is needed — matching on a bare tag pattern is a |
| 6a8cfe6 | 37 | direct `i32` comparison regardless of whether the enum has other payload variants. |
| 6a8cfe6 | 38 | |
| 6a8cfe6 | 39 | ## Checker changes (`plum-checker`) |
| 6a8cfe6 | 40 | |
| 6a8cfe6 | 41 | `EnumVariants` (currently `BTreeMap<variant_name, enum_name>`) is extended to also |
| 6a8cfe6 | 42 | carry, per variant: its 0-based tag index and its field type names (parallel to |
| 6a8cfe6 | 43 | `EnumVariant.fields: Vec<String>` in the AST). This is purely additive — existing |
| 6a8cfe6 | 44 | lookups (`contains_key`, enum-name-of) are unaffected, and no currently-accepted or |
| 6a8cfe6 | 45 | currently-rejected program changes behavior. Type-checking of enum construction and |
| 6a8cfe6 | 46 | matching already works today per the README; this just carries enough data through |
| 6a8cfe6 | 47 | for codegen to compute struct layouts and payload types without re-deriving them |
| 6a8cfe6 | 48 | from the AST. |
| 6a8cfe6 | 49 | |
| 6a8cfe6 | 50 | ## Codegen — construction |
| 6a8cfe6 | 51 | |
| 6a8cfe6 | 52 | A capitalized call expression (`Some(v)`, `Pair(a, b)`) currently only has meaning |
| 6a8cfe6 | 53 | for class instantiation. It gets a parallel variant-construction path, chosen by |
| 6a8cfe6 | 54 | looking up the callee name in the (extended) enum variants table: |
| 6a8cfe6 | 55 | |
| 6a8cfe6 | 56 | - **No-payload variant used as a bare value** (`None`, `Red`, and `True`/`False` |
| 6a8cfe6 | 57 | unchanged): compiles to `i32.const <tag>`. |
| 6a8cfe6 | 58 | - **Payload variant call**: bump-allocate `4 * (1 + field_count)` bytes, store the |
| 6a8cfe6 | 59 | tag at offset 0, store each argument at its field offset using the same |
| 6a8cfe6 | 60 | width-dependent store logic (i32/i64/f64) already used for class fields, leave |
| 6a8cfe6 | 61 | the base pointer on the stack as the resulting value. |
| 6a8cfe6 | 62 | |
| 6a8cfe6 | 63 | ## Codegen — match lowering |
| 6a8cfe6 | 64 | |
| 6a8cfe6 | 65 | Replace the current hardcoded-to-Bool error path |
| 6a8cfe6 | 66 | (`plum-wasm-codegen/src/lib.rs:901-918`, `compile_variant_eq_arm`) with a general |
| 6a8cfe6 | 67 | variant-tag test: |
| 6a8cfe6 | 68 | |
| 6a8cfe6 | 69 | - **Bare tag pattern** (`Red`, `None`, `True`, `False`): `subject == i32.const <tag>`. |
| 6a8cfe6 | 70 | - **Constructor pattern** (`Some(v)`, `Pair(a, b)`): test that `subject`'s stored |
| 6a8cfe6 | 71 | tag word (load `i32` at offset 0 of the pointer) equals the variant's tag, then |
| 6a8cfe6 | 72 | for each pattern-bound name, load the corresponding field offset from the |
| 6a8cfe6 | 73 | pointer and bind it as a local before evaluating the arm body. |
| 6a8cfe6 | 74 | |
| 6a8cfe6 | 75 | Multi-subject `match` and non-constructor/non-tag patterns (already-known gaps) |
| 6a8cfe6 | 76 | remain out of scope. |
| 6a8cfe6 | 77 | |
| 6a8cfe6 | 78 | ## Testing plan |
| 6a8cfe6 | 79 | |
| 6a8cfe6 | 80 | - **Checker tests** (`plum-checker/tests/checker_tests.rs`): payload-free enum |
| 6a8cfe6 | 81 | (`Color`), payload enum (concrete, non-generic `Option`-shaped enum), and |
| 6a8cfe6 | 82 | constructor-pattern matches — confirm the extended `EnumVariants` table changes |
| 6a8cfe6 | 83 | no pass/fail outcome versus today. |
| 6a8cfe6 | 84 | - **Codegen tests** (`plum-wasm-codegen/tests/codegen_tests.rs`, compiled and run |
| 6a8cfe6 | 85 | via wasmtime matching existing style): construct + match a payload-free variant; |
| 6a8cfe6 | 86 | construct + destructure a payload variant in `match`; a multi-field variant |
| 6a8cfe6 | 87 | (`Pair(a, b)`); a variant used as a class field/method to confirm interop with |
| 6a8cfe6 | 88 | existing class codegen. |
| 6a8cfe6 | 89 | - **Examples**: extend `examples/match.plum` (or add a new example) with a |
| 6a8cfe6 | 90 | concrete user-declared enum exercised end-to-end; update the README's Known |
| 6a8cfe6 | 91 | Gaps section to drop the now-solved bullet (non-Bool enum-tag and constructor |
| 6a8cfe6 | 92 | patterns), while keeping the user-defined-generics bullet as-is. |
| 6a8cfe6 | 93 | |
| 6a8cfe6 | 94 | ## Out of scope |
| 6a8cfe6 | 95 | |
| 6a8cfe6 | 96 | - Generics monomorphization (separate, larger effort — blocks `libs/std`'s actual |
| 6a8cfe6 | 97 | `Option`/`Result`/`List`/`Map`). |
| 6a8cfe6 | 98 | - Multi-subject `match`. |
| 6a8cfe6 | 99 | - Any pattern kind beyond bare tag / constructor (literal, binding, wildcard |
| 6a8cfe6 | 100 | already work and are untouched). |