plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
c664d4f
— Peter John
2026-07-24T12:22:08+05:30
docs: add design specs for enum discriminant values and nested methods
docs/superpowers/specs/2026-07-24-enum-discriminant-values-design.md
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# Enum Discriminant Values — Design Spec
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Let an enum declare one or more shared, uniformly-typed fields that every
|
|
6
|
+
variant supplies a concrete value for, readable via ordinary field access
|
|
7
|
+
(`self.n`) regardless of which variant a given instance actually is:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
enum Step(n: Int) =
|
|
11
|
+
| READ_MIN_OCCURANCES(0)
|
|
12
|
+
| READ_MAX_OCCURANCES(1)
|
|
13
|
+
| READ_CHAR_TO_COUNT(2)
|
|
14
|
+
| COUNT_OCCURANCES(3)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Here every `Step` value carries an `Int` field `n`; `READ_MIN_OCCURANCES.n`
|
|
18
|
+
(or `self.n` inside a method) reads `0` without needing a `match`. This is
|
|
19
|
+
different from today's per-variant payload (`Some(a)`/`Ok(a)`), where each
|
|
20
|
+
variant's payload has a different shape and can only be read by destructuring
|
|
21
|
+
via `match`.
|
|
22
|
+
|
|
23
|
+
## Non-goals
|
|
24
|
+
|
|
25
|
+
- No mixing of enum-level discriminant params with today's per-variant
|
|
26
|
+
generic-type payload (`| Some[T]`) in the same enum — an enum is either
|
|
27
|
+
a "discriminant enum" (this feature) or an ordinary/generic enum
|
|
28
|
+
(today's feature), not both, for v1. If a real use case for combining them
|
|
29
|
+
shows up later, that's a follow-up.
|
|
30
|
+
- No change to how ordinary enums (`Option`, `Result`, `Bool`, `Color`, ...)
|
|
31
|
+
are declared, checked, or compiled — this is purely additive.
|
|
32
|
+
- No automatic derivation of a discriminant from declaration order (that's
|
|
33
|
+
`plum-checker`'s existing internal *tag* numbering, which already exists
|
|
34
|
+
and is unrelated — see Current State). This feature is about a
|
|
35
|
+
user-declared, user-typed, user-readable field, not the internal tag.
|
|
36
|
+
|
|
37
|
+
## Current state (relevant constraints found during design)
|
|
38
|
+
|
|
39
|
+
This is the key architectural fact the design has to work around:
|
|
40
|
+
|
|
41
|
+
- **Payload-free variants are bare integers today, not heap values.** A
|
|
42
|
+
nullary variant like `None`/`True`/`Red` compiles to a plain `i32.const`
|
|
43
|
+
with **no heap allocation** (`plum-wasm-codegen`'s
|
|
44
|
+
`compile_variant_construction`: `if field_types.is_empty() { return
|
|
45
|
+
I32Const(tag) }`). This is a real optimization the checker/codegen rely on
|
|
46
|
+
elsewhere (pattern-match codegen explicitly branches on "is this variant a
|
|
47
|
+
small int or a heap pointer").
|
|
48
|
+
- **Payload variants are heap-allocated as `[tag: i32][field0][field1]...`**,
|
|
49
|
+
each slot at an 8-byte stride, sized independently per variant (no
|
|
50
|
+
"widest variant" padding).
|
|
51
|
+
- **Field access (`self.field`) is class-only today.** It requires the
|
|
52
|
+
receiver's type to resolve to a `Class` in `ClassEnv`, looks up the
|
|
53
|
+
field's positional index, and loads at `field_idx * 8` from the class's
|
|
54
|
+
heap pointer. Enums have no entry in `ClassEnv` and are never consulted
|
|
55
|
+
by this code path today.
|
|
56
|
+
- **A bare variant reference used as a value** (`None`, `True`, `Red` with
|
|
57
|
+
no explicit constructor call) always compiles to just the constant tag
|
|
58
|
+
integer — there is no existing mechanism for a bare reference to also
|
|
59
|
+
carry an accompanying value.
|
|
60
|
+
|
|
61
|
+
The consequence: a variant that carries a discriminant field **cannot** use
|
|
62
|
+
today's bare-int optimization — if `READ_MIN_OCCURANCES` compiled to a plain
|
|
63
|
+
`i32.const 0` tag, there'd be nowhere to also store the field value `0`
|
|
64
|
+
itself (and no way to tell "tag" from "field value" if they happened to
|
|
65
|
+
collide numerically for a different variant). So a discriminant enum's
|
|
66
|
+
variants must **always heap-allocate**, exactly like today's payload
|
|
67
|
+
variants — this feature reuses that existing scheme rather than inventing a
|
|
68
|
+
new one.
|
|
69
|
+
|
|
70
|
+
## Design
|
|
71
|
+
|
|
72
|
+
### Grammar
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
enum: ($) =>
|
|
76
|
+
seq(
|
|
77
|
+
"enum",
|
|
78
|
+
field("name", $.type_identifier),
|
|
79
|
+
field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
|
|
80
|
+
"=",
|
|
81
|
+
$._indent,
|
|
82
|
+
optional(repeat(alias($.enum_field, $.field))),
|
|
83
|
+
$._dedent,
|
|
84
|
+
),
|
|
85
|
+
|
|
86
|
+
enum_param: ($) =>
|
|
87
|
+
seq(field("name", $.var_identifier), ":", field("type", $.type)),
|
|
88
|
+
|
|
89
|
+
enum_field: ($) =>
|
|
90
|
+
seq(
|
|
91
|
+
"|",
|
|
92
|
+
field("name", $.type_identifier),
|
|
93
|
+
field(
|
|
94
|
+
"parameters",
|
|
95
|
+
optional(choice(
|
|
96
|
+
seq("[", commaSep1(choice($.type_identifier, $.generic)), "]"), // existing: generic type payload
|
|
97
|
+
seq("(", commaSep1($.expression), ")"), // new: discriminant value literals
|
|
98
|
+
)),
|
|
99
|
+
),
|
|
100
|
+
),
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The bracket form (`[...]`, existing) and the paren form (`(...)`, new) are
|
|
104
|
+
distinguished by delimiter, matching the rest of the language's convention
|
|
105
|
+
post-migration: brackets for type-level content, parens for value-level
|
|
106
|
+
content — a discriminant value list is values, not types, so it naturally
|
|
107
|
+
takes parens, the same delimiter a constructor call already uses for its
|
|
108
|
+
arguments.
|
|
109
|
+
|
|
110
|
+
### AST
|
|
111
|
+
|
|
112
|
+
```rust
|
|
113
|
+
pub struct Enum {
|
|
114
|
+
pub name: String,
|
|
115
|
+
pub params: Vec<EnumParam>, // new; empty for every enum declared without "(...)"
|
|
116
|
+
pub variants: Vec<EnumVariant>,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub struct EnumParam { // new
|
|
120
|
+
pub name: String,
|
|
121
|
+
pub ty: Type,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub struct EnumVariant {
|
|
125
|
+
pub name: String,
|
|
126
|
+
pub fields: Vec<String>, // existing: generic/type payload names, unchanged meaning
|
|
127
|
+
pub values: Vec<Expr>, // new: discriminant literal values, empty for ordinary variants
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
A variant has either a non-empty `fields` (today's generic payload) or a
|
|
132
|
+
non-empty `values` (this feature), never both — enforced by the checker
|
|
133
|
+
(see below), not the grammar (the grammar can't tell a bare type name from a
|
|
134
|
+
literal expression apart at the same position without this kind of
|
|
135
|
+
cross-check, so it accepts either shape structurally and the checker
|
|
136
|
+
rejects the mixed case with a clear error).
|
|
137
|
+
|
|
138
|
+
### Checker
|
|
139
|
+
|
|
140
|
+
- **Declaration-time validation:** for an enum with non-empty `params`,
|
|
141
|
+
every variant must supply exactly `params.len()` values, and each value's
|
|
142
|
+
inferred type must unify with the corresponding param's declared type
|
|
143
|
+
(e.g. the literal `0` against `Int`). A variant supplying the wrong count,
|
|
144
|
+
or a value of the wrong type, is a checker error. An enum with EMPTY
|
|
145
|
+
`params` must have every variant's `values` also empty (this is what "an
|
|
146
|
+
enum is either a discriminant enum or an ordinary enum" means in
|
|
147
|
+
practice) — a variant with non-empty `values` on a param-less enum is
|
|
148
|
+
also a checker error (most likely to happen by a typo like accidentally
|
|
149
|
+
writing `Some(5)` instead of `Some[Int]` for an ordinary generic enum;
|
|
150
|
+
the error should say so plainly).
|
|
151
|
+
- **Field access (`self.n`, `obj.n`):** extend the existing field-access
|
|
152
|
+
check (today: receiver type must resolve to a `Class` in `ClassEnv`) with
|
|
153
|
+
a second lookup path: if the receiver's type resolves to an `Enum` name
|
|
154
|
+
that has non-empty `params` (tracked in a new `EnumParams` map alongside
|
|
155
|
+
the existing `EnumVariants`/`ClassEnv` context maps), resolve the field
|
|
156
|
+
by name against that enum's declared params exactly as a class field
|
|
157
|
+
would be resolved — same "field not found" error shape, just checked
|
|
158
|
+
against `EnumParams` instead of `ClassEnv` when the name isn't a class.
|
|
159
|
+
Field access on an enum with EMPTY params still falls through to today's
|
|
160
|
+
behavior (class-only, or the permissive "unmodeled type" escape hatch)
|
|
161
|
+
— unchanged.
|
|
162
|
+
|
|
163
|
+
### Codegen
|
|
164
|
+
|
|
165
|
+
- **Construction of a bare discriminant-variant reference**
|
|
166
|
+
(`READ_MIN_OCCURANCES` used as a value, no explicit call): today this
|
|
167
|
+
compiles `Expr::TypeName(n)` to a bare `I32Const(tag)` when the variant
|
|
168
|
+
is payload-free. For a variant belonging to a discriminant enum (non-empty
|
|
169
|
+
declared `values`), codegen instead reuses the EXISTING payload-variant
|
|
170
|
+
construction path (`compile_variant_construction`) directly, using the
|
|
171
|
+
variant's own declared literal `values` (compiled as constants) as the
|
|
172
|
+
"field values" to store — structurally as if the user had written
|
|
173
|
+
`READ_MIN_OCCURANCES(0)` explicitly at every use site, except the `0`
|
|
174
|
+
comes from the variant's declaration, not the call site (there is no call
|
|
175
|
+
site; syntactically it's still a bare reference). This means these
|
|
176
|
+
variants always heap-allocate, matching the Current State constraint
|
|
177
|
+
above — the bare-int optimization is simply not applied to them.
|
|
178
|
+
- **Field access codegen (`self.n`):** mirrors the existing class field-load
|
|
179
|
+
codegen exactly (`I32Load`/`I64Load`/`F64Load` at `(field_idx + 1) * 8`
|
|
180
|
+
from the heap pointer — `+1` because slot 0 is always the tag, matching
|
|
181
|
+
today's payload-variant layout) — the only change is resolving
|
|
182
|
+
`field_idx`/width from the enum's declared params (via the new
|
|
183
|
+
`EnumParams` lookup) instead of `ClassEnv`, when the receiver's static
|
|
184
|
+
type is a discriminant enum rather than a class.
|
|
185
|
+
- **Pattern matching** (`match` against a discriminant-enum value by variant
|
|
186
|
+
name, e.g. `match step \n READ_MIN_OCCURANCES => ...`) is unaffected —
|
|
187
|
+
since these variants are now always heap pointers (never bare ints), the
|
|
188
|
+
existing payload-variant tag-load-and-compare path in match codegen
|
|
189
|
+
applies uniformly, with no need for the "is this a small int or a
|
|
190
|
+
pointer" branch discriminant enums currently force ordinary enums to
|
|
191
|
+
have (every variant of a discriminant enum is a pointer, so that branch
|
|
192
|
+
is simply never taken for this kind of enum).
|
|
193
|
+
|
|
194
|
+
### Testing strategy
|
|
195
|
+
|
|
196
|
+
- Tree-sitter corpus: a discriminant-enum declaration (`enum Step(n: Int) =
|
|
197
|
+
| READ_MIN_OCCURANCES(0) | ...`), confirming the new `enum_param` and the
|
|
198
|
+
paren-form `enum_field` parameters parse into the expected tree; confirm
|
|
199
|
+
today's bracket-form (`| Some[T]`) and truly nullary (`| Red`) variants
|
|
200
|
+
are unaffected.
|
|
201
|
+
- `plum-checker` tests: correct arity/type-checked discriminant values pass;
|
|
202
|
+
wrong arity, wrong value type, and mixing discriminant values into a
|
|
203
|
+
param-less enum's variant each produce a clear error; `self.n` field
|
|
204
|
+
access on a discriminant-enum method type-checks and resolves to the
|
|
205
|
+
right type; `self.n` on an ordinary enum (no declared params) still
|
|
206
|
+
errors exactly as it does today.
|
|
207
|
+
- `plum-wasm-codegen` tests: a method that reads `self.n` off a
|
|
208
|
+
discriminant-enum value returns the declared literal for each variant
|
|
209
|
+
(covering at least two different variants, to prove the field is
|
|
210
|
+
correctly positioned per-instance, not hardcoded to one variant's
|
|
211
|
+
value); confirm a discriminant-enum value used in `match` (destructuring
|
|
212
|
+
by variant name, not by field) still resolves and runs correctly,
|
|
213
|
+
exercising the "always a pointer, never a bare int" codegen path noted
|
|
214
|
+
above.
|
docs/superpowers/specs/2026-07-24-nested-method-declarations-design.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# Nested Method Declarations — Design Spec
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
|
|
5
|
+
Let methods be declared indented directly inside a `type`/`enum` body,
|
|
6
|
+
implicitly bound to that type as their receiver, instead of always requiring
|
|
7
|
+
a separate top-level declaration with an explicit `<Receiver>` annotation:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
enum Step(n: Int) =
|
|
11
|
+
| READ_MIN_OCCURANCES(0)
|
|
12
|
+
| READ_MAX_OCCURANCES(1)
|
|
13
|
+
| READ_CHAR_TO_COUNT(2)
|
|
14
|
+
| COUNT_OCCURANCES(3)
|
|
15
|
+
|
|
16
|
+
toNumber(self) =
|
|
17
|
+
match self
|
|
18
|
+
READ_MIN_OCCURANCES => 0
|
|
19
|
+
READ_MAX_OCCURANCES => 1
|
|
20
|
+
READ_CHAR_TO_COUNT => 2
|
|
21
|
+
COUNT_OCCURANCES => 3
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`toNumber(self) = ...` here is exactly equivalent to writing
|
|
25
|
+
`toNumber<Step>(self) = ...` at the top level today — nesting inside the
|
|
26
|
+
enum body is sugar for the receiver annotation, not a new binding mechanism.
|
|
27
|
+
|
|
28
|
+
## Non-goals
|
|
29
|
+
|
|
30
|
+
- **Purely additive, not a replacement.** Today's top-level
|
|
31
|
+
`methodName<Receiver>(...) = ...` form keeps working unchanged, and can be
|
|
32
|
+
freely mixed with nested declarations for the same type (some methods
|
|
33
|
+
nested, some declared the old way) — this spec only adds a second surface
|
|
34
|
+
syntax for the same underlying `ast::Fn { type_param: Some(receiver), ... }`
|
|
35
|
+
shape; the checker and codegen need no awareness that this feature exists.
|
|
36
|
+
- **`trait` bodies are out of scope.** A trait's body holds method
|
|
37
|
+
*signatures* (`trait_field`, no body) — nesting a full method
|
|
38
|
+
*implementation* inside a trait would be a "default method" feature,
|
|
39
|
+
a materially different and separate concept from what's being asked for
|
|
40
|
+
here (which is about `enum`/`type`, per the motivating example). Not
|
|
41
|
+
addressed by this spec.
|
|
42
|
+
- No change to how the receiver annotation `<Receiver>` behaves when used
|
|
43
|
+
explicitly at the top level — untouched.
|
|
44
|
+
|
|
45
|
+
## Current state
|
|
46
|
+
|
|
47
|
+
- `class`/`enum` grammar rules (`tooling/tree-sitter-plum/grammar.js`) only
|
|
48
|
+
allow `repeat(alias($.class_field, $.field))` /
|
|
49
|
+
`optional(repeat(alias($.enum_field, $.field)))` inside their body — no
|
|
50
|
+
path for a full `fn` declaration to appear nested inside either.
|
|
51
|
+
- `fn` (top-level today) already supports an optional receiver annotation:
|
|
52
|
+
`field("type", optional(alias($.fn_type, $.type)))`, parsed by
|
|
53
|
+
`plum-core/src/parser.rs`'s `parse_fn` into `ast::Fn.type_param: Option<String>`.
|
|
54
|
+
A method declared today (`toNumber<Step>(self) -> Int = ...`) already
|
|
55
|
+
produces exactly the AST shape this feature's nested form should also
|
|
56
|
+
produce — the only thing missing is a grammar/parser path that arrives at
|
|
57
|
+
that same shape without writing `<Step>` explicitly, by inferring it from
|
|
58
|
+
nesting position instead.
|
|
59
|
+
- `parse_source` (`plum-core/src/parser.rs`) builds `Source.items: Vec<Item>`
|
|
60
|
+
by matching each top-level child's `kind()` (`"class"`, `"trait"`,
|
|
61
|
+
`"enum"`, `"fn"`, `"const"`) to one `Item` each — there is currently no
|
|
62
|
+
concept of one top-level declaration producing MORE than one `Item`.
|
|
63
|
+
|
|
64
|
+
## Design
|
|
65
|
+
|
|
66
|
+
### Grammar
|
|
67
|
+
|
|
68
|
+
`class` and `enum` each gain an optional trailing `repeat($.fn)` after their
|
|
69
|
+
existing fields, reusing the `fn` rule completely unmodified (a nested
|
|
70
|
+
method is byte-for-byte the same grammar as a top-level method, just
|
|
71
|
+
appearing at a different position in the tree — including still allowing
|
|
72
|
+
(but not requiring) its own explicit `<Receiver>` annotation, which would be
|
|
73
|
+
redundant but not a parse error, same as any other harmless redundancy the
|
|
74
|
+
grammar doesn't specifically forbid elsewhere):
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
class: ($) =>
|
|
78
|
+
seq(
|
|
79
|
+
"type",
|
|
80
|
+
field("name", $.type_identifier),
|
|
81
|
+
field("generics", optional($.generics)),
|
|
82
|
+
field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
|
|
83
|
+
"=",
|
|
84
|
+
$._indent,
|
|
85
|
+
field("fields", optional(repeat(alias($.class_field, $.field)))),
|
|
86
|
+
field("methods", optional(repeat($.fn))),
|
|
87
|
+
$._dedent,
|
|
88
|
+
),
|
|
89
|
+
|
|
90
|
+
enum: ($) =>
|
|
91
|
+
seq(
|
|
92
|
+
"enum",
|
|
93
|
+
field("name", $.type_identifier),
|
|
94
|
+
field("params", optional(seq("(", commaSep1($.enum_param), ")"))),
|
|
95
|
+
"=",
|
|
96
|
+
$._indent,
|
|
97
|
+
optional(repeat(alias($.enum_field, $.field))),
|
|
98
|
+
field("methods", optional(repeat($.fn))),
|
|
99
|
+
$._dedent,
|
|
100
|
+
),
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Fields and methods are distinguished structurally without any new
|
|
104
|
+
lookahead/conflict: a `class_field`/`enum_field` always starts
|
|
105
|
+
`identifier ":" ...` (or `"|" identifier ...` for enum variants), while `fn`
|
|
106
|
+
always starts `identifier ("<" ... ">")? "(" ...` — the token immediately
|
|
107
|
+
after the leading identifier (`:` vs `<`/`(`) already disambiguates them, the
|
|
108
|
+
same way the grammar already disambiguates other same-position alternatives
|
|
109
|
+
elsewhere.
|
|
110
|
+
|
|
111
|
+
### Parser
|
|
112
|
+
|
|
113
|
+
Rather than changing `parse_class`/`parse_enum`'s existing return types
|
|
114
|
+
(`Class`/`Enum`, unchanged — no other caller/test should need to know this
|
|
115
|
+
feature exists), add one new helper:
|
|
116
|
+
|
|
117
|
+
```rust
|
|
118
|
+
/// Collects any `fn` named children nested directly inside a class/enum/etc.
|
|
119
|
+
/// body and parses each as an ordinary top-level `Fn`, with `type_param`
|
|
120
|
+
/// forced to `owner` regardless of whatever the nested `fn` itself parsed
|
|
121
|
+
/// (a nested method's receiver is implicit from its enclosing declaration,
|
|
122
|
+
/// not from its own optional `<Receiver>` annotation — which, if written
|
|
123
|
+
/// explicitly and redundantly, is simply overridden, not treated as a
|
|
124
|
+
/// conflict/error).
|
|
125
|
+
fn collect_nested_fns(&self, node: Node, owner: &str) -> Vec<Fn> {
|
|
126
|
+
self.children_of_kind(node, "fn")
|
|
127
|
+
.into_iter()
|
|
128
|
+
.map(|n| {
|
|
129
|
+
let mut f = self.parse_fn(n);
|
|
130
|
+
f.type_param = Some(owner.to_string());
|
|
131
|
+
f
|
|
132
|
+
})
|
|
133
|
+
.collect()
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`parse_source` calls this alongside `parse_class`/`parse_enum` for each
|
|
138
|
+
`"class"`/`"enum"` top-level child, pushing the resulting `Fn`s as
|
|
139
|
+
additional `Item::Fn(...)` entries immediately after the owning
|
|
140
|
+
`Item::Class(...)`/`Item::Enum(...)` — i.e. one `class`/`enum` grammar node
|
|
141
|
+
can now expand to MULTIPLE `Item`s in `Source.items`, in declaration order
|
|
142
|
+
(class/enum first, then each of its nested methods in the order written).
|
|
143
|
+
This is the one structural change to `parse_source`'s loop; everything
|
|
144
|
+
downstream of `Source.items` (checker, codegen) already iterates
|
|
145
|
+
`Item::Fn` as a flat list and needs no changes — a nested method is
|
|
146
|
+
indistinguishable from a top-level one by the time it reaches `Item::Fn`.
|
|
147
|
+
|
|
148
|
+
### Checker / Codegen
|
|
149
|
+
|
|
150
|
+
No changes. By construction, a nested method desugars to exactly the
|
|
151
|
+
`ast::Fn { type_param: Some(receiver), ... }` shape a top-level
|
|
152
|
+
`<Receiver>`-annotated method already produces — every downstream consumer
|
|
153
|
+
(dispatch resolution, monomorphization, codegen) is already correct for
|
|
154
|
+
that shape and has no way to observe which surface syntax produced it.
|
|
155
|
+
|
|
156
|
+
### Testing strategy
|
|
157
|
+
|
|
158
|
+
- Tree-sitter corpus: an `enum`/`type` with fields followed by one or more
|
|
159
|
+
nested `fn` declarations, confirming the new `fn`-inside-`enum`/`class`
|
|
160
|
+
node shape parses; confirm a `class`/`enum` with NO nested methods
|
|
161
|
+
(today's shape) is unaffected; confirm a `trait` is unaffected (no `fn`
|
|
162
|
+
nesting added there).
|
|
163
|
+
- `plum-core` parser tests: parsing a nested method produces an `Item::Fn`
|
|
164
|
+
with `type_param == Some("EnclosingTypeName")`, appearing in `Source.items`
|
|
165
|
+
immediately after the owning `Item::Class`/`Item::Enum`, in the same
|
|
166
|
+
relative order as multiple nested methods were written.
|
|
167
|
+
- `plum-checker`/`plum-wasm-codegen` tests: a nested method type-checks,
|
|
168
|
+
dispatches, and runs identically to the same method written in today's
|
|
169
|
+
top-level `<Receiver>` form (e.g. compile the `Step`/`toNumber` example
|
|
170
|
+
from this spec's Goal section, or a smaller equivalent, and confirm it
|
|
171
|
+
runs correctly end-to-end) — proving the desugaring is truly
|
|
172
|
+
behavior-identical, not just parse-identical.
|