plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
019d1f0
— Peter John
2026-11-01T18:19:00+05:30
docs(plans): union enums Phase B machine plan
docs/plans/2026-11-01-union-enums.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# Enums as unions of member types (Phase B)
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-01-struct-value-types.review.md
|
|
4
|
+
**Goal:** `enum Shape = Circle | Rectangle`. Members are types in scope (structs, records, `Float`, `Str`, `List[Json]`, ...) or bare tags. A member converts to its enum implicitly, a bare member name in a pattern narrows the subject, and methods every member shares auto-dispatch.
|
|
5
|
+
**Architecture:** lower to today's enums.
|
|
6
|
+
1. A loader pass (`plum_core::unions::resolveMembers`) turns each member into a hidden positional variant `Shape__Circle(Circle)`.
|
|
7
|
+
2. The checker accepts the union forms and records a `UnionPlan` (expression, pattern and case addresses) on the program it checks.
|
|
8
|
+
3. `plum_core::union_lower::applyPlan` rewrites that program in place into plain variant constructions, nested patterns, renamed narrowed bindings and `match` dispatches. The checker then checks the result again and codegen compiles it. This is the same path `for`-loop lowering uses (`TypedInfo::lowered`).
|
|
9
|
+
|
|
10
|
+
Codegen is unchanged.
|
|
11
|
+
**Tech stack:** Rust workspace, tree-sitter.
|
|
12
|
+
**Spec:** docs/plans/2026-11-01-struct-value-types.review.md (Design + "Changed since revision 3" + "More design", Phase B steps 6-13)
|
|
13
|
+
**Detail:** contracts
|
|
14
|
+
**Target-min:** 40
|
|
15
|
+
|
|
16
|
+
## Global constraints
|
|
17
|
+
- Syntax:
|
|
18
|
+
- one line: `enum Name[G] = A | B[X] | C`, with no body;
|
|
19
|
+
- multi-line: `enum Name[G] =` NEWLINE ` | A` / ` | B[X]` ... followed by optional consts and methods.
|
|
20
|
+
|
|
21
|
+
Ruling: only the multi-line form takes a body. Discriminant enums (`enum Step(n: Int) = | ReadMin(10)`) are unchanged.
|
|
22
|
+
- **Member resolution:** a payload-less, value-less variant name is a type member if it names an item in the merged source (any enum/struct/record/primitive) or one of `Int`, `Float`, `Byte`. Otherwise it is a tag. Generic args (`List[Json]`) make it a member, or else a resolve error: `unknown member type List`.
|
|
23
|
+
- Hidden variant name: `<Enum>__<Member base name>`, e.g. `Shape__Circle`, `Json__List`. Its field is the member type, positional, with no field name.
|
|
24
|
+
- Errors, verbatim:
|
|
25
|
+
- `enum Json has two List members; a pattern 'List' would be ambiguous`
|
|
26
|
+
- `inline variant payloads are removed: declare record Circle(r: Float) and list Circle in enum Shape` (raised for any non-discriminant variant with a payload)
|
|
27
|
+
- `enum User has a single variant named User — write record User(...)`
|
|
28
|
+
- `s is a copy of the Circle inside Shape; assign it to a local to change it`
|
|
29
|
+
- `Shape has no method 'area': member Red is a tag`
|
|
30
|
+
- `Shape has no method 'area': member Square doesn't define it`
|
|
31
|
+
- `Shape.area: members disagree: Circle.area is <t1>, Square.area is <t2>`
|
|
32
|
+
- `cannot join Circle and Oval: no single enum has both members` (for an if/match with branches of different member types)
|
|
33
|
+
- A type may be a member of several enums. Narrowing applies only when the subject is a local, a param or `self`. Auto-dispatch covers method calls and `"{e}"` interpolation (via `toStr`), not operators (ruling: operators on enums stay as they are today).
|
|
34
|
+
- Regenerate tree-sitter only with `--disable-optimizations`. Never run `plum format` on repo files. Read node fields with `child_by_field_name`. `touch` edited `.rs` files before trusting a build. Use multi-letter type names in tests (a single uppercase letter is a generic).
|
|
35
|
+
- Tests: `cargo test -p <crate>`; `cd plum-tooling/tree-sitter-plum && tree-sitter test`; `cargo build --release -p plum-cli && ./target/release/plum test plum-std plum-examples`.
|
|
36
|
+
|
|
37
|
+
## Main diagram
|
|
38
|
+
```mermaid
|
|
39
|
+
flowchart LR
|
|
40
|
+
P["parser: EnumVariant.member candidate"] --> R["unions::resolveMembers (loader)"]
|
|
41
|
+
R --> C2["checker pass: unions rules + recordWrap/Pattern/Narrow/Dispatch"]
|
|
42
|
+
C2 --> AP["union_lower::applyPlan (in place)"]
|
|
43
|
+
AP --> C3["checker re-check"]
|
|
44
|
+
C3 --> CG["codegen (unchanged)"]
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## File structure
|
|
48
|
+
- grammar.js + generated src, corpus `union_enum.txt`, `parser.rs`, `ast.rs`: syntax and the `member` field.
|
|
49
|
+
- `plum-core/src/unions.rs`: resolution, hidden names, the UnionPlan recorder.
|
|
50
|
+
- `plum-core/src/union_lower.rs`: applyPlan.
|
|
51
|
+
- `plum-checker/src/unions.rs` plus hooks in `plum-checker/src/lib.rs`: typing rules.
|
|
52
|
+
- `plum-examples/unions.plum`: end-to-end tests.
|
|
53
|
+
- Migration across std, examples, tests and README.
|
|
54
|
+
|
|
55
|
+
## Task graph
|
|
56
|
+
```mermaid
|
|
57
|
+
flowchart LR
|
|
58
|
+
T1[grammar + parser]
|
|
59
|
+
T2[resolve + plan recorder]
|
|
60
|
+
T3[checker coercions]
|
|
61
|
+
T4[checker patterns + narrowing]
|
|
62
|
+
T5[checker auto-dispatch]
|
|
63
|
+
T6[applyPlan rewrite]
|
|
64
|
+
T7[pipeline wiring + e2e]
|
|
65
|
+
T8[migration + legacy errors]
|
|
66
|
+
T9[Json + README]
|
|
67
|
+
%% taskgraph
|
|
68
|
+
T1 --> T2
|
|
69
|
+
T2 --> T3
|
|
70
|
+
T3 --> T4
|
|
71
|
+
T4 --> T5
|
|
72
|
+
T2 --> T6
|
|
73
|
+
T5 --> T7
|
|
74
|
+
T6 --> T7
|
|
75
|
+
T7 --> T8
|
|
76
|
+
T8 --> T9
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```mermaid
|
|
80
|
+
%% schedule
|
|
81
|
+
gantt
|
|
82
|
+
title Predicted schedule (mm:ss)
|
|
83
|
+
dateFormat X
|
|
84
|
+
axisFormat %M:%S
|
|
85
|
+
section lane 1
|
|
86
|
+
T1 grammar + parser :t1, 0, 173s
|
|
87
|
+
T2 resolve + plan recorder :t2, 173, 140s
|
|
88
|
+
T6 applyPlan rewrite :t6, 313, 123s
|
|
89
|
+
T5 checker auto-dispatch :t5, 541, 107s
|
|
90
|
+
T7 pipeline wiring + e2e :t7, 648, 179s
|
|
91
|
+
T8 migration + legacy errors :t8, 827, 202s
|
|
92
|
+
T9 Json + README :t9, 1029, 171s
|
|
93
|
+
section lane 2
|
|
94
|
+
T3 checker coercions :t3, 313, 113s
|
|
95
|
+
T4 checker patterns + narrowing :t4, 426, 115s
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
critical path: T1 → T2 → T3 → T4 → T5 → T7 → T8 → T9 (20.0 min) · peak width 2 · predicted build 20.0 min · predicted total ≈ 23.5 min (target ✓)
|
|
99
|
+
Ruling: sequential - T3/T4/T5 all hook `plum-checker/src/lib.rs` (the same coercion/pattern/call sites), and T8/T9 need working unions to keep the suite green - cost: a long critical path, accepted.
|
|
100
|
+
|
|
101
|
+
## Predicted schedule
|
|
102
|
+
|
|
103
|
+
## Syntax
|
|
104
|
+
|
|
105
|
+
### T1 grammar + parser · Complexity 4 · Est. context 45k
|
|
106
|
+
**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/union_enum.txt, plum-tooling/tree-sitter-plum/queries/plum/format.scm, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/tests/parser_test.rs, plum-core/tests/formatter_test.rs
|
|
107
|
+
**Components:** 2
|
|
108
|
+
**LOC owned:** 180
|
|
109
|
+
**Tests:** slow
|
|
110
|
+
**Model:** sonnet
|
|
111
|
+
**Est. time:** 4 min
|
|
112
|
+
**Produces:** `ast::EnumVariant.member: Option<ast::Type>`. The parser sets it to `Some(Type{name, generics})` for every variant with no payload and no values, and leaves it `None` otherwise. It also accepts generic args on a variant name: `| Node[T]`, `| List[Json]`.
|
|
113
|
+
e.g. parse("enum Shape = Circle | Rectangle") -> Enum{name:"Shape", variants:[{name:"Circle", member:Some(Circle), fields:[]}, {name:"Rectangle", member:Some(Rectangle)}]}
|
|
114
|
+
e.g. parse("enum Tree[T] =\n | Leaf\n | Node[T]\n") -> variants [{name:"Leaf", member:Some(Leaf)}, {name:"Node", member:Some(Node[T])}]
|
|
115
|
+
**Review Focus:** `one_line_then_fun`: `enum Sh = Ci | Re\nfun f() -> Int = 1` gives 2 items with no ERROR; `one_line_generic_member`: `enum Js = JsNull | List[Js] | Map[Str, Js]` has member generics [Js] and [Str, Js]; `discriminant_unchanged`: `enum Step(n: Int) =\n | ReadMin(10)` has values [10] and member None; `inline_payload_still_parses`: `| Circle(r: Float)` parses (T8 rejects it later)
|
|
116
|
+
**Test code (contracts):** corpus `union_enum.txt`: one-line; one-line with generics; multi-line members plus a method body; one-line followed by `fun`; a closure containing `|` in a const after an enum. Parser tests for the 4 Review Focus cases plus both e.g. lines. format.scm: one-line enum stays one line, with ` | ` between members (formatter test `formatOneLineEnum`, idempotent).
|
|
117
|
+
**Steps:** grammar, regenerate, corpus, parser plus the `member` field, format rule, tests.
|
|
118
|
+
|
|
119
|
+
## Core
|
|
120
|
+
|
|
121
|
+
### T2 resolve + plan recorder · Complexity 4 · Est. context 40k
|
|
122
|
+
**Owns:** plum-core/src/unions.rs, plum-core/src/lib.rs, plum-core/src/loader.rs, plum-core/tests/unions_test.rs
|
|
123
|
+
**Components:** 2
|
|
124
|
+
**LOC owned:** 200
|
|
125
|
+
**Tests:** fast
|
|
126
|
+
**Model:** sonnet
|
|
127
|
+
**Est. time:** 4 min
|
|
128
|
+
**Consumes:** `ast::EnumVariant.member: Option<ast::Type>` (T1)
|
|
129
|
+
**Produces:** `unions::hiddenVariant(enumName: &str, member: &str) -> String` · `unions::resolveMembers(source: &mut Source) -> Vec<String>` · `unions::UnionPlan { wraps: HashMap<usize, String>, patterns: HashMap<usize, String>, narrowings: HashMap<usize, (String, String, String)>, dispatches: HashMap<usize, Vec<String>> }` · `unions::beginPlan()` · `unions::recordWrap(exprAddr: usize, variant: String)` · `unions::recordPattern(patAddr: usize, variant: String)` · `unions::recordNarrow(caseAddr: usize, subject: String, variant: String)` · `unions::recordDispatch(callAddr: usize, variants: Vec<String>)` · `unions::takePlan() -> UnionPlan` · `unions::memberOf(source: &Source, enumName: &str, member: &str) -> Option<String>`
|
|
130
|
+
e.g. hiddenVariant("Shape", "Circle") -> "Shape__Circle"
|
|
131
|
+
e.g. resolveMembers on `struct Circle(r: Float)` + `enum Shape = Circle | Red` -> Shape.variants == [{name:"Shape__Circle", fields:[Circle], member:Some(Circle)}, {name:"Red", fields:[], member:None}], errors []
|
|
132
|
+
e.g. recordNarrow(0x10, "s", "Shape__Circle") then takePlan().narrowings[0x10] == ("s", "Shape__Circle", "s__n1") (the fresh name is `<subject>__n<k>`, with k counting up within one plan)
|
|
133
|
+
**Review Focus:** `dup_base_name`: `enum Js = List[Int] | List[Str]` gives the duplicate error verbatim; `member_in_two_enums`: Circle in Shape and Round gives the hidden variants Shape__Circle and Round__Circle; `idempotent`: running resolveMembers twice doesn't double-mangle
|
|
134
|
+
**Test code (contracts):** unions_test.rs: the e.g. lines, the 3 Review Focus cases, `memberOf(src, "Shape", "Circle") == Some("Shape__Circle")`, and `memberOf(src, "Shape", "Oval") == None`. loader.rs calls resolveMembers after merging and before forms; a non-empty error list becomes the loader's Err, one line each. Export the modules from lib.rs.
|
|
135
|
+
**Notes:** the plan recorder is a thread_local, like `forms::begin`. The three legacy errors (inline payload, single self-variant) are written here as `unions::legacyErrors(source: &Source) -> Vec<String>` but NOT called (T8 turns them on).
|
|
136
|
+
|
|
137
|
+
### T6 applyPlan rewrite · Complexity 6 · Est. context 50k
|
|
138
|
+
**Owns:** plum-core/src/union_lower.rs, plum-core/tests/union_lower_test.rs
|
|
139
|
+
**Components:** 1
|
|
140
|
+
**LOC owned:** 260
|
|
141
|
+
**Tests:** fast
|
|
142
|
+
**Model:** sonnet
|
|
143
|
+
**Est. time:** 5 min
|
|
144
|
+
**Consumes:** `unions::UnionPlan` (T2)
|
|
145
|
+
**Produces:** `union_lower::applyPlan(source: &mut Source, plan: &UnionPlan)`. It works in place and post-order (children before parents), so every recorded address is still valid when it's visited.
|
|
146
|
+
e.g. a wrap on `Circle(r: 2.0)` with "Shape__Circle" -> the expr becomes the AST the parser builds for `Shape__Circle(Circle(r: 2.0))`
|
|
147
|
+
e.g. a pattern on `Circle(r)` with "Shape__Circle" -> `Shape__Circle(Circle(r))`; a pattern on bare `Circle` -> `Shape__Circle(_)`
|
|
148
|
+
e.g. a narrowing on case `Circle => s.r` with ("s","Shape__Circle","s__n1") -> case `Shape__Circle(s__n1) => s__n1.r` (rename in the guard and body only, not in shadowed inner scopes)
|
|
149
|
+
e.g. a dispatch on `s.area()` with ["Shape__Circle","Shape__Square"] -> `match s` / `Shape__Circle(d__1) => d__1.area()` / `Shape__Square(d__2) => d__2.area()`
|
|
150
|
+
**Review Focus:** `narrow_closure_capture`: a closure inside the arm that captures `s` is renamed too; `narrow_self`: subject `self` is renamed to `self__n1` in the arm; `narrow_in_pattern_if`: a Bind `Circle := s` in an `if` renames in the then-block only; `dispatch_in_interpolation`: an interpolated `{s}` with a dispatch on it becomes `{match s ... => d.toStr()}`; `nested_wrap_inside_dispatch_arg`: both rewrites apply
|
|
151
|
+
**Test code (contracts):** union_lower_test.rs builds a plan by hand from addresses taken out of a parsed Source (a helper finds an expr by its source text). Cover each e.g. line and each Review Focus case, and assert on the pretty-printed or matched AST shapes.
|
|
152
|
+
|
|
153
|
+
## Checker
|
|
154
|
+
|
|
155
|
+
### T3 checker coercions · Complexity 6 · Est. context 55k
|
|
156
|
+
**Owns:** plum-checker/src/unions.rs, plum-checker/src/lib.rs, plum-checker/tests/unions_coerce_test.rs
|
|
157
|
+
**Components:** 1
|
|
158
|
+
**LOC owned:** 180
|
|
159
|
+
**Tests:** fast
|
|
160
|
+
**Model:** sonnet
|
|
161
|
+
**Est. time:** 5 min
|
|
162
|
+
**Consumes:** `unions::recordWrap(exprAddr: usize, variant: String)` (T2) · `unions::memberOf(source: &Source, enumName: &str, member: &str) -> Option<String>` (T2) · `unions::resolveMembers(source: &mut Source) -> Vec<String>` (T2)
|
|
163
|
+
**Review Focus:** `literal_into_json`: `j: Json = 3.0` records a wrap with Json__Float; `wrap_at_arg_return_push`: a Shape param, a `-> Shape` return and `List[Shape].push(Circle(..))` each record one wrap; `join_if_branches`: `x := if c then Circle(..) else Square(..)` has type Shape plus 2 wraps; `join_ambiguous`: with Circle and Oval in no common enum, the join error appears verbatim; `no_wrap_same_type`: `c: Circle = Circle(..)` records none
|
|
164
|
+
**Test code (contracts):** unions_coerce_test.rs parses, runs resolveMembers, checks, then inspects `takePlan().wraps` (count and variant names) for each Review Focus case, plus `Circle(..)` alone having type Circle (not Shape). The hooks sit next to the 3 existing `recordErrCoercion` sites and the if/match branch unification.
|
|
165
|
+
|
|
166
|
+
### T4 checker patterns + narrowing · Complexity 6 · Est. context 55k
|
|
167
|
+
**Owns:** plum-checker/src/unions.rs, plum-checker/src/lib.rs, plum-checker/tests/unions_pattern_test.rs
|
|
168
|
+
**Components:** 1
|
|
169
|
+
**LOC owned:** 200
|
|
170
|
+
**Tests:** fast
|
|
171
|
+
**Model:** sonnet
|
|
172
|
+
**Est. time:** 5 min
|
|
173
|
+
**Consumes:** `unions::recordPattern(patAddr: usize, variant: String)` (T2) · `unions::recordNarrow(caseAddr: usize, subject: String, variant: String)` (T2)
|
|
174
|
+
**Review Focus:** `destructure_member`: `match s` / `Circle(r) => r` types r as Float; `narrow_match`: `Rectangle => s.w` types s as Rectangle in the arm only (after the match, s is Shape); `narrow_struct_write_error`: `Circle => s.r = 1.0` gives the copy error verbatim; `narrow_record_write_ok`: the same with a record member gives no error; `exhaustive_members`: arms Circle and Rectangle cover Shape, and missing one reports the existing non-exhaustive error naming the member; `expr_subject_no_narrow`: `match f()` / `Circle => 1` records a pattern, not a narrowing; `pattern_if`: `if Circle := s` narrows in the then-block
|
|
175
|
+
**Test code (contracts):** unions_pattern_test.rs covers the Review Focus cases and asserts errors verbatim and `takePlan()` contents.
|
|
176
|
+
|
|
177
|
+
### T5 checker auto-dispatch · Complexity 5 · Est. context 50k
|
|
178
|
+
**Owns:** plum-checker/src/unions.rs, plum-checker/src/lib.rs, plum-checker/tests/unions_dispatch_test.rs
|
|
179
|
+
**Components:** 1
|
|
180
|
+
**LOC owned:** 140
|
|
181
|
+
**Tests:** fast
|
|
182
|
+
**Model:** sonnet
|
|
183
|
+
**Est. time:** 4 min
|
|
184
|
+
**Consumes:** `unions::recordDispatch(callAddr: usize, variants: Vec<String>)` (T2)
|
|
185
|
+
**Review Focus:** `dispatch_ok`: `s.area()` has type Float and dispatches to [Shape__Circle, Shape__Rectangle]; `own_method_wins`: an enum body method `area` gives no dispatch; `tag_member_error`, `missing_method_error`, `signature_mismatch_error`: all verbatim; `interpolation_toStr`: `"{s}"` dispatches toStr when every member has toStr; `generic_member_dispatch`: `j.toStr()` on Json with List[Json]/Map members resolves through the instantiated method types
|
|
186
|
+
**Test code (contracts):** unions_dispatch_test.rs covers each Review Focus case.
|
|
187
|
+
|
|
188
|
+
## Integration
|
|
189
|
+
|
|
190
|
+
### T7 pipeline wiring + e2e · Complexity 5 · Est. context 50k
|
|
191
|
+
**Owns:** plum-checker/src/lib.rs, plum-examples/unions.plum, plum-wasm-codegen/tests/unions_e2e_test.rs
|
|
192
|
+
**Components:** 2
|
|
193
|
+
**LOC owned:** 220
|
|
194
|
+
**Tests:** slow
|
|
195
|
+
**Model:** sonnet
|
|
196
|
+
**Est. time:** 5 min
|
|
197
|
+
**Consumes:** `union_lower::applyPlan(source: &mut Source, plan: &UnionPlan)` (T6) · `unions::takePlan() -> UnionPlan` (T2) · `unions::beginPlan()` (T2)
|
|
198
|
+
**Review Focus:** `struct_member_copy`: a struct taken out of a Shape by destructure, then modified, leaves the Shape's copy unchanged; `list_of_shapes`: `List[Shape]` holding Circle and Rectangle sums `area()` correctly; `tree_generic`: `enum Tree[T] = Leaf | Node[T]` recursive sum; `json_like_primitives`: `enum Jv = JvNull | Bool | Float | Str | List[Jv]` builds and prints by matching; `two_enums_one_type`: Circle passes as both a Shape and a Round
|
|
199
|
+
**Test code (contracts):** in checkSourceFull and checkSourceOnce's caller: beginPlan, then the pass whose program codegen will compile; if takePlan() is non-empty, run applyPlan on that owned program (the forms-lowered copy, or a clone for a non-form source taken BEFORE that pass), check it again, and set `info.lowered`. `plum-examples/unions.plum` holds `test` blocks for each Review Focus case plus `if Circle := s` and auto-dispatch. unions_e2e_test.rs compiles and runs 3 of them through codegen.
|
|
200
|
+
|
|
201
|
+
## Migration
|
|
202
|
+
|
|
203
|
+
### T8 migration + legacy errors · Complexity 4 · Est. context 60k
|
|
204
|
+
**Owns:** plum-core/src/loader.rs, plum-std/Array.plum, plum-examples/types.plum, plum-examples/generics.plum, plum-examples/match.plum, plum-examples/functions.plum, plum-examples/oop_visitor.plum, plum-examples/values.plum, plum-examples/pattern_if.plum, plum-examples/constants.plum, plum-examples/chains.plum, plum-examples/forms.plum, plum-examples/methods.plum, plum-examples/operators.plum, plum-examples/optionals.plum, plum-examples/safe_navigation.plum, plum-examples/closures.plum, plum-checker/tests/checker_tests.rs, plum-checker/tests/error_union_tests.rs, plum-wasm-codegen/tests/codegen_tests.rs, plum-core/tests/parser_test.rs, plum-core/tests/loader_test.rs, plum-cli/tests/assert_once_test.rs, plum-lsp/src/main.rs, plum-tooling/tree-sitter-plum/test/corpus/enum.txt, plum-tooling/tree-sitter-plum/test/corpus/match.txt, plum-tooling/tree-sitter-plum/test/corpus/type.txt
|
|
205
|
+
**Components:** 2
|
|
206
|
+
**LOC owned:** 400
|
|
207
|
+
**Tests:** slow
|
|
208
|
+
**Model:** sonnet
|
|
209
|
+
**Est. time:** 6 min
|
|
210
|
+
**Consumes:** `unions::legacyErrors(source: &Source) -> Vec<String>` (T2)
|
|
211
|
+
**Review Focus:** `same_test_count`: record the count before; afterwards `plum test plum-std plum-examples` passes the same number of tests (or more); `legacy_error_on`: a test asserting the inline-payload error and the single-self-variant error verbatim
|
|
212
|
+
**Test code (contracts):** A scratchpad script (not committed):
|
|
213
|
+
- hoists each inline payload variant into `record <Name>(fields)` after its enum, naming positional payloads `v`, `v0`, `v1`...;
|
|
214
|
+
- lists the name as a member;
|
|
215
|
+
- rewrites the remaining self-named single-variant enums to `record`;
|
|
216
|
+
- handles `\n`-escaped Rust snippets and reports clashes.
|
|
217
|
+
|
|
218
|
+
Plum-std/Json.plum is left for T9. Excluding Json.plum from legacyErrors until T9 is not allowed: T8 must keep the suite green, so if Json breaks it, T8 converts Json's variants to records too (T9 then simplifies them to direct members). Then call `legacyErrors` in loader.rs.
|
|
219
|
+
**Notes:** Ruling: tests whose point was the old syntax are rewritten to assert the new error.
|
|
220
|
+
|
|
221
|
+
### T9 Json + README · Complexity 3 · Est. context 45k
|
|
222
|
+
**Owns:** plum-std/Json.plum, README.md
|
|
223
|
+
**Components:** 2
|
|
224
|
+
**LOC owned:** 160
|
|
225
|
+
**Tests:** slow
|
|
226
|
+
**Model:** sonnet
|
|
227
|
+
**Est. time:** 4 min
|
|
228
|
+
**Review Focus:** `json_roundtrip`: Json.plum's existing tests pass after `enum Json = JsonNull | Bool | Float | Str | List[Json] | Map[Str, Json]` with narrowing (`Float => j.toStr()`); `readme_snippets_parse`: `cargo test -p plum-lsp` stays green
|
|
229
|
+
**Test code (contracts):**
|
|
230
|
+
- Rewrite Json per the review plan's Goal block.
|
|
231
|
+
- README: rewrite the Enums section (members, tags, implicit conversion, narrowing, auto-dispatch, one type in several enums) and the Pattern-if section (bare member narrows), and update the cheat-sheet enum row to `enum Shape = Circle | Rectangle`.
|
|
232
|
+
- Verify: `plum test plum-std/Json.plum`, `cargo test -p plum-lsp`.
|
|
233
|
+
|
|
234
|
+
## Review Focus
|
|
235
|
+
- core and checker (compiler library): deep, state, encoding. Risks: stale addresses in the plan after an in-place rewrite; narrowing leaking past its arm; generic member substitution; double check passes changing error messages or positions. **Probe entry:** run=./target/release/plum test plum-examples/unions.plum
|
|
236
|
+
- migration (file generator): file, deep. **Probe entry:** run=./target/release/plum test plum-std plum-examples
|
|
237
|
+
|
|
238
|
+
## Self-review
|
|
239
|
+
Every Phase B step (6-13) maps to T1-T9. The Consumes lines match the Produces lines of T2/T6. The checker tasks are chained because they share lib.rs. T6 runs parallel to T3-T5.
|