plum

#treesitter#compiler#wasm

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

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


docs/superpowers/specs/2026-07-24-nested-method-declarations-design.md
# Nested Method Declarations — Design Spec

## Goal

Let methods be declared indented directly inside a `type`/`enum` body,
implicitly bound to that type as their receiver, instead of always requiring
a separate top-level declaration with an explicit `<Receiver>` annotation:

```
enum Step(n: Int) =
  | READ_MIN_OCCURANCES(0)
  | READ_MAX_OCCURANCES(1)
  | READ_CHAR_TO_COUNT(2)
  | COUNT_OCCURANCES(3)

  toNumber(self) =
    match self
      READ_MIN_OCCURANCES => 0
      READ_MAX_OCCURANCES => 1
      READ_CHAR_TO_COUNT => 2
      COUNT_OCCURANCES => 3
```

`toNumber(self) = ...` here is exactly equivalent to writing
`toNumber<Step>(self) = ...` at the top level today — nesting inside the
enum body is sugar for the receiver annotation, not a new binding mechanism.

## Non-goals

- **Purely additive, not a replacement.** Today's top-level
  `methodName<Receiver>(...) = ...` form keeps working unchanged, and can be
  freely mixed with nested declarations for the same type (some methods
  nested, some declared the old way) — this spec only adds a second surface
  syntax for the same underlying `ast::Fn { type_param: Some(receiver), ... }`
  shape; the checker and codegen need no awareness that this feature exists.
- **`trait` bodies are out of scope.** A trait's body holds method
  *signatures* (`trait_field`, no body) — nesting a full method
  *implementation* inside a trait would be a "default method" feature,
  a materially different and separate concept from what's being asked for
  here (which is about `enum`/`type`, per the motivating example). Not
  addressed by this spec.
- No change to how the receiver annotation `<Receiver>` behaves when used
  explicitly at the top level — untouched.

## Current state

- `class`/`enum` grammar rules (`tooling/tree-sitter-plum/grammar.js`) only
  allow `repeat(alias($.class_field, $.field))` /
  `optional(repeat(alias($.enum_field, $.field)))` inside their body — no
  path for a full `fn` declaration to appear nested inside either.
- `fn` (top-level today) already supports an optional receiver annotation:
  `field("type", optional(alias($.fn_type, $.type)))`, parsed by
  `plum-core/src/parser.rs`'s `parse_fn` into `ast::Fn.type_param: Option<String>`.
  A method declared today (`toNumber<Step>(self) -> Int = ...`) already
  produces exactly the AST shape this feature's nested form should also
  produce — the only thing missing is a grammar/parser path that arrives at
  that same shape without writing `<Step>` explicitly, by inferring it from
  nesting position instead.
- `parse_source` (`plum-core/src/parser.rs`) builds `Source.items: Vec<Item>`
  by matching each top-level child's `kind()` (`"class"`, `"trait"`,
  `"enum"`, `"fn"`, `"const"`) to one `Item` each — there is currently no
  concept of one top-level declaration producing MORE than one `Item`.

## Design

### Grammar

`class` and `enum` each gain an optional trailing `repeat($.fn)` after their
existing fields, reusing the `fn` rule completely unmodified (a nested
method is byte-for-byte the same grammar as a top-level method, just
appearing at a different position in the tree — including still allowing
(but not requiring) its own explicit `<Receiver>` annotation, which would be
redundant but not a parse error, same as any other harmless redundancy the
grammar doesn't specifically forbid elsewhere):

```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)))),
    field("methods", optional(repeat($.fn))),
    $._dedent,
  ),

enum: ($) =>
  seq(
    "enum",
    field("name", $.type_identifier),
    field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
    "=",
    $._indent,
    optional(repeat(alias($.enum_field, $.field))),
    field("methods", optional(repeat($.fn))),
    $._dedent,
  ),
```

Fields and methods are distinguished structurally without any new
lookahead/conflict: a `class_field`/`enum_field` always starts
`identifier ":" ...` (or `"|" identifier ...` for enum variants), while `fn`
always starts `identifier ("<" ... ">")? "(" ...` — the token immediately
after the leading identifier (`:` vs `<`/`(`) already disambiguates them, the
same way the grammar already disambiguates other same-position alternatives
elsewhere.

### Parser

Rather than changing `parse_class`/`parse_enum`'s existing return types
(`Class`/`Enum`, unchanged — no other caller/test should need to know this
feature exists), add one new helper:

```rust
/// Collects any `fn` named children nested directly inside a class/enum/etc.
/// body and parses each as an ordinary top-level `Fn`, with `type_param`
/// forced to `owner` regardless of whatever the nested `fn` itself parsed
/// (a nested method's receiver is implicit from its enclosing declaration,
/// not from its own optional `<Receiver>` annotation — which, if written
/// explicitly and redundantly, is simply overridden, not treated as a
/// conflict/error).
fn collect_nested_fns(&self, node: Node, owner: &str) -> Vec<Fn> {
    self.children_of_kind(node, "fn")
        .into_iter()
        .map(|n| {
            let mut f = self.parse_fn(n);
            f.type_param = Some(owner.to_string());
            f
        })
        .collect()
}
```

`parse_source` calls this alongside `parse_class`/`parse_enum` for each
`"class"`/`"enum"` top-level child, pushing the resulting `Fn`s as
additional `Item::Fn(...)` entries immediately after the owning
`Item::Class(...)`/`Item::Enum(...)` — i.e. one `class`/`enum` grammar node
can now expand to MULTIPLE `Item`s in `Source.items`, in declaration order
(class/enum first, then each of its nested methods in the order written).
This is the one structural change to `parse_source`'s loop; everything
downstream of `Source.items` (checker, codegen) already iterates
`Item::Fn` as a flat list and needs no changes — a nested method is
indistinguishable from a top-level one by the time it reaches `Item::Fn`.

### Checker / Codegen

No changes. By construction, a nested method desugars to exactly the
`ast::Fn { type_param: Some(receiver), ... }` shape a top-level
`<Receiver>`-annotated method already produces — every downstream consumer
(dispatch resolution, monomorphization, codegen) is already correct for
that shape and has no way to observe which surface syntax produced it.

### Testing strategy

- Tree-sitter corpus: an `enum`/`type` with fields followed by one or more
  nested `fn` declarations, confirming the new `fn`-inside-`enum`/`class`
  node shape parses; confirm a `class`/`enum` with NO nested methods
  (today's shape) is unaffected; confirm a `trait` is unaffected (no `fn`
  nesting added there).
- `plum-core` parser tests: parsing a nested method produces an `Item::Fn`
  with `type_param == Some("EnclosingTypeName")`, appearing in `Source.items`
  immediately after the owning `Item::Class`/`Item::Enum`, in the same
  relative order as multiple nested methods were written.
- `plum-checker`/`plum-wasm-codegen` tests: a nested method type-checks,
  dispatches, and runs identically to the same method written in today's
  top-level `<Receiver>` form (e.g. compile the `Step`/`toNumber` example
  from this spec's Goal section, or a smaller equivalent, and confirm it
  runs correctly end-to-end) — proving the desugaring is truly
  behavior-identical, not just parse-identical.