parser-lr 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,325 +1,135 @@
1
- # parser-lr enhancement proposals (EduBASIC consumer)
1
+ # parser-lr open requests (EduBASIC)
2
2
 
3
- This document is for agents working on [parser-lr](https://github.com/adamrmoss/parser-lr). It lists concrete improvements suggested by the EduBASIC migration (`parser-lr` **0.4.1**, grammar at [`src/lang/parsing/grammars/edu-basic.grammar`](../src/lang/parsing/grammars/edu-basic.grammar)).
3
+ Audience: agents working on [parser-lr](https://github.com/adamrmoss/parser-lr).
4
4
 
5
- **Goal:** let grammar authors write stable `ast` + `transform` rules for statements the same way expressions already work, so downstream bridges do not need `keyword-fallbacks.ts`-style workarounds.
5
+ Consumer: **edu-basic** (`parser-lr` **0.5.0** in [`package.json`](../package.json)). Reference grammar: [`src/lang/parsing/grammars/edu-basic.grammar`](../src/lang/parsing/grammars/edu-basic.grammar).
6
+
7
+ This document lists **only what parser-lr still needs to change**. Items resolved in 0.5.0 are noted so they are not re-opened.
6
8
 
7
9
  ---
8
10
 
9
- ## Reference consumer
11
+ ## Resolved in 0.5.0 (no further work)
10
12
 
11
- | Artifact | Path |
12
- |----------|------|
13
- | Grammar | [`src/lang/parsing/grammars/edu-basic.grammar`](../src/lang/parsing/grammars/edu-basic.grammar) |
14
- | Generated table | [`src/lang/parsing/edu-basic.table.json`](../src/lang/parsing/edu-basic.table.json) |
15
- | AST bridge | [`src/lang/parsing/ast-bridge/`](../src/lang/parsing/ast-bridge/) |
16
- | Keyword fallbacks (debt) | [`src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts`](../src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts) |
17
- | Dispatch table | [`src/lang/parsing/ast-bridge/statements/dispatch.ts`](../src/lang/parsing/ast-bridge/statements/dispatch.ts) |
13
+ | Item | Notes |
14
+ |------|--------|
15
+ | **`pass()` preserves production symbol** | Bare `CLS` → `cls_stmt`, not `kw_cls`. Same for `PRINT`, `END`, `ELSE`, `WEND`, `DO`, `LOOP`, etc. |
16
+ | **`table validate` command** | Documented in README; implementation exists but is blocked by packaging bug below. |
17
+ | **CJS `require` export** | `package.json` `exports.require` → `dist/lib-cjs/index.js`. |
18
18
 
19
- Regenerate table after grammar changes:
19
+ After upgrading to a build that includes the pass fix, EduBASIC can delete [`keyword-fallbacks.ts`](../src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts) (kept temporarily for older tables / edge cases).
20
20
 
21
- ```bash
22
- npm run parser:table
23
- ```
21
+ ---
22
+
23
+ ## 1. CLI cannot find `grammar.json` (blocker)
24
24
 
25
- Parse smoke test:
25
+ ### Symptom
26
26
 
27
27
  ```bash
28
- node -e "
29
- import { ParseContext } from 'parser-lr';
30
- import table from './src/lang/parsing/edu-basic.table.json' with { type: 'json' };
31
- const ctx = ParseContext.fromTableJson(JSON.stringify(table));
32
- console.log(JSON.stringify(ctx.parseSource('CLS'), null, 2));
33
- "
28
+ npm run parser:table
29
+ # ENOENT: .../node_modules/parser-lr/bin/grammar.json
34
30
  ```
35
31
 
36
- ---
37
-
38
- ## Current asymmetry
39
-
40
- **Expressions** in EduBASIC have full `ast` + `transform` coverage (`comparison_expr.#binary`, `flatten(expr_list)`, etc.). Bridge code walks predictable node shapes.
41
-
42
- **Statements** use `transform` only at the `statement` dispatcher:
32
+ Same failure for:
43
33
 
44
- ```ebnf
45
- statement ->
46
- #cls pass(stmt)
47
- | #print pass(stmt)
48
- | … ;
34
+ ```bash
35
+ npx parser-lr table validate -g src/lang/parsing/grammars/edu-basic.grammar
49
36
  ```
50
37
 
51
- Individual `*_stmt` productions have **no** `ast` entries and **no** `transform` rules. When `pass(stmt)` runs and the matched production has a single terminal child, the output often **collapses** to that terminal instead of preserving the production symbol.
38
+ ### Actual layout in published 0.5.0
52
39
 
53
- That forces ~70 lines of `keyword-fallbacks.ts` plus duplicate handling in domain builders.
40
+ | Path | Present? |
41
+ |------|----------|
42
+ | `dist/lib/grammar/grammar.json` | Yes |
43
+ | `dist/lib-cjs/grammar/grammar.json` | Yes |
44
+ | `bin/grammar.json` | **No** |
54
45
 
55
- ---
46
+ CLI bundle resolves meta-grammar relative to `bin/`, not `dist/lib/grammar/`.
56
47
 
57
- ## Priority 1: `pass()` must preserve production symbol
48
+ ### Required fix
58
49
 
59
- ### Problem
50
+ Bundle CLI against the same `grammarJsonDirectory()` helper the library uses, or copy `grammar.json` beside `bin/parser-lr.js` in `prepack`.
60
51
 
61
- Grammar author binds `[stmt]:cls_stmt` and expects consumers to key off `cls_stmt`. Today bare `CLS` yields `kw_cls`.
52
+ ### Acceptance test (parser-lr repo)
62
53
 
63
- **Grammar fragment (reproducer):**
54
+ ```bash
55
+ npm pack
56
+ tmpdir=$(mktemp -d)
57
+ tar -xzf parser-lr-*.tgz -C "$tmpdir"
58
+ cd "$tmpdir/package"
59
+ node bin/parser-lr.js table validate -g grammars/calc.grammar
60
+ node bin/parser-lr.js table generate -g grammars/calc.grammar -o /tmp/calc.json -a lalr
61
+ ```
64
62
 
65
- ```ebnf
66
- name "pass-collapse-repro" ;
63
+ Both commands must exit 0 on a clean install from the tarball.
67
64
 
68
- tokens
69
- kw_cls = /CLS/i ;
70
- kw_with = /WITH/i ;
71
- ident = /[a-z]+/ ;
65
+ ---
72
66
 
73
- start program_line ;
67
+ ## 2. Table-only import must not load meta-grammar (blocker for Jest)
74
68
 
75
- grammar
76
- program_line =
77
- #statement [stmt]:cls_stmt
78
- ;
69
+ ### Symptom
79
70
 
80
- cls_stmt =
81
- kw_cls [ kw_with ident ]
82
- ;
71
+ EduBASIC tests import `ParseContext` / `AstNode` from `parser-lr` only to parse a pre-built table. Jest fails:
83
72
 
84
- transform
85
- program_line ->
86
- #statement pass(stmt) ;
87
73
  ```
88
-
89
- **Input:** `CLS`
90
-
91
- **Actual output today:**
92
-
93
- ```json
94
- {
95
- "symbol": "kw_cls",
96
- "children": [],
97
- "text": "CLS",
98
- "variant": null
99
- }
74
+ SyntaxError: Cannot use 'import.meta' outside a module
75
+ at grammar-json-path.esm.js
100
76
  ```
101
77
 
102
- **Required output:**
78
+ Import chain today:
103
79
 
104
- ```json
105
- {
106
- "symbol": "cls_stmt",
107
- "children": [
108
- { "symbol": "kw_cls", "text": "CLS", "children": [] }
109
- ],
110
- "variant": null
111
- }
112
80
  ```
113
-
114
- **Input:** `CLS WITH foo`
115
-
116
- **Required output (variant optional but symbol must stay `cls_stmt`):**
117
-
118
- ```json
119
- {
120
- "symbol": "cls_stmt",
121
- "children": [
122
- { "symbol": "kw_cls", "text": "CLS", "children": [] },
123
- { "symbol": "kw_with", "text": "WITH", "children": [] },
124
- { "symbol": "ident", "text": "foo", "children": [] }
125
- ],
126
- "variant": null
127
- }
81
+ dist/lib/index.js
82
+ grammar/index.js
83
+ → read-grammar.js
84
+ meta-grammar-table.js (readFileSync grammar.json at module load)
85
+ → grammar-json-path.esm.js (uses import.meta.url)
128
86
  ```
129
87
 
130
- ### Semantics proposal
88
+ `readGrammar()` is not called; meta-grammar still loads because it sits on the public barrel.
131
89
 
132
- `pass(slot)` where `slot` is bound to production `P`:
90
+ ### Required fix (pick one)
133
91
 
134
- 1. Always emit an AST node whose `symbol` is `P` (the production name).
135
- 2. Children are the transformed slots of the matched alternative (terminals and subtrees).
136
- 3. Set `variant` from the alternative label (`#program`, `#bare`, …) when present.
137
- 4. **Never** hoist a sole child terminal to replace the `P` node unless the transform explicitly says `pass(thatTerminal)` at the **same** rule (not a parent rule).
92
+ **Option A (preferred):** Lazy-load meta-grammar inside `readGrammar()` / `metaGrammarTable()` only. Do not import `meta-grammar-table.js` from `grammar/index.js` at top level. Split exports:
138
93
 
139
- ### EduBASIC statements affected (bare input wrong symbol today)
94
+ - `parser-lr` parse runtime (`ParseContext`, `AstNode`, lexer, shift-reduce, transform)
95
+ - `parser-lr/grammar` or `parser-lr/read-grammar` — grammar file parsing (`readGrammar`, `validateGrammarTable`)
140
96
 
141
- | Input | Expected symbol | Actual symbol today |
142
- |-------|-----------------|---------------------|
143
- | `CLS` | `cls_stmt` | `kw_cls` |
144
- | `PRINT` | `print_stmt` | `kw_print` |
145
- | `END` | `end_stmt` (`variant: program`) | `kw_end` |
146
- | `ELSE` | `else_stmt` | `kw_else` |
147
- | `WEND` | `wend_stmt` | `kw_wend` |
148
- | `UEND` | `uend_stmt` | `kw_uend` |
149
- | `TRY` | `try_stmt` | `kw_try` |
150
- | `CATCH` | `catch_stmt` | `kw_catch` |
151
- | `FINALLY` | `finally_stmt` | `kw_finally` |
152
- | `RETURN` | `return_stmt` | `kw_return` |
153
- | `NEXT` | `next_stmt` | `kw_next` |
154
- | `DO` | `do_stmt` (`variant: bare`) | `kw_do` |
155
- | `LOOP` | `loop_stmt` (`variant: bare`) | `kw_loop` |
156
- | `RANDOMIZE` | `randomize_stmt` | `kw_randomize` |
157
-
158
- `END IF` correctly yields `end_stmt` with `variant: if` because multiple children prevent collapse.
159
-
160
- ### Consumer cleanup after fix
161
-
162
- Delete [`keyword-fallbacks.ts`](../src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts) and remove the fallback call from [`dispatch.ts`](../src/lang/parsing/ast-bridge/statements/dispatch.ts). Remove duplicate `buildNullaryControlFlowStatement` paths that exist only for `*_stmt` vs `kw_*` pairs.
163
-
164
- ### Suggested parser-lr tests
165
-
166
- Add fixtures under `grammars/fixtures/pass-preserves-production/`:
167
-
168
- - `bare-terminal.grammar` (reproducer above)
169
- - `labeled-variant.grammar` (`end_stmt` with `#program` / `#if`)
170
- - `optional-present.grammar` (`CLS WITH x`)
171
- - `optional-absent.grammar` (`CLS`)
172
-
173
- Assert `node.symbol` after transform, not only parse success.
174
-
175
- ### Suggested CLI warning
176
-
177
- At `table generate` time, warn when:
178
-
179
- - Parent transform uses `pass(boundSlot)`
180
- - Bound production has **no** `transform` rule
181
- - Production can match with exactly one terminal child
182
-
183
- ---
184
-
185
- ## Priority 2: Statement-level `ast` + `transform` (parity with expressions)
186
-
187
- ### Problem
188
-
189
- EduBASIC `ast` section today only describes expression shapes. Statements cannot use `build` / `flatten` / labeled variants.
190
-
191
- ### Target pattern for EduBASIC
192
-
193
- After Priority 1, EduBASIC should be able to add rules like:
194
-
195
- ```ebnf
196
- ast
197
- cls_stmt =
198
- #bare kw_cls
199
- | #with kw_cls kw_with [bg]:expr
200
- ;
201
-
202
- print_stmt =
203
- #bare kw_print
204
- | #items kw_print [list]:print_list
205
- ;
97
+ **Option B:** Ensure `dist/lib-cjs/` is safe under Jest without `import.meta`, and document that table-only consumers must `require('parser-lr')` (CJS). EduBASIC can point Jest at CJS, but the ESM entry should still not pull meta-grammar for consumers that only call `ParseContext.fromTableJson`.
206
98
 
207
- print_list =
208
- #list [first]:print_segment { [rest]:print_segment }
209
- ;
210
-
211
- print_segment =
212
- #bare [value]:expr
213
- | #trailingComma [value]:expr [comma]:comma
214
- | #trailingSemicolon [value]:expr [semi]:semicolon
215
- ;
216
-
217
- end_stmt =
218
- #program kw_end
219
- | #if kw_end kw_if
220
- | #unless kw_end kw_unless
221
- | #select kw_end kw_select
222
- | #sub kw_end kw_sub
223
- | #try kw_end kw_try
224
- ;
99
+ ### Acceptance test (parser-lr repo)
225
100
 
226
- transform
227
- cls_stmt ->
228
- #bare cls_stmt.#bare(kw_cls)
229
- | #with cls_stmt.#with(kw_cls, kw_with, bg) ;
230
-
231
- print_stmt ->
232
- #bare print_stmt.#bare(kw_print)
233
- | #items print_stmt.#items(kw_print, list) ;
234
-
235
- print_list ->
236
- #list flatten(print_list.#list, first, rest) ;
237
-
238
- print_segment ->
239
- #bare print_segment.#bare(value)
240
- | #trailingComma print_segment.#trailingComma(value, comma)
241
- | #trailingSemicolon print_segment.#trailingSemicolon(value, semi) ;
242
-
243
- end_stmt ->
244
- #program end_stmt.#program(kw_end)
245
- | #if end_stmt.#if(kw_end, kw_if)
246
- | … ;
101
+ ```javascript
102
+ // table-only-smoke.cjs — must not throw under plain node
103
+ const { ParseContext } = require('parser-lr');
104
+ const table = require('./grammars/calc.table.json'); // or generate inline
105
+ const ctx = ParseContext.fromTableJson(JSON.stringify(table));
106
+ console.log(ctx.parseSource('1 + 2')?.symbol);
247
107
  ```
248
108
 
249
- Bridge code then becomes:
250
-
251
- ```typescript
252
- export function buildClsStatement(node: AstNode): ClsStatement
253
- {
254
- if (node.variant === 'with')
255
- {
256
- return new ClsStatement(buildExpression(findChildBySymbol(node, 'bg')!));
257
- }
258
-
259
- return new ClsStatement();
260
- }
109
+ ```javascript
110
+ // table-only-smoke.mjs — same for ESM default entry
111
+ import { ParseContext } from 'parser-lr';
261
112
  ```
262
113
 
263
- No scanning for `kw_cls` at the root, no `findExpressionRoot` guessing.
264
-
265
- ### Enhancement requests for parser-lr
266
-
267
- 1. **Document** recommended statement authoring pattern (mirror expression docs).
268
- 2. **Optional shorthand:** if `ast` defines `cls_stmt` but `transform` omits it, apply identity build `cls_stmt.#alt(...)` from grammar bindings (opt-in flag `--infer-identity-transforms`).
269
- 3. **Table validation:** error if `transform` references `type.#variant` not declared in `ast`.
114
+ Neither import may read `grammar.json` or evaluate `import.meta`.
270
115
 
271
116
  ---
272
117
 
273
- ## Priority 3: Stable shapes for nested statement helpers
118
+ ## 3. Preserve intermediate production nodes (high)
274
119
 
275
- ### 3a. `help_topic` collapse
276
-
277
- **Grammar:**
278
-
279
- ```ebnf
280
- help_stmt = kw_help help_topic ;
281
- help_topic = #keyword help_kw | #punct help_punct ;
282
- ```
283
-
284
- **Input:** `HELP PRINT`
285
-
286
- **Observed:** `help_stmt` with children `kw_help`, `kw_print` (no `help_topic` wrapper).
287
-
288
- **Desired:** either preserve `help_topic` node:
289
-
290
- ```json
291
- {
292
- "symbol": "help_stmt",
293
- "children": [
294
- { "symbol": "kw_help", "text": "HELP" },
295
- {
296
- "symbol": "help_topic",
297
- "variant": "keyword",
298
- "children": [{ "symbol": "kw_print", "text": "PRINT" }]
299
- }
300
- ]
301
- }
302
- ```
303
-
304
- or require explicit transform:
120
+ ### Problem
305
121
 
306
- ```ebnf
307
- help_topic ->
308
- #keyword help_topic.#keyword(help_kw)
309
- | #punct help_topic.#punct(help_punct) ;
310
- ```
122
+ `pass()` fix covers **bound slots** on statement dispatch (`[stmt]:cls_stmt`). Nested productions used inside statements still **flatten** when they have no `transform` rule, even when the grammar names them explicitly.
311
123
 
312
- **Consumer workaround today:** [`buildHelpStatement`](../src/lang/parsing/ast-bridge/statements/io.ts) scans for any child that is not `kw_help`.
124
+ EduBASIC bridge code must scan for raw tokens or alternate shapes.
313
125
 
314
- ### 3b. `case_selector` and `comparison_op`
126
+ ### 3a. `comparison_op` inside `case_selector`
315
127
 
316
- **Grammar:**
128
+ **Grammar (already in edu-basic):**
317
129
 
318
130
  ```ebnf
319
131
  case_selector =
320
- #value expr
321
- | #range expr kw_to expr
322
- | #relational kw_is comparison_op expr
132
+ #relational kw_is comparison_op expr
323
133
  ;
324
134
 
325
135
  comparison_op =
@@ -328,221 +138,200 @@ comparison_op =
328
138
  | … ;
329
139
  ```
330
140
 
331
- **Input:** `CASE IS < 0`
332
-
333
- **Observed `case_selector` (variant `relational`) children:** `kw_is`, `less`, `postfix_expr` — no `comparison_op` node.
334
-
335
- **Desired:**
336
-
337
- ```json
338
- {
339
- "symbol": "case_selector",
340
- "variant": "relational",
341
- "children": [
342
- { "symbol": "kw_is", "text": "IS" },
343
- {
344
- "symbol": "comparison_op",
345
- "variant": "less",
346
- "children": [{ "symbol": "less", "text": "<" }]
347
- },
348
- { "symbol": "postfix_expr", "…": "…" }
349
- ]
350
- }
351
- ```
352
-
353
- With transform:
141
+ `comparison_op` **has** expression-level transforms:
354
142
 
355
143
  ```ebnf
356
- case_selector ->
357
- #value case_selector.#value(expr)
358
- | #range case_selector.#range(expr, kw_to, expr)
359
- | #relational case_selector.#relational(kw_is, comparison_op, expr) ;
360
-
361
144
  comparison_op ->
362
- #less comparison_op.#less(less)
363
- | #greater_equal comparison_op.#greaterEqual(greater_equal)
145
+ #less pass(tok)
146
+ | #greaterEqual pass(tok)
364
147
  | … ;
365
148
  ```
366
149
 
367
- **Consumer workaround today:** [`buildCaseSelector`](../src/lang/parsing/ast-bridge/statements/control-flow.ts) matches raw token names (`less_equal`) via `isComparisonOpSymbol`.
150
+ **Input:** `CASE IS < 0`
151
+
152
+ **Actual `case_selector` children (variant `relational`):** `kw_is`, `less`, `expr`
153
+
154
+ **Required:** `kw_is`, `comparison_op` (variant `less`, child `less`), `expr`
368
155
 
369
- ### 3c. `print_list` / repeat tails
156
+ Without this, consumers match lexer token names (`less`, `greater_equal`) instead of the grammar non-terminal `comparison_op`.
157
+
158
+ ### 3b. `let_target` inside `let_stmt`
370
159
 
371
160
  **Grammar:**
372
161
 
373
162
  ```ebnf
374
- print_stmt = kw_print [ print_list ] ;
375
- print_list = print_segment { print_segment } ;
163
+ let_stmt = kw_let let_target compound_op expr ;
164
+ let_target = identifier assn_path_suffix ;
376
165
  ```
377
166
 
378
- Bridge must handle:
167
+ **Input:** `LET x% = 1`
379
168
 
380
- - `print_list` wrapper present or absent
381
- - `print_list$repeat_0` flatten tails
382
- - bare trailing `postfix_expr` siblings (when list flattening inlines items)
169
+ **Actual:** flat `identifier`, `equal`, `expr` under `let_stmt` (no `let_target` wrapper).
383
170
 
384
- **Desired:** consistent `flatten(print_list.#list, first, rest)` output (same as `expr_list` for expressions).
171
+ **Required:** `let_stmt` children include `let_target` wrapping `identifier` (and optional `assn_path_suffix`).
385
172
 
386
- **Consumer workaround today:** [`buildPrintSegmentsFromStatement`](../src/lang/parsing/ast-bridge/statements/io.ts) accepts `print_list`, `print_segment`, or raw expression children interchangeably.
173
+ EduBASIC workaround: [`assignment-helpers.ts`](../src/lang/parsing/ast-bridge/assignment-helpers.ts) `buildLetAssignmentTarget` accepts bare `identifier` at statement root.
387
174
 
388
- ### 3d. `let_target` / assignment paths
175
+ ### 3c. `print_list` / `print_segment`
389
176
 
390
177
  **Grammar:**
391
178
 
392
179
  ```ebnf
393
- let_stmt = kw_let let_target compound_op expr ;
394
- let_target = identifier assn_path_suffix ;
180
+ print_stmt = kw_print [ print_list ] ;
181
+ print_list = print_segment { print_segment } ;
182
+ print_segment = #bare expr | #trailingComma expr comma | … ;
395
183
  ```
396
184
 
397
- Simple `LET x% = 1` often arrives as flat `identifier`, `equal`, `expr` without `let_target` wrapper.
398
-
399
- **Desired:** always `let_target` node with `identifier` child and optional `assn_path_suffix` list (possibly empty).
185
+ Bridge must accept: `print_list` wrapper, `print_list$repeat_*` tails, bare `print_segment`, or trailing `postfix_expr` siblings interchangeably ([`io.ts`](../src/lang/parsing/ast-bridge/statements/io.ts) `buildPrintSegmentsFromStatement`).
400
186
 
401
- **Consumer workaround today:** [`assignment-helpers.ts`](../src/lang/parsing/ast-bridge/assignment-helpers.ts) handles both `let_target` and bare `identifier` at the statement root.
402
-
403
- ---
187
+ **Required:** stable list shape matching expression `flatten` output (same as `expr_list`).
404
188
 
405
- ## Priority 4: `flatten()` repeat semantics
189
+ ### Proposed semantics
406
190
 
407
- ### Problem
191
+ When a CST node is a **non-terminal production** `P` and `P` has **no** `transform` rule:
408
192
 
409
- EduBASIC relies on `flatten` for expression lists, array literals, struct members, and (target) print lists. Bridge code defends against:
193
+ 1. Emit AST node `{ symbol: P, variant: <alt label>, children: [ transformed children ] }`.
194
+ 2. Do **not** hoist single-child terminals to replace `P`.
195
+ 3. Apply existing `transform` rules when present (as with `comparison_op` in expression context).
410
196
 
411
- - Empty repeat tail nodes (`postfix_expr` with `variant: primary` and zero children) — see [`isEmptyPostfixTail`](../src/lang/parsing/ast-bridge/ast-node-helpers.ts)
412
- - Repeat segments named `foo$repeat_0` that drop or duplicate items
197
+ This generalizes the 0.5.0 `pass()` bound-slot fix to **all referenced non-terminals**.
413
198
 
414
- ### Minimal reproducer grammar
199
+ ### Minimal reproducer grammar (add to parser-lr fixtures)
415
200
 
416
201
  ```ebnf
417
- name "flatten-repeat-repro" ;
202
+ name "nested-production-repro" ;
418
203
 
419
204
  tokens
205
+ kw_is = /IS/i ;
206
+ kw_case = /CASE/i ;
207
+ less = /</ ;
420
208
  ident = /[a-z]+/ ;
421
- comma = /,/ ;
209
+ integer = /[0-9]+/ ;
422
210
 
423
- start list ;
211
+ start line ;
424
212
 
425
213
  grammar
426
- list = item { comma item } ;
214
+ line = kw_case case_selector ;
215
+ case_selector = #relational kw_is comparison_op integer ;
216
+ comparison_op = #less less ;
427
217
 
428
218
  ast
429
- list = #list [first]:item { [rest]:item } ;
219
+ comparison_op =
220
+ #less [tok]:less
221
+ ;
430
222
 
431
223
  transform
432
- list -> flatten(list.#list, first, rest) ;
224
+ comparison_op ->
225
+ #less comparison_op.#less(tok) ;
433
226
  ```
434
227
 
435
- **Input:** `a,b,c`
436
-
437
- **Required:** `list` node with three `item` payloads in order; no spurious empty tail nodes that confuse consumers.
438
-
439
- ### Migration note
228
+ **Input:** `CASE IS < 0`
440
229
 
441
- EduBASIC previously carried a local patch to `applyFlatten` in `node_modules/parser-lr` for repeat-list correctness. That fix belongs upstream with fixture tests.
230
+ **Assert:** `case_selector` child at index 1 has `symbol === "comparison_op"`, not `less`.
442
231
 
443
232
  ---
444
233
 
445
- ## Priority 5: `build()` optional slot handling
234
+ ## 4. `table validate` must work on edu-basic grammar (medium)
446
235
 
447
- ### Problem
448
-
449
- When a production has optional slots (`[ kw_with expr ]`) and transform uses `build`, absent optionals should be omitted from the child list (or surfaced as explicit `null`), not shift subsequent bindings.
450
-
451
- ### Minimal reproducer
452
-
453
- ```ebnf
454
- cls_stmt = kw_cls [ kw_with ident ] ;
236
+ Depends on **§1** (CLI packaging).
455
237
 
456
- ast
457
- cls_stmt =
458
- #main kw_cls [withKw]:kw_with [target]:ident
459
- ;
238
+ Once the CLI runs, validate this consumer grammar:
460
239
 
461
- transform
462
- cls_stmt -> cls_stmt.#main(kw_cls, withKw, target) ;
240
+ ```bash
241
+ npx parser-lr table validate -g src/lang/parsing/grammars/edu-basic.grammar
463
242
  ```
464
243
 
465
- **Input:** `CLS` `withKw` and `target` absent.
466
-
467
- **Required:** `cls_stmt` node with only `kw_cls` child; optional named slots not required for downstream indexing.
244
+ Useful warnings (already described in README) should include:
468
245
 
469
- ### Migration note
246
+ - `pass(boundSlot)` where bound production has no `transform` and can match a single terminal (legacy warning; should disappear after §3).
247
+ - `transform` references `type.#variant` not declared in `ast`.
248
+ - Duplicate `grammar` / `ast` / `transform` sections (edu-basic currently duplicates statement blocks; validator should flag).
470
249
 
471
- EduBASIC previously patched `applyBuild` optional-slot skip in `node_modules/parser-lr`.
250
+ `--strict` should be usable in CI.
472
251
 
473
252
  ---
474
253
 
475
- ## Priority 6: Labeled alternatives on unary productions
254
+ ## 5. Document transform contract (medium)
476
255
 
477
- ### Problem
256
+ Update [`docs/grammar.md`](https://github.com/adamrmoss/parser-lr/blob/main/docs/grammar.md) with explicit rules:
478
257
 
479
- Productions like `end_stmt` use labeled alternatives:
258
+ | Transform | Behavior |
259
+ |-----------|----------|
260
+ | `pass(slot)` | Preserve bound production symbol; never collapse to sole terminal child. |
261
+ | No rule for production `P` | Identity node `{ symbol: P, children }` (proposed §3). |
262
+ | `build(type.#variant, …)` | Absent optional slots omitted from children, not shifted. |
263
+ | `flatten(type.#list, head, tail)` | Repeat tails: no empty placeholder nodes; no dropped items. |
480
264
 
481
- ```ebnf
482
- end_stmt =
483
- #program kw_end
484
- | #if kw_end kw_if
485
- ;
486
- ```
265
+ Include before/after JSON for `CLS`, `CASE IS < 0`, and `LET x = 1`.
487
266
 
488
- When Priority 1 is fixed, `variant` on the output `end_stmt` node must reflect `#program` vs `#if` even for bare `END` (single child `kw_end`).
267
+ ---
489
268
 
490
- **Input:** `END` `{ symbol: "end_stmt", variant: "program", children: […] }`
269
+ ## 6. Regression fixtures to add in parser-lr (medium)
491
270
 
492
- **Input:** `END IF` `{ symbol: "end_stmt", variant: "if", children: […] }`
271
+ | Fixture | Covers |
272
+ |---------|--------|
273
+ | `pass-preserves-production/` | Bare optional production (`cls_stmt = kw_cls [ … ]`) |
274
+ | `nested-production-repro/` | §3 `comparison_op` inside parent without parent transform |
275
+ | `let-target-repro/` | §3b `let_target` wrapper |
276
+ | `flatten-repeat/` | `a,b,c` list with no spurious empty tails |
277
+ | `build-optional-slots/` | Optional grammar slots absent in `build()` output |
278
+ | `table-only-import/` | §2 smoke scripts |
493
279
 
494
- Same for `do_stmt` / `loop_stmt` (`#bare`, `#while`, `#until`).
280
+ Each fixture: `.grammar`, input line(s), expected AST JSON golden file.
495
281
 
496
282
  ---
497
283
 
498
- ## Priority 7: Tooling and packaging
284
+ ## EduBASIC verification (run after parser-lr release)
499
285
 
500
- | Item | Rationale |
501
- |------|-----------|
502
- | `parser-lr table validate` | Check transform/`ast` consistency, pass-collapse warnings |
503
- | Fixture corpus | Grammars + inputs + expected AST JSON (like EduBASIC cases above) |
504
- | CommonJS `require` entry | Jest consumers (see `jest.config.js` `moduleNameMapper` for `parser-lr`) |
505
- | Document `pass` vs `build` vs `flatten` | Include “do not collapse production symbol” rule |
286
+ Run these locally after bumping `parser-lr` and regenerating the table:
506
287
 
507
- ---
288
+ ```bash
289
+ npm install parser-lr@<version>
290
+ npm run parser:table
291
+ npm run parser:check
292
+ npm test
293
+ ```
508
294
 
509
- ## What “elegant EduBASIC grammar” looks like after fixes
295
+ Spot checks:
510
296
 
511
- 1. **Remove** duplicate `grammar` / `ast` / `transform` blocks (EduBASIC currently duplicates statement sections ~lines 611–1400 and ~1694–2123).
512
- 2. **Add** `ast` + `transform` for all `*_stmt` productions (or rely on Priority 1 + identity build).
513
- 3. **Delete** [`keyword-fallbacks.ts`](../src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts).
514
- 4. **Shrink** bridge helpers that only exist for flat/collapsed trees (`buildPrintSegmentsFromStatement` multi-path scan, `buildHelpStatement` flat child scan, `isComparisonOpSymbol` for `CASE IS`).
515
- 5. **Keep** [`dispatch.ts`](../src/lang/parsing/ast-bridge/statements/dispatch.ts) as a thin `symbol → builder` table keyed on stable `*_stmt` names.
297
+ ```bash
298
+ node -e "
299
+ import { ParseContext } from 'parser-lr';
300
+ import table from './src/lang/parsing/edu-basic.table.json' with { type: 'json' };
301
+ const ctx = ParseContext.fromTableJson(JSON.stringify(table));
302
+ for (const line of ['CLS', 'PRINT', 'CASE IS < 0', 'LET x% = 1', 'HELP PRINT']) {
303
+ const ast = ctx.parseSource(line);
304
+ console.log(line, '=>', ast?.symbol, JSON.stringify(ast?.children?.map(c => c.symbol)));
305
+ }
306
+ "
307
+ ```
516
308
 
517
- Rough expected line reduction in `ast-bridge/`: on the order of **15–25%**, mostly from deleting fallback and defensive tree-walk code.
309
+ Expected after all items above:
518
310
 
519
- ---
311
+ | Line | Root symbol | Notable children |
312
+ |------|-------------|------------------|
313
+ | `CLS` | `cls_stmt` | `kw_cls` |
314
+ | `PRINT` | `print_stmt` | `kw_print` |
315
+ | `CASE IS < 0` | `case_stmt` | selector child includes `comparison_op` |
316
+ | `LET x% = 1` | `let_stmt` | includes `let_target` |
317
+ | `HELP PRINT` | `help_stmt` | `kw_help`, `help_topic` |
520
318
 
521
- ## Suggested implementation order for parser-lr agents
319
+ Then remove from edu-basic:
522
320
 
523
- 1. Fix `pass()` production-symbol preservation + tests (unblocks everything).
524
- 2. Land `flatten` and `build` optional-slot fixes with fixtures.
525
- 3. Add `table validate` warnings for missing statement transforms.
526
- 4. Document statement authoring guide with full mini-grammar (calc-style, but statements).
527
- 5. Work with EduBASIC to add statement `ast`/`transform` incrementally and delete fallbacks.
321
+ - [`keyword-fallbacks.ts`](../src/lang/parsing/ast-bridge/statements/keyword-fallbacks.ts)
322
+ - Flat-tree fallback branches in [`io.ts`](../src/lang/parsing/ast-bridge/statements/io.ts) (`buildHelpStatement`, `buildPrintSegmentsFromStatement`)
323
+ - `isComparisonOpSymbol` token-name matching in [`control-flow.ts`](../src/lang/parsing/ast-bridge/statements/control-flow.ts)
324
+ - Bare `identifier` path in [`assignment-helpers.ts`](../src/lang/parsing/ast-bridge/assignment-helpers.ts)
528
325
 
529
326
  ---
530
327
 
531
- ## EduBASIC verification checklist
532
-
533
- After each parser-lr release, run in this repo:
534
-
535
- ```bash
536
- npm install parser-lr@<version>
537
- npm run parser:table
538
- npm test
539
- ```
540
-
541
- Key tests:
328
+ ## Priority summary
542
329
 
543
- - [`specs/units/parsers/grammar-table.spec.ts`](../specs/units/parsers/grammar-table.spec.ts) bare `CLS`, bare `PRINT`
544
- - [`specs/units/parsers/ast-bridge.spec.ts`](../specs/units/parsers/ast-bridge.spec.ts) — dispatch and `kw_print` fallback (remove after Priority 1)
545
- - [`specs/units/parsers/parser-service.spec.ts`](../specs/units/parsers/parser-service.spec.ts) full statement coverage
546
- - [`programs/SelectCaseTorture.bas`](../programs/SelectCaseTorture.BAS) `CASE IS` relational selectors
330
+ | Priority | Item | Blocks edu-basic |
331
+ |----------|------|------------------|
332
+ | P0 | §1 CLI `grammar.json` path | `npm run parser:table`, `table validate` |
333
+ | P0 | §2 Lazy meta-grammar / split entry | Jest and any `import { ParseContext } from 'parser-lr'` |
334
+ | P1 | §3 Intermediate production nodes | Bridge simplification; `CASE IS`, `LET`, `PRINT` |
335
+ | P2 | §4–§6 Validate, docs, fixtures | CI confidence, upstream maintenance |
547
336
 
548
- When Priority 1 ships, add an assertion that `parseSource('CLS').symbol === 'cls_stmt'` and remove `keyword-fallbacks` from the codebase in the same PR that bumps the parser-lr version.
337
+ Target: **parser-lr 0.5.1** (or 0.6.0) with tarball acceptance tests for §1 and §2.