plum

#treesitter#compiler#wasm

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

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


4079c0ePeter John 2026-07-24T10:27:49+05:30
docs: add design spec for bracket generics syntax migration
docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md ADDED
@@ -0,0 +1,187 @@
1
+ # Bracket Generics Syntax — Design Spec
2
+
3
+ ## Goal
4
+
5
+ Migrate Plum's generic-type syntax from parenthesized, lowercase-letter declarations
6
+ (`type Node(a) = ...`, `a`/`b`/`c`/`d` only) to bracketed, uppercase-letter declarations
7
+ (`type Node[T] = ...`, any single uppercase letter), and apply the same bracket
8
+ convention everywhere a generic type parameter appears — declarations, field types,
9
+ return types, and enum variant payloads. Parens are reserved for value-level argument
10
+ lists (function calls, class/enum constructors) and trait "implements" lists.
11
+
12
+ This is a pure syntax migration: it does not change generics semantics, monomorphization
13
+ behavior, or add new type-system features. `libs/std/list.plum` already has one
14
+ class (`Node`) partially migrated (`type Node[T] =`, uncommitted) which is what
15
+ prompted this spec — the rest of the language needs to catch up to (and formalize)
16
+ that shape.
17
+
18
+ ## Current state (pre-migration)
19
+
20
+ - **Declaration syntax**: `type Foo(a) =`, `type Foo(Trait)(a: Trait) =`,
21
+ `trait Foo(a: Bound) =` — parens, implements-list before generics-list.
22
+ - **Generic parameter names**: exactly one of the 4 hardcoded lowercase letters
23
+ `a`, `b`, `c`, `d` (`tooling/tree-sitter-plum/grammar.js`'s `generic` rule is
24
+ `choice($.a, $.b, $.c, $.d)`, literal tokens). No 5th letter is possible today.
25
+ - **Usage sites**: `type` (field types, generic-arg lists like `Option[Node]` /
26
+ `Option(a)`) already accepts *both* `[...]` and `(...)` — this is the one place
27
+ ahead of the rest of the grammar. `return_type` does NOT reuse this rule; it has
28
+ its own paren-only `generics` field, so `-> Option[Node]` doesn't currently parse
29
+ as a return type, only `-> Option(a)` does (existing asymmetry, fixed by this
30
+ migration — see below).
31
+ - **Enum variant payloads**: `| Some(a)`, `| Ok(a)` — parens, sharing surface syntax
32
+ with a value-level constructor call.
33
+ - **Method receiver annotation** (`get<List>(self, ...)`) is a separate, unrelated
34
+ mechanism (`fn_type: "<" type_identifier ">"`) — it names which class/enum a method
35
+ dispatches on, never introduces or binds a type parameter, and is untouched by
36
+ this migration.
37
+ - **`plum-checker/src/monomorphize.rs`**'s `is_generic_param_name` — the single
38
+ centralized check used everywhere a `Fn`'s or `Enum`'s generic parameters are
39
+ *inferred* (`Class`/`Trait` instead read an explicit `generics` list off the AST)
40
+ — currently defines "generic parameter name" as "exactly one ASCII lowercase
41
+ letter."
42
+
43
+ ## New syntax
44
+
45
+ ### Lexical rule
46
+
47
+ - `generic` becomes a single uppercase ASCII letter: `/[A-Z]/`. Any letter A-Z is a
48
+ valid generic name now (no more 4-letter cap).
49
+ - `type_identifier` becomes `/[A-Z][a-zA-Z0-9]+/` — **2 or more characters**. This
50
+ is the key disambiguating change: today's `/[A-Z][a-zA-Z0-9]*/` (0-or-more) also
51
+ matches a single letter, which would collide with the new uppercase `generic`
52
+ token. Requiring 2+ characters means concrete type names (`List`, `Option`, `Node`,
53
+ ...) and generic parameter names (`T`, `U`, `K`, `V`, ...) are lexically disjoint
54
+ by construction — no grammar conflict, no context-sensitive lookahead needed.
55
+ - **Trade-off (confirmed with user):** single-letter type names (`T`, `A`, `X`, ...)
56
+ become permanently illegal as concrete type names. Acceptable since every real
57
+ type in this codebase is a multi-letter word.
58
+
59
+ ### Declarations
60
+
61
+ ```
62
+ type Foo[T] =
63
+ value: T
64
+
65
+ type List[T: Stringable](Stringable) =
66
+ head: Option[Node]
67
+ ...
68
+
69
+ trait Comparable[T: Ord] =
70
+ compareTo(other: T) -> Int
71
+ ```
72
+
73
+ - `generics` rule: `"[" commaSep1(generic_type) "]"` (was `"(" ... ")"`).
74
+ - Field order swaps: **generics-with-bounds come first, implements-list comes
75
+ second** — `type List[T: Stringable](Stringable) =`, not the old
76
+ implements-then-generics order. The parser's "implements = leading
77
+ `type_identifier`s before the first field" derivation needs updating for the new
78
+ field order.
79
+ - `generic_type` (the bound syntax, `T: Bound`) is unchanged structurally — only
80
+ the enclosing bracket and the letter case change.
81
+
82
+ ### Usage sites (field types, return types, generic-arg lists)
83
+
84
+ ```
85
+ type Node[T] =
86
+ value: T
87
+ prev: Option[Node]
88
+ next: Option[Node]
89
+
90
+ get<List>(self, i: Int) -> Option[T] =
91
+ ...
92
+ ```
93
+
94
+ - `type`'s existing dual bracket/paren acceptance collapses to bracket-only.
95
+ - `return_type` stops being its own paren-only rule with its own
96
+ `Vec<GenericParam>` AST shape (`ast.rs`'s `ReturnType.generics`) and instead
97
+ reuses `$.type` directly, matching `Type.generics: Vec<Type>`. This fixes the
98
+ existing `Type` vs `ReturnType` asymmetry as a side effect of the migration
99
+ rather than carrying it forward.
100
+
101
+ ### Enum variant payloads
102
+
103
+ ```
104
+ enum Option[T] =
105
+ | Some[T]
106
+ | None
107
+ ```
108
+
109
+ - `enum_field`'s payload list moves from `"(" commaSep1(choice(type_identifier,
110
+ generic)) ")"` to the bracketed form, for full consistency with every other
111
+ generic-type appearance in the language.
112
+
113
+ ## What does NOT change
114
+
115
+ - Method receiver annotation `get<List>(self, ...)` — angle brackets, orthogonal
116
+ mechanism, untouched.
117
+ - Value-level constructor/call parens (`Ok(5)`, `List(head: None, ...)`,
118
+ `add(1, 2, 3)`) — parens stay parens; this migration only touches type-level
119
+ generic syntax.
120
+ - Trait "implements" lists (`(Stringable)` in `type List[T: Stringable](Stringable)
121
+ =`, `type Str(Comparable, Stringable, ...) =`) — stay parenthesized; they're a
122
+ list of trait names being implemented, not a generic-parameter declaration.
123
+ - Generics semantics, inference, monomorphization behavior, bounds checking — all
124
+ unchanged. This is syntax only.
125
+
126
+ ## Implementation impact by layer
127
+
128
+ - **`tooling/tree-sitter-plum/grammar.js`**: `generic`, `generics`, `type_identifier`,
129
+ `class`, `trait`, `return_type`, `enum_field` rules change per above. Regenerate
130
+ the parser. Update corpus tests: `test/corpus/type.txt`, `trait.txt`, `enum.txt`,
131
+ `function.txt`.
132
+ - **`plum-core/src/ast.rs`**: `ReturnType` drops its separate `generics: Vec<GenericParam>`
133
+ field/shape, reuses `Type`'s representation (`Vec<Type>`) instead.
134
+ - **`plum-core/src/parser.rs`**: `parse_generics_field` and `parse_enum_variant`
135
+ currently match node `kind()` against the literal set `"a"|"b"|"c"|"d"`; since
136
+ `generic` becomes one regex-based token, this collapses to a single node-kind
137
+ check. `parse_class`'s implements-list derivation (currently "leading
138
+ `type_identifier`s before the first field") needs updating for the new
139
+ generics-then-implements field order. `parse_return_type` is simplified to just
140
+ call the same logic as `parse_type`.
141
+ - **`plum-checker/src/monomorphize.rs`**: `is_generic_param_name` flips from
142
+ "single ASCII lowercase letter" to "single ASCII uppercase letter." This is the
143
+ only definition site (confirmed, nothing else re-derives the convention), so this
144
+ is a one-line-condition change plus updating its doc comment.
145
+ - **`plum-wasm-codegen`**: no source changes — codegen only ever sees fully
146
+ monomorphized (generic-free) AST. Only test fixtures change.
147
+ - **Stdlib** (`libs/std/`): `list.plum` (finish what `Node[T]` started — `List`,
148
+ every method signature), `map.plum` (`Pair[K, V]`, `Map[K, V]`, method
149
+ signatures), `option.plum` (`Some[T]`), `result.plum` (`Ok[T]`, `Err[E]`).
150
+ Per-slot letter choices favor readability over always defaulting to `T`
151
+ (`K`/`V` for maps, `T`/`U` for a two-param list/function context, `E` for error
152
+ types) where a clearer letter fits.
153
+ - **Examples** (`examples/`): `types.plum` (`Box(a)` → `Box[T]`, `Comparable(a: Ord)`
154
+ → `Comparable[T: Ord]`).
155
+ - **Tests**: `plum-checker/tests/checker_tests.rs`, `plum-checker/tests/monomorphize_tests.rs`,
156
+ `plum-wasm-codegen/tests/codegen_tests.rs` — embedded `.plum` source strings
157
+ updated to new syntax.
158
+ - **Design docs**: `docs/superpowers/specs/2026-07-20-generics-monomorphization-design.md`
159
+ and `2026-07-20-generic-enum-multi-instantiation-design.md` explicitly document
160
+ the *old* syntax as canonical (e.g. "a, b, c, d — the grammar's only legal
161
+ generic-parameter spelling"). These are historical records of already-shipped
162
+ work and are **not** rewritten by this migration — readers should treat them as
163
+ describing pre-migration syntax.
164
+
165
+ ## Known conflict: `2026-07-24-list-methods.md` plan
166
+
167
+ The untracked implementation plan `docs/superpowers/plans/2026-07-24-list-methods.md`
168
+ (and its spec) is written entirely in the old syntax (`type Node(a) =`) and has not
169
+ been executed yet. It also predates `list.plum`'s current on-disk state (which
170
+ already has `Node[T]` on line 6), so it's already stale independent of this
171
+ migration. This plan should be treated as **blocked** — it needs rewriting against
172
+ both the new bracket syntax and the current `list.plum` contents before anyone
173
+ executes it. This migration does not rewrite that plan; that's a separate,
174
+ follow-up piece of work.
175
+
176
+ ## Testing strategy
177
+
178
+ - Update tree-sitter corpus tests first (grammar-level), verify `tree-sitter test`
179
+ passes against the new syntax.
180
+ - Update `plum-core` parser tests if any cover generics shape (survey found none
181
+ currently do — `parser_test.rs`/`formatter_test.rs` have zero matches — so this
182
+ migration is a good opportunity to add minimal coverage of the new bracket shape).
183
+ - Update `plum-checker`'s `checker_tests.rs` and `monomorphize_tests.rs` fixtures.
184
+ - Update `plum-wasm-codegen`'s `codegen_tests.rs` fixtures.
185
+ - Update stdlib and examples, confirm `cargo test --workspace` passes throughout.
186
+ - No behavior changes are expected — every currently-passing test should still pass
187
+ with only its embedded source syntax rewritten, and results (assertions) unchanged.