plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
4079c0e
— Peter 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.
|