plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
c56df83
— Peter John
2026-11-01T17:31:34+05:30
docs(plans): struct/record + union enums review plan, Phase A machine plan
docs/plans/2026-11-01-struct-record.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# struct and record declarations (Phase A)
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-01-struct-value-types.review.md
|
|
4
|
+
**Goal:** `struct Name(f: T, ...)` (value type, replaces `value enum`) and `record Name[G](f: T, ...)` (reference type, replaces the self-named single-variant enum), each with an optional `=` + indented body of consts/methods.
|
|
5
|
+
**Architecture:** parser-only lowering. Both forms build today's `ast::Enum` with ONE variant named like the type (`isValue: true` for struct, new `isRecord: true` for record). Checker type rules and codegen are unchanged.
|
|
6
|
+
**Tech stack:** Rust workspace, tree-sitter grammar (`plum-tooling/tree-sitter-plum`), Topiary `format.scm`.
|
|
7
|
+
**Spec:** docs/plans/2026-11-01-struct-value-types.review.md (Phase A, steps 1-5)
|
|
8
|
+
**Detail:** contracts
|
|
9
|
+
**Target-min:** 14
|
|
10
|
+
|
|
11
|
+
## Global constraints
|
|
12
|
+
- Exact syntax: `struct Vec2(x: Float, y: Float)` with no body has NO `=`. With a body: `struct Vec2(x: Float, y: Float) =` NEWLINE INDENT (fn | const)* DEDENT. Same for `record`, which also allows generics: `record Pair[K, V](key: K, val: V) =`. Fields are `enum_named_field` (named, `fn(...)` types allowed), at least one.
|
|
13
|
+
- `struct` takes no generics (grammar rejects `struct P[T](a: T)`).
|
|
14
|
+
- `value enum` is REMOVED: no `valueKw`, no `value` keyword anywhere (grammar, highlights, format.scm, tmLanguage, LSP keyword list).
|
|
15
|
+
- The self-named single-variant enum form (`enum User = | User(...)`) still parses in Phase A (its removal is Phase B); new code and migrated code use `record`.
|
|
16
|
+
- Checker struct messages, verbatim: `struct field '<name>' must be Int, Float, Bool or a struct` and `struct <Name> cannot contain itself`. Delete the "exactly one constructor", "constructor must be named" and "cannot be generic" checks.
|
|
17
|
+
- Regenerate tree-sitter ONLY with `tree-sitter generate --disable-optimizations` (default optimizer silently breaks unrelated parses).
|
|
18
|
+
- Never run `plum format` on real repo files (it rewrites in place); format tests use temp copies.
|
|
19
|
+
- Read node names via `child_by_field_name`, not positional `named_child(i)` (comments shift positions).
|
|
20
|
+
- `cargo build --quiet` can skip edited files: `touch` edited `.rs` files before trusting a build.
|
|
21
|
+
- Test commands: `cargo test -p <crate>`; `cd plum-tooling/tree-sitter-plum && tree-sitter test`; `./target/release/plum test plum-std plum-examples` after `cargo build --release -p plum-cli`.
|
|
22
|
+
|
|
23
|
+
## Main diagram
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart LR
|
|
26
|
+
G["grammar.js: struct / record rules"] --> PC["parser.c (regenerated)"]
|
|
27
|
+
PC --> P["parser.rs parseStruct/parseRecord"]
|
|
28
|
+
P --> A["ast::Enum isValue / isRecord"]
|
|
29
|
+
A --> C["checker checkValueEnums (renamed msgs)"]
|
|
30
|
+
C --> W["codegen (unchanged)"]
|
|
31
|
+
G --> Q["highlights.scm / format.scm / tmLanguage / LSP"]
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## File structure
|
|
35
|
+
- `plum-tooling/tree-sitter-plum/grammar.js`, `src/*` (generated), `test/corpus/struct.txt`, `test/corpus/enum.txt`: syntax.
|
|
36
|
+
- `plum-core/src/ast.rs`, `plum-core/src/parser.rs`, `plum-core/tests/parser_test.rs`: lowering.
|
|
37
|
+
- `plum-checker/src/lib.rs`, `plum-checker/tests/checker_tests.rs`: struct validation.
|
|
38
|
+
- `plum-tooling/tree-sitter-plum/queries/plum/{highlights,format}.scm`, `plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json`, `plum-lsp/src/completion.rs`: tooling.
|
|
39
|
+
- Every `.plum` and Rust test snippet declaring a record or value enum: migration.
|
|
40
|
+
- `README.md`: docs.
|
|
41
|
+
|
|
42
|
+
## Task graph
|
|
43
|
+
```mermaid
|
|
44
|
+
flowchart LR
|
|
45
|
+
T1[core syntax + lowering]
|
|
46
|
+
T2[tooling queries]
|
|
47
|
+
T3[record migration]
|
|
48
|
+
T4[README]
|
|
49
|
+
%% taskgraph
|
|
50
|
+
T1 --> T2
|
|
51
|
+
T1 --> T3
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```mermaid
|
|
55
|
+
%% schedule
|
|
56
|
+
gantt
|
|
57
|
+
title Predicted schedule (mm:ss)
|
|
58
|
+
dateFormat X
|
|
59
|
+
axisFormat %M:%S
|
|
60
|
+
section lane 1
|
|
61
|
+
T1 core syntax + lowering :t1, 0, 208s
|
|
62
|
+
T3 record migration :t3, 208, 189s
|
|
63
|
+
section lane 2
|
|
64
|
+
T4 README :t4, 0, 94s
|
|
65
|
+
T2 tooling queries :t2, 208, 162s
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
critical path: T1 → T3 (6.6 min) · peak width 2 · predicted build 6.6 min · predicted total ≈ 10.1 min (target ✓)
|
|
69
|
+
Ruling: sequential - T2 and T3 run the regenerated parser in their tests (topiary format, the full plum suite), so they need T1's grammar committed - cost: about 5 min on the critical path.
|
|
70
|
+
|
|
71
|
+
## Predicted schedule
|
|
72
|
+
|
|
73
|
+
## Core
|
|
74
|
+
|
|
75
|
+
### T1 core syntax + lowering · Complexity 5 · Est. context 55k
|
|
76
|
+
**Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/parser.c, plum-tooling/tree-sitter-plum/src/grammar.json, plum-tooling/tree-sitter-plum/src/node-types.json, plum-tooling/tree-sitter-plum/test/corpus/struct.txt, plum-tooling/tree-sitter-plum/test/corpus/enum.txt, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/tests/parser_test.rs, plum-checker/src/lib.rs, plum-checker/tests/checker_tests.rs, plum-examples/values.plum, plum-examples/constants.plum, plum-examples/pattern_if.plum, plum-wasm-codegen/tests/codegen_tests.rs
|
|
77
|
+
**Components:** 3
|
|
78
|
+
**LOC owned:** 260
|
|
79
|
+
**Tests:** slow
|
|
80
|
+
**Model:** sonnet
|
|
81
|
+
**Est. time:** 5 min
|
|
82
|
+
**Produces:** tree-sitter nodes `struct` and `record` (fields: `name` type_identifier, `generics` (record only), `field` enum_named_field+, `methods` (fn | const)*) · `ast::Enum.isRecord: bool` · `AstParser::parseStruct(node) -> Enum` · `AstParser::parseRecord(node) -> Enum`
|
|
83
|
+
e.g. parse("struct Vec2(x: Float, y: Float)") -> Item::Enum(Enum{name:"Vec2", isValue:true, isRecord:false, generics:[], variants:[EnumVariant{name:"Vec2", field_names:["x","y"], fields:[Float, Float]}]})
|
|
84
|
+
e.g. parse("record Pair[K, V](key: K, val: V)") -> Item::Enum(Enum{name:"Pair", isValue:false, isRecord:true, generics:[K, V], variants:[EnumVariant{name:"Pair", field_names:["key","val"]}]})
|
|
85
|
+
**Review Focus:** `struct_no_body_then_fun`: `struct A(x: Int)\nfun f() -> Int = 1` parses as 2 items with no ERROR node; `struct_body_comment_before_fn`: a `# c` line between `=` and the first `fun` keeps the name `Vec2`; `value_enum_rejected`: `value enum V =\n | V(x: Int)` has a parse error; `struct_generic_rejected`: `struct P[T](a: T)` has a parse error; `record_fn_field`: `record Num[T](add: fn(T, T) -> T, zero: T)` gives field types [fn(T,T)->T, T]
|
|
86
|
+
**Test code (contracts):**
|
|
87
|
+
- corpus `struct.txt`: struct no body; struct with const + method body; record generic with body; record fn-typed field; struct nested struct field; body-less struct followed by `fun`; body-less record followed by `enum`. Remove the `value enum` case from `enum.txt`.
|
|
88
|
+
- parser_test.rs: `parseStructNoBody`, `parseStructWithBody` (1 const `ZERO`, 1 method `add` with `type_param == Some("Vec2")`), `parseRecordGeneric`, `parseRecordFnField`, plus the 4 Review Focus cases.
|
|
89
|
+
- checker_tests.rs: rewrite the 4 `valueErrors` tests. Drop the multi-constructor one (now a grammar error). `struct Named(label: Str, n: Int)` gives `struct field 'label' must be Int, Float, Bool or a struct`. `struct Ping(b: Pong)` + `struct Pong(a: Ping)` gives `struct Ping cannot contain itself`. `struct Vec2(..)` + `struct Rect(pos: Vec2, size: Vec2)` gives no errors.
|
|
90
|
+
- Migrate every `value enum` (values.plum x4, constants.plum, pattern_if.plum, codegen_tests.rs x3) to `struct`. Then `cargo test -p plum-core -p plum-checker -p plum-wasm-codegen` and `tree-sitter test` are green, and `plum test plum-examples/values.plum plum-examples/constants.plum plum-examples/pattern_if.plum` passes.
|
|
91
|
+
**Steps:** 1 grammar + regenerate + corpus, 2 failing parser tests, 3 parseStruct/parseRecord (dispatch the `struct`/`record` node kinds next to `"enum" =>` at parser.rs:85), 4 checker messages, 5 migrate the value enums, 6 run the listed suites.
|
|
92
|
+
**Notes:** the body reuses the enum body's `(fn | const)` handling so methods get `type_param` = the type name, exactly as enum methods do. Keep `isValue` semantics identical to today's `value enum`.
|
|
93
|
+
|
|
94
|
+
## Tooling
|
|
95
|
+
|
|
96
|
+
### T2 tooling queries · Complexity 2 · Est. context 30k
|
|
97
|
+
**Owns:** plum-tooling/tree-sitter-plum/queries/plum/highlights.scm, plum-tooling/tree-sitter-plum/queries/plum/format.scm, plum-tooling/vscode-plum/syntaxes/plum.tmLanguage.json, plum-lsp/src/completion.rs, plum-core/tests/formatter_test.rs
|
|
98
|
+
**Components:** 2
|
|
99
|
+
**LOC owned:** 90
|
|
100
|
+
**Tests:** slow
|
|
101
|
+
**Model:** sonnet
|
|
102
|
+
**Est. time:** 2.5 min
|
|
103
|
+
**Consumes:** tree-sitter nodes `struct` and `record` (T1)
|
|
104
|
+
**Review Focus:** `format_struct_idempotent`: formatting `struct Vec2(x: Float, y: Float) =\n ZERO = Vec2(x: 0.0, y: 0.0)\n\n fun len(self) -> Float =\n self.x\n` twice gives identical output that equals the input; `format_record_no_body`: `record Pair[K, V](key: K, val: V)` stays one line
|
|
105
|
+
**Test code (contracts):** formatter_test.rs: `formatStructWithBody`, `formatStructNoBody`, `formatRecordGeneric`, each asserting output == input and format(format(x)) == format(x). highlights: `struct`/`record` are @keyword, `value` is removed. tmLanguage keyword regex: replace `value` with `struct|record`. completion.rs keyword list: replace `"value"` with `"struct", "record"`. Repo-wide idempotency check on temp copies of `plum-std/*.plum` and `plum-examples/*.plum`, never in place.
|
|
106
|
+
**Steps:** 1 failing formatter tests, 2 format.scm rules (copy the enum rules: keyword space, `, ` between fields, space before `=`, body indent, blank lines kept), 3 the other three files, 4 run `cargo test -p plum-core --test formatter_test -p plum-lsp`.
|
|
107
|
+
|
|
108
|
+
## Migration
|
|
109
|
+
|
|
110
|
+
### T3 record migration · Complexity 3 · Est. context 60k
|
|
111
|
+
**Owns:** plum-std/*.plum, plum-examples/chains.plum, plum-examples/closures.plum, plum-examples/forms.plum, plum-examples/functions.plum, plum-examples/generics.plum, plum-examples/match.plum, plum-examples/methods.plum, plum-examples/oop_visitor.plum, plum-examples/operators.plum, plum-examples/optionals.plum, plum-examples/safe_navigation.plum, plum-examples/types.plum, plum-checker/tests/error_union_tests.rs, plum-cli/tests/assert_once_test.rs, plum-core/tests/loader_test.rs, plum-tooling/tree-sitter-plum/test/aa.plum, plum-tooling/tree-sitter-plum/test/highlight/sample.plum, plum-tooling/tree-sitter-plum/test/corpus/const.txt, plum-tooling/tree-sitter-plum/test/corpus/type.txt
|
|
112
|
+
**Components:** 2
|
|
113
|
+
**LOC owned:** 300
|
|
114
|
+
**Tests:** slow
|
|
115
|
+
**Model:** sonnet
|
|
116
|
+
**Est. time:** 5 min
|
|
117
|
+
**Consumes:** tree-sitter nodes `struct` and `record` (T1)
|
|
118
|
+
**Review Focus:** `migrated_methods_keep_receiver`: after migration, `plum test plum-std` passes with the same test count as before (record it first); `no_self_named_single_variant_left`: the scratchpad scan script finds 0 `enum X =\n | X(` declarations in owned files
|
|
119
|
+
**Test code (contracts):** The rewrite script lives in the session scratchpad and is NOT committed. It rewrites `enum X[G] =\n | X(fields)` to `record X[G](fields)`, adding ` =` only when an indented body follows. It handles the `\n`-escaped snippets inside Rust string literals and leaves a payload-less self-named variant alone (`enum Array[T] =\n | Array`), reporting it. It skips files T1 owns (values.plum, constants.plum, pattern_if.plum, checker_tests.rs, codegen_tests.rs, parser_test.rs, corpus enum.txt) and prints every skip and every site it didn't rewrite. Corpus expected trees in const.txt/type.txt are updated with `tree-sitter test --update` only after checking the diff is just enum->record. Green: `cargo test --workspace`, `tree-sitter test`, `plum test plum-std plum-examples` (release build).
|
|
120
|
+
**Steps:** 1 record the baseline test counts, 2 write and run the script, 3 hand-fix the reported sites, 4 run the full suites.
|
|
121
|
+
**Notes:** Ruling: records in T1-owned files (checker_tests.rs, codegen_tests.rs, parser_test.rs, values.plum Player, pattern_if.plum Soldier, constants.plum Tile/Wrap) stay as `enum` in Phase A - the old form still parses, and Phase B's removal error forces them over - cost: mixed style until Phase B.
|
|
122
|
+
|
|
123
|
+
## Docs
|
|
124
|
+
|
|
125
|
+
### T4 README · Complexity 1 · Est. context 25k
|
|
126
|
+
**Owns:** README.md
|
|
127
|
+
**Components:** 1
|
|
128
|
+
**LOC owned:** 70
|
|
129
|
+
**Tests:** fast
|
|
130
|
+
**Kind:** mechanical
|
|
131
|
+
**Model:** haiku
|
|
132
|
+
**Est. time:** 1.5 min
|
|
133
|
+
**Review Focus:** `readme_no_value_enum`: `grep -c "value enum" README.md` is 0
|
|
134
|
+
**Test code (contracts):**
|
|
135
|
+
- Rename the section "Types: enums, records and value enums" to "Types: struct, record and enum".
|
|
136
|
+
- In that section, show `struct Vec2(x: Float, y: Float) =` with `ZERO` and `add`, `record User(name: Str, age: Int)`, and a generic `record Pair[K, V](key: K, val: V)`.
|
|
137
|
+
- Add a 4-line copy-vs-share example: `a := Vec2(..); b := a; b.x = 9.0` leaves `a.x` unchanged, while `u2 := u; u2.age = 4` changes `u.age`.
|
|
138
|
+
- Struct fields are only Int/Float/Bool/other structs.
|
|
139
|
+
- Cheat-sheet rows: `a small value type | struct Vec2(x: Float, y: Float)` and `a record | record User(name: Str, age: Int)`.
|
|
140
|
+
- Update the overview snippet (README lines 32-38) and the pattern-if note at about line 343 (`on a struct`).
|
|
141
|
+
- Check: `grep -c "value enum" README.md` gives 0.
|
|
142
|
+
**Steps:** 1 edit, 2 grep check.
|
|
143
|
+
|
|
144
|
+
## Review Focus
|
|
145
|
+
- Package core (parser/library): deep, encoding, state. Risks: an external-scanner indent opened after a body-less declaration; comment-shifted positional indexing; methods losing `type_param`. **Probe entry:** run=./target/release/plum test plum-examples/values.plum
|
|
146
|
+
- Package tooling (CLI/formatter): file, encoding. Risk: format.scm collapsing struct bodies or breaking idempotency. **Probe entry:** run=cargo test -p plum-core --test formatter_test
|
|
147
|
+
- Package migration (file I/O generator): file, deep. Risk: a dropped body or method during the rewrite. **Probe entry:** run=./target/release/plum test plum-std plum-examples
|
|
148
|
+
|
|
149
|
+
## Self-review
|
|
150
|
+
Coverage: Phase A steps 1-5 map to T1 (steps 1-3), T2 (tooling in step 1), T3 (step 4) and T4 (step 5). No placeholders. The Consumes in T2/T3 match T1's Produces. The only edges are T1->T2 and T1->T3, both execution dependencies. Owns don't overlap.
|
docs/plans/2026-11-01-struct-value-types.review.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# `struct`, `record`, and enums as unions of types (review plan)
|
|
2
|
+
|
|
3
|
+
**Status:** awaiting approval (revision 4: decision 3 replaced by subject narrowing; any type may be a member, so `Json` uses std types directly).
|
|
4
|
+
|
|
5
|
+
## Goal
|
|
6
|
+
```plum
|
|
7
|
+
struct Circle(r: Float) = # value type: copied, flattened
|
|
8
|
+
fun area(self) -> Float = self.r * self.r * Float.PI
|
|
9
|
+
struct Rectangle(w: Float, h: Float) =
|
|
10
|
+
fun area(self) -> Float = self.w * self.h
|
|
11
|
+
record User(name: Str, age: Int) # reference type: heap, shared
|
|
12
|
+
record Pair[K, V](key: K, val: V)
|
|
13
|
+
|
|
14
|
+
enum Shape = Circle | Rectangle # a Shape is a Circle or a Rectangle
|
|
15
|
+
enum Color = Red | Green | Blue # undeclared names are payload-less tags
|
|
16
|
+
enum Json = JsonNull | Bool | Float | Str | List[Json] | Map[Str, Json]
|
|
17
|
+
|
|
18
|
+
s: Shape = Circle(r: 2.0) # a member converts to its enum implicitly
|
|
19
|
+
s.area() # auto-dispatch: every member has area()
|
|
20
|
+
|
|
21
|
+
match s
|
|
22
|
+
Circle(r) => r # destructure, as today
|
|
23
|
+
Rectangle => s.w * s.h # bare member name: `s` is a Rectangle here
|
|
24
|
+
|
|
25
|
+
fun toStr(j: Json) -> Str =
|
|
26
|
+
match j
|
|
27
|
+
JsonNull => "null"
|
|
28
|
+
Bool => if j then "true" else "false" # j: Bool
|
|
29
|
+
Float => j.toStr() # j: Float
|
|
30
|
+
Str => "\"" + escapeJsonStr(j) + "\"" # j: Str
|
|
31
|
+
List => "[" + j.join(",") + "]" # j: List[Json]
|
|
32
|
+
Map => "{{" + jsonMapToStr(j) + "}}" # j: Map[Str, Json]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Rulings (user, 2026-11-01)
|
|
36
|
+
- `=` follows the header only when an indented body follows. `struct Color(r: Int)` with no body is one line.
|
|
37
|
+
- Members are types or bare tags. Inline payload variants (`| Circle(r: Float)`) are removed. Discriminant enums keep their literal values (`enum Step(n: Int) = | ReadMin(10)`, unchanged).
|
|
38
|
+
- **Auto-dispatch:** `s.area()` works when the enum doesn't define `area` itself and every member defines it with the same signature. Otherwise the error names the first member that lacks it or differs. This also covers operators and `toStr`.
|
|
39
|
+
- `struct`: no generics; fields only `Int`/`Float`/`Bool`/other structs; no cycles. `record`: generics and any field types, including `fn(...)`.
|
|
40
|
+
- `value enum` and `enum User = | User(...)` are removed with no aliases. The error names the new form.
|
|
41
|
+
|
|
42
|
+
## Changed since revision 3 (please confirm)
|
|
43
|
+
1. **Decision 3 (`Circle as c`) is dropped and replaced by subject narrowing.**
|
|
44
|
+
- Why I dropped it: `as` would be a new keyword, and `if Circle as c := s` reads badly, with two binders pointing in opposite directions.
|
|
45
|
+
- What replaces it: Plum already uses a bare name as a test pattern (tags, and `if TooBig := e`). So a bare member name in a `match` arm or pattern `if` narrows the **subject variable** to that member type for the arm: `if Circle := s` makes `s` a `Circle` inside the block.
|
|
46
|
+
- It needs no new syntax and works on `self` too (`match self` / `Str => self.length()`).
|
|
47
|
+
- Limits:
|
|
48
|
+
- The subject must be a local, a param or `self`. For an expression subject (`match f()`), assign it first or destructure.
|
|
49
|
+
- For a **struct** member, the narrowed `s` is a copy, so `s.r = 3.0` inside the arm is an error: `s is a copy of the Circle inside Shape; assign it to a local to modify it`. Without that error, the write would silently not stick.
|
|
50
|
+
- For a **record** member, `s` is the same reference, so writes stick.
|
|
51
|
+
2. **Any type can be a member, and a type may be in several enums.** This answers the Json question, and it reverses your earlier "one enum each" answer, so please confirm.
|
|
52
|
+
- You picked "one enum each" because I said sharing needed runtime type tests. It doesn't: I now lower every member as a **boxed payload** (tag plus value), which is exactly today's `JsonBool(Bool)` layout. `enum Json = Bool | Float | ...` compiles to the same machine shape as today's Json.
|
|
53
|
+
- Primitives (`Float`, `Bool`), std types (`Str`), generic instantiations (`List[Json]`, `Map[Str, Json]`) and recursive references all work, and Circle can be in both `Shape` and `Round`.
|
|
54
|
+
- Cost: a record member sits behind one extra pointer inside an enum. Folding a same-module record's tag into its own object is a possible later optimization.
|
|
55
|
+
- Two members with the same base name (`List[Int] | List[Str]`) is an error, because the pattern `List` would be ambiguous.
|
|
56
|
+
|
|
57
|
+
## More design (my rulings)
|
|
58
|
+
- **Resolution.** A member name that names a type in scope (declared, imported or built in) is a type member. Any other name is a tag.
|
|
59
|
+
- Risk: a typo (`Strr`) silently becomes a tag. It still fails loudly at the first use (`j: Json = "x"` has no `Str` member), and the error lists the enum's tags.
|
|
60
|
+
- **Typing.**
|
|
61
|
+
- `Circle(r: 2.0)` has type `Circle`. It converts to `Shape` at assignment, argument, return, `List[Shape].push` and `Shape?` (the slot `T -> T?` uses).
|
|
62
|
+
- Literals convert too: `j: Json = 3.0`.
|
|
63
|
+
- `Int` and `Float` are distinct members, and a literal goes to the member of its own type.
|
|
64
|
+
- **Patterns.**
|
|
65
|
+
- A bare member name tests and narrows. `Circle(r)` / `Circle(r: r)` destructures a struct or record member. Tags match by name.
|
|
66
|
+
- Exhaustiveness covers the member list.
|
|
67
|
+
- A generic member is matched by its base name (`List`).
|
|
68
|
+
- **Generic enums:** `enum Tree[T] = Leaf | Node[T]`, with `record Node[T](v: T, l: Tree[T], r: Tree[T])`.
|
|
69
|
+
- **Lowering.** A lowering pass rewrites each enum into today's representation with hidden variants mangled per enum (`Shape.Circle(v: Circle)`). Mangling keeps the global variant map collision-free, which is what makes sharing possible. Codegen sees ordinary positional-payload variants and stays almost unchanged. New checker and lowering work:
|
|
70
|
+
- member-to-enum wrapping at coercion sites;
|
|
71
|
+
- narrowing, which rebinds the subject to the payload in the arm (unboxed into locals for a struct);
|
|
72
|
+
- auto-dispatch, which desugars to a `match` calling each member's method.
|
|
73
|
+
|
|
74
|
+
```mermaid
|
|
75
|
+
flowchart LR
|
|
76
|
+
src["struct / record / enum A | B"] --> parser["parser"]
|
|
77
|
+
parser --> res["resolve members: type or tag"]
|
|
78
|
+
res --> low["lower: hidden variants Shape.Circle(v: Circle)"]
|
|
79
|
+
low --> chk["checker: wrap coercions, narrowing, auto-dispatch"]
|
|
80
|
+
chk --> cg["codegen: today's variant layout"]
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Work, in order
|
|
84
|
+
**Phase A: `struct` and `record`.** It ships on its own and doesn't change semantics.
|
|
85
|
+
1. **Grammar and tooling.**
|
|
86
|
+
- Add the `struct` and `record` rules and regenerate **with `--disable-optimizations`**.
|
|
87
|
+
- Add corpus cases.
|
|
88
|
+
- Update `highlights.scm`, `format.scm` (idempotent; never run `plum format` on real files to check), the tmLanguage file and the LSP keywords.
|
|
89
|
+
2. **Parser.** Lower to today's single-variant `ast::Enum` (`isValue` / new `isRecord`). Tests, plus errors for `value enum` and `struct P[T]`.
|
|
90
|
+
3. **Checker.** Rename the struct messages and drop the 3 checks that can no longer fire.
|
|
91
|
+
4. **Migration script.** About 134 self-named single-variant enums plus the `value enum`s, across std, examples, the Rust test snippets and the corpus.
|
|
92
|
+
5. **README** "Types", then verify: `cargo test`, `tree-sitter test`, `plum test plum-std plum-examples` (release build).
|
|
93
|
+
|
|
94
|
+
**Phase B: enums as unions.** It depends on A.
|
|
95
|
+
|
|
96
|
+
6. **Grammar.** `enum Name[G] = A | B[X] | C` on one line or as a `|` list, with an optional `=` body. Inline payload variants are removed.
|
|
97
|
+
7. **Resolution and lowering pass.** Members and tags, generic members, duplicate-base-name errors, mangled hidden variants. Remove the single-self-variant form, with an error that says `write record X(...)`.
|
|
98
|
+
8. **Checker.**
|
|
99
|
+
- Wrap coercions, including literals.
|
|
100
|
+
- Narrowing, with the struct-copy write error.
|
|
101
|
+
- Destructuring through a member, and exhaustiveness.
|
|
102
|
+
- Auto-dispatch and its errors.
|
|
103
|
+
9. **Codegen.** Wrap and unwrap at the coercion and narrowing sites; struct members box their leaves. Auto-dispatch arrives already desugared.
|
|
104
|
+
10. **Migration script 2.**
|
|
105
|
+
- Hoist each inline payload variant into a `record` after the enum, with positional payloads named `v`, then `v0, v1, …`. That's about 130 variants across about 32 `.plum` files plus the Rust snippets.
|
|
106
|
+
- A single-payload variant whose payload is already a type (`JsonFloat(Float)`) is offered as a direct member, for hand review.
|
|
107
|
+
- The script reports clashes.
|
|
108
|
+
11. **Json rewrite.** `plum-std/Json.plum` becomes the `enum Json = JsonNull | Bool | Float | Str | List[Json] | Map[Str, Json]` above, and its 37 uses of the old variant names are rewritten with narrowing. `Json.parse` builds values through the implicit conversion.
|
|
109
|
+
12. **Tests.** `plum-examples/unions.plum` plus checker and codegen tests:
|
|
110
|
+
- struct and record members;
|
|
111
|
+
- tags mixed with types;
|
|
112
|
+
- primitive and generic members;
|
|
113
|
+
- one type in two enums;
|
|
114
|
+
- generic `Tree[T]`;
|
|
115
|
+
- narrowing in `match` / `if` / `self`;
|
|
116
|
+
- the struct-copy write error;
|
|
117
|
+
- the `List[Int] | List[Str]` error;
|
|
118
|
+
- auto-dispatch success, plus its missing-method and mismatch errors;
|
|
119
|
+
- a `List[Shape]` holding both kinds;
|
|
120
|
+
- a Json parse and print round-trip.
|
|
121
|
+
|
|
122
|
+
README: rewrite Enums and Pattern if.
|
|
123
|
+
13. **Verify** as in step 5, then a final review.
|
|
124
|
+
|
|
125
|
+
## Risks to watch
|
|
126
|
+
- **Grammar:** `enum A = B | C[D]` on one line next to the `|` list form, and generic member brackets. Needs corpus cases.
|
|
127
|
+
- **Narrowing scope:** the subject is rebound to a different type inside the arm. Closures that capture `s` in the arm must capture the narrowed binding. This needs a test.
|
|
128
|
+
- **Recursive members** (`List[Json]` inside `Json`) go through the GC type registry. Today's `JsonList(List[Json])` already works this way, so keep the round-trip test early.
|
|
129
|
+
- **The 2026-09 "variant-as-type" code** (`v: Red` params, field-access downcast) may be superseded by member types. Check its uses before deleting it.
|
|
130
|
+
- **Branch:** build on its own branch off `master`, not `superlite/map-literal`.
|
|
131
|
+
|
|
132
|
+
## Not in scope
|
|
133
|
+
- Flow typing outside patterns (`if s is Circle`).
|
|
134
|
+
- Generic structs.
|
|
135
|
+
- The same-module record tag-folding optimization.
|