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