parser-lr 0.4.0 → 0.5.0

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.
Files changed (198) hide show
  1. package/README.md +35 -1
  2. package/bin/parser-lr.js +369 -9068
  3. package/bin/parser-lr.js.map +1 -1
  4. package/dist/lib/grammar/grammar-json-path.d.ts +2 -0
  5. package/dist/lib/grammar/grammar-json-path.d.ts.map +1 -0
  6. package/dist/lib/grammar/grammar-json-path.esm.d.ts +5 -0
  7. package/dist/lib/grammar/grammar-json-path.esm.d.ts.map +1 -0
  8. package/dist/lib/grammar/grammar-json-path.esm.js +9 -0
  9. package/dist/lib/grammar/grammar-json-path.esm.js.map +1 -0
  10. package/dist/lib/grammar/grammar-json-path.js +2 -0
  11. package/dist/lib/grammar/grammar-json-path.js.map +1 -0
  12. package/dist/lib/grammar/grammar.json +1218 -4476
  13. package/dist/lib/grammar/index.d.ts +2 -0
  14. package/dist/lib/grammar/index.d.ts.map +1 -1
  15. package/dist/lib/grammar/index.js +1 -0
  16. package/dist/lib/grammar/index.js.map +1 -1
  17. package/dist/lib/grammar/meta-grammar-table.d.ts.map +1 -1
  18. package/dist/lib/grammar/meta-grammar-table.js +4 -2
  19. package/dist/lib/grammar/meta-grammar-table.js.map +1 -1
  20. package/dist/lib/grammar/table-validator.d.ts +26 -0
  21. package/dist/lib/grammar/table-validator.d.ts.map +1 -0
  22. package/dist/lib/grammar/table-validator.js +211 -0
  23. package/dist/lib/grammar/table-validator.js.map +1 -0
  24. package/dist/lib/index.d.ts +2 -0
  25. package/dist/lib/index.d.ts.map +1 -1
  26. package/dist/lib/index.js +1 -0
  27. package/dist/lib/index.js.map +1 -1
  28. package/dist/lib/transform/cst-transformer.d.ts +14 -0
  29. package/dist/lib/transform/cst-transformer.d.ts.map +1 -1
  30. package/dist/lib/transform/cst-transformer.js +82 -11
  31. package/dist/lib/transform/cst-transformer.js.map +1 -1
  32. package/dist/lib/transform/grammar-fixture.d.ts +56 -0
  33. package/dist/lib/transform/grammar-fixture.d.ts.map +1 -0
  34. package/dist/lib/transform/grammar-fixture.js +101 -0
  35. package/dist/lib/transform/grammar-fixture.js.map +1 -0
  36. package/dist/lib-cjs/ast/ast-node.js +70 -0
  37. package/dist/lib-cjs/ast/ast-node.js.map +1 -0
  38. package/dist/lib-cjs/errors/index.js +9 -0
  39. package/dist/lib-cjs/errors/index.js.map +1 -0
  40. package/dist/lib-cjs/errors/parser-lr-error.js +52 -0
  41. package/dist/lib-cjs/errors/parser-lr-error.js.map +1 -0
  42. package/dist/lib-cjs/grammar/ast-schema.js +43 -0
  43. package/dist/lib-cjs/grammar/ast-schema.js.map +1 -0
  44. package/dist/lib-cjs/grammar/ast-type.js +3 -0
  45. package/dist/lib-cjs/grammar/ast-type.js.map +1 -0
  46. package/dist/lib-cjs/grammar/expression.js +3 -0
  47. package/dist/lib-cjs/grammar/expression.js.map +1 -0
  48. package/dist/lib-cjs/grammar/grammar-from-cst.js +641 -0
  49. package/dist/lib-cjs/grammar/grammar-from-cst.js.map +1 -0
  50. package/dist/lib-cjs/grammar/grammar-json-path.js +13 -0
  51. package/dist/lib-cjs/grammar/grammar-json-path.js.map +1 -0
  52. package/dist/lib-cjs/grammar/grammar-literals.js +71 -0
  53. package/dist/lib-cjs/grammar/grammar-literals.js.map +1 -0
  54. package/dist/lib-cjs/grammar/grammar.js +64 -0
  55. package/dist/lib-cjs/grammar/grammar.js.map +1 -0
  56. package/dist/lib-cjs/grammar/grammar.json +5760 -0
  57. package/dist/lib-cjs/grammar/index.js +19 -0
  58. package/dist/lib-cjs/grammar/index.js.map +1 -0
  59. package/dist/lib-cjs/grammar/meta-grammar-table.js +39 -0
  60. package/dist/lib-cjs/grammar/meta-grammar-table.js.map +1 -0
  61. package/dist/lib-cjs/grammar/production.js +3 -0
  62. package/dist/lib-cjs/grammar/production.js.map +1 -0
  63. package/dist/lib-cjs/grammar/read-grammar-error.js +22 -0
  64. package/dist/lib-cjs/grammar/read-grammar-error.js.map +1 -0
  65. package/dist/lib-cjs/grammar/read-grammar.js +24 -0
  66. package/dist/lib-cjs/grammar/read-grammar.js.map +1 -0
  67. package/dist/lib-cjs/grammar/table-validator.js +215 -0
  68. package/dist/lib-cjs/grammar/table-validator.js.map +1 -0
  69. package/dist/lib-cjs/grammar/token-rule.js +3 -0
  70. package/dist/lib-cjs/grammar/token-rule.js.map +1 -0
  71. package/dist/lib-cjs/grammar/transform-expression.js +3 -0
  72. package/dist/lib-cjs/grammar/transform-expression.js.map +1 -0
  73. package/dist/lib-cjs/grammar/transform-rule.js +3 -0
  74. package/dist/lib-cjs/grammar/transform-rule.js.map +1 -0
  75. package/dist/lib-cjs/grammar/transform-schema.js +43 -0
  76. package/dist/lib-cjs/grammar/transform-schema.js.map +1 -0
  77. package/dist/lib-cjs/index.js +66 -0
  78. package/dist/lib-cjs/index.js.map +1 -0
  79. package/dist/lib-cjs/lexer/index.js +32 -0
  80. package/dist/lib-cjs/lexer/index.js.map +1 -0
  81. package/dist/lib-cjs/lexer/lexer-compile-error.js +22 -0
  82. package/dist/lib-cjs/lexer/lexer-compile-error.js.map +1 -0
  83. package/dist/lib-cjs/lexer/lexer-compile.js +201 -0
  84. package/dist/lib-cjs/lexer/lexer-compile.js.map +1 -0
  85. package/dist/lib-cjs/lexer/lexer-error.js +22 -0
  86. package/dist/lib-cjs/lexer/lexer-error.js.map +1 -0
  87. package/dist/lib-cjs/lexer/lexer-input-error.js +17 -0
  88. package/dist/lib-cjs/lexer/lexer-input-error.js.map +1 -0
  89. package/dist/lib-cjs/lexer/lexer-state-error.js +19 -0
  90. package/dist/lib-cjs/lexer/lexer-state-error.js.map +1 -0
  91. package/dist/lib-cjs/lexer/lexer.js +278 -0
  92. package/dist/lib-cjs/lexer/lexer.js.map +1 -0
  93. package/dist/lib-cjs/lexer/token.js +44 -0
  94. package/dist/lib-cjs/lexer/token.js.map +1 -0
  95. package/dist/lib-cjs/package.json +3 -0
  96. package/dist/lib-cjs/parse-context-error.js +19 -0
  97. package/dist/lib-cjs/parse-context-error.js.map +1 -0
  98. package/dist/lib-cjs/parse-context.js +137 -0
  99. package/dist/lib-cjs/parse-context.js.map +1 -0
  100. package/dist/lib-cjs/parse-output-error.js +19 -0
  101. package/dist/lib-cjs/parse-output-error.js.map +1 -0
  102. package/dist/lib-cjs/parse-output.js +18 -0
  103. package/dist/lib-cjs/parse-output.js.map +1 -0
  104. package/dist/lib-cjs/parse-table/analysis/first-follow.js +294 -0
  105. package/dist/lib-cjs/parse-table/analysis/first-follow.js.map +1 -0
  106. package/dist/lib-cjs/parse-table/analysis/index.js +9 -0
  107. package/dist/lib-cjs/parse-table/analysis/index.js.map +1 -0
  108. package/dist/lib-cjs/parse-table/bnf/bnf-grammar.js +140 -0
  109. package/dist/lib-cjs/parse-table/bnf/bnf-grammar.js.map +1 -0
  110. package/dist/lib-cjs/parse-table/bnf/bnf-production.js +3 -0
  111. package/dist/lib-cjs/parse-table/bnf/bnf-production.js.map +1 -0
  112. package/dist/lib-cjs/parse-table/bnf/bnf-symbol.js +39 -0
  113. package/dist/lib-cjs/parse-table/bnf/bnf-symbol.js.map +1 -0
  114. package/dist/lib-cjs/parse-table/bnf/desugar-ebnf.js +224 -0
  115. package/dist/lib-cjs/parse-table/bnf/desugar-ebnf.js.map +1 -0
  116. package/dist/lib-cjs/parse-table/bnf/index.js +13 -0
  117. package/dist/lib-cjs/parse-table/bnf/index.js.map +1 -0
  118. package/dist/lib-cjs/parse-table/build-lr-table.js +171 -0
  119. package/dist/lib-cjs/parse-table/build-lr-table.js.map +1 -0
  120. package/dist/lib-cjs/parse-table/index.js +49 -0
  121. package/dist/lib-cjs/parse-table/index.js.map +1 -0
  122. package/dist/lib-cjs/parse-table/lr-algorithm-error.js +19 -0
  123. package/dist/lib-cjs/parse-table/lr-algorithm-error.js.map +1 -0
  124. package/dist/lib-cjs/parse-table/lr-algorithm.js +31 -0
  125. package/dist/lib-cjs/parse-table/lr-algorithm.js.map +1 -0
  126. package/dist/lib-cjs/parse-table/lr0/index.js +14 -0
  127. package/dist/lib-cjs/parse-table/lr0/index.js.map +1 -0
  128. package/dist/lib-cjs/parse-table/lr0/lr0-item-set.js +286 -0
  129. package/dist/lib-cjs/parse-table/lr0/lr0-item-set.js.map +1 -0
  130. package/dist/lib-cjs/parse-table/lr1/index.js +18 -0
  131. package/dist/lib-cjs/parse-table/lr1/index.js.map +1 -0
  132. package/dist/lib-cjs/parse-table/lr1/lr1-item-set.js +432 -0
  133. package/dist/lib-cjs/parse-table/lr1/lr1-item-set.js.map +1 -0
  134. package/dist/lib-cjs/parse-table/parse-table-build-error.js +24 -0
  135. package/dist/lib-cjs/parse-table/parse-table-build-error.js.map +1 -0
  136. package/dist/lib-cjs/parse-table/parse-table-error.js +19 -0
  137. package/dist/lib-cjs/parse-table/parse-table-error.js.map +1 -0
  138. package/dist/lib-cjs/parse-table/parse-table-json.js +12 -0
  139. package/dist/lib-cjs/parse-table/parse-table-json.js.map +1 -0
  140. package/dist/lib-cjs/parse-table/parse-table.js +345 -0
  141. package/dist/lib-cjs/parse-table/parse-table.js.map +1 -0
  142. package/dist/lib-cjs/parse-table/slr/index.js +13 -0
  143. package/dist/lib-cjs/parse-table/slr/index.js.map +1 -0
  144. package/dist/lib-cjs/parse-table/slr/parse-action.js +8 -0
  145. package/dist/lib-cjs/parse-table/slr/parse-action.js.map +1 -0
  146. package/dist/lib-cjs/parse-table/slr/slr-table.js +18 -0
  147. package/dist/lib-cjs/parse-table/slr/slr-table.js.map +1 -0
  148. package/dist/lib-cjs/parse-table/table/index.js +21 -0
  149. package/dist/lib-cjs/parse-table/table/index.js.map +1 -0
  150. package/dist/lib-cjs/parse-table/table/lr-parse-table.js +192 -0
  151. package/dist/lib-cjs/parse-table/table/lr-parse-table.js.map +1 -0
  152. package/dist/lib-cjs/parse-table/table/parse-action.js +54 -0
  153. package/dist/lib-cjs/parse-table/table/parse-action.js.map +1 -0
  154. package/dist/lib-cjs/parse-table/table/table-builder-base.js +153 -0
  155. package/dist/lib-cjs/parse-table/table/table-builder-base.js.map +1 -0
  156. package/dist/lib-cjs/parse-table/token-inventory.js +18 -0
  157. package/dist/lib-cjs/parse-table/token-inventory.js.map +1 -0
  158. package/dist/lib-cjs/parser-lr.js +143 -0
  159. package/dist/lib-cjs/parser-lr.js.map +1 -0
  160. package/dist/lib-cjs/shift-reduce/index.js +11 -0
  161. package/dist/lib-cjs/shift-reduce/index.js.map +1 -0
  162. package/dist/lib-cjs/shift-reduce/shift-reduce-engine.js +224 -0
  163. package/dist/lib-cjs/shift-reduce/shift-reduce-engine.js.map +1 -0
  164. package/dist/lib-cjs/transform/binding-map.js +82 -0
  165. package/dist/lib-cjs/transform/binding-map.js.map +1 -0
  166. package/dist/lib-cjs/transform/cst-transformer.js +480 -0
  167. package/dist/lib-cjs/transform/cst-transformer.js.map +1 -0
  168. package/dist/lib-cjs/transform/grammar-fixture.js +109 -0
  169. package/dist/lib-cjs/transform/grammar-fixture.js.map +1 -0
  170. package/dist/lib-cjs/transform/index.js +12 -0
  171. package/dist/lib-cjs/transform/index.js.map +1 -0
  172. package/docs/grammar.md +80 -0
  173. package/docs/parser-lr-enhancement-proposals.md +548 -0
  174. package/grammars/6502.grammar +233 -0
  175. package/grammars/calc.grammar +30 -0
  176. package/grammars/ferrite.grammar +991 -0
  177. package/grammars/fixtures/build-optional/build-optional.grammar +32 -0
  178. package/grammars/fixtures/build-optional/input.txt +1 -0
  179. package/grammars/fixtures/comparison-op/comparison-op.grammar +28 -0
  180. package/grammars/fixtures/comparison-op/input.txt +1 -0
  181. package/grammars/fixtures/flatten-repeat/flatten-repeat.grammar +28 -0
  182. package/grammars/fixtures/flatten-repeat/input.txt +1 -0
  183. package/grammars/fixtures/help-topic/help-topic.grammar +28 -0
  184. package/grammars/fixtures/help-topic/input.txt +1 -0
  185. package/grammars/fixtures/let-target/input.txt +1 -0
  186. package/grammars/fixtures/let-target/let-target.grammar +33 -0
  187. package/grammars/fixtures/pass-preserves-production/bare-terminal/bare-terminal.grammar +24 -0
  188. package/grammars/fixtures/pass-preserves-production/bare-terminal/input.txt +1 -0
  189. package/grammars/fixtures/pass-preserves-production/labeled-variant/input-end-if.txt +1 -0
  190. package/grammars/fixtures/pass-preserves-production/labeled-variant/input-end.txt +1 -0
  191. package/grammars/fixtures/pass-preserves-production/labeled-variant/labeled-variant.grammar +24 -0
  192. package/grammars/fixtures/pass-preserves-production/optional-absent/input.txt +1 -0
  193. package/grammars/fixtures/pass-preserves-production/optional-absent/optional-absent.grammar +24 -0
  194. package/grammars/fixtures/pass-preserves-production/optional-present/input.txt +1 -0
  195. package/grammars/fixtures/pass-preserves-production/optional-present/optional-present.grammar +24 -0
  196. package/grammars/grammar.grammar +428 -0
  197. package/grammars/lisp.grammar +95 -0
  198. package/package.json +10 -5
