plum
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.
|