plum

#treesitter#compiler#wasm

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

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


13fd165Peter John 2026-07-24T10:46:43+05:30
docs: add implementation plan for bracket generics syntax migration
docs/superpowers/plans/2026-07-24-bracket-generics-syntax.md ADDED
@@ -0,0 +1,1606 @@
1
+ # Bracket Generics Syntax Migration Implementation Plan
2
+
3
+ > **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.
4
+
5
+ **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.
6
+
7
+ **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.
8
+
9
+ **Tech Stack:** tree-sitter (JS grammar + generated Rust/C parser), Rust (`plum-core`, `plum-checker`, `plum-wasm-codegen`), Cargo workspace tests.
10
+
11
+ ## Global Constraints
12
+
13
+ - Spec: `docs/superpowers/specs/2026-07-24-bracket-generics-syntax-design.md`
14
+ - `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.
15
+ - Declaration order: generics-with-bounds first, implements-list second — `type List[T: Stringable](Stringable) =`.
16
+ - 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.
17
+ - Method receiver annotation (`get<List>(self, ...)`) is untouched — it's an unrelated mechanism.
18
+ - `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.
19
+ - `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.
20
+ - 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.
21
+ - 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.
22
+ - 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`.
23
+
24
+ ---
25
+
26
+ ### Task 1: Grammar changes and corpus tests (`tooling/tree-sitter-plum`)
27
+
28
+ **Files:**
29
+ - Modify: `tooling/tree-sitter-plum/grammar.js`
30
+ - Modify: `tooling/tree-sitter-plum/test/corpus/type.txt`
31
+ - Modify: `tooling/tree-sitter-plum/test/corpus/trait.txt`
32
+ - Modify: `tooling/tree-sitter-plum/test/corpus/enum.txt`
33
+ - Modify: `tooling/tree-sitter-plum/test/corpus/function.txt`
34
+
35
+ **Interfaces:**
36
+ - Consumes: nothing (this is the bottom of the pipeline).
37
+ - 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"`).
38
+
39
+ - [ ] **Step 1: Edit `grammar.js`'s generic/type-identifier tokens and the `generics` bracket**
40
+
41
+ In `tooling/tree-sitter-plum/grammar.js`, replace:
42
+
43
+ ```js
44
+ inline: ($) => [$.generic_type, $.generic],
45
+ ```
46
+
47
+ with:
48
+
49
+ ```js
50
+ inline: ($) => [$.generic_type],
51
+ ```
52
+
53
+ (`$.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.)
54
+
55
+ Replace:
56
+
57
+ ```js
58
+ generics: ($) => seq("(", commaSep1($.generic_type), ")"),
59
+ ```
60
+
61
+ with:
62
+
63
+ ```js
64
+ generics: ($) => seq("[", commaSep1($.generic_type), "]"),
65
+ ```
66
+
67
+ Replace:
68
+
69
+ ```js
70
+ type: ($) =>
71
+ choice(
72
+ seq(
73
+ $.type_identifier,
74
+ field(
75
+ "generics",
76
+ optional(
77
+ choice(
78
+ seq("[", commaSep1($.type), "]"),
79
+ seq("(", commaSep1($.type), ")"),
80
+ ),
81
+ ),
82
+ ),
83
+ ),
84
+ $.generic,
85
+ ),
86
+ ```
87
+
88
+ with:
89
+
90
+ ```js
91
+ type: ($) =>
92
+ choice(
93
+ seq(
94
+ $.type_identifier,
95
+ field(
96
+ "generics",
97
+ optional(seq("[", commaSep1($.type), "]")),
98
+ ),
99
+ ),
100
+ $.generic,
101
+ ),
102
+ ```
103
+
104
+ - [ ] **Step 2: Swap `class`'s field order (generics before implements) and drop `return_type`**
105
+
106
+ Replace:
107
+
108
+ ```js
109
+ class: ($) =>
110
+ seq(
111
+ "type",
112
+ field("name", $.type_identifier),
113
+ field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
114
+ field("generics", optional($.generics)),
115
+ "=",
116
+ $._indent,
117
+ field("fields", optional(repeat(alias($.class_field, $.field)))),
118
+ $._dedent,
119
+ ),
120
+ ```
121
+
122
+ with:
123
+
124
+ ```js
125
+ class: ($) =>
126
+ seq(
127
+ "type",
128
+ field("name", $.type_identifier),
129
+ field("generics", optional($.generics)),
130
+ field("implements", optional(seq("(", commaSep1($.type_identifier), ")"))),
131
+ "=",
132
+ $._indent,
133
+ field("fields", optional(repeat(alias($.class_field, $.field)))),
134
+ $._dedent,
135
+ ),
136
+ ```
137
+
138
+ Replace:
139
+
140
+ ```js
141
+ trait_field: ($) =>
142
+ seq(
143
+ field("name", $.fn_identifier),
144
+ field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")),
145
+ field("returns", optional(seq("->", $.return_type))),
146
+ ),
147
+ ```
148
+
149
+ with:
150
+
151
+ ```js
152
+ trait_field: ($) =>
153
+ seq(
154
+ field("name", $.fn_identifier),
155
+ field("params", seq("(", optional(commaSep1(choice($.self, $.param))), ")")),
156
+ field("returns", optional(seq("->", $.type))),
157
+ ),
158
+ ```
159
+
160
+ Delete this rule entirely (return positions now parse as plain `$.type`):
161
+
162
+ ```js
163
+ return_type: ($) =>
164
+ seq($.type_identifier, field("generics", optional($.generics))),
165
+ ```
166
+
167
+ Replace, in `enum_field`:
168
+
169
+ ```js
170
+ enum_field: ($) =>
171
+ seq(
172
+ "|",
173
+ field("name", $.type_identifier),
174
+ field("parameters", optional(seq("(", commaSep1(choice($.type_identifier, $.generic)), ")"))),
175
+ ),
176
+ ```
177
+
178
+ with:
179
+
180
+ ```js
181
+ enum_field: ($) =>
182
+ seq(
183
+ "|",
184
+ field("name", $.type_identifier),
185
+ field("parameters", optional(seq("[", commaSep1(choice($.type_identifier, $.generic)), "]"))),
186
+ ),
187
+ ```
188
+
189
+ Replace, in `fn`:
190
+
191
+ ```js
192
+ field("returns", optional(seq("->", $.return_type))),
193
+ ```
194
+
195
+ with:
196
+
197
+ ```js
198
+ field("returns", optional(seq("->", $.type))),
199
+ ```
200
+
201
+ - [ ] **Step 3: Replace the hardcoded 4-letter `generic` rule with a single regex token**
202
+
203
+ Replace:
204
+
205
+ ```js
206
+ generic: ($) => choice($.a, $.b, $.c, $.d), // single letter
207
+ a: (_) => token("a"),
208
+ b: (_) => token("b"),
209
+ c: (_) => token("c"),
210
+ d: (_) => token("d"),
211
+ ```
212
+
213
+ with:
214
+
215
+ ```js
216
+ generic: (_) => /[A-Z]/, // any single uppercase letter — reserved, illegal as a type_identifier
217
+ ```
218
+
219
+ Replace:
220
+
221
+ ```js
222
+ type_identifier: (_) => /[A-Z][a-zA-Z0-9]*/, // capital case
223
+ ```
224
+
225
+ with:
226
+
227
+ ```js
228
+ type_identifier: (_) => /[A-Z][a-zA-Z0-9]+/, // capital case, 2+ chars (single uppercase letters are reserved for `generic`)
229
+ ```
230
+
231
+ - [ ] **Step 4: Regenerate the parser**
232
+
233
+ Run: `cd tooling/tree-sitter-plum && npx tree-sitter generate`
234
+ 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.
235
+
236
+ - [ ] **Step 5: Update `test/corpus/type.txt`**
237
+
238
+ Replace the entire file contents with:
239
+
240
+ ```
241
+ ================================================================================
242
+ type
243
+ ================================================================================
244
+
245
+ type Dog =
246
+ name: Str
247
+ age: B
248
+
249
+ type Cat(Stringable) =
250
+ name: Str
251
+ age: Int
252
+
253
+ init<Cat>(name: Str) -> Cat =
254
+ Cat(name: name, age: 0)
255
+
256
+ withName<Cat>(name: Str) -> Cat =
257
+ Cat(name: name, age: 0)
258
+
259
+ withAge<Cat>(age: Int) -> Cat =
260
+ Cat(name: "", age: age)
261
+
262
+ toStr<Cat>() -> Str =
263
+ "Cat({self.name}, {self.age})"
264
+
265
+ --------------------------------------------------------------------------------
266
+
267
+ (source
268
+ (class
269
+ (type_identifier)
270
+ (field
271
+ (var_identifier)
272
+ (type
273
+ (type_identifier)))
274
+ (field
275
+ (var_identifier)
276
+ (type
277
+ (generic))))
278
+ (class
279
+ (type_identifier)
280
+ (type_identifier)
281
+ (field
282
+ (var_identifier)
283
+ (type
284
+ (type_identifier)))
285
+ (field
286
+ (var_identifier)
287
+ (type
288
+ (type_identifier))))
289
+ (fn
290
+ (fn_identifier)
291
+ (type
292
+ (type_identifier))
293
+ (param
294
+ (var_identifier)
295
+ (type
296
+ (type_identifier)))
297
+ (type
298
+ (type_identifier))
299
+ (body
300
+ (expression
301
+ (primary_expression
302
+ (class_call
303
+ (type_identifier)
304
+ (class_argument_list
305
+ (var_identifier)
306
+ (expression
307
+ (primary_expression
308
+ (var_identifier)))
309
+ (var_identifier)
310
+ (expression
311
+ (primary_expression
312
+ (integer)))))))))
313
+ (fn
314
+ (fn_identifier)
315
+ (type
316
+ (type_identifier))
317
+ (param
318
+ (var_identifier)
319
+ (type
320
+ (type_identifier)))
321
+ (type
322
+ (type_identifier))
323
+ (body
324
+ (expression
325
+ (primary_expression
326
+ (class_call
327
+ (type_identifier)
328
+ (class_argument_list
329
+ (var_identifier)
330
+ (expression
331
+ (primary_expression
332
+ (var_identifier)))
333
+ (var_identifier)
334
+ (expression
335
+ (primary_expression
336
+ (integer)))))))))
337
+ (fn
338
+ (fn_identifier)
339
+ (type
340
+ (type_identifier))
341
+ (param
342
+ (var_identifier)
343
+ (type
344
+ (type_identifier)))
345
+ (type
346
+ (type_identifier))
347
+ (body
348
+ (expression
349
+ (primary_expression
350
+ (class_call
351
+ (type_identifier)
352
+ (class_argument_list
353
+ (var_identifier)
354
+ (expression
355
+ (primary_expression
356
+ (string
357
+ (string_start)
358
+ (string_end))))
359
+ (var_identifier)
360
+ (expression
361
+ (primary_expression
362
+ (var_identifier)))))))))
363
+ (fn
364
+ (fn_identifier)
365
+ (type
366
+ (type_identifier))
367
+ (type
368
+ (type_identifier))
369
+ (body
370
+ (expression
371
+ (primary_expression
372
+ (string
373
+ (string_start)
374
+ (string_content)
375
+ (interpolation
376
+ (primary_expression
377
+ (attribute
378
+ (primary_expression
379
+ (self))
380
+ (fn_identifier))))
381
+ (string_content)
382
+ (interpolation
383
+ (primary_expression
384
+ (attribute
385
+ (primary_expression
386
+ (self))
387
+ (fn_identifier))))
388
+ (string_content)
389
+ (string_end)))))))
390
+ ```
391
+
392
+ - [ ] **Step 6: Update `test/corpus/trait.txt`**
393
+
394
+ 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:
395
+
396
+ ```
397
+ (source
398
+ (trait
399
+ (type_identifier)
400
+ (field
401
+ (fn_identifier)
402
+ (type
403
+ (type_identifier))))
404
+ (trait
405
+ (type_identifier)
406
+ (field
407
+ (fn_identifier)
408
+ (type
409
+ (type_identifier)))
410
+ (field
411
+ (fn_identifier)
412
+ (type
413
+ (type_identifier)))
414
+ (field
415
+ (fn_identifier))
416
+ (field
417
+ (fn_identifier)
418
+ (param
419
+ (var_identifier)
420
+ (type
421
+ (type_identifier)))
422
+ (type
423
+ (type_identifier)))))
424
+ ```
425
+
426
+ (The source section above the divider is unchanged — no generics appear in this file's source.)
427
+
428
+ - [ ] **Step 7: Update `test/corpus/enum.txt`**
429
+
430
+ Replace the entire file contents with:
431
+
432
+ ```
433
+ ================================================================================
434
+ enum
435
+ ================================================================================
436
+
437
+ enum Bool =
438
+ | True
439
+ | False
440
+
441
+ toStr<Bool>() -> Str =
442
+ "Bool"
443
+
444
+ --------------------------------------------------------------------------------
445
+
446
+ (source
447
+ (enum
448
+ (type_identifier)
449
+ (field
450
+ (type_identifier))
451
+ (field
452
+ (type_identifier)))
453
+ (fn
454
+ (fn_identifier)
455
+ (type
456
+ (type_identifier))
457
+ (type
458
+ (type_identifier))
459
+ (body
460
+ (expression
461
+ (primary_expression
462
+ (string
463
+ (string_start)
464
+ (string_content)
465
+ (string_end)))))))
466
+
467
+ ================================================================================
468
+ enum - generic variant fields
469
+ ================================================================================
470
+
471
+ enum Option =
472
+ | Some[T]
473
+ | None
474
+
475
+ --------------------------------------------------------------------------------
476
+
477
+ (source
478
+ (enum
479
+ (type_identifier)
480
+ (field
481
+ (type_identifier)
482
+ (generic))
483
+ (field
484
+ (type_identifier))))
485
+ ```
486
+
487
+ - [ ] **Step 8: Update `test/corpus/function.txt`**
488
+
489
+ 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:
490
+
491
+ Run:
492
+ ```bash
493
+ cd tooling/tree-sitter-plum/test/corpus
494
+ sed -i '' 's/(return_type/(type/' function.txt
495
+ ```
496
+
497
+ 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:
498
+
499
+ ```
500
+ ================================================================================
501
+ function - generics
502
+ ================================================================================
503
+
504
+ add(param: T, param2: List[U]) -> List[U] =
505
+ todo
506
+
507
+ --------------------------------------------------------------------------------
508
+
509
+ (source
510
+ (fn
511
+ (fn_identifier)
512
+ (param
513
+ (var_identifier)
514
+ (type
515
+ (generic)))
516
+ (param
517
+ (var_identifier)
518
+ (type
519
+ (type_identifier)
520
+ (type
521
+ (generic))))
522
+ (type
523
+ (type_identifier)
524
+ (type
525
+ (generic)))
526
+ (body
527
+ (todo))))
528
+ ```
529
+
530
+ Then find the block starting `function - method with explicit self param` and replace its source + expected tree:
531
+
532
+ ```
533
+ ================================================================================
534
+ function - method with explicit self param
535
+ ================================================================================
536
+
537
+ remove<List>(self, v: T) =
538
+ todo
539
+
540
+ --------------------------------------------------------------------------------
541
+
542
+ (source
543
+ (fn
544
+ (fn_identifier)
545
+ (type
546
+ (type_identifier))
547
+ (self)
548
+ (param
549
+ (var_identifier)
550
+ (type
551
+ (generic)))
552
+ (body
553
+ (todo))))
554
+ ```
555
+
556
+ - [ ] **Step 9: Run the corpus tests**
557
+
558
+ Run: `cd tooling/tree-sitter-plum && npx tree-sitter test 2>&1 | tail -100`
559
+ 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.
560
+
561
+ - [ ] **Step 10: Commit**
562
+
563
+ ```bash
564
+ 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
565
+ git commit -m "feat(grammar): migrate generics to bracket syntax with uppercase letters"
566
+ ```
567
+
568
+ (`tooling/tree-sitter-plum/src` covers the regenerated `parser.c`/`grammar.json`/etc. produced by Step 4 — include whatever `tree-sitter generate` wrote there.)
569
+
570
+ ---
571
+
572
+ ### Task 2: `plum-core` — AST and parser updates
573
+
574
+ **Files:**
575
+ - Modify: `plum-core/src/ast.rs`
576
+ - Modify: `plum-core/src/parser.rs`
577
+
578
+ **Interfaces:**
579
+ - Consumes: the regenerated grammar from Task 1 (node kinds `"generic"`, `"generics"`, `"type"` — no more `"return_type"`, `"a"`/`"b"`/`"c"`/`"d"`).
580
+ - 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.
581
+
582
+ - [ ] **Step 1: Remove `ReturnType` from `ast.rs`, retype `returns` fields**
583
+
584
+ In `plum-core/src/ast.rs`, replace:
585
+
586
+ ```rust
587
+ #[derive(Debug, Clone, PartialEq)]
588
+ pub struct TraitMethod {
589
+ pub name: String,
590
+ pub params: Vec<Param>,
591
+ pub returns: Option<ReturnType>,
592
+ }
593
+ ```
594
+
595
+ with:
596
+
597
+ ```rust
598
+ #[derive(Debug, Clone, PartialEq)]
599
+ pub struct TraitMethod {
600
+ pub name: String,
601
+ pub params: Vec<Param>,
602
+ pub returns: Option<Type>,
603
+ }
604
+ ```
605
+
606
+ Replace:
607
+
608
+ ```rust
609
+ #[derive(Debug, Clone, PartialEq)]
610
+ pub struct Fn {
611
+ pub name: String,
612
+ /// Type parameter for method dispatch, e.g. `<Cat>` in `toStr<Cat>()`
613
+ pub type_param: Option<String>,
614
+ pub params: Vec<Param>,
615
+ pub returns: Option<ReturnType>,
616
+ pub body: FnBody,
617
+ }
618
+ ```
619
+
620
+ with:
621
+
622
+ ```rust
623
+ #[derive(Debug, Clone, PartialEq)]
624
+ pub struct Fn {
625
+ pub name: String,
626
+ /// Type parameter for method dispatch, e.g. `<Cat>` in `toStr<Cat>()`
627
+ pub type_param: Option<String>,
628
+ pub params: Vec<Param>,
629
+ pub returns: Option<Type>,
630
+ pub body: FnBody,
631
+ }
632
+ ```
633
+
634
+ Delete this struct entirely (its shape is now identical to `Type`, which every `returns` field uses instead):
635
+
636
+ ```rust
637
+ #[derive(Debug, Clone, PartialEq)]
638
+ pub struct ReturnType {
639
+ pub name: String,
640
+ pub generics: Vec<GenericParam>,
641
+ }
642
+ ```
643
+
644
+ - [ ] **Step 2: Update `parse_class`'s implements-list derivation for the new field order**
645
+
646
+ In `plum-core/src/parser.rs`, replace:
647
+
648
+ ```rust
649
+ fn parse_class(&self, node: Node) -> Class {
650
+ // class: "type" type_identifier ("(" type_identifier,* ")")? generics? "=" body
651
+ // Named children in order: type_identifier (name), type_identifier* (implements), field*
652
+ let mut cursor = node.walk();
653
+ let named: Vec<Node> = node.named_children(&mut cursor).collect();
654
+
655
+ let name = named.first().map(|n| self.text(*n)).unwrap_or_default();
656
+
657
+ // implements = type_identifiers that appear before any `field` node
658
+ let implements: Vec<String> = named[1..]
659
+ .iter()
660
+ .take_while(|n| n.kind() == "type_identifier")
661
+ .map(|n| self.text(*n))
662
+ .collect();
663
+
664
+ let generics = self.parse_generics_field(node);
665
+
666
+ let fields: Vec<Field> = named
667
+ .iter()
668
+ .filter(|n| n.kind() == "field")
669
+ .map(|n| self.parse_field(*n))
670
+ .collect();
671
+
672
+ Class { name, implements, generics, fields }
673
+ }
674
+ ```
675
+
676
+ with:
677
+
678
+ ```rust
679
+ fn parse_class(&self, node: Node) -> Class {
680
+ // class: "type" type_identifier generics? ("(" type_identifier,* ")")? "=" body
681
+ // Named children in order: type_identifier (name), generics? (declaration), type_identifier* (implements), field*
682
+ let mut cursor = node.walk();
683
+ let named: Vec<Node> = node.named_children(&mut cursor).collect();
684
+
685
+ let name = named.first().map(|n| self.text(*n)).unwrap_or_default();
686
+
687
+ // Skip the optional `generics` declaration node before looking for implements.
688
+ let after_generics = if named.get(1).map(|n| n.kind()) == Some("generics") { 2 } else { 1 };
689
+
690
+ // implements = type_identifiers that appear before any `field` node
691
+ let implements: Vec<String> = named[after_generics..]
692
+ .iter()
693
+ .take_while(|n| n.kind() == "type_identifier")
694
+ .map(|n| self.text(*n))
695
+ .collect();
696
+
697
+ let generics = self.parse_generics_field(node);
698
+
699
+ let fields: Vec<Field> = named
700
+ .iter()
701
+ .filter(|n| n.kind() == "field")
702
+ .map(|n| self.parse_field(*n))
703
+ .collect();
704
+
705
+ Class { name, implements, generics, fields }
706
+ }
707
+ ```
708
+
709
+ - [ ] **Step 3: Update `parse_generics_field` and `parse_enum_variant` to match on `"generic"` instead of `"a"|"b"|"c"|"d"`**
710
+
711
+ Replace:
712
+
713
+ ```rust
714
+ fn parse_generics_field(&self, node: Node) -> Vec<GenericParam> {
715
+ // generics: "(" generic_type,* ")" where generic_type: generic (":" sep1(type_identifier, "+"))?
716
+ //
717
+ // Both `generic_type` and `generic` are `inline`d in the grammar, so the
718
+ // `generics` node has NO `generic_type` children — its named children are
719
+ // the single-letter generic nodes (`a`/`b`/`c`/`d`) each optionally
720
+ // followed by their bound `type_identifier` nodes, all flattened together.
721
+ // Reconstruct each `GenericParam` by starting a new one at every generic
722
+ // letter and attaching any following `type_identifier`s as its bounds
723
+ // until the next generic letter.
724
+ let Some(generics_node) = self.children_of_kind(node, "generics").into_iter().next() else {
725
+ return Vec::new();
726
+ };
727
+ let mut cursor = generics_node.walk();
728
+ let mut params: Vec<GenericParam> = Vec::new();
729
+ for child in generics_node.named_children(&mut cursor) {
730
+ match child.kind() {
731
+ "a" | "b" | "c" | "d" => {
732
+ params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
733
+ }
734
+ "type_identifier" => {
735
+ if let Some(last) = params.last_mut() {
736
+ last.bounds.push(self.text(child));
737
+ }
738
+ }
739
+ _ => {}
740
+ }
741
+ }
742
+ params
743
+ }
744
+ ```
745
+
746
+ with:
747
+
748
+ ```rust
749
+ fn parse_generics_field(&self, node: Node) -> Vec<GenericParam> {
750
+ // generics: "[" generic_type,* "]" where generic_type: generic (":" sep1(type_identifier, "+"))?
751
+ //
752
+ // `generic_type` is `inline`d in the grammar, so the `generics` node has NO
753
+ // `generic_type` children — its named children are the single-uppercase-letter
754
+ // `generic` nodes, each optionally followed by their bound `type_identifier`
755
+ // nodes, all flattened together. Reconstruct each `GenericParam` by starting a
756
+ // new one at every `generic` node and attaching any following
757
+ // `type_identifier`s as its bounds until the next `generic` node.
758
+ let Some(generics_node) = self.children_of_kind(node, "generics").into_iter().next() else {
759
+ return Vec::new();
760
+ };
761
+ let mut cursor = generics_node.walk();
762
+ let mut params: Vec<GenericParam> = Vec::new();
763
+ for child in generics_node.named_children(&mut cursor) {
764
+ match child.kind() {
765
+ "generic" => {
766
+ params.push(GenericParam { name: self.text(child), bounds: Vec::new() });
767
+ }
768
+ "type_identifier" => {
769
+ if let Some(last) = params.last_mut() {
770
+ last.bounds.push(self.text(child));
771
+ }
772
+ }
773
+ _ => {}
774
+ }
775
+ }
776
+ params
777
+ }
778
+ ```
779
+
780
+ Replace:
781
+
782
+ ```rust
783
+ fn parse_enum_variant(&self, node: Node) -> EnumVariant {
784
+ // enum_field (aliased to field): "|" type_identifier ("(" (type_identifier | generic),* ")")?
785
+ // named children: type_identifier (name), then each field type inside "()" — a
786
+ // `type_identifier` (concrete, e.g. `Int`) or an inlined generic letter node
787
+ // (`a`/`b`/`c`/`d`, since `generic` is inlined in the grammar).
788
+ let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
789
+ let fields: Vec<String> = (1..node.named_child_count())
790
+ .filter_map(|i| node.named_child(i as u32))
791
+ .filter(|n| matches!(n.kind(), "type_identifier" | "a" | "b" | "c" | "d"))
792
+ .map(|n| self.text(n))
793
+ .collect();
794
+ EnumVariant { name, fields }
795
+ }
796
+ ```
797
+
798
+ with:
799
+
800
+ ```rust
801
+ fn parse_enum_variant(&self, node: Node) -> EnumVariant {
802
+ // enum_field (aliased to field): "|" type_identifier ("[" (type_identifier | generic),* "]")?
803
+ // named children: type_identifier (name), then each field type inside "[]" — a
804
+ // `type_identifier` (concrete, e.g. `Int`) or a `generic` node (single uppercase letter).
805
+ let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
806
+ let fields: Vec<String> = (1..node.named_child_count())
807
+ .filter_map(|i| node.named_child(i as u32))
808
+ .filter(|n| matches!(n.kind(), "type_identifier" | "generic"))
809
+ .map(|n| self.text(n))
810
+ .collect();
811
+ EnumVariant { name, fields }
812
+ }
813
+ ```
814
+
815
+ - [ ] **Step 4: Update `parse_trait_method` and `parse_fn` to read `returns` as a `type` node by field name; remove `parse_return_type`**
816
+
817
+ Replace:
818
+
819
+ ```rust
820
+ fn parse_trait_method(&self, node: Node) -> TraitMethod {
821
+ // trait_field (aliased to field): fn_identifier "(" params ")" ("->" return_type)?
822
+ let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
823
+ let params = self.collect_params_from(node);
824
+ let returns = node
825
+ .named_children(&mut node.walk())
826
+ .find(|n| n.kind() == "return_type")
827
+ .map(|n| self.parse_return_type(n));
828
+ TraitMethod { name, params, returns }
829
+ }
830
+ ```
831
+
832
+ with:
833
+
834
+ ```rust
835
+ fn parse_trait_method(&self, node: Node) -> TraitMethod {
836
+ // trait_field (aliased to field): fn_identifier "(" params ")" ("->" type)?
837
+ let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
838
+ let params = self.collect_params_from(node);
839
+ let returns = node.child_by_field_name("returns").map(|n| self.parse_type(n));
840
+ TraitMethod { name, params, returns }
841
+ }
842
+ ```
843
+
844
+ In `parse_fn`, replace:
845
+
846
+ ```rust
847
+ let returns = named
848
+ .iter()
849
+ .find(|n| n.kind() == "return_type")
850
+ .map(|n| self.parse_return_type(*n));
851
+
852
+ // body is the last named child — it is either a `body` node (block)
853
+ // or an expression node when the body is a single expression.
854
+ let body = named.last().and_then(|last| {
855
+ match last.kind() {
856
+ // Skip non-body trailing nodes
857
+ "fn_identifier" | "type" | "param" | "self" | "return_type" => None,
858
+ "body" => Some(FnBody::Block(self.parse_block(*last))),
859
+ _ => {
860
+ let unwrapped = self.unwrap_expr_node(*last);
861
+ Some(FnBody::Expr(self.parse_expression(unwrapped)))
862
+ }
863
+ }
864
+ }).unwrap_or(FnBody::Block(Block { stmts: vec![] }));
865
+ ```
866
+
867
+ with:
868
+
869
+ ```rust
870
+ let returns = node.child_by_field_name("returns").map(|n| self.parse_type(n));
871
+
872
+ // body is the last named child — it is either a `body` node (block)
873
+ // or an expression node when the body is a single expression. The
874
+ // `<Cat>` receiver annotation and the `returns` type both have kind
875
+ // "type" now (return_type no longer exists as a separate node kind),
876
+ // but that's fine: neither can ever be the LAST named child when a
877
+ // body is present, since `body`/the trailing expression always comes
878
+ // after them in the grammar — so this match doesn't need to
879
+ // distinguish the two "type" cases from each other, only from `body`.
880
+ let body = named.last().and_then(|last| {
881
+ match last.kind() {
882
+ // Skip non-body trailing nodes
883
+ "fn_identifier" | "type" | "param" | "self" => None,
884
+ "body" => Some(FnBody::Block(self.parse_block(*last))),
885
+ _ => {
886
+ let unwrapped = self.unwrap_expr_node(*last);
887
+ Some(FnBody::Expr(self.parse_expression(unwrapped)))
888
+ }
889
+ }
890
+ }).unwrap_or(FnBody::Block(Block { stmts: vec![] }));
891
+ ```
892
+
893
+ Delete `parse_return_type` entirely:
894
+
895
+ ```rust
896
+ fn parse_return_type(&self, node: Node) -> ReturnType {
897
+ // return_type: type_identifier generics?
898
+ // named_child(0) = type_identifier
899
+ let name = node.named_child(0).map(|n| self.text(n)).unwrap_or_default();
900
+ let generics = self.parse_generics_field(node);
901
+ ReturnType { name, generics }
902
+ }
903
+ ```
904
+
905
+ - [ ] **Step 5: Run `plum-core`'s tests**
906
+
907
+ Run: `cargo test -p plum-core 2>&1 | tail -60`
908
+ 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.)
909
+
910
+ - [ ] **Step 6: Commit**
911
+
912
+ ```bash
913
+ git add plum-core/src/ast.rs plum-core/src/parser.rs
914
+ git commit -m "feat(plum-core): parse bracket generics, drop ReturnType in favor of Type"
915
+ ```
916
+
917
+ ---
918
+
919
+ ### Task 3: `plum-checker` and `plum-wasm-codegen` — uppercase convention and `ReturnType` cleanup
920
+
921
+ **Files:**
922
+ - Modify: `plum-checker/src/monomorphize.rs`
923
+ - Modify: `plum-checker/src/lib.rs`
924
+ - Modify: `plum-wasm-codegen/src/lib.rs`
925
+
926
+ **Interfaces:**
927
+ - Consumes: `Fn.returns: Option<Type>` / `TraitMethod.returns: Option<Type>` from Task 2.
928
+ - Produces: `is_generic_param_name` now recognizes single ASCII uppercase letters; every `ast::ReturnType` construction/reference becomes `ast::Type`.
929
+
930
+ - [ ] **Step 1: Flip `is_generic_param_name`'s case check**
931
+
932
+ In `plum-checker/src/monomorphize.rs`, replace:
933
+
934
+ ```rust
935
+ /// A single lowercase letter (`a`, `b`, `c`, `d`, ...) is the grammar's only legal
936
+ /// spelling for a generic type parameter — this is how we recognize one, since
937
+ /// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
938
+ /// generics declaration list.
939
+ pub fn is_generic_param_name(name: &str) -> bool {
940
+ let mut chars = name.chars();
941
+ match (chars.next(), chars.next()) {
942
+ (Some(c), None) => c.is_ascii_lowercase(),
943
+ _ => false,
944
+ }
945
+ }
946
+ ```
947
+
948
+ with:
949
+
950
+ ```rust
951
+ /// A single uppercase letter (`T`, `U`, `K`, ...) is the grammar's only legal
952
+ /// spelling for a generic type parameter — this is how we recognize one, since
953
+ /// `ast::Fn` and `ast::Enum` (unlike `ast::Class`/`ast::Trait`) carry no explicit
954
+ /// generics declaration list.
955
+ pub fn is_generic_param_name(name: &str) -> bool {
956
+ let mut chars = name.chars();
957
+ match (chars.next(), chars.next()) {
958
+ (Some(c), None) => c.is_ascii_uppercase(),
959
+ _ => false,
960
+ }
961
+ }
962
+ ```
963
+
964
+ Also update the two doc comments in this file that describe the old lowercase convention:
965
+
966
+ Replace:
967
+
968
+ ```rust
969
+ /// The generic parameter names implicitly introduced by a `Fn` — every distinct
970
+ /// single-lowercase-letter type name appearing in its params or return type, in
971
+ /// first-appearance order.
972
+ ```
973
+
974
+ with:
975
+
976
+ ```rust
977
+ /// The generic parameter names implicitly introduced by a `Fn` — every distinct
978
+ /// single-uppercase-letter type name appearing in its params or return type, in
979
+ /// first-appearance order.
980
+ ```
981
+
982
+ Replace:
983
+
984
+ ```rust
985
+ /// The generic parameter names implicitly introduced by an `Enum` — every distinct
986
+ /// single-lowercase-letter variant field type name, in first-appearance order.
987
+ ```
988
+
989
+ with:
990
+
991
+ ```rust
992
+ /// The generic parameter names implicitly introduced by an `Enum` — every distinct
993
+ /// single-uppercase-letter variant field type name, in first-appearance order.
994
+ ```
995
+
996
+ - [ ] **Step 2: Replace `ast::ReturnType` construction sites with `ast::Type`**
997
+
998
+ In `plum-checker/src/monomorphize.rs`, replace:
999
+
1000
+ ```rust
1001
+ returns: f.returns.as_ref().map(|r| {
1002
+ let substituted = substitute_type(&ast::Type { name: r.name.clone(), generics: vec![] }, subst);
1003
+ ast::ReturnType { name: substituted.name, generics: vec![] }
1004
+ }),
1005
+ ```
1006
+
1007
+ with:
1008
+
1009
+ ```rust
1010
+ returns: f.returns.as_ref().map(|r| substitute_type(r, subst)),
1011
+ ```
1012
+
1013
+ Replace:
1014
+
1015
+ ```rust
1016
+ if needs {
1017
+ f.returns = Some(ast::ReturnType { name: t.to_string(), generics: vec![] });
1018
+ }
1019
+ ```
1020
+
1021
+ with:
1022
+
1023
+ ```rust
1024
+ if needs {
1025
+ f.returns = Some(ast::Type { name: t.to_string(), generics: vec![] });
1026
+ }
1027
+ ```
1028
+
1029
+ - [ ] **Step 3: Simplify the two `ast::Type` reconstructions in `plum-checker/src/lib.rs`**
1030
+
1031
+ `f.returns`/`r` is already `&ast::Type` after Task 2 — reconstructing a fresh `ast::Type` from its own fields is now redundant. Replace:
1032
+
1033
+ ```rust
1034
+ let ret = f.returns.as_ref()
1035
+ .map(|r| plum_type_from_ast(&ast::Type { name: r.name.clone(), generics: vec![] }))
1036
+ .unwrap_or(PlumType::TUnit);
1037
+ ```
1038
+
1039
+ with:
1040
+
1041
+ ```rust
1042
+ let ret = f.returns.as_ref()
1043
+ .map(plum_type_from_ast)
1044
+ .unwrap_or(PlumType::TUnit);
1045
+ ```
1046
+
1047
+ Replace:
1048
+
1049
+ ```rust
1050
+ let declared_ret = f.returns.as_ref()
1051
+ .map(|r| {
1052
+ let ast_ty = ast::Type { name: r.name.clone(), generics: vec![] };
1053
+ plum_type_from_ast(&ast_ty)
1054
+ })
1055
+ .unwrap_or(PlumType::TUnit);
1056
+ ```
1057
+
1058
+ with:
1059
+
1060
+ ```rust
1061
+ let declared_ret = f.returns.as_ref()
1062
+ .map(plum_type_from_ast)
1063
+ .unwrap_or(PlumType::TUnit);
1064
+ ```
1065
+
1066
+ - [ ] **Step 4: Retype `ret_type_to_wasm` in `plum-wasm-codegen`**
1067
+
1068
+ In `plum-wasm-codegen/src/lib.rs`, replace:
1069
+
1070
+ ```rust
1071
+ fn ret_type_to_wasm(ret: Option<&ast::ReturnType>) -> Option<ValType> {
1072
+ ret.and_then(|r| ast_type_to_wasm(&r.name))
1073
+ }
1074
+ ```
1075
+
1076
+ with:
1077
+
1078
+ ```rust
1079
+ fn ret_type_to_wasm(ret: Option<&ast::Type>) -> Option<ValType> {
1080
+ ret.and_then(|r| ast_type_to_wasm(&r.name))
1081
+ }
1082
+ ```
1083
+
1084
+ (Its two call sites, `f.returns.as_ref()` at both usages, already produce `Option<&ast::Type>` after Task 2 — no call-site changes needed.)
1085
+
1086
+ - [ ] **Step 5: Build the workspace to confirm the Rust-level refactor compiles**
1087
+
1088
+ Run: `cargo build --workspace 2>&1 | tail -80`
1089
+ 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.)
1090
+
1091
+ - [ ] **Step 6: Commit**
1092
+
1093
+ ```bash
1094
+ git add plum-checker/src/monomorphize.rs plum-checker/src/lib.rs plum-wasm-codegen/src/lib.rs
1095
+ git commit -m "feat(plum-checker,plum-wasm-codegen): recognize uppercase generic params, use Type instead of ReturnType"
1096
+ ```
1097
+
1098
+ ---
1099
+
1100
+ ### Task 4: Update `plum-checker` test fixtures
1101
+
1102
+ **Files:**
1103
+ - Modify: `plum-checker/tests/checker_tests.rs`
1104
+ - Modify: `plum-checker/tests/monomorphize_tests.rs`
1105
+
1106
+ **Interfaces:**
1107
+ - Consumes: the Task 1-3 grammar/parser/checker changes.
1108
+ - 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).
1109
+
1110
+ - [ ] **Step 1: Run the checker test suite to see which tests fail on old syntax**
1111
+
1112
+ Run: `cargo test -p plum-checker 2>&1 | tail -150`
1113
+ 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).
1114
+
1115
+ - [ ] **Step 2: Rewrite every failing test's embedded source string using this mechanical rule**
1116
+
1117
+ 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):
1118
+
1119
+ - `a` → `T`
1120
+ - `b` → `U`
1121
+ - `c` → `V`
1122
+ - `d` → `W`
1123
+
1124
+ 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`.
1125
+
1126
+ 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.
1127
+
1128
+ As a concrete worked example, in `checker_tests.rs` around line 322-323:
1129
+
1130
+ ```rust
1131
+ type Box(a) =
1132
+ value: a
1133
+ ```
1134
+
1135
+ becomes:
1136
+
1137
+ ```rust
1138
+ type Box[T] =
1139
+ value: T
1140
+ ```
1141
+
1142
+ and around line 356:
1143
+
1144
+ ```rust
1145
+ pair(first: a, second: b) -> Bool =
1146
+ ```
1147
+
1148
+ becomes:
1149
+
1150
+ ```rust
1151
+ pair(first: T, second: U) -> Bool =
1152
+ ```
1153
+
1154
+ (here `first`/`second` are param *names*, left untouched; `a`/`b` are the param *types*, rewritten.)
1155
+
1156
+ - [ ] **Step 3: Re-run until the checker suite passes**
1157
+
1158
+ Run: `cargo test -p plum-checker 2>&1 | tail -150`
1159
+ 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).
1160
+
1161
+ - [ ] **Step 4: Commit**
1162
+
1163
+ ```bash
1164
+ git add plum-checker/tests/checker_tests.rs plum-checker/tests/monomorphize_tests.rs
1165
+ git commit -m "test(plum-checker): migrate test fixtures to bracket generics syntax"
1166
+ ```
1167
+
1168
+ ---
1169
+
1170
+ ### Task 5: Update `plum-wasm-codegen` test fixtures
1171
+
1172
+ **Files:**
1173
+ - Modify: `plum-wasm-codegen/tests/codegen_tests.rs`
1174
+
1175
+ **Interfaces:**
1176
+ - Consumes: the Task 1-3 grammar/parser/checker changes.
1177
+ - 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.
1178
+
1179
+ - [ ] **Step 1: Run the codegen test suite to see which tests fail on old syntax**
1180
+
1181
+ Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -150`
1182
+ 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).
1183
+
1184
+ - [ ] **Step 2: Rewrite every failing test's embedded source string using the same mechanical rule as Task 4**
1185
+
1186
+ 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).
1187
+
1188
+ As a concrete worked example, the `identity` pattern around lines 877/909/1177:
1189
+
1190
+ ```rust
1191
+ type Box(a) =
1192
+ value: a
1193
+
1194
+ identity(value: a) -> a =
1195
+ value
1196
+ ```
1197
+
1198
+ becomes:
1199
+
1200
+ ```rust
1201
+ type Box[T] =
1202
+ value: T
1203
+
1204
+ identity(value: T) -> T =
1205
+ value
1206
+ ```
1207
+
1208
+ - [ ] **Step 3: Re-run until the codegen suite passes**
1209
+
1210
+ Run: `cargo test -p plum-wasm-codegen 2>&1 | tail -150`
1211
+ 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).
1212
+
1213
+ - [ ] **Step 4: Run the full workspace suite**
1214
+
1215
+ Run: `cargo test --workspace 2>&1 | tail -100`
1216
+ 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`).
1217
+
1218
+ - [ ] **Step 5: Commit**
1219
+
1220
+ ```bash
1221
+ git add plum-wasm-codegen/tests/codegen_tests.rs
1222
+ git commit -m "test(plum-wasm-codegen): migrate test fixtures to bracket generics syntax"
1223
+ ```
1224
+
1225
+ ---
1226
+
1227
+ ### Task 6: Rewrite the stdlib (`libs/std`)
1228
+
1229
+ **Files:**
1230
+ - Modify: `libs/std/list.plum`
1231
+ - Modify: `libs/std/map.plum`
1232
+ - Modify: `libs/std/option.plum`
1233
+ - Modify: `libs/std/result.plum`
1234
+
1235
+ **Interfaces:**
1236
+ - Consumes: the new grammar (Task 1) — these files must parse under it.
1237
+ - Produces: stdlib source using bracket/uppercase generics throughout, matching the `Node[T]` shape already present in `list.plum`.
1238
+
1239
+ - [ ] **Step 1: Rewrite `libs/std/option.plum`**
1240
+
1241
+ Replace the entire file contents with:
1242
+
1243
+ ```
1244
+ module std
1245
+
1246
+ enum Option =
1247
+ | Some[T]
1248
+ | None
1249
+ ```
1250
+
1251
+ - [ ] **Step 2: Rewrite `libs/std/result.plum`**
1252
+
1253
+ Replace the entire file contents with:
1254
+
1255
+ ```
1256
+ module std
1257
+
1258
+ enum Result =
1259
+ | Ok[T]
1260
+ | Err[E]
1261
+
1262
+ # checks whether the result is an Ok value
1263
+ isOk<Result>(self) -> Bool =
1264
+ match self
1265
+ Ok(_) =>
1266
+ True
1267
+ Err(_) =>
1268
+ False
1269
+ ```
1270
+
1271
+ - [ ] **Step 3: Rewrite `libs/std/map.plum`**
1272
+
1273
+ Replace the entire file contents with:
1274
+
1275
+ ```
1276
+ module std
1277
+
1278
+ # A Pair is a grouping of a key with a value
1279
+ type Pair[K, V] =
1280
+ key: K
1281
+ val: V
1282
+
1283
+ # A Map is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
1284
+ # A Map is not an array. A slice describes a piece of an array.
1285
+ type Map[K, V] =
1286
+ items: List[Pair[K, V]]
1287
+
1288
+ init<Map>(self, kvs: ...Pair) -> Map =
1289
+ Map().add(kvs)
1290
+
1291
+ # adds the specified elements to the start of the list
1292
+ add<Map>(self, kvs: ...Pair) =
1293
+ self.items.add(kvs)
1294
+
1295
+ # gets a value from the Map using key k
1296
+ get<Map>(self, k: K) -> Option[V] =
1297
+ for p in self.items
1298
+ if p.key == k
1299
+ return Some(p.val)
1300
+ None
1301
+
1302
+ # puts a value into the Map
1303
+ set<Map>(self, k: K, v: V) =
1304
+ self.items.add(Pair(key: k, val: v))
1305
+
1306
+ # puts a value into the Map if its not already present
1307
+ putIfAbsent<Map>(self, k: K, v: V) =
1308
+ todo
1309
+
1310
+ map<Map>(self, cb: fn(Pair[K, V]) -> Pair[X, Y]) -> Map[X, Y] =
1311
+ self.items.map(cb)
1312
+ ```
1313
+
1314
+ - [ ] **Step 4: Rewrite `libs/std/list.plum`**
1315
+
1316
+ Replace the entire file contents with:
1317
+
1318
+ ```
1319
+ module std
1320
+
1321
+ import std/option
1322
+
1323
+ # A node stores the data in a list and contains pointers to the previous and next sibling nodes
1324
+ type Node[T] =
1325
+ value: T
1326
+ prev: Option[Node]
1327
+ next: Option[Node]
1328
+
1329
+ # A list is a data structure describing a contiguous section of an array stored separately from the slice variable itself.
1330
+ # It contains the pointers to the start and end nodes (head, tail) and maintains the size as well
1331
+ type List[T: Stringable](Stringable) =
1332
+ head: Option[Node]
1333
+ tail: Option[Node]
1334
+ size: Int
1335
+
1336
+ makeList(values: ...T) -> List =
1337
+ List(None, None, 0).add(values)
1338
+
1339
+ # gets the element at i'th index of the list
1340
+ get<List>(self, i: Int) -> Option[T] =
1341
+ current = self.head
1342
+ index = 0
1343
+ while current != None
1344
+ match current
1345
+ Some(node) =>
1346
+ if index == i
1347
+ return Some(node.value)
1348
+ current = node.next
1349
+ index = index + 1
1350
+ None =>
1351
+ break
1352
+ None
1353
+
1354
+ # sets the element at i'th index of the list
1355
+ set<List>(self, i: Int, v: T) -> Option[T] =
1356
+ todo
1357
+
1358
+ # returns the no of elements in the list
1359
+ length<List>(self) -> Int =
1360
+ self.size
1361
+
1362
+ # adds the specified elements to the start of the list
1363
+ add<List>(self, values: ...T) =
1364
+ todo
1365
+
1366
+ # removes the element at i'th index of the list
1367
+ removeAt<List>(self, i: Int) =
1368
+ todo
1369
+
1370
+ # removes the element v from list
1371
+ remove<List>(self, v: T) =
1372
+ todo
1373
+
1374
+ # removes all objects from this list
1375
+ clear<List>(self) =
1376
+ todo
1377
+
1378
+ # returns a new list with the elements in reverse order.
1379
+ reverse<List>(self, v: fn(T) -> Bool) -> List =
1380
+ todo
1381
+
1382
+ # returns a new list with the elements sorted by sorter
1383
+ sort<List>(self, sorter: fn(T) -> Bool) -> List =
1384
+ todo
1385
+
1386
+ # returns an item and index in the list if the item is is equal to search item
1387
+ find<List>(self, search: T) -> Option[T] =
1388
+ todo
1389
+
1390
+ # returns the index of an item in the list if present and comparable otherwise None
1391
+ contains<List>(self, v: T) -> Bool =
1392
+ todo
1393
+
1394
+ # calls f for each elem in the list
1395
+ each<List>(self, cb: fn(T)) -> Unit =
1396
+ current = self.head
1397
+ while current != None
1398
+ match current
1399
+ Some(node) =>
1400
+ cb(node.value)
1401
+ current = node.next
1402
+ None =>
1403
+ break
1404
+
1405
+ # returns a list made up of b elements for each elem in the list
1406
+ map<List>(self, cb: fn(T) -> U) -> List[U] =
1407
+ nl = List()
1408
+ current = self.head
1409
+ while current != None
1410
+ match current
1411
+ Some(node) =>
1412
+ item = cb(node.value)
1413
+ nl.add(item)
1414
+ current = node.next
1415
+ None =>
1416
+ break
1417
+ nl
1418
+
1419
+ # returns a new list with each element flat-mapped
1420
+ flatMap<List>(self) =
1421
+ todo
1422
+
1423
+ # returns a new list with the elements that matched the predicate
1424
+ retain<List>(self, predicate: fn(T) -> T) -> List =
1425
+ todo
1426
+
1427
+ # returns a new list with the elements that matched the predicate removed
1428
+ reject<List>(self, predicate: fn(T) -> T) -> List =
1429
+ todo
1430
+
1431
+ # returns true if any element in the list satisfies the predicate
1432
+ any<List>(self, predicate: fn(T) -> Bool) -> Bool =
1433
+ todo
1434
+
1435
+ # returns true if all of the elements in the list satisfies the predicate
1436
+ every<List>(self, predicate: fn(T) -> Bool) -> Bool =
1437
+ todo
1438
+
1439
+ # returns the accumulated value of all the elements in the list
1440
+ reduce<List>(self, acc: U, cb: fn(T) -> T) -> Option[U] =
1441
+ todo
1442
+
1443
+ # returns the first element in the list
1444
+ first<List>(self) -> Option[T] =
1445
+ match self.head
1446
+ Some(node) =>
1447
+ Some(node.value)
1448
+ None =>
1449
+ None
1450
+
1451
+ # returns the last element in the list
1452
+ last<List>(self) -> Option[T] =
1453
+ match self.tail
1454
+ Some(node) =>
1455
+ Some(node.value)
1456
+ None =>
1457
+ None
1458
+
1459
+ # returns a list containing the first n elements of the given list
1460
+ sublist<List>(self, start: Int, end: Int) -> List =
1461
+ todo
1462
+
1463
+ # returns a list containing the first n elements of the given list
1464
+ take<List>(self, n: Int) -> List =
1465
+ todo
1466
+
1467
+ # returns a list containing the first n elements of the given list
1468
+ skip<List>(self, n: Int) -> List =
1469
+ todo
1470
+
1471
+ # returns a list containing the first n elements of the given list
1472
+ drop<List>(self, n: Int) -> List =
1473
+ todo
1474
+
1475
+ # returns a new list with some of the elements taken randomly
1476
+ sample<List>(self) =
1477
+ todo
1478
+
1479
+ # returns a new list with all elements shuffled
1480
+ shuffle<List>(self) =
1481
+ todo
1482
+
1483
+ # returns a new list with all elements grouped by adjacent pairs
1484
+ partition<List>(self) =
1485
+ todo
1486
+
1487
+ # returns a new list with all elements grouped into chunks
1488
+ chunk<List>(self) =
1489
+ todo
1490
+
1491
+ # returns a new list with all elements grouped
1492
+ groupBy<List>(self) =
1493
+ todo
1494
+
1495
+ join<List>(self, sep: Str = ",") -> Str =
1496
+ res = Buffer()
1497
+ self.each(|v|
1498
+ res.write(v.toStr())
1499
+ res.write(sep)
1500
+ )
1501
+ res.toStr()
1502
+ ```
1503
+
1504
+ (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.)
1505
+
1506
+ - [ ] **Step 5: Confirm these files aren't exercised by any Rust test (so this step can't be verified by `cargo test`)**
1507
+
1508
+ Run: `grep -rn "libs/std" plum-checker/tests plum-wasm-codegen/tests plum-core/tests 2>/dev/null`
1509
+ 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).
1510
+
1511
+ - [ ] **Step 6: Commit**
1512
+
1513
+ ```bash
1514
+ git add libs/std/option.plum libs/std/result.plum libs/std/map.plum libs/std/list.plum
1515
+ git commit -m "feat(libs/std): migrate stdlib generics to bracket syntax"
1516
+ ```
1517
+
1518
+ ---
1519
+
1520
+ ### Task 7: Rewrite `examples/types.plum`
1521
+
1522
+ **Files:**
1523
+ - Modify: `examples/types.plum`
1524
+
1525
+ **Interfaces:**
1526
+ - Consumes: the new grammar (Task 1).
1527
+ - 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.
1528
+
1529
+ - [ ] **Step 1: Rewrite the file**
1530
+
1531
+ Replace the entire file contents with:
1532
+
1533
+ ```
1534
+ type Point =
1535
+ x: Int
1536
+ y: Int
1537
+
1538
+ type Named(Stringable) =
1539
+ name: Str
1540
+
1541
+ type Box[T] =
1542
+ value: T
1543
+
1544
+ trait Shape =
1545
+ area() -> Float
1546
+ perimeter() -> Float
1547
+
1548
+ trait Comparable[T: Ord] =
1549
+ compareTo(other: T) -> Int
1550
+
1551
+ enum Color =
1552
+ | Red
1553
+ | Green
1554
+ | Blue
1555
+
1556
+ enum Option =
1557
+ | Some(Int)
1558
+ | None
1559
+
1560
+ makeIntBox() -> Box =
1561
+ Box(value: 5)
1562
+
1563
+ makeStrBox() -> Box =
1564
+ Box(value: "x")
1565
+ ```
1566
+
1567
+ - [ ] **Step 2: Confirm this file isn't parsed by any automated test**
1568
+
1569
+ Run: `grep -rn "examples/types" plum-checker/tests plum-wasm-codegen/tests plum-core/tests plum-cli 2>/dev/null`
1570
+ Expected: no output — same reasoning as Task 6 Step 5; this file exists as a hand-written reference example, not something `cargo test` exercises.
1571
+
1572
+ - [ ] **Step 3: Commit**
1573
+
1574
+ ```bash
1575
+ git add examples/types.plum
1576
+ git commit -m "feat(examples): migrate types.plum generics to bracket syntax"
1577
+ ```
1578
+
1579
+ ---
1580
+
1581
+ ### Task 8: Final full-workspace verification
1582
+
1583
+ **Files:** none (verification only).
1584
+
1585
+ **Interfaces:**
1586
+ - Consumes: everything from Tasks 1-7.
1587
+ - Produces: confirmation that the migration is complete and the workspace is green end-to-end.
1588
+
1589
+ - [ ] **Step 1: Run the full Rust test suite**
1590
+
1591
+ Run: `cargo test --workspace 2>&1 | tail -100`
1592
+ Expected: all tests PASS.
1593
+
1594
+ - [ ] **Step 2: Run the tree-sitter corpus suite**
1595
+
1596
+ Run: `cd tooling/tree-sitter-plum && npx tree-sitter test 2>&1 | tail -60`
1597
+ Expected: all tests PASS.
1598
+
1599
+ - [ ] **Step 3: Confirm no old-syntax generics remain in tracked `.plum` files**
1600
+
1601
+ Run: `grep -rn '([a-d])\|([a-d],\|(Stringable)([a-d]\|| Some(a)\|| Ok(a)\|| Err(b)' libs/std examples 2>/dev/null`
1602
+ 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.)
1603
+
1604
+ - [ ] **Step 4: No commit needed**
1605
+
1606
+ 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.