plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
tooling/tree-sitter-plum/test/corpus/.recall/history.md
# Recall History — corpus
## Session afc99e8b — 2026-07-24 10:40
**You:** Can we update the Generic syntax to this one?
# A node stores the data in a list and contains pointers to the previous and next sibling nodes
type Node[T] =
value: T
prev: Option[Node]
next: Option[Node]
- `Skill: superpowers:brainstorming`
**You:** Base directory for this skill: /Users/pyrossh/.claude/plugins/cache/superpowers-dev/superpowers/6.1.1/skills/brainstorming
# Brainstorming Ideas Into Designs
Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
<HARD-GATE>
Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
</HARD-GATE>
## Anti-Pattern: "This Is Too Simple To Need A Design"
Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
## Checklist
You MUST create a task for each of these items and complete them in order:
1. **Explore project context** — check files, docs, recent commits
2. **Offer the visual companion just-in-time** — NOT upfront. The first time a question would genuinely be clearer shown than described, offer it then (its own message); on approval its browser tab opens for you. If no visual question ever arises, never offer it. See the Visual Companion section below.
3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
4. **Propose 2-3 approaches** — with trade-offs and your recommendation
5. **Present design** — in sections scaled to their complexity, get user approval after each section
6. **Write design doc** — save to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md` and commit
7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below)
8. **User reviews written spec** — ask user to review the spec file before proceeding
9. **Transition to implementation** — invoke writing-plans skill to create implementation plan
## Process Flow
```dot
digraph brainstorming {
"Explore project context" [shape=box];
"Ask clarifying questions" [shape=box];
"Propose 2-3 approaches" [shape=box];
"Present design sections" [shape=box];
"User approves design?" [shape=diamond];
"Write design doc" [shape=box];
"Spec self-review\n(fix inline)" [shape=box];
"User reviews spec?" [shape=diamond];
"Invoke writing-plans skill" [shape=doublecircle];
"Explore project context" -> "Ask clarifying questions";
"Ask clarifying questions" -> "Propose 2-3 approaches";
"Propose 2-3 approaches" -> "Present design sections";
"Present design sections" -> "User approves design?";
"User approves design?" -> "Present design sections" [label="no, revise"];
"User approves design?" -> "Write design doc" [label="yes"];
"Write design doc" -> "Spec self-review\n(fix inline)";
"Spec self-review\n(fix inline)" -> "User reviews spec?";
"User reviews spec?" -> "Write design doc" [label="changes requested"];
"User reviews spec?" -> "Invoke writing-plans skill" [label="approved"];
}
```
**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
## The Process
**Understanding the idea:**
- Check out the current project state first (files, docs, recent commits)
- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
- For appropriately-scoped projects, ask questions one at a time to refine the idea
- Prefer multiple choice questions when possible, but open-ended is fine too
- Only one question per message - if a topic needs more exploration, break it into multiple questions
- Focus on understanding: purpose, constraints, success criteria
**Exploring approaches:**
- Propose 2-3 different approaches with trade-offs
- Present options conversationally with your recommendation and reasoning
- Lead with your recommended option and explain why
**Presenting the design:**
- Once you believe you understand what you're building, present the design
- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
- Ask after each section whether it looks right so far
- Cover: architecture, components, data flow, error handling, testing
- Be ready to go back and clarify if something doesn't make sense
**Design for isolation and clarity:**
- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
**Working in existing codebases:**
- Explore the current structure before proposing changes. Follow existing patterns.
- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
## After the Design
**Documentation:**
- Write the validated design (spec) to `docs/superpowers/specs/YYYY-MM-DD-<topic>-design.md`
- (User preferences for spec location override this default)
- Use elements-of-style:writing-clearly-and-concisely skill if available
- Commit the design document to git
**Spec Self-Review:**
After writing the spec document, look at it with fresh eyes:
1. **Placeholder scan:** Any "TBD", "TODO", incomplete sections, or vague requirements? Fix them.
2. **Internal consistency:** Do any sections contradict each other? Does the architecture match the feature descriptions?
3. **Scope check:** Is this focused enough for a single implementation plan, or does it need decomposition?
4. **Ambiguity check:** Could any requirement be interpreted two different ways? If so, pick one and make it explicit.
Fix any issues inline. No need to re-review — just fix and move on.
**User Review Gate:**
After the spec review loop passes, ask the user to review the written spec before proceeding:
> "Spec written and committed to `<path>`. Please review it and let me know if you want to make any changes before we start writing out the implementation plan."
Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves.
**Implementation:**
- Invoke the writing-plans skill to create a detailed implementation plan
- Do NOT invoke any other skill. writing-plans is the next step.
## Key Principles
- **One question at a time** - Don't overwhelm with multiple questions
- **Multiple choice preferred** - Easier to answer than open-ended when possible
- **YAGNI ruthlessly** - Remove unnecessary features from all designs
- **Explore alternatives** - Always propose 2-3 approaches before settling
- **Incremental validation** - Present design, get approval before moving on
- **Be flexible** - Go back and clarify when something doesn't make sense
## Visual Companion
A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
**Offering the companion (just-in-time):** Do NOT offer it upfront. Wait until a question would genuinely be clearer shown than told — a real mockup / layout / diagram question, not merely a UI *topic*. The first time that happens, offer it then, as its own message:
> "This next part might be easier if I show you — I can put together mockups, diagrams, and comparisons in a browser tab as we go. It's still new and can be token-intensive. Want me to? I'll open it for you."
**This offer MUST be its own message.** Only the offer — no clarifying question, summary, or other content. Wait for the user's response. If they accept, start the server with `--open` so their browser opens to the first screen automatically. If they decline, continue text-only and don't offer again unless they raise it.
**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
If they agree to the companion, read the detailed guide before proceeding:
`skills/brainstorming/visual-companion.md`
- `Bash: grep -n "Node\[" /Users/pyrossh/Code/plum/libs/std/*.plum /Users/pyrossh/Code/plum/examples/*.plum 2>/dev/null | head -30`
- `Bash: grep -rn "type.*\[T\]\|Option\[" /Users/pyrossh/Code/plum/plum-core/src/*.rs /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/grammar.js 2>/dev/null | head -40`
- `Read: /Users/pyrossh/Code/plum/libs/std/list.plum`
- `Bash: grep -n "generic\|type_param\|\\[T\\]\|angle" /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/grammar.js | head -40`
- `Bash: echo "=== grammar.js generics-related section ===" && sed -n '50,170p' tooling/tree-sitter-plum/grammar.js`
- `Bash: git diff libs/std/list.plum examples/methods.plum | head -100`
- `Read: /Users/pyrossh/Code/plum/docs/superpowers/plans/2026-07-24-list-methods.md`
**Claude:** I want to scope this properly since it touches the grammar, parser, checker, codegen, and stdlib — not just a stdlib file. Quick question first:
- `AskUserQuestion`
- `AskUserQuestion`
**Claude:** The user wants to describe a different form for the trait-bounded case. What should it look like?
**You:** type List[T: Stringable](Stringable) is the format
- `AskUserQuestion`
- `Agent: Survey generic syntax usage across plum codebase`
**Claude:** I've kicked off a research agent to survey how generics syntax is used across the grammar, parser, checker, codegen, and stdlib/examples. I'll report back once it's done, then we can nail down the full design.
**You:** <task-notification>
<task-id>a91ff831a50382494</task-id>
<tool-use-id>toolu_011XrLpAW4pMK314FtB8sGSi</tool-use-id>
<output-file>/private/tmp/claude-501/-Users-pyrossh-Code-plum/afc99e8b-d8ff-47b5-bbcd-86e87353f6e2/tasks/a91ff831a50382494.output</output-file>
<status>completed</status>
<summary>Agent "Survey generic syntax usage across plum codebase" finished</summary>
<note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note>
<result># Inventory Report: Generic-Type Syntax Migration `(a)` → `[T]`
## 1. tree-sitter grammar (`tooling/tree-sitter-plum/grammar.js`)
Declaration-site generics (paren-only today):
- `generics: seq("(", commaSep1($.generic_type), ")")` (line 72) — used by `class` (98), `trait` (110), and `return_type` (142, reusing the same paren `generics` rule).
- `generic_type: seq($.generic, optional(seq(":", sep1($.type_identifier, "+"))))` (73-74) — the bound.
- `generic: choice($.a, $.b, $.c, $.d)` (529) with literal tokens `a`/`b`/`c`/`d` (530-533) — **hardcoded to exactly 4 single lowercase letters**; no `e`, `f`, etc. This is the single biggest grammar constraint to lift.
- `class` (93-103): `"type" type_identifier ("(" type_identifier,* ")")?[implements] generics?[paren]"=" ...` — implements-list and generics-list are both parenthesized today; disambiguated only by the parser reading `implements` as leading `type_identifier`s before any `field`, and `generics` as the trailing `(generic_type,*)`.
- `trait` (106-115): `"trait" type_identifier generics? "=" ...` — same paren `generics`.
- `enum_field` (154-159): variant field types are `choice($.type_identifier, $.generic)` inside `(...)` — concrete types and single-letter generics share the same paren list; no separate generics-declaration concept for enums (generic-ness is inferred structurally in the checker, see §3).
Usage-site generics (**already dual bracket/paren** — the one place ahead of the rest):
- `type` rule (75-90) already accepts `choice(seq("[", commaSep1($.type), "]"), seq("(", commaSep1($.type), ")"))` for a type's generic arguments — i.e. `Option[Node]` and `Option(a)` both parse today, producing the same `type` node shape either way.
- `return_type` (141-142) is NOT this rule — it points at the paren-only `generics` rule, so `-> Option[Node]`-shaped return types (bracket usage) are **not currently parseable as return types**, only `-> Option(a)` is. This is an existing gap/inconsistency to note.
`fn`'s angle-bracket receiver syntax is a **separate, orthogonal mechanism** — not generics at all:
- `fn_type: seq("<", commaSep1($.type_identifier), ">")` (177), used as `field("type", optional(alias($.fn_type, $.type)))` in `fn` (165). This is purely a **method-receiver annotation** (`get<List>(self, ...)`, `toStr<Cat>()`), naming which class/enum the method is dispatched on — it names a concrete/generic type by its bare name, never introduces or binds a type parameter itself, and syntactically occupies a completely different slot (`fn_identifier <...>` vs. `type_identifier (...)`/`[...]`). No interaction/overlap with the class/trait `generics` rule other than that a receiver name (`List`) may itself refer to a generic class declared with `generics`.
Corpus files with generic examples: `tooling/tree-sitter-plum/test/corpus/type.txt` (field `age: b`, `Cat(Stringable)`), `trait.txt` (no generics currently, plain traits only), `enum.txt` (`enum.txt:36-50`, `| Some(a)`), `function.txt` (`function.txt:72-96` `add(param: a, param2: List(b)) -> List(b)`; `function.txt:123-137` `remove<List>(self, v: a)`; `function.txt:357-374` `each(cb: fn(a) -> b) -> Bool`). All of these bake in lowercase-letter/paren generics and will need updated expected trees.
## 2. plum-core (`ast.rs`, `parser.rs`)
AST (`plum-core/src/ast.rs`):
- `GenericParam { name: String, bounds: Vec<String> }` (38-41) — generic name is a plain `String`, no letter/case constraint encoded structurally; used by `Class.generics` (33), `Trait.generics` (52), and oddly by `ReturnType.generics: Vec<GenericParam>` (118) even though grammatically a return type's generics come from the same paren-usage `generics` rule as declarations, not from a `type`'s bracket/paren generics (`Type.generics: Vec<Type>`, line 112) — this is an existing asymmetry between `Type` (usage, `Vec<Type>`) and `ReturnType` (also usage, but oddly `Vec<GenericParam>`).
- `EnumVariant.fields: Vec<String>` (70-73) — variant field types stored as bare name strings only, no `Type`/generic structure; the checker infers generic-ness later by string-testing each name (see §3).
- `Fn.type_param: Option<String>` (77-85) — the `<List>`/`<Cat>` receiver annotation, confirmed separate from `generics`.
Parser (`plum-core/src/parser.rs`):
- `parse_type` (289-301): handles usage-site generics uniformly — it just filters named children by `kind() == "type"`, so it is **already agnostic to `[` vs `(`** (both grammar branches produce `type` nodes). No change needed here for usage-site brackets vs parens.
- `parse_generics_field` (99-128): **the parser's own hardcoded logic**, distinct from `parse_type`. It walks the `generics` node's flattened children and matches `child.kind()` against the literal strings `"a" | "b" | "c" | "d"` (line 116) to detect a generic-letter node, attaching any following `type_identifier` as a bound. This is grammar-shape-coupled twice: once via the grammar's `a`/`b`/`c`/`d` node kinds, and again via this exact match arm. Used by `parse_class` (88), `parse_trait` (143), `parse_return_type` (307).
- `parse_enum_variant` (171-183): matches `n.kind()` against `"type_identifier" | "a" | "b" | "c" | "d"` (179) to decide which variant-field children are "generic" vs concrete — same hardcoded set.
- `parse_class` (73-97) derives `implements` by taking leading `type_identifier` children before the first `field` (81-86) — this convention (implements-list is whatever comes before fields, since generics are consumed separately by `parse_generics_field`) will need re-deriving once implements moves to `(Trait)` and generics to `[T: Trait]` — order/field-boundaries in the grammar output will change.
## 3. plum-checker (`types.rs`, `monomorphize.rs`, `lib.rs`)
- `types.rs`: no generic-parameter-name assumptions; `PlumType::TVar(String)` is a plain string type variable (used for inference, not tied to source syntax). `InferState::fresh_var` (64-68) generates internal names like `a0`, `a1`, ... (unrelated to source syntax, just an internal fresh-name convention — not something the migration touches, though the shared prefix `a` is worth noting to avoid confusion in code review).
- `monomorphize.rs` — **this is where the naming convention is load-bearing, not just cosmetic**:
- `is_generic_param_name` (9-15): `true` iff the name is exactly one ASCII **lowercase** letter. This is the sole test used everywhere to decide "is this type name a generic parameter" for `Fn` and `Enum` (which carry no explicit `generics` list in the AST, unlike `Class`/`Trait`).
- `fn_generic_params` (25-45) and `enum_generic_params` (49-59) both call `is_generic_param_name` to *infer* a function's/enum's generic parameters implicitly, by scanning param/return/variant-field type names for single lowercase letters.
- `class_generic_params` (18-20) instead reads `Class.generics` explicitly (populated by the parser's `parse_generics_field`), so a class's generic names aren't string-sniffed — but everywhere else (`Fn`, `Enum`) the *entire* generic-detection mechanism depends on the lowercase-single-letter convention holding.
- Extensive downstream logic (specialization, mangling, bare-generic-reference detection: `fns_bare_generic`, `enums_generic_by_variant`, `resolve_bare_generic_fn_instantiation`, etc., throughout lines 60-882) all consumes the parameter-name lists produced above, so switching the convention to uppercase requires updating `is_generic_param_name` (and its doc comment, which explicitly says "single lowercase letter... is the grammar's only legal spelling") plus re-verifying no other code path re-derives the convention independently (a search shows this is the only definition site — good, it's centralized).
- `lib.rs`: no direct generic-name-convention logic found; only constructs placeholder `ast::Type`/`ast::ReturnType` with empty `generics` in various spots (95, 126, 191, 211) — unaffected by the syntax change itself.
## 4. plum-wasm-codegen
No string/pattern-based parsing of generic syntax in `src/` — codegen operates purely on the already-monomorphized (fully concrete, generic-free) AST; the one `generic` mention in `src/lib.rs:487` is just a comment. All generic syntax exposure is in **test fixtures**, which embed `.plum` source as string literals:
- `plum-wasm-codegen/tests/codegen_tests.rs`: ~30 lines matching old-syntax generic patterns, including explicit `type Box(a) =` declarations at lines **856, 891, 1010**, and `identity(value: a) -> a =` at **877, 909, 1177**, plus additional `Option(a)`/`<List>`-style occurrences throughout.
- `plum-checker/tests/checker_tests.rs`: 19 matching lines using paren/lowercase generics.
- `plum-checker/tests/monomorphize_tests.rs`: 27 matching lines (heaviest concentration — this suite specifically exercises the monomorphization pass described in §3).
- `plum-core/tests/parser_test.rs` and `formatter_test.rs`: 0 matches — apparently don't currently exercise generic syntax at all.
## 5. Full list of `.plum` files using generics
**`libs/std/list.plum`** (183 lines) — mixed old/new syntax already:
- Line 6: `type Node[T] =` — **already migrated to bracket/uppercase** (with field types `value: T`, `prev: Option[Node]`, `next: Option[Node]` at lines 7-9, also already bracket-style, though `Option[Node]` omits `Node`'s own type argument, likely intentionally scoped-out/pre-existing).
- Line 13: `type List(Stringable)(a: Stringable) =` — old-style, both implements-parens and generics-parens with lowercase `a`.
- Lines 18, 22, 37, 45, 49, 53, 61, 65, 69, 73, 77, 88, 102, 106, 110, 114, 118, 122, 126, 134, 142, 146, 150, 154, 158, 162, 166, 170, 174, 177: every method using `a`/`b` as generic type in params/returns (e.g. `get<List>(self, i: Int) -> Option(a)` at 22; `map<List>(self, cb: fn(a) -> b) -> List(b)` at 88; `reduce<List>(self, acc: b, cb: fn(a) -> a) -> Option(b)` at 122).
**`libs/std/map.plum`** (36 lines):
- Line 4: `type Pair(a, b) =`
- Line 10: `type Map(a, b) =`
- Line 11: `items: List(Pair(a, b))`
- Lines 21, 28, 32: `get<Map>(self, k: a) -> Option(b)`, `set<Map>(self, k: a, v: b)`, `putIfAbsent<Map>(self, k: a, v: b)`
- Line 35: `map<Map>(self, cb: fn(Pair(a, b)) -> Pair(c, d)) -> Map(c, d) =` — uses all four grammar-supported letters (`a,b,c,d`) simultaneously, a good stress case for the migration.
**`libs/std/option.plum`** (5 lines): line 4, `| Some(a)`.
**`libs/std/result.plum`** (13 lines): line 4 `| Ok(a)`, line 5 `| Err(b)`.
**`libs/std/json.plum`** (71 lines): line 23, `type JsonParseError(Err) =` — this is an `implements`-list usage (paren, trait name `Err`), not a generic-parameter declaration; flagged here only because it shares surface syntax with `class`'s parens and is worth double-checking it's unaffected by the migration (implements stays parenthesized).
**`libs/std/str.plum`** (132 lines): line 10, `type Str(Comparable, Stringable, Readable, Writable) =` — also purely an `implements` list, no generics; unaffected.
**`examples/types.plum`** (31 lines):
- Line 5: `type Named(Stringable) =` (implements only, unaffected)
- Line 8: `type Box(a) =` (generic declaration, needs migration)
- Line 15: `trait Comparable(a: Ord) =` (generic trait declaration with bound, needs migration to `trait Comparable[T: Ord] =`)
**`examples/match.plum`** (48 lines): line 40, `describeOption(opt: Option) -> Int =` — bare (non-generic-argument) reference to generic enum `Option`; no letter/paren syntax present, unaffected by the syntax migration itself.
No other `.plum` files (checked `libs/std/*` fully, `examples/*`, no `.plum` under `test/`, `website/`, or `hica/`) contain generic syntax.
## 6. Existing design docs — syntax assumptions
Both committed specs assume the **old** `(a)`/lowercase syntax explicitly and will need re-reading against the new syntax before/after this migration lands:
- `docs/superpowers/specs/2026-07-20-generics-monomorphization-design.md`: line 6, `type Box(a) = value: a`; line 7, `trait Comparable(a: Ord) = compareTo(other: a) -> Int`; line 37, explicitly notes "both bracket and paren syntax are already accepted per the grammar" for *usage* sites (`Box[Int]`, `Box(Int)`) — i.e. this doc is aware of the dual-syntax usage rule but still treats `a`/`b`/`c`/`d` paren-declarations as canonical; line 68, states directly "(`a`, `b`, `c`, `d` — the grammar's only legal generic-parameter spelling)".
- `docs/superpowers/specs/2026-07-20-generic-enum-multi-instantiation-design.md`: repeatedly refers to "lowercase-letter generic params" and "bare generic-typed param" throughout — same old-syntax assumption, no bracket mentions.
- `docs/superpowers/specs/2026-07-24-list-methods-design.md` and `docs/superpowers/plans/2026-07-24-list-methods.md` (pending, not yet executed): **entirely old-syntax** — every code sample uses `type Node(a) =`, `type List(a) =`, `Option(a)`, `Some(a)`, `get<List>(self, i: Int) -> Option(a)`, etc. (dozens of occurrences throughout the plan's inline test fixtures, e.g. plan lines 43, 46, 51, 56, 152, 159, 192, 206, 211, 216, 244, and repeating through ~line 1032). Notably this plan's own target file, `libs/std/list.plum`, **already has `Node[T]` in bracket form on disk** (line 6) while the plan still describes it as `Node(a)` — the plan predates that on-disk edit and is now out of sync with the current file it's meant to extend, independent of this migration. This is worth flagging to whoever executes that plan next: it needs re-basing onto both the bracket-generics syntax and the current `list.plum` contents before execution.</result>
<usage><subagent_tokens>65652</subagent_tokens><tool_uses>31</tool_uses><duration_ms>130912</duration_ms></usage>
</task-notification>
- `Bash: grep -n "type_identifier\s*:\|var_identifier\s*:\|fn_identifier\s*:" /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/grammar.js`
**Claude:** I now have a full picture. Here's the design, in sections — let me know if each looks right before I move to the next.
**1. The core lexical trade-off (needs your call)**
Today `generic` is a fixed set of 4 lowercase tokens (`a`,`b`,`c`,`d`) — zero conflict with `type_identifier` (`/[A-Z][a-zA-Z0-9]*/`) since real types must start uppercase. Switching generics to uppercase creates a genuine collision: `type_identifier`'s regex already matches single uppercase letters like `"T"`.
The clean fix: change `type_identifier` to require **2+ characters** (`/[A-Z][a-zA-Z0-9]+/`) and make `generic` a single uppercase letter (`/[A-Z]/`). Result: **any bare single uppercase letter is always a generic parameter; any multi-character capitalized name is always a concrete type.** This also removes the old 4-letter cap (`a`-`d` only) — any of A-Z works as a generic name now.
The cost: you can no longer name a real type `T`, `A`, `X`, etc. (single letter type names become permanently reserved for generics). Given real types in this codebase are always words (`List`, `Option`, `Node`...), this seems like a fine trade — but it's a real, permanent constraint worth confirming.
- `AskUserQuestion`
**Claude:** **2. Grammar structure changes**
- `generics` declaration: `(` → `[`, i.e. `generics: "[" commaSep1(generic_type) "]"`.
- `class`/`trait` field order swaps to match your example: generics come first, then implements — `type List[T: Stringable](Stringable) =`. (Today it's implements-then-generics; the parser's "implements = leading type_identifiers before generics" logic needs re-deriving for the new order.)
- `return_type` currently duplicates generics via its own paren-only rule (`ReturnType.generics: Vec<GenericParam>`), which is why `-> Option[Node]` doesn't even parse today — only `-> Option(a)` does. I'll unify `return_type` to just reuse `$.type` (same shape as field types), fixing this existing asymmetry as part of the migration rather than carrying it forward.
- `enum_field` payload lists (`| Some(a)`, `| Ok(a)`) — I'd leave these as parens. They read as constructor/argument shape (mirrors `Some(5)` at the value level), not a type-parameter declaration, so migrating them to brackets (`| Some[a]`) would look odd and isn't structurally the same thing as a field type. Flagging this as a scoping call — want brackets here too, or parens is right?
- `AskUserQuestion`
**Claude:** **3. Implementation scope & impact**
This touches every layer:
- **Grammar** (`tooling/tree-sitter-plum/grammar.js`): rules above, plus regenerating the parser and updating corpus tests (`type.txt`, `trait.txt`, `enum.txt`, `function.txt`) to the new bracket/uppercase forms.
- **`plum-core`**: `ast.rs` — fix the `Type` vs `ReturnType` generics asymmetry (`ReturnType` reuses `Type`'s shape now); `parser.rs` — `parse_generics_field` and `parse_enum_variant` currently hardcode node-kind checks against literal `"a"|"b"|"c"|"d"`; since `generic` becomes a single regex-based token, these become a single check (single uppercase letter) instead of an enumerated match. Also re-derive `implements` now that it comes *after* generics in field order.
- **`plum-checker`**: `monomorphize.rs`'s `is_generic_param_name` flips from "single lowercase letter" to "single uppercase letter" — this is the one load-bearing convention check, and it's centralized (good — nothing else re-derives it).
- **`plum-wasm-codegen`**: no source changes (codegen only sees fully-monomorphized, generic-free AST) — only test fixtures change.
- **Stdlib rewrite**: `libs/std/list.plum`, `map.plum`, `option.plum`, `result.plum` — every `(a)`/`(a, b)` declaration and lowercase usage becomes bracket/uppercase (`Node[T]`, `List[T: Stringable](Stringable)`, `Pair[K, V]`, `Some[T]`, `Ok[T]`/`Err[E]`, etc. — I'll pick sensible per-slot letters, e.g. `T`/`U` for generic lists, `K`/`V` for maps, rather than always `T`).
- **Examples**: `examples/types.plum` (`Box(a)` → `Box[T]`, `Comparable(a: Ord)` → `Comparable[T: Ord]`).
- **Tests**: `plum-checker/tests/checker_tests.rs` (19 lines), `monomorphize_tests.rs` (27 lines), `plum-wasm-codegen/tests/codegen_tests.rs` (~30 lines) all need their embedded `.plum` source strings updated.
- **Design docs**: the two committed generics specs (`2026-07-20-generics-monomorphization-design.md`, `2026-07-20-generic-enum-multi-instantiation-design.md`) explicitly document the *old* syntax as canonical — I'll note in the new spec that they describe pre-migration syntax, without rewriting them (they're historical records of already-shipped work).
**Important conflict to flag:** the untracked plan `docs/superpowers/plans/2026-07-24-list-methods.md` is written entirely in old syntax (`type Node(a) =`) and is now out of sync with `list.plum`'s current on-disk state (which already has `Node[T]` on line 6). If we do this syntax migration first, that plan's inline test fixtures will all need rewriting before anyone executes it — I'd flag it as blocked/needs-rebase rather than touch it now, since it's a separate, not-yet-started plan.
Does this scope and approach look right? If so, I'll write it up as a formal design spec next.
**You:** Yes
- `Write: /Users/pyrossh/Code/plum/docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md`
**Claude:** Self-review pass: no placeholders, sections are internally consistent (lexical rule → grammar rules → per-layer impact all agree), scope is bounded to syntax only, and the one known ambiguity (list-methods plan conflict) is called out rather than silently ignored. Committing it now.
- `Bash: git add docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md && git commit -m "$(cat <<'EOF' docs: add design spec for bracket generics syntax mi`
**Claude:** Spec written and committed to `docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md`. Please review it and let me know if you want any changes before we write the implementation plan.
**You:** Yes
- `Skill: superpowers:writing-plans`
**You:** Base directory for this skill: /Users/pyrossh/.claude/plugins/cache/superpowers-dev/superpowers/6.1.1/skills/writing-plans
# Writing Plans
## Overview
Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.
**Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
**Context:** If working in an isolated worktree, it should have been created via the `superpowers:using-git-worktrees` skill at execution time.
**Save plans to:** `docs/superpowers/plans/YYYY-MM-DD-<feature-name>.md`
- (User preferences for plan location override this default)
## Scope Check
If the spec covers multiple independent subsystems, it should have been broken into sub-project specs during brainstorming. If it wasn't, suggest breaking this into separate plans — one per subsystem. Each plan should produce working, testable software on its own.
## File Structure
Before defining tasks, map out which files will be created or modified and what each one is responsible for. This is where decomposition decisions get locked in.
- Design units with clear boundaries and well-defined interfaces. Each file should have one clear responsibility.
- You reason best about code you can hold in context at once, and your edits are more reliable when files are focused. Prefer smaller, focused files over large ones that do too much.
- Files that change together should live together. Split by responsibility, not by technical layer.
- In existing codebases, follow established patterns. If the codebase uses large files, don't unilaterally restructure - but if a file you're modifying has grown unwieldy, including a split in the plan is reasonable.
This structure informs the task decomposition. Each task should produce self-contained changes that make sense independently.
## Task Right-Sizing
A task is the smallest unit that carries its own test cycle and is worth a
fresh reviewer's gate. When drawing task boundaries: fold setup,
configuration, scaffolding, and documentation steps into the task whose
deliverable needs them; split only where a reviewer could meaningfully
reject one task while approving its neighbor. Each task ends with an
independently testable deliverable.
## Bite-Sized Task Granularity
**Each step is one action (2-5 minutes):**
- "Write the failing test" - step
- "Run it to make sure it fails" - step
- "Implement the minimal code to make the test pass" - step
- "Run the tests and make sure they pass" - step
- "Commit" - step
## Plan Document Header
**Every plan MUST start with this header:**
```markdown
# [Feature Name] Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** [One sentence describing what this builds]
**Architecture:** [2-3 sentences about approach]
**Tech Stack:** [Key technologies/libraries]
## Global Constraints
[The spec's project-wide requirements — version floors, dependency limits,
naming and copy rules, platform requirements — one line each, with exact
values copied verbatim from the spec. Every task's requirements implicitly
include this section.]
---
```
## Task Structure
````markdown
### Task N: [Component Name]
**Files:**
- Create: `exact/path/to/file.py`
- Modify: `exact/path/to/existing.py:123-145`
- Test: `tests/exact/path/to/test.py`
**Interfaces:**
- Consumes: [what this task uses from earlier tasks — exact signatures]
- Produces: [what later tasks rely on — exact function names, parameter
and return types. A task's implementer sees only their own task; this
block is how they learn the names and types neighboring tasks use.]
- [ ] **Step 1: Write the failing test**
```python
def test_specific_behavior():
result = function(input)
assert result == expected
```
- [ ] **Step 2: Run test to verify it fails**
Run: `pytest tests/path/test.py::test_name -v`
Expected: FAIL with "function not defined"
- [ ] **Step 3: Write minimal implementation**
```python
def function(input):
return expected
```
- [ ] **Step 4: Run test to verify it passes**
Run: `pytest tests/path/test.py::test_name -v`
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add tests/path/test.py src/path/file.py
git commit -m "feat: add specific feature"
```
````
## No Placeholders
Every step must contain the actual content an engineer needs. These are **plan failures** — never write them:
- "TBD", "TODO", "implement later", "fill in details"
- "Add appropriate error handling" / "add validation" / "handle edge cases"
- "Write tests for the above" (without actual test code)
- "Similar to Task N" (repeat the code — the engineer may be reading tasks out of order)
- Steps that describe what to do without showing how (code blocks required for code steps)
- References to types, functions, or methods not defined in any task
## Remember
- Exact file paths always
- Complete code in every step — if a step changes code, show the code
- Exact commands with expected output
- DRY, YAGNI, TDD, frequent commits
## Self-Review
After writing the complete plan, look at the spec with fresh eyes and check the plan against it. This is a checklist you run yourself — not a subagent dispatch.
**1. Spec coverage:** Skim each section/requirement in the spec. Can you point to a task that implements it? List any gaps.
**2. Placeholder scan:** Search your plan for red flags — any of the patterns from the "No Placeholders" section above. Fix them.
**3. Type consistency:** Do the types, method signatures, and property names you used in later tasks match what you defined in earlier tasks? A function called `clearLayers()` in Task 3 but `clearFullLayers()` in Task 7 is a bug.
If you find issues, fix them inline. No need to re-review — just fix and move on. If you find a spec requirement with no task, add the task.
## Execution Handoff
After saving the plan, offer execution choice:
**"Plan complete and saved to `docs/superpowers/plans/<filename>.md`. Two execution options:**
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
**Which approach?"**
**If Subagent-Driven chosen:**
- **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
- Fresh subagent per task + two-stage review
**If Inline Execution chosen:**
- **REQUIRED SUB-SKILL:** Use superpowers:executing-plans
- Batch execution with checkpoints for review
- `Bash: sed -n '1,20p' tooling/tree-sitter-plum/grammar.js && echo "---FULL---" && wc -l tooling/tree-sitter-plum/grammar.js plum-core/src/ast.rs plum-core/src/parser.r`
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/grammar.js`
- `Read: /Users/pyrossh/Code/plum/plum-core/src/ast.rs`
- `Bash: grep -n "fn parse_generics_field\|fn parse_class\|fn parse_trait\|fn parse_enum_variant\|fn parse_return_type\|fn parse_type\b\|fn parse_fn\b" /Users/pyrossh/Co`
- `Read: /Users/pyrossh/Code/plum/plum-core/src/parser.rs`
- `Bash: sed -n '1,70p' /Users/pyrossh/Code/plum/plum-checker/src/monomorphize.rs`
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/type.txt`
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/trait.txt`
- `Bash: grep -rn "ReturnType\|\.returns\b" plum-checker/src plum-wasm-codegen/src plum-core/src | grep -v "^Binary"`
- `Read: /Users/pyrossh/Code/plum/libs/std/option.plum`
- `Read: /Users/pyrossh/Code/plum/libs/std/result.plum`
- `Read: /Users/pyrossh/Code/plum/libs/std/map.plum`
- `Read: /Users/pyrossh/Code/plum/examples/types.plum`
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/enum.txt`
- `Bash: wc -l /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/function.txt`
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus && grep -l "return_type" *.txt && echo --- && grep -c "return_type" *.txt`
- `Bash: cd /Users/pyrossh/Code/plum && grep -n "return_type\|(a)\|(b)\|(a,\|, b)\| a)\| b)\|: a\b\|: b\b\|List(b)\|Option(a)\|Option(b)\|Pair(a\|fn(a)\|-> a\b\|-> b\b"`
- `Read: /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus/function.txt`
- `Bash: cd /Users/pyrossh/Code/plum/tooling/tree-sitter-plum/test/corpus && grep -n -A2 "return_type" function.txt | grep -B2 "generics"`