@@ -0,0 +1,548 @@
1
+ # parser-lr enhancement proposals (EduBASIC consumer)
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)).
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.
6
+
7
+ ---
8
+
9
+ ## Reference consumer
10
+
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) |
18
+
19
+ Regenerate table after grammar changes:
20
+
21
+ ```bash
22
+ npm run parser:table
23
+ ```
24
+
25
+ Parse smoke test:
26
+
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
+ "
34
+ ```
35
+
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:
43
+
44
+ ```ebnf
45
+ statement ->
46
+ #cls pass(stmt)
47
+ | #print pass(stmt)
48
+ | … ;
49
+ ```
50
+
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.
52
+
53
+ That forces ~70 lines of `keyword-fallbacks.ts` plus duplicate handling in domain builders.
54
+
55
+ ---
56
+
57
+ ## Priority 1: `pass()` must preserve production symbol
58
+
59
+ ### Problem
60
+
61
+ Grammar author binds `[stmt]:cls_stmt` and expects consumers to key off `cls_stmt`. Today bare `CLS` yields `kw_cls`.
62
+
63
+ **Grammar fragment (reproducer):**
64
+
65
+ ```ebnf
66
+ name "pass-collapse-repro" ;
67
+
68
+ tokens
69
+ kw_cls = /CLS/i ;
70
+ kw_with = /WITH/i ;
71
+ ident = /[a-z]+/ ;
72
+
73
+ start program_line ;
74
+
75
+ grammar
76
+ program_line =
77
+ #statement [stmt]:cls_stmt
78
+ ;
79
+
80
+ cls_stmt =
81
+ kw_cls [ kw_with ident ]
82
+ ;
83
+
84
+ transform
85
+ program_line ->
86
+ #statement pass(stmt) ;
87
+ ```
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
+ }
100
+ ```
101
+
102
+ **Required output:**
103
+
104
+ ```json
105
+ {
106
+ "symbol": "cls_stmt",
107
+ "children": [
108
+ { "symbol": "kw_cls", "text": "CLS", "children": [] }
109
+ ],
110
+ "variant": null
111
+ }
112
+ ```
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
+ }
128
+ ```
129
+
130
+ ### Semantics proposal
131
+
132
+ `pass(slot)` where `slot` is bound to production `P`:
133
+
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).
138
+
139
+ ### EduBASIC statements affected (bare input → wrong symbol today)
140
+
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
+ ;
206
+
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
+ ;
225
+
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
+ | … ;
247
+ ```
248
+
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
+ }
261
+ ```
262
+
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`.
270
+
271
+ ---
272
+
273
+ ## Priority 3: Stable shapes for nested statement helpers
274
+
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:
305
+
306
+ ```ebnf
307
+ help_topic ->
308
+ #keyword help_topic.#keyword(help_kw)
309
+ | #punct help_topic.#punct(help_punct) ;
310
+ ```
311
+
312
+ **Consumer workaround today:** [`buildHelpStatement`](../src/lang/parsing/ast-bridge/statements/io.ts) scans for any child that is not `kw_help`.
313
+
314
+ ### 3b. `case_selector` and `comparison_op`
315
+
316
+ **Grammar:**
317
+
318
+ ```ebnf
319
+ case_selector =
320
+ #value expr
321
+ | #range expr kw_to expr
322
+ | #relational kw_is comparison_op expr
323
+ ;
324
+
325
+ comparison_op =
326
+ #less less
327
+ | #greater_equal greater_equal
328
+ | … ;
329
+ ```
330
+
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:
354
+
355
+ ```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
+ comparison_op ->
362
+ #less comparison_op.#less(less)
363
+ | #greater_equal comparison_op.#greaterEqual(greater_equal)
364
+ | … ;
365
+ ```
366
+
367
+ **Consumer workaround today:** [`buildCaseSelector`](../src/lang/parsing/ast-bridge/statements/control-flow.ts) matches raw token names (`less_equal`) via `isComparisonOpSymbol`.
368
+
369
+ ### 3c. `print_list` / repeat tails
370
+
371
+ **Grammar:**
372
+
373
+ ```ebnf
374
+ print_stmt = kw_print [ print_list ] ;
375
+ print_list = print_segment { print_segment } ;
376
+ ```
377
+
378
+ Bridge must handle:
379
+
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)
383
+
384
+ **Desired:** consistent `flatten(print_list.#list, first, rest)` output (same as `expr_list` for expressions).
385
+
386
+ **Consumer workaround today:** [`buildPrintSegmentsFromStatement`](../src/lang/parsing/ast-bridge/statements/io.ts) accepts `print_list`, `print_segment`, or raw expression children interchangeably.
387
+
388
+ ### 3d. `let_target` / assignment paths
389
+
390
+ **Grammar:**
391
+
392
+ ```ebnf
393
+ let_stmt = kw_let let_target compound_op expr ;
394
+ let_target = identifier assn_path_suffix ;
395
+ ```
396
+
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).
400
+
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
+ ---
404
+
405
+ ## Priority 4: `flatten()` repeat semantics
406
+
407
+ ### Problem
408
+
409
+ EduBASIC relies on `flatten` for expression lists, array literals, struct members, and (target) print lists. Bridge code defends against:
410
+
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
413
+
414
+ ### Minimal reproducer grammar
415
+
416
+ ```ebnf
417
+ name "flatten-repeat-repro" ;
418
+
419
+ tokens
420
+ ident = /[a-z]+/ ;
421
+ comma = /,/ ;
422
+
423
+ start list ;
424
+
425
+ grammar
426
+ list = item { comma item } ;
427
+
428
+ ast
429
+ list = #list [first]:item { [rest]:item } ;
430
+
431
+ transform
432
+ list -> flatten(list.#list, first, rest) ;
433
+ ```
434
+
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
440
+
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.
442
+
443
+ ---
444
+
445
+ ## Priority 5: `build()` optional slot handling
446
+
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 ] ;
455
+
456
+ ast
457
+ cls_stmt =
458
+ #main kw_cls [withKw]:kw_with [target]:ident
459
+ ;
460
+
461
+ transform
462
+ cls_stmt -> cls_stmt.#main(kw_cls, withKw, target) ;
463
+ ```
464
+
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.
468
+
469
+ ### Migration note
470
+
471
+ EduBASIC previously patched `applyBuild` optional-slot skip in `node_modules/parser-lr`.
472
+
473
+ ---
474
+
475
+ ## Priority 6: Labeled alternatives on unary productions
476
+
477
+ ### Problem
478
+
479
+ Productions like `end_stmt` use labeled alternatives:
480
+
481
+ ```ebnf
482
+ end_stmt =
483
+ #program kw_end
484
+ | #if kw_end kw_if
485
+ ;
486
+ ```
487
+
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`).
489
+
490
+ **Input:** `END` → `{ symbol: "end_stmt", variant: "program", children: […] }`
491
+
492
+ **Input:** `END IF` → `{ symbol: "end_stmt", variant: "if", children: […] }`
493
+
494
+ Same for `do_stmt` / `loop_stmt` (`#bare`, `#while`, `#until`).
495
+
496
+ ---
497
+
498
+ ## Priority 7: Tooling and packaging
499
+
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 |
506
+
507
+ ---
508
+
509
+ ## What “elegant EduBASIC grammar” looks like after fixes
510
+
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.
516
+
517
+ Rough expected line reduction in `ast-bridge/`: on the order of **15–25%**, mostly from deleting fallback and defensive tree-walk code.
518
+
519
+ ---
520
+
521
+ ## Suggested implementation order for parser-lr agents
522
+
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.
528
+
529
+ ---
530
+
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:
542
+
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
547
+
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.