plum

#treesitter#compiler#wasm

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

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


docs/superpowers/plans/2026-07-24-bracket-generics-syntax.md
# Bracket Generics Syntax Migration 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:** Migrate Plum's generic-type syntax from parenthesized/lowercase (`type Foo(a) =`, `Option(a)`) to bracketed/uppercase (`type Foo[T] =`, `Option[T]`) across the grammar, parser, checker, stdlib, and examples — a pure syntax migration with no semantic changes.

**Architecture:** Bottom-up through the compiler pipeline: grammar (tree-sitter) → AST/parser (`plum-core`) → monomorphization convention (`plum-checker`) → downstream `.name`-reader call sites (`plum-checker`/`plum-wasm-codegen`) → Rust test fixtures → `.plum` source files (stdlib, examples). Each layer is verified before moving to the next, since later layers depend on earlier ones compiling/parsing correctly.

**Tech Stack:** tree-sitter (JS grammar + generated Rust/C parser), Rust (`plum-core`, `plum-checker`, `plum-wasm-codegen`), Cargo workspace tests.

## Global Constraints

- Spec: `docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md`
- `generic` becomes any single uppercase ASCII letter (`/[A-Z]/`); `type_identifier` becomes 2+ characters (`/[A-Z][a-zA-Z0-9]+/`) — single-letter type names are now permanently illegal.
- Declaration order: generics-with-bounds first, implements-list second — `type List[T: Stringable](Stringable) =`.
- Every generic-type appearance uses brackets: declarations, field types, return types, enum variant payloads (`| Some[T]`). Parens stay for value-level constructor/call argument lists and trait "implements" lists.
- Method receiver annotation (`get<List>(self, ...)`) is untouched — it's an unrelated mechanism.
- `is_generic_param_name` (the sole convention-detection function, in `plum-checker/src/monomorphize.rs`) flips from lowercase to uppercase; this is the only place the letter-case convention is defined.
- `ReturnType` (a separate, narrower AST shape than `Type`) is removed; every `returns` field becomes `Option<ast::Type>`, fixing the pre-existing `Type`/`ReturnType` asymmetry as part of this migration.
- The untracked plan `docs/superpowers/plans/2026-07-24-list-methods.md` is written in the OLD syntax and is **not** touched by this plan — it is blocked/stale and needs its own rebase before anyone executes it. Do not edit it here.
- Run `cargo test --workspace` after every task from Task 2 onward — expect it to start failing once the grammar changes land (Task 1), and to be fully green again only after Task 5.
- Letter conventions used in stdlib rewrites (Task 6): `List`/`Node``T` (map's callback output → `U`); `Map`/`Pair``K`, `V` (map's callback output pair → `X`, `Y`); `Option``T`; `Result``T` (Ok), `E` (Err); `examples/types.plum`'s `Box``T`, `Comparable``T`.

---

### Task 1: Grammar changes and corpus tests (`tooling/tree-sitter-plum`)

**Files:**
- Modify: `tooling/tree-sitter-plum/grammar.js`
- Modify: `tooling/tree-sitter-plum/test/corpus/type.txt`
- Modify: `tooling/tree-sitter-plum/test/corpus/trait.txt`
- Modify: `tooling/tree-sitter-plum/test/corpus/enum.txt`
- Modify: `tooling/tree-sitter-plum/test/corpus/function.txt`

**Interfaces:**
- Consumes: nothing (this is the bottom of the pipeline).
- Produces: a regenerated tree-sitter parser where `generic` is `/[A-Z]/` (any single uppercase letter, node kind `"generic"`), `type_identifier` is `/[A-Z][a-zA-Z0-9]+/` (2+ chars), `generics` declarations use `[...]`, `class`/`trait` generics come before implements, `enum_field` payloads use `[...]`, and `return_type` no longer exists as its own rule — return positions parse as plain `$.type` (node kind `"type"`, not `"return_type"`).

- [ ] **Step 1: Edit `grammar.js`'s generic/type-identifier tokens and the `generics` bracket**

In `tooling/tree-sitter-plum/grammar.js`, replace:

```js
  inline: ($) => [$.generic_type, $.generic],
```

with:

```js
  inline: ($) => [$.generic_type],
```

