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
c664d4f 1
# Nested Method Declarations — Design Spec
c664d4f 2
c664d4f 3
## Goal
c664d4f 4
c664d4f 5
Let methods be declared indented directly inside a `type`/`enum` body,
c664d4f 6
implicitly bound to that type as their receiver, instead of always requiring
c664d4f 7
a separate top-level declaration with an explicit `<Receiver>` annotation:
c664d4f 8
c664d4f 9
```
c664d4f 10
enum Step(n: Int) =
c664d4f 11
  | READ_MIN_OCCURANCES(0)
c664d4f 12
  | READ_MAX_OCCURANCES(1)
c664d4f 13
  | READ_CHAR_TO_COUNT(2)
c664d4f 14
  | COUNT_OCCURANCES(3)
c664d4f 15
c664d4f 16
  toNumber(self) =
c664d4f 17
    match self
c664d4f 18
      READ_MIN_OCCURANCES => 0
c664d4f 19
      READ_MAX_OCCURANCES => 1
c664d4f 20
      READ_CHAR_TO_COUNT => 2
c664d4f 21
      COUNT_OCCURANCES => 3
c664d4f 22
```
c664d4f 23
c664d4f 24
`toNumber(self) = ...` here is exactly equivalent to writing
c664d4f 25
`toNumber<Step>(self) = ...` at the top level today — nesting inside the
c664d4f 26
enum body is sugar for the receiver annotation, not a new binding mechanism.
c664d4f 27
c664d4f 28
## Non-goals
c664d4f 29
c664d4f 30
- **Purely additive, not a replacement.** Today's top-level
c664d4f 31
  `methodName<Receiver>(...) = ...` form keeps working unchanged, and can be
c664d4f 32
  freely mixed with nested declarations for the same type (some methods
c664d4f 33
  nested, some declared the old way) — this spec only adds a second surface
c664d4f 34
  syntax for the same underlying `ast::Fn { type_param: Some(receiver), ... }`
c664d4f 35
  shape; the checker and codegen need no awareness that this feature exists.
c664d4f 36
- **`trait` bodies are out of scope.** A trait's body holds method
c664d4f 37
  *signatures* (`trait_field`, no body) — nesting a full method
c664d4f 38
  *implementation* inside a trait would be a "default method" feature,
c664d4f 39
  a materially different and separate concept from what's being asked for
c664d4f 40
  here (which is about `enum`/`type`, per the motivating example). Not
c664d4f 41
  addressed by this spec.
c664d4f 42
- No change to how the receiver annotation `<Receiver>` behaves when used
c664d4f 43
  explicitly at the top level — untouched.
c664d4f 44
c664d4f 45
## Current state
c664d4f 46
c664d4f 47
- `class`/`enum` grammar rules (`tooling/tree-sitter-plum/grammar.js`) only
c664d4f 48
  allow `repeat(alias($.class_field, $.field))` /
c664d4f 49
  `optional(repeat(alias($.enum_field, $.field)))` inside their body — no
c664d4f 50
  path for a full `fn` declaration to appear nested inside either.
c664d4f 51
- `fn` (top-level today) already supports an optional receiver annotation:
c664d4f 52
  `field("type", optional(alias($.fn_type, $.type)))`, parsed by
c664d4f 53
  `plum-core/src/parser.rs`'s `parse_fn` into `ast::Fn.type_param: Option<String>`.
c664d4f 54
  A method declared today (`toNumber<Step>(self) -> Int = ...`) already
c664d4f 55
  produces exactly the AST shape this feature's nested form should also
c664d4f 56
  produce — the only thing missing is a grammar/parser path that arrives at
c664d4f 57
  that same shape without writing `<Step>` explicitly, by inferring it from
c664d4f 58
  nesting position instead.
c664d4f 59
- `parse_source` (`plum-core/src/parser.rs`) builds `Source.items: Vec<Item>`
c664d4f 60
  by matching each top-level child's `kind()` (`"class"`, `"trait"`,
c664d4f 61
  `"enum"`, `"fn"`, `"const"`) to one `Item` each — there is currently no
c664d4f 62
  concept of one top-level declaration producing MORE than one `Item`.
