plum

#treesitter#compiler#wasm

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

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


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