plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
e369b75
— Peter John
2026-11-03T14:34:52+05:30
docs(plans): inline variants + = match plans; enum/union split shelved
docs/plans/2026-11-03-enum-union-split.review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Split enum and union: review plan
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** shelved (2026-11-03). Inline record variants came back instead (docs/plans/2026-11-03-inline-variants-match-body.review.md): `enum` keeps doing both jobs and may list records.
|
|
4
4
|
|
|
5
5
|
## Summary
|
|
6
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.
|
docs/plans/2026-11-03-inline-variants-match-body.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Inline record variants and `= match` bodies
|
|
2
|
+
|
|
3
|
+
**Review plan:** docs/plans/2026-11-03-inline-variants-match-body.review.md
|
|
4
|
+
**Goal:** `enum Link[T] = Nil | Cons(head: T, tail: Link[T])` declares a variant with named fields inline: a hidden record plus a member, where `Cons` is a constructor and a pattern only and `Link[T]` is the only type. And `fun f(xs: Link[T]) -> Int = match xs` with indented arms is a valid body for functions and methods.
|
|
5
|
+
**Architecture:** T1: grammar `fn` body accepts `$.match` after `=`; the parser maps it to `FnBody::Block` with one `Stmt::Match` (same AST as the indented form); format.scm keeps it and fixes `when` spacing. T2 (plum-core, before `resolveMembers`): `unions::hoistInlineVariants` replaces the inline-payload legacy error: each enum variant with named fields becomes `record <Name>[gens](fields)` (gens = the enum generics its field types mention, in enum order) with `inlineOf: Some(<enum>)`, and the variant becomes a member `Name[gens]`; `unions::inlineVariantTypeErrors` rejects the name in any type position. T3 (checker): a construction of an inline record is typed as its enum (generics from the member binding, else the expected type) and recorded as a member wrap; `.field` on an inline record type is an error. Codegen unchanged.
|
|
6
|
+
**Tech stack:** Rust workspace, tree-sitter grammar (`--disable-optimizations`), Plum std/examples.
|
|
7
|
+
**Spec:** docs/plans/2026-11-03-inline-variants-match-body.review.md
|
|
8
|
+
**Detail:** contracts
|
|
9
|
+
**Target-min:** 20
|
|
10
|
+
**Budget-usd:** 50
|
|
11
|
+
|
|
12
|
+
Global constraints:
|
|
13
|
+
- Fast tests: `cargo test -p <crate> --test <file>`; Plum `cargo build --quiet && ./target/debug/plum test <path>` run from the repo root; `touch` edited .rs first; full suite via `superlite-cli run_tests --full`; `run_tests --fast=-p,<crate>,--test,<name>`.
|
|
14
|
+
- Grammar: regenerate with `cd plum-tooling/tree-sitter-plum && npx --yes tree-sitter-cli generate --disable-optimizations` (the default optimizer silently breaks unrelated parses), then `npx --yes tree-sitter-cli test`.
|
|
15
|
+
- Gotchas: single uppercase letters lex as generics (multi-letter type names in tests); no `+=`; no `? :` (use `if c then a else b`); inline `if` cannot mix a value and `null`; a function cannot return `fn(..)` (wrap in a record); a generic fn called with a bare `Nil` needs a typed local; never `plum format` a real file (it rewrites in place; use a copy).
|
|
16
|
+
- Error strings (exact): "Cons is a variant of Link, not a type; use Link[...]"; "enum Link declares Cons inline, but Cons is already declared; list it as a member instead"; "variant Some(T) needs named fields: Some(value: T)" (positional types joined by ", "); "read Cons's fields with a pattern: Cons(...)" (field access on an inline variant value).
|
|
17
|
+
- Hand-written records listed as members (`record Circle` + `enum Shape = Circle | Rect`) are unchanged and remain standalone types. Discriminant variants (`| ReadMin(10)`) and tags are unchanged.
|
|
18
|
+
|
|
19
|
+
```mermaid
|
|
20
|
+
flowchart LR
|
|
21
|
+
SRC["enum Link = Nil or Cons(head, tail); fun f = match x"] --> G["T1 grammar + parser + format.scm"]
|
|
22
|
+
G --> H["T2 hoistInlineVariants + type-position errors (plum-core loader)"]
|
|
23
|
+
H --> RM[resolveMembers] --> C["T3 checker: construction typed as enum; no .field"]
|
|
24
|
+
C --> CG[codegen unchanged]
|
|
25
|
+
C --> X["T4 functional.plum, README"]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## File structure
|
|
29
|
+
- plum-tooling/tree-sitter-plum/grammar.js, src/, test/corpus/function.txt: `= match` body.
|
|
30
|
+
- plum-tooling/tree-sitter-plum/queries/plum/format.scm: `= match` layout, `when` spacing, inline variant fields.
|
|
31
|
+
- plum-core/src/parser.rs: fn body from a `match` node; `Enum.inlineOf` in literals.
|
|
32
|
+
- plum-core/src/ast.rs: `Enum.inlineOf: Option<String>`.
|
|
33
|
+
- plum-core/src/unions.rs: `hoistInlineVariants`, `inlineVariantTypeErrors`; `legacyErrors` keeps only the self-named-variant error.
|
|
34
|
+
- plum-core/src/loader.rs: run hoisting per file in `mergeItems` (before the merge), type errors after the merge.
|
|
35
|
+
- plum-checker/src/lib.rs, plum-checker/src/unions.rs: construction typing, field-access error.
|
|
36
|
+
- plum-examples/functional.plum, README.md: example and docs.
|
|
37
|
+
|
|
38
|
+
## Task graph
|
|
39
|
+
```mermaid
|
|
40
|
+
flowchart LR
|
|
41
|
+
T1[match body grammar and formatter]
|
|
42
|
+
T2[inline variant hoisting]
|
|
43
|
+
T3[variant-only checker typing]
|
|
44
|
+
T4[example and docs]
|
|
45
|
+
%% taskgraph
|
|
46
|
+
T1 --> T2
|
|
47
|
+
T2 --> T3
|
|
48
|
+
T3 --> T4
|
|
49
|
+
T1 --> T4
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Package A: compiler
|
|
53
|
+
|
|
54
|
+
**Probe entry:** call=plum_core::unions::hoistInlineVariants
|
|
55
|
+
|
|
56
|
+
### T1 `= match` body: grammar, parser, formatter · Complexity 4 · Est. context 40k
|
|
57
|
+
**Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/, plum-tooling/tree-sitter-plum/test/corpus/function.txt, plum-tooling/tree-sitter-plum/queries/plum/format.scm, plum-core/src/parser.rs, plum-core/tests/match_body_parse_test.rs, plum-core/tests/formatter_test.rs
|
|
58
|
+
**Components:** 2
|
|
59
|
+
**LOC owned:** 120
|
|
60
|
+
**Tests:** fast
|
|
61
|
+
**Kind:** design
|
|
62
|
+
**Model:** sonnet
|
|
63
|
+
**Est. time:** 5 min
|
|
64
|
+
**Consumes:** none
|
|
65
|
+
**Produces:** parser: `fun name(..) -> R = match <subjects>` + indented cases parses to `FnBody::Block(Block { stmts: [Stmt::Match(..)] })`, identical to the indented form
|
|
66
|
+
e.g. parse("fun ln(xs: Int) -> Int = match xs\n 0 => 1\n _ => 2\n") -> Fn ln, body Block with 1 stmt, Stmt::Match with 2 cases (equal to parsing the `=\n match xs\n ...` form)
|
|
67
|
+
e.g. parse("enum Shp = Ci | Re\n fun area(self) -> Int = match self\n Ci => 1\n Re => 2\n") -> method area with a Match body
|
|
68
|
+
e.g. parse("fun pair(a: Bool, b: Bool) -> Int = match a, b\n true, true => 1\n _, _ => 0\n") -> Match with 2 subjects
|
|
69
|
+
**Review Focus:** grammar: `= match` must not change `= expr`/`= <indented body>` parses (whole corpus + every examples file still parse with no ERROR); `when` guards in a `= match`; `match` with no subject (guard_case form) after `=`; a `= match` method followed by another method in the same body (dedent handling); format.scm: `fun f(x: Int) -> Int = match x` stays on one line with arms indented one level, `Circle(r) when r == 0.0 => 1` keeps spaces around `when`, `| Cons(head: T, tail: Link[T])` kept as written; formatter idempotent on plum-std and plum-examples (run on copies).
|
|
70
|
+
**Test code:** match_body_parse_test.rs: `match_body_equals_indented`, `match_body_method`, `match_body_two_subjects`, `match_body_with_when_guard`, `match_body_then_next_method`; corpus case `fun = match` in function.txt; formatter_test.rs: `format_match_body_one_line`, `format_when_guard_spacing`, `format_inline_variant_fields`, each asserting exact output and idempotence.
|
|
71
|
+
**Steps:** failing tests, grammar + regenerate, parser branch in `parseFn` (`"match"` -> Block), format.scm, PASS.
|
|
72
|
+
|
|
73
|
+
### T2 inline variant hoisting in plum-core · Complexity 6 · Est. context 50k
|
|
74
|
+
**Owns:** plum-core/src/ast.rs, plum-core/src/unions.rs, plum-core/src/loader.rs, plum-core/src/traits.rs, plum-core/tests/inline_variants_test.rs, plum-core/tests/loader_test.rs, plum-core/tests/unions_test.rs
|
|
75
|
+
**Components:** 2
|
|
76
|
+
**LOC owned:** 220
|
|
77
|
+
**Tests:** fast
|
|
78
|
+
**Kind:** design
|
|
79
|
+
**Model:** sonnet
|
|
80
|
+
**Est. time:** 5.5 min
|
|
81
|
+
**Consumes:** none
|
|
82
|
+
**Produces:** plum_core::ast::Enum.inlineOf: Option<String> (Some(enum name) on a record generated from an inline variant, None everywhere else; every `Enum { .. }` literal, including parser.rs, sets it)
|
|
83
|
+
e.g. after hoisting `enum Link[T] = Nil | Cons(head: T, tail: Link[T])` the record item Cons has inlineOf Some("Link")
|
|
84
|
+
**Produces:** plum_core::unions::hoistInlineVariants(source: &mut Source) -> Vec<String>
|
|
85
|
+
e.g. "enum Link[T] = Nil | Cons(head: T, tail: Link[T])" -> [] ; items: enum Link with variants Nil (tag) and member Cons[T]; new `record Cons[T](head: T, tail: Link[T])` placed right after Link, isRecord true, inlineOf Some("Link")
|
|
86
|
+
e.g. "enum Tree[T] = Leaf(v: Int) | Node(l: Tree[T], r: Tree[T])" -> [] ; Leaf non-generic record, member `Leaf`; Node record generics [T], member `Node[T]`
|
|
87
|
+
e.g. "record Cons(a: Int)\nenum Link = Nil | Cons(head: Int)" -> ["enum Link declares Cons inline, but Cons is already declared; list it as a member instead"]
|
|
88
|
+
e.g. "enum Opt[T] = None | Some(T)" -> ["variant Some(T) needs named fields: Some(value: T)"]
|
|
89
|
+
**Produces:** plum_core::unions::inlineVariantTypeErrors(source: &Source) -> Vec<String>
|
|
90
|
+
e.g. a param `c: Cons[Int]` (Cons inline in Link) -> ["Cons is a variant of Link, not a type; use Link[...]"]; also return types, record/struct/trait field types, annotated locals, generic args (`List[Cons]`), fn-type params; construction `Cons(head: 1, tail: Nil)` and patterns are fine
|
|
91
|
+
**Review Focus:** state: idempotent (running hoist twice adds nothing); runs per file in `mergeItems` where `legacyErrors` ran (that keeps only the self-named-variant error), so a clash with a record in ANOTHER file is caught by the existing duplicate-name check; `inlineVariantTypeErrors` runs on the merged source before `resolveMembers` (find every call site of resolveMembers in loader.rs/parser.rs and keep order: hoist before it); deep: enum with both inline variants and listed members (`| Nil | Cons(..) | Int`), multi-line `|` form, enum with methods body, generic order (`enum Ab[K, V] = Ea(v: V) | Eb(k: K, v: V)` -> Ea[V], Eb[K, V]); fn-typed fields (`run: fn(T) -> T`) mention T; discriminant variants untouched; loader_test asserting the old removal error is updated to the new behaviour.
|
|
92
|
+
**Test code:** inline_variants_test.rs: one named test per e.g. above (`hoist_link`, `hoist_tree_generics`, `hoist_clash`, `hoist_positional_rejected`, `type_position_errors`, `hoist_idempotent`, `generic_order`), plus `loader_accepts_inline_variant` loading a temp file through the loader.
|
|
93
|
+
**Steps:** failing tests, ast field + literals (parser.rs/traits.rs literals are 1-line edits), hoist + type errors, loader wiring, PASS.
|
|
94
|
+
|
|
95
|
+
### T3 variant-only typing in the checker · Complexity 6 · Est. context 55k
|
|
96
|
+
**Owns:** plum-checker/src/lib.rs, plum-checker/src/unions.rs, plum-checker/tests/inline_variants_tests.rs, plum-wasm-codegen/tests/inline_variants_e2e_test.rs
|
|
97
|
+
**Components:** 1
|
|
98
|
+
**LOC owned:** 200
|
|
99
|
+
**Tests:** fast
|
|
100
|
+
**Kind:** design
|
|
101
|
+
**Model:** sonnet
|
|
102
|
+
**Est. time:** 5 min
|
|
103
|
+
**Consumes:** plum_core::unions::hoistInlineVariants(source: &mut Source) -> Vec<String> (T2) · plum_core::ast::Enum.inlineOf: Option<String> (T2)
|
|
104
|
+
**Produces:** checker: `Cons(head: 1, tail: Nil)` (Cons inline in Link[T]) infers `Link[Int]` and records a member wrap (`recordMemberWrap`), so codegen builds the hidden variant; `.head` on a value whose type is the inline record is "read Cons's fields with a pattern: Cons(...)"
|
|
105
|
+
e.g. `fun mk() -> Link[Int] = Cons(head: 1, tail: Nil)` checks; `x := Cons(head: 1, tail: Nil)` gives x: Link[Int]
|
|
106
|
+
e.g. `fun lf() -> Tree[Str] = Leaf(v: 3)` checks (T from the expected type); `x := Leaf(v: 3)` with Tree[T] generic and nothing fixing T -> error naming Tree and asking for an annotation
|
|
107
|
+
e.g. `if Cons := xs` then `xs.head` -> "read Cons's fields with a pattern: Cons(...)"
|
|
108
|
+
**Review Focus:** construction inside a generic fn (`fun cons(h: T, t: Link[T]) -> Link[T] = Cons(head: h, tail: t)`); nested construction (`Cons(head: 1, tail: Cons(head: 2, tail: Nil))`); explicit args `Cons[Int](head: 1, tail: Nil)`; construction as a call argument, return value, list element, `if`/`match` branch value; the union-lowering re-check pass (the checker runs again on the lowered program) must not trip the field-access error or double-wrap (look at how `applyPlan` output is re-checked); patterns `Cons(h, t)` and nested `Cons(a, Cons(b, _))` unchanged; messages never print the hidden record where the user wrote nothing (a mismatch reports `Link[Int]`); hand-written member records (`Circle`) keep their own type.
|
|
109
|
+
**Test code:** inline_variants_tests.rs (checker): `construct_types_as_enum`, `construct_generic_from_expected`, `construct_unpinned_generic_errors`, `field_access_rejected`, `explicit_type_args`, `member_record_unchanged`. inline_variants_e2e_test.rs (compile and run through the loader): cons list length/map/foldl with `= match` bodies returning 6 for [1,2,3] sum, tree insert/size, `Leaf(v: 3)` into Tree[Str].
|
|
110
|
+
**Steps:** failing tests, implement in inferClassCallRaw's caller / union coercion, PASS.
|
|
111
|
+
|
|
112
|
+
## Package B: example and docs
|
|
113
|
+
|
|
114
|
+
**Probe entry:** run=./target/debug/plum test plum-examples/functional.plum
|
|
115
|
+
|
|
116
|
+
### T4 functional.plum example and README · Complexity 3 · Est. context 40k
|
|
117
|
+
**Owns:** plum-examples/functional.plum, README.md
|
|
118
|
+
**Components:** 1
|
|
119
|
+
**LOC owned:** 140
|
|
120
|
+
**Tests:** fast
|
|
121
|
+
**Kind:** design
|
|
122
|
+
**Model:** sonnet
|
|
123
|
+
**Est. time:** 3.5 min
|
|
124
|
+
**Consumes:** checker typing of inline variants (T3) · `= match` bodies (T1)
|
|
125
|
+
**Produces:** plum-examples/functional.plum passing `plum test`
|
|
126
|
+
**Review Focus:** start from /private/tmp/claude-501/-Users-pyrossh-Code-plum/ea4a6dd3-c970-4882-b24c-3468d9fb6b79/scratchpad/functional.plum (11 passing tests): convert `Link`, `Tree`, `Shape`, `Node` (scene graph) and `UiKind`-style enums to inline variants where the record is only used as a variant, every match-only function to `= match`, and replace record field reads on variants with patterns; keep every test and assertion value; README: Enums section documents inline variants (variant-only, errors) and Functions section documents `= match`; README ```plum blocks must parse; `plum test plum-std plum-examples` count = before + 11 or more.
|
|
127
|
+
**Test code:** the 11 existing tests in the file plus `inline_variant_not_a_type_doc` style snippets only in README (no new Rust tests).
|
|
128
|
+
**Steps:** copy, convert, run, README, run full plum test.
|
|
129
|
+
|
|
130
|
+
## Review Focus (probe plan)
|
|
131
|
+
- Package A (library / parser / checker): deep (nested enums and generics), state (idempotent passes, re-check after lowering), encoding of names (hidden record never leaks in messages). Risks: grammar conflicts; generic inference when a variant mentions no generic; `.field` rejection hitting compiler-generated code.
|
|
132
|
+
- Package B (examples/docs): file; README snippet parsing.
|
|
133
|
+
|
|
134
|
+
## Self-review
|
|
135
|
+
Coverage: inline variants (T2, T3), not-a-type and clash/positional errors (T2), field access error (T3), `= match` (T1), formatter + `when` fix (T1), example + README (T4). The enum/union split plan was marked shelved in the review phase. Edges: T1->T2 (shared parser.rs), T2->T3 and T3->T4 run real code, T1->T4 the example uses `= match`.
|
docs/plans/2026-11-03-inline-variants-match-body.review.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Inline record variants and `= match` bodies: review plan
|
|
2
|
+
|
|
3
|
+
**Status:** written, waiting for approval (2026-11-03). Orchestrator: human.
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
Two readability changes so recursive data reads like Gleam or Haskell. First, an enum variant may declare its fields inline again: `enum Link[T] = Nil | Cons(head: T, tail: Link[T])`. As in Haskell and Gleam, `Link[T]` is the only type: `Cons` is a constructor and a pattern, never a type of its own. Under the hood the loader still generates a hidden record and lists it as a member, so code generation is unchanged; the checker only makes sure `Cons` never appears as a type and that building one gives a `Link`. Second, a function or method whose whole body is one `match` can start it right after the `=`, with the arms indented below, instead of indenting the `match` on its own line.
|
|
7
|
+
|
|
8
|
+
```plum
|
|
9
|
+
enum Link[T] =
|
|
10
|
+
| Nil
|
|
11
|
+
| Cons(head: T, tail: Link[T])
|
|
12
|
+
|
|
13
|
+
enum Tree = Leaf | Branch(left: Tree, value: Int, right: Tree)
|
|
14
|
+
|
|
15
|
+
fun length(xs: Link[T]) -> Int = match xs
|
|
16
|
+
Nil => 0
|
|
17
|
+
Cons(_, t) => 1 + length(t)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## What you will get
|
|
21
|
+
- Inline record variants with named fields (`| Cons(head: T, tail: Link[T])`), on one line or in the `|` list form, in generic and non-generic enums, alongside tags (`Nil`) and listed members (`Int`, `Circle`, `List[Json]`).
|
|
22
|
+
- `Cons` is a constructor and a pattern only:
|
|
23
|
+
- `Cons(head: 1, tail: Nil)` has type `Link[Int]` (the enum's generics are inferred from the fields, or from the expected type: `xs: Link[Int] := Nil`).
|
|
24
|
+
- Its fields are read by matching: `match xs` with `Cons(h, t) =>`, or `if Cons(h, t) := xs`. There is no `xs.head` on a `Link` (a `Nil` has no head).
|
|
25
|
+
- `Cons` has no methods of its own; methods go on the enum (`enum Link[T] = ...` with a `fun` body using `match self`).
|
|
26
|
+
- The old "inline variant payloads are removed" error is gone. New clear errors:
|
|
27
|
+
- `Cons` used as a type (`c: Cons[Int]`, `-> Cons`, `List[Cons]`): "Cons is a variant of Link, not a type; use Link[...]"
|
|
28
|
+
- a variant whose name is already a type: "enum Link declares Cons inline, but Cons is already declared; list it as a member instead"
|
|
29
|
+
- positional fields: "variant Some(T) needs named fields: Some(value: T)"
|
|
30
|
+
- Hand-written records listed as members (`record Circle(r: Float)` in `enum Shape = Circle | Rect`) keep working as today, including as standalone types.
|
|
31
|
+
- `fun name(...) -> R = match subject` followed by indented arms, for free functions, methods (enum, record, struct, trait bodies) and `match a, b`. `when` guards work as in any `match`. The indented form stays valid.
|
|
32
|
+
- The formatter keeps both new forms as written (one-line `= match`, inline variant fields), and its `when` bug (`Circle(r) when r == 0.0` printed as `Circle(r)whenr == 0.0`) is fixed, since these examples use guards.
|
|
33
|
+
- `plum-examples/functional.plum`: the cons list, persistent tree, pattern matching and pure scene graph example from today, rewritten with both forms and run by `plum test`.
|
|
34
|
+
- README: Enums and Functions sections updated; tree-sitter highlighting unchanged (no new keywords).
|
|
35
|
+
- The parked enum/union split plan is marked shelved (done with this plan, per your answer).
|
|
36
|
+
|
|
37
|
+
## What will be built
|
|
38
|
+
| Part | What it does | Size | Built by |
|
|
39
|
+
|---|---|---|---|
|
|
40
|
+
| Inline variant lowering | In the loader, turns each inline variant into a hidden generated record plus a member (marked as inline); replaces the removal error with the clash and positional-field errors | M | a careful helper |
|
|
41
|
+
| Variant-only typing | In the checker, a construction of an inline variant is typed as its enum (generics inferred from fields or the expected type), and an inline variant name in any type position is the "not a type" error | M | a careful helper |
|
|
42
|
+
| `= match` grammar | Lets a function body be a `match` right after `=`; regenerates the parser (`--disable-optimizations`) and maps it to the same body the indented form produces | S | a careful helper |
|
|
43
|
+
| Formatter | Keeps `= match` on one line and inline variant fields; fixes the `when` spacing bug; idempotent over std and examples | S | a careful helper |
|
|
44
|
+
| Example, docs, tests | `functional.plum` in examples, README sections, checker and parser tests for both forms and both errors | S | a fast helper |
|
|
45
|
+
|
|
46
|
+
## How it fits together
|
|
47
|
+
```mermaid
|
|
48
|
+
flowchart LR
|
|
49
|
+
SRC["source: enum with Cons(head, tail) / fun f = match x"] --> TS["tree-sitter grammar"]
|
|
50
|
+
TS --> P["parser: EnumVariant with named fields; fn body = match block"]
|
|
51
|
+
P --> H["loader: inline variant lowering (new)"]
|
|
52
|
+
H --> U["unions: resolveMembers (unchanged)"]
|
|
53
|
+
U --> C["checker: inline variant builds type as its enum; never a type (new)"]
|
|
54
|
+
C --> G["codegen (unchanged)"]
|
|
55
|
+
TS --> F["formatter (format.scm)"]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## How it gets built
|
|
59
|
+
```mermaid
|
|
60
|
+
flowchart TB
|
|
61
|
+
subgraph W1["Wave 1, in parallel"]
|
|
62
|
+
A["Inline variant lowering"]
|
|
63
|
+
B["= match grammar"]
|
|
64
|
+
end
|
|
65
|
+
A --> V["Variant-only typing"]
|
|
66
|
+
V --> D["Example, docs, tests"]
|
|
67
|
+
B --> D
|
|
68
|
+
B --> F["Formatter"]
|
|
69
|
+
F --> D
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Decisions you can change
|
|
73
|
+
- Variant fields after narrowing: read only through patterns (default) | also `xs.head` inside `if Cons := xs` / a `Cons =>` arm.
|
|
74
|
+
- Hidden record kind: `record` (default) | `struct` when every field is a plain value type.
|
|
75
|
+
- Positional fields (`Some(T)`): rejected with a hint (default, as you chose) | allowed with generated names `v0`, `v1`.
|
|
76
|
+
- `=` followed directly by a block form: only `match` (default) | also `if` / `for` blocks.
|
|
77
|
+
- Existing examples that declare `record X` and list `X` separately: left as they are (default; both forms stay valid) | rewritten to the inline form.
|
|
78
|
+
|
|
79
|
+
## Risks
|
|
80
|
+
- Name clashes: a generated record named like another type or a variant in another enum. Caught by the clash error above; the merged-name table in the loader already rejects duplicates across files.
|
|
81
|
+
- Grammar conflicts: `= match` next to `= expression` could shift other parses. The parser is regenerated with `--disable-optimizations` and the full corpus plus every example must still parse unchanged.
|
|
82
|
+
- Generic inference: `Cons(head: 1, tail: Nil)` must come out as `Link[Int]` with no type args, and a variant that mentions no generic (`Nil`-like, or `Leaf(v: Int)` in `Tree[T]`) must take `T` from the expected type. Covered by named tests; where nothing fixes `T`, the error asks for an annotation.
|
|
83
|
+
- Error messages from deeper passes might still say the hidden record's name; the checker maps it back to `Link[...]` in messages, with a test.
|
|
84
|
+
- Discriminant variants (`| ReadMin(10)`) and value enums must not be mistaken for inline records: they are told apart by values vs named fields, as the parser already does.
|
|
85
|
+
|
|
86
|
+
## Out of scope
|
|
87
|
+
- Positional variant fields, and the record constructor taking positional arguments.
|
|
88
|
+
- Tail calls (`return_call`), a std `Link[T]`/`Tree[T]` library, functions returning functions (`-> fn(A) -> B`), inline `if` mixing a value and `null`, and the `isEmpty(Nil)` codegen gap found today (each noted for a later plan).
|
|
89
|
+
- Exhaustiveness changes, and the scene graph engine itself.
|
|
90
|
+
|
|
91
|
+
## How we will know it works
|
|
92
|
+
- `enum Link[T] = Nil | Cons(head: T, tail: Link[T])` with `length`/`map`/`foldl` written as `= match` passes `plum test`.
|
|
93
|
+
- `c := Cons(head: 1, tail: Nil)` has type `Link[Int]`; `if Cons(h, _) := c` gives `h == 1`.
|
|
94
|
+
- `c: Cons[Int] := ...` gives "Cons is a variant of Link, not a type; use Link[...]".
|
|
95
|
+
- `| Some(T)` and an inline `Cons` next to an existing `record Cons` give the two error messages above.
|
|
96
|
+
- `plum test plum-std plum-examples` passes with at least today's count plus the new tests; `cargo test` workspace green.
|
|
97
|
+
- `plum format` on a copy of `functional.plum` changes nothing (idempotent) and prints `when` guards correctly.
|
|
98
|
+
|
|
99
|
+
## Rough time and cost
|
|
100
|
+
Rough, until the machine plan is linted: 5 tasks, about 20-30 minutes of build time, around $30-50.
|