c664d4f 63
c664d4f 64
## Design
c664d4f 65
c664d4f 66
### Grammar
c664d4f 67
c664d4f 68
`class` and `enum` each gain an optional trailing `repeat($.fn)` after their
c664d4f 69
existing fields, reusing the `fn` rule completely unmodified (a nested
c664d4f 70
method is byte-for-byte the same grammar as a top-level method, just
c664d4f 71
appearing at a different position in the tree — including still allowing
c664d4f 72
(but not requiring) its own explicit `<Receiver>` annotation, which would be
c664d4f 73
redundant but not a parse error, same as any other harmless redundancy the
c664d4f 74
grammar doesn't specifically forbid elsewhere):
c664d4f 75
c664d4f 76
```js
c664d4f 77
class: ($) =>
c664d4f 78
  seq(
c664d4f 79
    "type",
c664d4f 80
    field("name", $.type_identifier),
c664d4f 81
    field("generics", optional($.generics)),
c664d4f 82
    field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
c664d4f 83
    "=",
c664d4f 84
    $._indent,
c664d4f 85
    field("fields", optional(repeat(alias($.class_field, $.field)))),
c664d4f 86
    field("methods", optional(repeat($.fn))),
c664d4f 87
    $._dedent,
c664d4f 88
  ),
c664d4f 89
c664d4f 90
enum: ($) =>
c664d4f 91
  seq(
c664d4f 92
    "enum",
c664d4f 93
    field("name", $.type_identifier),
c664d4f 94
    field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
c664d4f 95
    "=",
c664d4f 96
    $._indent,
c664d4f 97
    optional(repeat(alias($.enum_field, $.field))),
c664d4f 98
    field("methods", optional(repeat($.fn))),
c664d4f 99
    $._dedent,
c664d4f 100
  ),
c664d4f 101
```
c664d4f 102
c664d4f 103
Fields and methods are distinguished structurally without any new
c664d4f 104
lookahead/conflict: a `class_field`/`enum_field` always starts
c664d4f 105
`identifier ":" ...` (or `"|" identifier ...` for enum variants), while `fn`
c664d4f 106
always starts `identifier ("<" ... ">")? "(" ...` — the token immediately
c664d4f 107
after the leading identifier (`:` vs `<`/`(`) already disambiguates them, the
c664d4f 108
same way the grammar already disambiguates other same-position alternatives
c664d4f 109
elsewhere.
c664d4f 110
c664d4f 111
### Parser
c664d4f 112
c664d4f 113
Rather than changing `parse_class`/`parse_enum`'s existing return types
c664d4f 114
(`Class`/`Enum`, unchanged — no other caller/test should need to know this
c664d4f 115
feature exists), add one new helper:
c664d4f 116
c664d4f 117
```rust
c664d4f 118
/// Collects any `fn` named children nested directly inside a class/enum/etc.
c664d4f 119
/// body and parses each as an ordinary top-level `Fn`, with `type_param`
c664d4f 120
/// forced to `owner` regardless of whatever the nested `fn` itself parsed
c664d4f 121
/// (a nested method's receiver is implicit from its enclosing declaration,
c664d4f 122
/// not from its own optional `<Receiver>` annotation — which, if written
c664d4f 123
/// explicitly and redundantly, is simply overridden, not treated as a
c664d4f 124
/// conflict/error).
c664d4f 125
fn collect_nested_fns(&self, node: Node, owner: &str) -> Vec<Fn> {
c664d4f 126
    self.children_of_kind(node, "fn")
c664d4f 127
        .into_iter()
c664d4f 128
        .map(|n| {
c664d4f 129
            let mut f = self.parse_fn(n);
c664d4f 130
            f.type_param = Some(owner.to_string());
c664d4f 131
            f
c664d4f 132
        })
c664d4f 133
        .collect()
c664d4f 134
}
c664d4f 135
```
c664d4f 136
c664d4f 137
`parse_source` calls this alongside `parse_class`/`parse_enum` for each
c664d4f 138
`"class"`/`"enum"` top-level child, pushing the resulting `Fn`s as
c664d4f 139
additional `Item::Fn(...)` entries immediately after the owning
c664d4f 140
`Item::Class(...)`/`Item::Enum(...)` — i.e. one `class`/`enum` grammar node
c664d4f 141
can now expand to MULTIPLE `Item`s in `Source.items`, in declaration order
c664d4f 142
(class/enum first, then each of its nested methods in the order written).
c664d4f 143
This is the one structural change to `parse_source`'s loop; everything
c664d4f 144
downstream of `Source.items` (checker, codegen) already iterates
c664d4f 145
`Item::Fn` as a flat list and needs no changes — a nested method is
c664d4f 146
indistinguishable from a top-level one by the time it reaches `Item::Fn`.
c664d4f 147
c664d4f 148
### Checker / Codegen
c664d4f 149
c664d4f 150
No changes. By construction, a nested method desugars to exactly the
c664d4f 151
`ast::Fn { type_param: Some(receiver), ... }` shape a top-level
c664d4f 152
`<Receiver>`-annotated method already produces — every downstream consumer
c664d4f 153
(dispatch resolution, monomorphization, codegen) is already correct for
c664d4f 154
that shape and has no way to observe which surface syntax produced it.
c664d4f 155
c664d4f 156
### Testing strategy
c664d4f 157
c664d4f 158
- Tree-sitter corpus: an `enum`/`type` with fields followed by one or more
c664d4f 159
  nested `fn` declarations, confirming the new `fn`-inside-`enum`/`class`
c664d4f 160
  node shape parses; confirm a `class`/`enum` with NO nested methods
c664d4f 161
  (today's shape) is unaffected; confirm a `trait` is unaffected (no `fn`
c664d4f 162
  nesting added there).
c664d4f 163
- `plum-core` parser tests: parsing a nested method produces an `Item::Fn`
c664d4f 164
  with `type_param == Some("EnclosingTypeName")`, appearing in `Source.items`
c664d4f 165
  immediately after the owning `Item::Class`/`Item::Enum`, in the same
c664d4f 166
  relative order as multiple nested methods were written.
c664d4f 167
- `plum-checker`/`plum-wasm-codegen` tests: a nested method type-checks,
c664d4f 168
  dispatches, and runs identically to the same method written in today's
c664d4f 169
  top-level `<Receiver>` form (e.g. compile the `Step`/`toNumber` example
c664d4f 170
  from this spec's Goal section, or a smaller equivalent, and confirm it
c664d4f 171
  runs correctly end-to-end) — proving the desugaring is truly
c664d4f 172
  behavior-identical, not just parse-identical.