plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


f8a1532 — Peter John 2026-11-03T11:52:16+05:30
docs(plans): enum/union split review plan (parked)
docs/plans/2026-11-03-enum-union-split.review.md ADDED
@@ -0,0 +1,69 @@
1
+ # Split enum and union: review plan
2
+
3
+ **Status:** written, not approved or scheduled (parked at the user's request, 2026-11-03).
4
+
5
+ ## Summary
6
+ Today one `enum` keyword does two jobs: named constants (`enum Color = Red | Green | Blue`) and a sum of existing types (`enum Json = | JsonNull | Bool | Float | Str | List[Json] | Map[Str, Json]`). This splits them. `enum` holds only constants, optionally with values. A new `union` keyword lists the types a value may be, but never a record or struct: records are grouped with a trait instead. Both keep exhaustive `match`; only a trait needs `_`. The compiler machinery stays: `union` is today's union enum, `enum` is today's tag enum, so codegen does not change.
7
+
8
+ ```plum
9
+ enum Color = Red | Green | Blue
10
+
11
+ enum State =
12
+ | Start = 1
13
+ | Stop = 2
14
+ | Go = 3
15
+
16
+ union Json =
17
+ | JsonNull # a bare tag: no payload
18
+ | Bool
19
+ | Float
20
+ | Str
21
+ | List[Json]
22
+ | Map[Str, Json]
23
+
24
+ union Key = Int | Str | Color # an enum can be a member
25
+
26
+ trait Shape = # records are grouped with a trait
27
+ fun area(self) -> Float
28
+
29
+ record Circle(Shape, r: Float) =
30
+ fun area(self) -> Float = Math.Pi * self.r * self.r
31
+ ```
32
+
33
+ ## What you will get
34
+ - `enum Name = A | B | C` and the `| A = 1` value form, plus the existing shared discriminant params (`enum Step(n: Int)`). Members are names only.
35
+ - `union Name = ...` whose members are built-ins (`Int`, `Float`, `Bool`, `Byte`, `Str`), std containers (`List[T]`, `Map[K, V]`, `Array[T]`), enums, other unions, traits, and bare tags. Generic unions (`union Opt[T] = T | Missing`) keep working where every member is allowed.
36
+ - Conversion into a union, `match` with exhaustiveness, member method dispatch and narrowing behave exactly as union enums do today.
37
+ - Errors:
38
+ - `enum` listing a type: "enum Pet lists type Str; an enum holds only constants, use union"
39
+ - `union` listing a record or struct: "union Shape lists record Circle; records are grouped with a trait: record Circle(Shape, ...)"
40
+ - the old `enum` used as a union is rejected with the same first message, so nothing silently changes meaning.
41
+ - Highlighting (`union` keyword), formatter, LSP hover/completion, README sections for both, and the type table in "Language at a glance".
42
+
43
+ ## What will be built
44
+ | Part | Size | Built by |
45
+ |---|---|---|
46
+ | Grammar and parser: `union` keyword; `enum` members restricted to names and values | S | a careful helper |
47
+ | Member rules and errors (plum-core unions + checker): which member kinds each keyword allows | M | a careful helper |
48
+ | Migrate std: `std/Json` (`union Json`; `JsonParse` from `JsonParsed`/`JsonParseFailed` records becomes a trait, or `parse` returns `JsonError!Json` — decision below) | S | a careful helper |
49
+ | Migrate examples and tests: record unions become traits (`Shape`, `Round`, `Book`, `Placed`, `Job`, ...); generic record sums rewritten (decision below); Rust test sources | M | a careful helper |
50
+ | Formatter, highlighting, LSP, README | S | a fast helper |
51
+
52
+ ## Decisions you can change
53
+ - **Generic sums of records** (`Tree[T] = Leaf | Node[T]`, `Parcel[T]`, `Reading[T]`, `GenericBox[T]`, `Stash[T]` in the examples): traits are not generic, so these can't move to a trait. Default: rewrite each example without the sum (an optional field, `T?`, or a record with a kind enum). Alternative: allow generic records as union members as a single exception.
54
+ - **`std/Json.parse` result** (`JsonParse = JsonParsed | JsonParseFailed`): default: `parse` returns `JsonError!Json` and the position/message move into a `lastError()`-style accessor or the error message | keep the two records under a `trait JsonParse`.
55
+ - **Traits as union members** (`union Value = Int | Str | Shape`): allowed (default) | not allowed.
56
+ - **Losing exhaustive `match` over a fixed set of records** (a trait needs `_`): accepted (default) | add `sealed trait` later for a closed set.
57
+
58
+ ## Risks
59
+ - Every record-based union in the examples and tests changes shape; `match` arms over them gain `_`.
60
+ - `kam_plum` is not migrated by this plan; it has no enum with record members today, but the plan lists any it gains before the build.
61
+ - Two keywords that both use `|` lists must give clear errors when mixed up.
62
+
63
+ ## Out of scope
64
+ - `sealed trait`, generic traits, pattern changes, codegen changes.
65
+
66
+ ## How we will know it works
67
+ - The examples above compile; each error has a test with its exact message.
68
+ - `std/Json` and all examples are migrated; the full Rust and Plum suites, the tree-sitter corpus and the browser checks pass.
69
+ - `plum format` is idempotent on files using both keywords.