(`$.generic` is now a genuine terminal token — a leaf rule can't meaningfully be "inlined," since there's no substructure to hoist. `$.generic_type` stays inlined so `generics`' children remain flat, matching the existing parser convention.)

Replace:

```js
    generics: ($) => seq("(", commaSep1($.generic_type), ")"),
```

with:

```js
    generics: ($) => seq("[", commaSep1($.generic_type), "]"),
```

Replace:

```js
    type: ($) =>
      choice(
        seq(
          $.type_identifier,
          field(
            "generics",
            optional(
              choice(
                seq("[", commaSep1($.type), "]"),
                seq("(", commaSep1($.type), ")"),
              ),
            ),
          ),
        ),
        $.generic,
      ),
```

with:

```js
    type: ($) =>
      choice(
        seq(
          $.type_identifier,
          field(
            "generics",
            optional(seq("[", commaSep1($.type), "]")),
          ),
        ),
        $.generic,
      ),
```

- [ ] **Step 2: Swap `class`'s field order (generics before implements) and drop `return_type`**

Replace:

```js
    class: ($) =>
      seq(
        "type",
        field("name", $.type_identifier),
        field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
        field("generics", optional($.generics)),
        "=",
        $._indent,
        field("fields", optional(repeat(alias($.class_field, $.field)))),
        $._dedent,
      ),
```

with:

```js
    class: ($) =>
      seq(
        "type",
        field("name", $.type_identifier),
        field("generics", optional($.generics)),
        field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
        "=",
        $._indent,
        field("fields", optional(repeat(alias($.class_field, $.field)))),
        $._dedent,
      ),
```

Replace:

```js
    trait_field: ($) =>
      seq(
        field("name", $.fn_identifier),
        field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")),
        field("returns", optional(seq("->", $.return_type))),
      ),
```

with:

```js
    trait_field: ($) =>
      seq(
        field("name", $.fn_identifier),
        field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")),
        field("returns", optional(seq("->", $.type))),
      ),
```

Delete this rule entirely (return positions now parse as plain `$.type`):

```js
    return_type: ($) =>
      seq($.type_identifier, field("generics", optional($.generics))),
```

Replace, in `enum_field`:

```js
    enum_field: ($) =>
      seq(
        "|",
        field("name", $.type_identifier),
        field("parameters", optional(seq("(", commaSep1(choice($.type_identifier, $.generic)), ")"))),
      ),
```

with:

```js
    enum_field: ($) =>
      seq(
        "|",
        field("name", $.type_identifier),
        field("parameters", optional(seq("[", commaSep1(choice($.type_identifier, $.generic)), "]"))),
      ),
```

Replace, in `fn`:

```js
          field("returns", optional(seq("->", $.return_type))),
```

with:

```js
          field("returns", optional(seq("->", $.type))),
```

- [ ] **Step 3: Replace the hardcoded 4-letter `generic` rule with a single regex token**

Replace:

```js
    generic: ($) => choice($.a, $.b, $.c, $.d), // single letter
    a: (_) => token("a"),
    b: (_) => token("b"),
    c: (_) => token("c"),
    d: (_) => token("d"),
```

with:

```js
    generic: (_) => /[A-Z]/, // any single uppercase letter — reserved, illegal as a type_identifier
```

Replace:

```js
    type_identifier: (_) => /[A-Z][a-zA-Z0-9]*/, // capital case
```

with:

```js
    type_identifier: (_) => /[A-Z][a-zA-Z0-9]+/, // capital case, 2+ chars (single uppercase letters are reserved for `generic`)
```

- [ ] **Step 4: Regenerate the parser**

Run: `cd tooling/tree-sitter-plum && npx tree-sitter generate`
Expected: succeeds with no grammar conflicts reported. If tree-sitter reports a conflict between `generic` and `type_identifier`, stop and report BLOCKED — it would mean the 2-char minimum on `type_identifier` didn't fully disambiguate the two tokens, contradicting this plan's core assumption from the design spec.

- [ ] **Step 5: Update `test/corpus/type.txt`**

Replace the entire file contents with:

```
================================================================================
type
================================================================================

type Dog =
  name: Str
  age: B

type Cat(Stringable) =
  name: Str
  age: Int

init<Cat>(name: Str) -> Cat =
  Cat(name: name, age: 0)

withName<Cat>(name: Str) -> Cat =
  Cat(name: name, age: 0)

withAge<Cat>(age: Int) -> Cat =
  Cat(name: "", age: age)

toStr<Cat>() -> Str =
  "Cat({self.name}, {self.age})"

--------------------------------------------------------------------------------

(source
  (class
    (type_identifier)
    (field
      (var_identifier)
      (type
        (type_identifier)))
    (field
      (var_identifier)
      (type
        (generic))))
  (class
    (type_identifier)
    (type_identifier)
    (field
      (var_identifier)
      (type
        (type_identifier)))
    (field
      (var_identifier)
      (type
        (type_identifier))))
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (param
      (var_identifier)
      (type
        (type_identifier)))
    (type
      (type_identifier))
    (body
      (expression
        (primary_expression
          (class_call
            (type_identifier)
            (class_argument_list
              (var_identifier)
              (expression
                (primary_expression
                  (var_identifier)))
              (var_identifier)
              (expression
                (primary_expression
                  (integer)))))))))
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (param
      (var_identifier)
      (type
        (type_identifier)))
    (type
      (type_identifier))
    (body
      (expression
        (primary_expression
          (class_call
            (type_identifier)
            (class_argument_list
              (var_identifier)
              (expression
                (primary_expression
                  (var_identifier)))
              (var_identifier)
              (expression
                (primary_expression
                  (integer)))))))))
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (param
      (var_identifier)
      (type
        (type_identifier)))
    (type
      (type_identifier))
    (body
      (expression
        (primary_expression
          (class_call
            (type_identifier)
            (class_argument_list
              (var_identifier)
              (expression
                (primary_expression
                  (string
                    (string_start)
                    (string_end))))
              (var_identifier)
              (expression
                (primary_expression
                  (var_identifier)))))))))
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (type
      (type_identifier))
    (body
      (expression
        (primary_expression
          (string
            (string_start)
            (string_content)
            (interpolation
              (primary_expression
                (attribute
                  (primary_expression
                    (self))
                  (fn_identifier))))
            (string_content)
            (interpolation
              (primary_expression
                (attribute
                  (primary_expression
                    (self))
                  (fn_identifier))))
            (string_content)
            (string_end)))))))
```

- [ ] **Step 6: Update `test/corpus/trait.txt`**

This file has no generics and only plain `-> Str`/`-> Int` return types, so the only change is `return_type``type` in the expected tree. Replace the expected-tree section (everything after the `---` divider) with:

```
(source
  (trait
    (type_identifier)
    (field
      (fn_identifier)
      (type
        (type_identifier))))
  (trait
    (type_identifier)
    (field
      (fn_identifier)
      (type
        (type_identifier)))
    (field
      (fn_identifier)
      (type
        (type_identifier)))
    (field
      (fn_identifier))
    (field
      (fn_identifier)
      (param
        (var_identifier)
        (type
          (type_identifier)))
      (type
        (type_identifier)))))
```

(The source section above the divider is unchanged — no generics appear in this file's source.)

- [ ] **Step 7: Update `test/corpus/enum.txt`**

Replace the entire file contents with:

```
================================================================================
enum
================================================================================

enum Bool =
  | True
  | False

toStr<Bool>() -> Str =
  "Bool"

--------------------------------------------------------------------------------

(source
  (enum
    (type_identifier)
    (field
      (type_identifier))
    (field
      (type_identifier)))
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (type
      (type_identifier))
    (body
      (expression
        (primary_expression
          (string
            (string_start)
            (string_content)
            (string_end)))))))

================================================================================
enum - generic variant fields
================================================================================

enum Option =
  | Some[T]
  | None

--------------------------------------------------------------------------------

(source
  (enum
    (type_identifier)
    (field
      (type_identifier)
      (generic))
    (field
      (type_identifier))))
```

- [ ] **Step 8: Update `test/corpus/function.txt`**

First, apply this mechanical substitution across the whole file — every remaining `(return_type` line (the ones NOT part of the "function - generics" test touched in the next step) becomes `(type`, with indentation untouched:

Run:
```bash
cd tooling/tree-sitter-plum/test/corpus
sed -i '' 's/(return_type/(type/' function.txt
```

Then hand-fix the two tests whose *source* text (not just the expected tree) also changes. Find the block starting `function - generics` and replace its source + expected tree:

```
================================================================================
function - generics
================================================================================

add(param: T, param2: List[U]) -> List[U] =
  todo

--------------------------------------------------------------------------------

(source
  (fn
    (fn_identifier)
    (param
      (var_identifier)
      (type
        (generic)))
    (param
      (var_identifier)
      (type
        (type_identifier)
        (type
          (generic))))
    (type
      (type_identifier)
      (type
        (generic)))
    (body
      (todo))))
```

Then find the block starting `function - method with explicit self param` and replace its source + expected tree:

```
================================================================================
function - method with explicit self param
================================================================================

remove<List>(self, v: T) =
  todo

--------------------------------------------------------------------------------

(source
  (fn
    (fn_identifier)
    (type
      (type_identifier))
    (self)
    (param
      (var_identifier)
      (type
        (generic)))
    (body
      (todo))))
```

- [ ] **Step 9: Run the corpus tests**

Run: `cd tooling/tree-sitter-plum && npx tree-sitter test 2>&1 | tail -100`
Expected: all tests PASS. If any test other than `type`, `trait`, `enum`, `function` fails, inspect its diff — it likely means a `return_type` occurrence was missed elsewhere, or the sed substitution touched something unintended.

- [ ] **Step 10: Commit**

```bash
git add tooling/tree-sitter-plum/grammar.js tooling/tree-sitter-plum/src tooling/tree-sitter-plum/test/corpus/type.txt tooling/tree-sitter-plum/test/corpus/trait.txt tooling/tree-sitter-plum/test/corpus/enum.txt tooling/tree-sitter-plum/test/corpus/function.txt
git commit -m "feat(grammar): migrate generics to bracket syntax with uppercase letters"
```

(`tooling/tree-sitter-plum/src` covers the regenerated `parser.c`/`grammar.json`/etc. produced by Step 4 — include whatever `tree-sitter generate` wrote there.)

---

### Task 2: `plum-core` — AST and parser updates

**Files:**
- Modify: `plum-core/src/ast.rs`
- Modify: `plum-core/src/parser.rs`

**Interfaces:**
- Consumes: the regenerated grammar from Task 1 (node kinds `"generic"`, `"generics"`, `"type"` — no more `"return_type"`, `"a"`/`"b"`/`"c"`/`"d"`).
- Produces: `TraitMethod.returns: Option<Type>`, `Fn.returns: Option<Type>` (was `Option<ReturnType>`); `parse_class`, `parse_generics_field`, `parse_enum_variant`, `parse_trait_method`, `parse_fn` updated to match the new grammar shape; `parse_return_type` removed entirely.

- [ ] **Step 1: Remove `ReturnType` from `ast.rs`, retype `returns` fields**

In `plum-core/src/ast.rs`, replace:

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct TraitMethod {
    pub name: String,
    pub params: Vec<Param>,
    pub returns: Option<ReturnType>,
}
```

with:

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct TraitMethod {
    pub name: String,
    pub params: Vec<Param>,
    pub returns: Option<Type>,
}
```

Replace:

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct Fn {
    pub name: String,
    /// Type parameter for method dispatch, e.g. `<Cat>` in `toStr<Cat>()`
    pub type_param: Option<String>,
    pub params: Vec<Param>,
    pub returns: Option<ReturnType>,
    pub body: FnBody,
}
```

with:

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct Fn {
    pub name: String,
    /// Type parameter for method dispatch, e.g. `<Cat>` in `toStr<Cat>()`
    pub type_param: Option<String>,
    pub params: Vec<Param>,
    pub returns: Option<Type>,
    pub body: FnBody,
}
```

Delete this struct entirely (its shape is now identical to `Type`, which every `returns` field uses instead):

```rust
#[derive(Debug, Clone, PartialEq)]
pub struct ReturnType {
    pub name: String,
    pub generics: Vec<GenericParam>,
}
```

- [ ] **Step 2: Update `parse_class`'s implements-list derivation for the new field order**

In `plum-core/src/parser.rs`, replace:

```rust
    fn parse_class(&self, node: Node) -> Class {
        // class: "type" type_identifier ("(" type_identifier,* ")")? generics? "=" body
        // Named children in order: type_identifier (name), type_identifier* (implements), field*
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let name = named.first().map(|n| self.text(*n)).unwrap_or_default();

        // implements = type_identifiers that appear before any `field` node
        let implements: Vec<String> = named[1..]
            .iter()
            .take_while(|n| n.kind() == "type_identifier")
            .map(|n| self.text(*n))
            .collect();

        let generics = self.parse_generics_field(node);

        let fields: Vec<Field> = named
            .iter()
            .filter(|n| n.kind() == "field")
            .map(|n| self.parse_field(*n))
            .collect();

        Class { name, implements, generics, fields }
    }
```

with:

```rust
    fn parse_class(&self, node: Node) -> Class {
        // class: "type" type_identifier generics? ("(" type_identifier,* ")")? "=" body
        // Named children in order: type_identifier (name), generics? (declaration), type_identifier* (implements), field*
        let mut cursor = node.walk();
        let named: Vec<Node> = node.named_children(&mut cursor).collect();

        let name = named.first().map(|n| self.text(*n)).unwrap_or_default();

        // Skip the optional `generics` declaration node before looking for implements.
        let after_generics = if named.get(1).map(|n| n.kind()) == Some("generics") { 2 } else { 1 };

        // implements = type_identifiers that appear before any `field` node
        let implements: Vec<String> = named[after_generics..]
            .iter()
            .take_while(|n| n.kind() == "type_identifier")
            .map(|n| self.text(*n))
            .collect();

        let generics = self.parse_generics_field(node);

        let fields: Vec<Field> = named
            .iter()
            .filter(|n| n.kind() == "field")
            .map(|n| self.parse_field(*n))
            .collect();

        Class { name, implements, generics, fields }
    }
```

- [ ] **Step 3: Update `parse_generics_field` and `parse_enum_variant` to match on `"generic"` instead of `"a"|"b"|"c"|"d"`**

Replace:

```rust
    fn parse_generics_field(&self, node: Node) -> Vec<GenericParam> {
        // generics: "(" generic_type,* ")"  where  generic_type: generic (":" sep1(type_identifier, "+"))?
        //
        // Both `generic_type` and `generic` are `inline`d in the grammar, so the
        // `generics` node has NO `generic_type` children — its named children are
        // the single-letter generic nodes (`a`/`b`/`c`/`d`) each optionally
        // followed by their bound `type_identifier` nodes, all flattened together.
        // Reconstruct each `GenericParam` by starting a new one at every generic
        // letter and attaching any following `type_identifier`s as its bounds
        // until the next generic letter.
        let Some(generics_node) = self.children_of_kind(node, "generics").into_iter().next() else {
            return Vec::new();
        };
        let mut cursor = generics_node.walk();
        let mut params: Vec<GenericParam> = Vec::new();
        for child in generics_node.named_children(&mut cursor) {
            match child.kind() {
                "a" | "b" | "c" | "d" => {
                    params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
                }
                "type_identifier" => {
                    if let Some(last) = params.last_mut() {
                        last.bounds.push(self.text(child));
                    }
                }
                _ => {}
            }
        }
        params
    }
```

with:

```rust
    fn parse_generics_field(&self, node: Node) -> Vec<GenericParam> {
        // generics: "[" generic_type,* "]"  where  generic_type: generic (":" sep1(type_identifier, "+"))?
        //
        // `generic_type` is `inline`d in the grammar, so the `generics` node has NO
        // `generic_type` children — its named children are the single-uppercase-letter
        // `generic` nodes, each optionally followed by their bound `type_identifier`
        // nodes, all flattened together. Reconstruct each `GenericParam` by starting a
        // new one at every `generic` node and attaching any following
        // `type_identifier`s as its bounds until the next `generic` node.
        let Some(generics_node) = self.children_of_kind(node, "generics").into_iter().next() else {
            return Vec::new();
        };
        let mut cursor = generics_node.walk();
        let mut params: Vec<GenericParam> = Vec::new();
        for child in generics_node.named_children(&mut cursor) {
            match child.kind() {
                "generic" => {
                    params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
                }
                "type_identifier" => {
                    if let Some(last) = params.last_mut() {
                        last.bounds.push(self.text(child));
                    }
                }
                _ => {}
            }
        }
        params
    }
```

Replace:

```rust
    fn parse_enum_variant(&self, node: Node) -> EnumVariant {
        // enum_field (aliased to field): "|" type_identifier ("(" (type_identifier | generic),* ")")?
        // named children: type_identifier (name), then each field type inside "()" — a
        // `type_identifier` (concrete, e.g. `Int`) or an inlined generic letter node
        // (`a`/`b`/`c`/`d`, since `generic` is inlined in the grammar).
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let fields: Vec<String> = (1..node.named_child_count())
            .filter_map(|i| node.named_child(i as u32))
            .filter(|n| matches!(n.kind(), "type_identifier" | "a" | "b" | "c" | "d"))
            .map(|n| self.text(n))
            .collect();
        EnumVariant { name, fields }
    }
```

with:

```rust
    fn parse_enum_variant(&self, node: Node) -> EnumVariant {
        // enum_field (aliased to field): "|" type_identifier ("[" (type_identifier | generic),* "]")?
        // named children: type_identifier (name), then each field type inside "[]" — a
        // `type_identifier` (concrete, e.g. `Int`) or a `generic` node (single uppercase letter).
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let fields: Vec<String> = (1..node.named_child_count())
            .filter_map(|i| node.named_child(i as u32))
            .filter(|n| matches!(n.kind(), "type_identifier" | "generic"))
            .map(|n| self.text(n))
            .collect();
        EnumVariant { name, fields }
    }
```

- [ ] **Step 4: Update `parse_trait_method` and `parse_fn` to read `returns` as a `type` node by field name; remove `parse_return_type`**

Replace:

```rust
    fn parse_trait_method(&self, node: Node) -> TraitMethod {
        // trait_field (aliased to field): fn_identifier "(" params ")" ("->" return_type)?
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let params = self.collect_params_from(node);
        let returns = node
            .named_children(&mut node.walk())
            .find(|n| n.kind() == "return_type")
            .map(|n| self.parse_return_type(n));
        TraitMethod { name, params, returns }
    }
```

with:

```rust
    fn parse_trait_method(&self, node: Node) -> TraitMethod {
        // trait_field (aliased to field): fn_identifier "(" params ")" ("->" type)?
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let params = self.collect_params_from(node);
        let returns = node.child_by_field_name("returns").map(|n| self.parse_type(n));
        TraitMethod { name, params, returns }
    }
```

In `parse_fn`, replace:

```rust
        let returns = named
            .iter()
            .find(|n| n.kind() == "return_type")
            .map(|n| self.parse_return_type(*n));

        // body is the last named child — it is either a `body` node (block)
        // or an expression node when the body is a single expression.
        let body = named.last().and_then(|last| {
            match last.kind() {
                // Skip non-body trailing nodes
                "fn_identifier" | "type" | "param" | "self" | "return_type" => None,
                "body" => Some(FnBody::Block(self.parse_block(*last))),
                _ => {
                    let unwrapped = self.unwrap_expr_node(*last);
                    Some(FnBody::Expr(self.parse_expression(unwrapped)))
                }
            }
        }).unwrap_or(FnBody::Block(Block { stmts: vec![] }));
```

with:

```rust
        let returns = node.child_by_field_name("returns").map(|n| self.parse_type(n));

        // body is the last named child — it is either a `body` node (block)
        // or an expression node when the body is a single expression. The
        // `<Cat>` receiver annotation and the `returns` type both have kind
        // "type" now (return_type no longer exists as a separate node kind),
        // but that's fine: neither can ever be the LAST named child when a
        // body is present, since `body`/the trailing expression always comes
        // after them in the grammar — so this match doesn't need to
        // distinguish the two "type" cases from each other, only from `body`.
        let body = named.last().and_then(|last| {
            match last.kind() {
                // Skip non-body trailing nodes
                "fn_identifier" | "type" | "param" | "self" => None,
                "body" => Some(FnBody::Block(self.parse_block(*last))),
                _ => {
                    let unwrapped = self.unwrap_expr_node(*last);
                    Some(FnBody::Expr(self.parse_expression(unwrapped)))
                }
            }
        }).unwrap_or(FnBody::Block(Block { stmts: vec![] }));
```

Delete `parse_return_type` entirely:

```rust
    fn parse_return_type(&self, node: Node) -> ReturnType {
        // return_type: type_identifier generics?
        // named_child(0) = type_identifier
        let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
        let generics = self.parse_generics_field(node);
        ReturnType { name, generics }
    }
```

- [ ] **Step 5: Run `plum-core`'s tests**

Run: `cargo test -p plum-core 2>&1 | tail -60`
Expected: PASS. (`plum-core`'s own parser/formatter tests don't currently exercise generics syntax — see the design spec's survey — so this mainly confirms the crate still compiles and existing non-generic tests are unaffected.)

- [ ] **Step 6: Commit**

```bash
git add plum-core/src/ast.rs plum-core/src/parser.rs
git commit -m "feat(plum-core): parse bracket generics, drop ReturnType in favor of Type"
```

---

### Task 3: `plum-checker` and `plum-wasm-codegen` — uppercase convention and `ReturnType` cleanup

**Files:**
- Modify: `plum-checker/src/monomorphize.rs`
- Modify: `plum-checker/src/lib.rs`
- Modify: `plum-wasm-codegen/src/lib.rs`

**Interfaces:**
- Consumes: `Fn.returns: Option<Type>` / `TraitMethod.returns: Option<Type>` from Task 2.
- Produces: `is_generic_param_name` now recognizes single ASCII uppercase letters; every `ast::ReturnType` construction/reference becomes `ast::Type`.

- [ ] **Step 1: Flip `is_generic_param_name`'s case check**

In `plum-checker/src/monomorphize.rs`, replace:

```rust
/// A single lowercase letter (`a`, `b`, `c`, `d`, ...) is the grammar's only legal
/// spelling for a generic type parameter — this is how we recognize one, since
/// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
/// generics declaration list.
pub fn is_generic_param_name(name: &str) -> bool {
    let mut chars = name.chars();
    match (chars.next(), chars.next()) {
        (Some(c), None) => c.is_ascii_lowercase(),
        _ => false,
    }
}
```

with:

```rust
/// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
/// spelling for a generic type parameter — this is how we recognize one, since
/// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
/// generics declaration list.
pub fn is_generic_param_name(name: &str) -> bool {
    let mut chars = name.chars();
    match (chars.next(), chars.next()) {
        (Some(c), None) => c.is_ascii_uppercase(),
        _ => false,
    }
}
```

Also update the two doc comments in this file that describe the old lowercase convention:

Replace:

```rust
/// The generic parameter names implicitly introduced by a `Fn` — every distinct
/// single-lowercase-letter type name appearing in its params or return type, in
/// first-appearance order.
```

with:

```rust
/// The generic parameter names implicitly introduced by a `Fn` — every distinct
/// single-uppercase-letter type name appearing in its params or return type, in
/// first-appearance order.
```

Replace:

```rust
/// The generic parameter names implicitly introduced by an `Enum` — every distinct
/// single-lowercase-letter variant field type name, in first-appearance order.
```

with:

```rust
/// The generic parameter names implicitly introduced by an `Enum` — every distinct
/// single-uppercase-letter variant field type name, in first-appearance order.
```

- [ ] **Step 2: Replace `ast::ReturnType` construction sites with `ast::Type`**

In `plum-checker/src/monomorphize.rs`, replace:

```rust
        returns: f.returns.as_ref().map(|r| {
            let substituted = substitute_type(&ast::Type { name: r.name.clone(), generics: vec![] }, subst);
            ast::ReturnType { name: substituted.name, generics: vec![] }
        }),
```

with:

```rust
        returns: f.returns.as_ref().map(|r| substitute_type(r, subst)),
```

Replace:

```rust
        if needs {
            f.returns = Some(ast::ReturnType { name: t.to_string(), generics: vec![] });
        }
```

with:

```rust
        if needs {
            f.returns = Some(ast::Type { name: t.to_string(), generics: vec![] });
        }
```

- [ ] **Step 3: Simplify the two `ast::Type` reconstructions in `plum-checker/src/lib.rs`**

`f.returns`/`r` is already `&ast::Type` after Task 2 — reconstructing a fresh `ast::Type` from its own fields is now redundant. Replace:

```rust
                let ret = f.returns.as_ref()
                    .map(|r| plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
                    .unwrap_or(PlumType::TUnit);
```

with:

```rust
                let ret = f.returns.as_ref()
                    .map(plum_type_from_ast)
                    .unwrap_or(PlumType::TUnit);
```

Replace:

```rust
    let declared_ret = f.returns.as_ref()
        .map(|r| {
            let ast_ty = ast::Type { name: r.name.clone(), generics: vec![] };
            plum_type_from_ast(&ast_ty)
        })
        .unwrap_or(PlumType::TUnit);
```

with:

```rust
    let declared_ret = f.returns.as_ref()
        .map(plum_type_from_ast)
        .unwrap_or(PlumType::TUnit);
```

- [ ] **Step 4: Retype `ret_type_to_wasm` in `plum-wasm-codegen`**

In `plum-wasm-codegen/src/lib.rs`, replace:

```rust
fn ret_type_to_wasm(ret: Option<&ast::ReturnType>) -> Option<ValType> {
    ret.and_then(|r| ast_type_to_wasm(&r.name))
}
```

with:

```rust
fn ret_type_to_wasm(ret: Option<&ast::Type>) -> Option<ValType> {
    ret.and_then(|r| ast_type_to_wasm(&r.name))
}
```

(Its two call sites, `f.returns.as_ref()` at both usages, already produce `Option<&ast::Type>` after Task 2 — no call-site changes needed.)

- [ ] **Step 5: Build the workspace to confirm the Rust-level refactor compiles**

Run: `cargo build --workspace 2>&1 | tail -80`
Expected: builds clean, no type errors. (Tests are expected to start failing at this point — old-syntax `.plum` source strings embedded in test fixtures no longer parse under the Task-1 grammar. That's addressed in Tasks 4-5.)

- [ ] **Step 6: Commit**

```bash
git add plum-checker/src/monomorphize.rs plum-checker/src/lib.rs plum-wasm-codegen/src/lib.rs
git commit -m "feat(plum-checker,plum-wasm-codegen): recognize uppercase generic params, use Type instead of ReturnType"
```

---

### Task 4: Update `plum-checker` test fixtures

**Files:**
- Modify: `plum-checker/tests/checker_tests.rs`
- Modify: `plum-checker/tests/monomorphize_tests.rs`

**Interfaces:**
- Consumes: the Task 1-3 grammar/parser/checker changes.
- Produces: every embedded `.plum` source string in these two files rewritten to the new syntax, with assertions unchanged (this migration doesn't change behavior, so every currently-passing test must still pass with the same expected values).

- [ ] **Step 1: Run the checker test suite to see which tests fail on old syntax**

Run: `cargo test -p plum-checker 2>&1 | tail -150`
Expected: FAIL — a batch of tests fail because their embedded source strings use old syntax (`type Box(a) =`, `| Some(a)`, `-> a`, etc.) that no longer parses (single lowercase letters are no longer a valid `generic` or `type_identifier` token per Task 1).

- [ ] **Step 2: Rewrite every failing test's embedded source string using this mechanical rule**

For each failing test in `checker_tests.rs` and `monomorphize_tests.rs`, find every place a bare single lowercase letter (`a`, `b`, `c`, `d`) appears in a **type position** — i.e. immediately after `:` in a field/param declaration, as a bare function return type, inside a class's generic-declaration parens, or inside an enum variant's payload parens — and rewrite it using this fixed per-letter mapping, consistently within each individual test (do not reuse letters across unrelated tests):

- `a``T`
- `b``U`
- `c``V`
- `d``W`

And convert the enclosing syntax per Task 1's grammar: `type Foo(a) =``type Foo[T] =`, `type Foo(a, b) =``type Foo[T, U] =`, `Foo(a)` (type-argument usage, e.g. `Option(a)`) → `Foo[T]`, `| Some(a)` (enum variant payload) → `| Some[T]`, bare `-> a` (return type) → `-> T`.

Do **not** touch: variable/parameter *names* that happen to be single lowercase letters (e.g. `add(a: Int, b: Int) -> Int`, `bothTrue(a: Bool, b: Bool)`) — these are `var_identifier`s, unaffected by this migration, and must stay exactly as they are. Only letters appearing in **type position** (after the `:` or as a bare type name) are generics and need rewriting.

As a concrete worked example, in `checker_tests.rs` around line 322-323:

```rust
type Box(a) =
  value: a
```

becomes:

```rust
type Box[T] =
  value: T
```

and around line 356:

```rust
pair(first: a, second: b) -> Bool =
```

becomes:

```rust
pair(first: T, second: U) -> Bool =
```

(here `first`/`second` are param *names*, left untouched; `a`/`b` are the param *types*, rewritten.)

- [ ] **Step 3: Re-run until the checker suite passes**

Run: `cargo test -p plum-checker 2>&1 | tail -150`
Expected: iterate Step 2 against each remaining failure until this command shows all tests PASS, with the exact same assertions/expected values as before this migration (only source syntax changed, not behavior).

- [ ] **Step 4: Commit**

```bash
git add plum-checker/tests/checker_tests.rs plum-checker/tests/monomorphize_tests.rs
git commit -m "test(plum-checker): migrate test fixtures to bracket generics syntax"
```

---

### Task 5: Update `plum-wasm-codegen` test fixtures

**Files:**
- Modify: `plum-wasm-codegen/tests/codegen_tests.rs`

**Interfaces:**
- Consumes: the Task 1-3 grammar/parser/checker changes.
- Produces: every embedded `.plum` source string in this file rewritten to the new syntax; same assertions, same expected `run_main`/`run_main_str` results as before.

- [ ] **Step 1: Run the codegen test suite to see which tests fail on old syntax**

Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -150`
Expected: FAIL — the same class of failures as Task 4, for `type Box(a) =`, `identity(value: a) -> a =`, and similar old-syntax fixtures (survey found occurrences around lines 856, 877, 891, 909, 1010, 1177, plus additional `Option(a)`/generic usages throughout).

- [ ] **Step 2: Rewrite every failing test's embedded source string using the same mechanical rule as Task 4**

Apply the identical rewrite rule from Task 4 Step 2 (bare single lowercase letters in type position → uppercase per the `a→T, b→U, c→V, d→W` mapping, consistently per-test; enclosing parens → brackets per Task 1's grammar; variable/parameter names left untouched).

As a concrete worked example, the `identity` pattern around lines 877/909/1177:

```rust
type Box(a) =
  value: a

identity(value: a) -> a =
  value
```

becomes:

```rust
type Box[T] =
  value: T

identity(value: T) -> T =
  value
```

- [ ] **Step 3: Re-run until the codegen suite passes**

Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -150`
Expected: iterate Step 2 against each remaining failure until this command shows all tests PASS with unchanged expected values (e.g. `run_main` result assertions).

- [ ] **Step 4: Run the full workspace suite**

Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS across every crate (`tooling/tree-sitter-plum`'s corpus tests were already verified in Task 1 via `tree-sitter test`, which is a separate command from `cargo test`).

- [ ] **Step 5: Commit**

```bash
git add plum-wasm-codegen/tests/codegen_tests.rs
git commit -m "test(plum-wasm-codegen): migrate test fixtures to bracket generics syntax"
```

---

### Task 6: Rewrite the stdlib (`libs/std`)

**Files:**
- Modify: `libs/std/list.plum`
- Modify: `libs/std/map.plum`
- Modify: `libs/std/option.plum`
- Modify: `libs/std/result.plum`

**Interfaces:**
- Consumes: the new grammar (Task 1) — these files must parse under it.
- Produces: stdlib source using bracket/uppercase generics throughout, matching the `Node[T]` shape already present in `list.plum`.

- [ ] **Step 1: Rewrite `libs/std/option.plum`**

Replace the entire file contents with:

```
module std

enum Option =
  | Some[T]
  | None
```

- [ ] **Step 2: Rewrite `libs/std/result.plum`**

Replace the entire file contents with:

```
module std

enum Result =
  | Ok[T]
  | Err[E]

# checks whether the result is an Ok value
isOk<Result>(self) -> Bool =
  match self
    Ok(_) =>
      True
    Err(_) =>
      False
```

- [ ] **Step 3: Rewrite `libs/std/map.plum`**

Replace the entire file contents with:

```
module std

# A Pair is a grouping of a key with a value
type Pair[K, V] =
  key: K
  val: V

# A Map is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
# A Map is not an array. A slice describes a piece of an array.
type Map[K, V] =
  items: List[Pair[K, V]]

init<Map>(self, kvs: ...Pair) -> Map =
  Map().add(kvs)

# adds the specified elements to the start of the list
add<Map>(self, kvs: ...Pair) =
  self.items.add(kvs)

# gets a value from the Map using key k
get<Map>(self, k: K) -> Option[V] =
  for p in self.items
    if p.key == k
      return Some(p.val)
  None

# puts a value into the Map
set<Map>(self, k: K, v: V) =
  self.items.add(Pair(key: k, val: v))

# puts a value into the Map if its not already present
putIfAbsent<Map>(self, k: K, v: V) =
  todo

map<Map>(self, cb: fn(Pair[K, V]) -> Pair[X, Y]) -> Map[X, Y] =
  self.items.map(cb)
```

- [ ] **Step 4: Rewrite `libs/std/list.plum`**

Replace the entire file contents with:

```
module std

import std/option

# 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]

# A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
# It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
type List[T: Stringable](Stringable) =
  head: Option[Node]
  tail: Option[Node]
  size: Int

makeList(values: ...T) -> List =
  List(None, None, 0).add(values)

# gets the element at i'th index of the list
get<List>(self, i: Int) -> Option[T] =
  current = self.head
  index = 0
  while current != None
    match current
      Some(node) =>
        if index == i
          return Some(node.value)
        current = node.next
        index = index + 1
      None =>
        break
  None

# sets the element at i'th index of the list
set<List>(self, i: Int, v: T) -> Option[T] =
  todo

# returns the no of elements in the list
length<List>(self) -> Int =
  self.size

# adds the specified elements to the start of the list
add<List>(self, values: ...T) =
  todo

# removes the element at i'th index of the list
removeAt<List>(self, i: Int) =
  todo

# removes the element v from list
remove<List>(self, v: T) =
  todo

# removes all objects from this list
clear<List>(self) =
  todo

# returns a new list with the elements in reverse order.
reverse<List>(self, v: fn(T) -> Bool) -> List =
  todo

# returns a new list with the elements sorted by sorter
sort<List>(self, sorter: fn(T) -> Bool) -> List =
  todo

# returns an item and index in the list if the item is is equal to search item
find<List>(self, search: T) -> Option[T] =
  todo

# returns the index of an item in the list if present and comparable otherwise None
contains<List>(self, v: T) -> Bool =
  todo

# calls f for each elem in the list
each<List>(self, cb: fn(T)) -> Unit =
  current = self.head
  while current != None
    match current
      Some(node) =>
        cb(node.value)
        current = node.next
      None =>
        break

# returns a list made up of b elements for each elem in the list
map<List>(self, cb: fn(T) -> U) -> List[U] =
  nl = List()
  current = self.head
  while current != None
    match current
      Some(node) =>
        item = cb(node.value)
        nl.add(item)
        current = node.next
      None =>
        break
  nl

# returns a new list with each element flat-mapped
flatMap<List>(self) =
  todo

# returns a new list with the elements that matched the predicate
retain<List>(self, predicate: fn(T) -> T) -> List =
  todo

# returns a new list with the elements that matched the predicate removed
reject<List>(self, predicate: fn(T) -> T) -> List =
  todo

# returns true if any element in the list satisfies the predicate
any<List>(self, predicate: fn(T) -> Bool) -> Bool =
  todo

# returns true if all of the elements in the list satisfies the predicate
every<List>(self, predicate: fn(T) -> Bool) -> Bool =
  todo

# returns the accumulated value of all the elements in the list
reduce<List>(self, acc: U, cb: fn(T) -> T) -> Option[U] =
  todo

# returns the first element in the list
first<List>(self) -> Option[T] =
  match self.head
    Some(node) =>
      Some(node.value)
    None =>
      None

# returns the last element in the list
last<List>(self) -> Option[T] =
  match self.tail
    Some(node) =>
      Some(node.value)
    None =>
      None

# returns a list containing the first n elements of the given list
sublist<List>(self, start: Int, end: Int) -> List =
  todo

# returns a list containing the first n elements of the given list
take<List>(self, n: Int) -> List =
  todo

# returns a list containing the first n elements of the given list
skip<List>(self, n: Int) -> List =
  todo

# returns a list containing the first n elements of the given list
drop<List>(self, n: Int) -> List =
  todo

# returns a new list with some of the elements taken randomly
sample<List>(self) =
  todo

# returns a new list with all elements shuffled
shuffle<List>(self) =
  todo

# returns a new list with all elements grouped by adjacent pairs
partition<List>(self) =
  todo

# returns a new list with all elements grouped into chunks
chunk<List>(self) =
  todo

# returns a new list with all elements grouped
groupBy<List>(self) =
  todo

join<List>(self, sep: Str = ",") -> Str =
  res = Buffer()
  self.each(|v|
    res.write(v.toStr())
    res.write(sep)
  )
  res.toStr()
```

(This is a straight letter-case/bracket rewrite of the file's pre-existing content — no method bodies change, including the still-`todo` ones. `Node[T]`'s already-migrated shape on disk is preserved as-is.)

- [ ] **Step 5: Confirm these files aren't exercised by any Rust test (so this step can't be verified by `cargo test`)**

Run: `grep -rn "libs/std" plum-checker/tests plum-wasm-codegen/tests plum-core/tests 2>/dev/null`
Expected: no output (or only unrelated matches) — confirming, per the design spec's survey, that no existing Rust test currently loads these files directly, so `cargo test --workspace` passing in Task 5 is unaffected by this task, and there is no automated check for these `.plum` files' correctness beyond `plum-cli` manually loading them (out of scope here — see Global Constraints on the blocked `list-methods` plan, which is what will eventually exercise `list.plum` end-to-end).

- [ ] **Step 6: Commit**

```bash
git add libs/std/option.plum libs/std/result.plum libs/std/map.plum libs/std/list.plum
git commit -m "feat(libs/std): migrate stdlib generics to bracket syntax"
```

---

### Task 7: Rewrite `examples/types.plum`

**Files:**
- Modify: `examples/types.plum`

**Interfaces:**
- Consumes: the new grammar (Task 1).
- Produces: the example file using bracket/uppercase generics for its one generic class and one generic trait; its non-generic declarations (`Point`, `Named(Stringable)`, `Shape`, `Color`, `Option`) are untouched.

- [ ] **Step 1: Rewrite the file**

Replace the entire file contents with:

```
type Point =
  x: Int
  y: Int

type Named(Stringable) =
  name: Str

type Box[T] =
  value: T

trait Shape =
  area() -> Float
  perimeter() -> Float

trait Comparable[T: Ord] =
  compareTo(other: T) -> Int

enum Color =
  | Red
  | Green
  | Blue

enum Option =
  | Some(Int)
  | None

makeIntBox() -> Box =
  Box(value: 5)

makeStrBox() -> Box =
  Box(value: "x")
```

- [ ] **Step 2: Confirm this file isn't parsed by any automated test**

Run: `grep -rn "examples/types" plum-checker/tests plum-wasm-codegen/tests plum-core/tests plum-cli 2>/dev/null`
Expected: no output — same reasoning as Task 6 Step 5; this file exists as a hand-written reference example, not something `cargo test` exercises.

- [ ] **Step 3: Commit**

```bash
git add examples/types.plum
git commit -m "feat(examples): migrate types.plum generics to bracket syntax"
```

---

### Task 8: Final full-workspace verification

**Files:** none (verification only).

**Interfaces:**
- Consumes: everything from Tasks 1-7.
- Produces: confirmation that the migration is complete and the workspace is green end-to-end.

- [ ] **Step 1: Run the full Rust test suite**

Run: `cargo test --workspace 2>&1 | tail -100`
Expected: all tests PASS.

- [ ] **Step 2: Run the tree-sitter corpus suite**

Run: `cd tooling/tree-sitter-plum && npx tree-sitter test 2>&1 | tail -60`
Expected: all tests PASS.

- [ ] **Step 3: Confirm no old-syntax generics remain in tracked `.plum` files**

Run: `grep -rn '([a-d])\|([a-d],\|(Stringable)([a-d]\|| Some(a)\|| Ok(a)\|| Err(b)' libs/std examples 2>/dev/null`
Expected: no output. (This is a narrow sanity grep for the exact old patterns this plan rewrote — not exhaustive old-syntax detection, since `a`/`b`/`c`/`d` as ordinary variable names elsewhere are legitimate and would false-positive on a broader pattern.)

- [ ] **Step 4: No commit needed**

This task is verification-only; if Steps 1-3 all pass, the migration is complete and every prior task's commit already captured the corresponding changes.