plum
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. |