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.
- package/README.md +4 -0
- package/bin/parser-lr.js +10574 -4699
- package/bin/parser-lr.js.map +1 -1
- package/dist/lib/grammar/index.d.ts +1 -0
- package/dist/lib/grammar/index.d.ts.map +1 -1
- package/dist/lib/grammar/index.js +1 -0
- package/dist/lib/grammar/index.js.map +1 -1
- package/dist/lib/grammar/meta-grammar-table.d.ts +12 -0
- package/dist/lib/grammar/meta-grammar-table.d.ts.map +1 -1
- package/dist/lib/grammar/meta-grammar-table.js +30 -2
- package/dist/lib/grammar/meta-grammar-table.js.map +1 -1
- package/dist/lib/grammar/table-validator.d.ts.map +1 -1
- package/dist/lib/grammar/table-validator.js +47 -0
- package/dist/lib/grammar/table-validator.js.map +1 -1
- package/dist/lib-cjs/grammar/index.js +3 -1
- package/dist/lib-cjs/grammar/index.js.map +1 -1
- package/dist/lib-cjs/grammar/meta-grammar-table.js +31 -2
- package/dist/lib-cjs/grammar/meta-grammar-table.js.map +1 -1
- package/dist/lib-cjs/grammar/table-validator.js +47 -0
- package/dist/lib-cjs/grammar/table-validator.js.map +1 -1
- package/docs/grammar.md +34 -1
- package/docs/parser-lr-enhancement-proposals.md +193 -404
- package/grammars/fixtures/nested-production-repro/input.txt +1 -0
- package/grammars/fixtures/nested-production-repro/nested-production-repro.grammar +26 -0
- package/grammars/fixtures/table-only-import/calc.table.json +190 -0
- package/grammars/fixtures/table-only-import/table-only-smoke.cjs +38 -0
- package/grammars/fixtures/table-only-import/table-only-smoke.mjs +18 -0
- package/package.json +21 -2
|
@@ -1,325 +1,135 @@
|
|
|
1
|
-
# parser-lr
|
|
1
|
+
# parser-lr open requests (EduBASIC)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Audience: agents working on [parser-lr](https://github.com/adamrmoss/parser-lr).
|
|
4
4
|
|
|
5
|
-
**
|
|
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
|
-
##
|
|
11
|
+
## Resolved in 0.5.0 (no further work)
|
|
10
12
|
|
|
11
|
-
|
|
|
12
|
-
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. CLI cannot find `grammar.json` (blocker)
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
### Symptom
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
```
|
|
45
|
-
|
|
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
|
-
|
|
38
|
+
### Actual layout in published 0.5.0
|
|
52
39
|
|
|
53
|
-
|
|
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
|
-
|
|
48
|
+
### Required fix
|
|
58
49
|
|
|
59
|
-
|
|
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
|
-
|
|
52
|
+
### Acceptance test (parser-lr repo)
|
|
62
53
|
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
name "pass-collapse-repro" ;
|
|
63
|
+
Both commands must exit 0 on a clean install from the tarball.
|
|
67
64
|
|
|
68
|
-
|
|
69
|
-
kw_cls = /CLS/i ;
|
|
70
|
-
kw_with = /WITH/i ;
|
|
71
|
-
ident = /[a-z]+/ ;
|
|
65
|
+
---
|
|
72
66
|
|
|
73
|
-
|
|
67
|
+
## 2. Table-only import must not load meta-grammar (blocker for Jest)
|
|
74
68
|
|
|
75
|
-
|
|
76
|
-
program_line =
|
|
77
|
-
#statement [stmt]:cls_stmt
|
|
78
|
-
;
|
|
69
|
+
### Symptom
|
|
79
70
|
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
88
|
+
`readGrammar()` is not called; meta-grammar still loads because it sits on the public barrel.
|
|
131
89
|
|
|
132
|
-
|
|
90
|
+
### Required fix (pick one)
|
|
133
91
|
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
118
|
+
## 3. Preserve intermediate production nodes (high)
|
|
274
119
|
|
|
275
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
124
|
+
EduBASIC bridge code must scan for raw tokens or alternate shapes.
|
|
313
125
|
|
|
314
|
-
###
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
363
|
-
| #
|
|
145
|
+
#less pass(tok)
|
|
146
|
+
| #greaterEqual pass(tok)
|
|
364
147
|
| … ;
|
|
365
148
|
```
|
|
366
149
|
|
|
367
|
-
**
|
|
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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
163
|
+
let_stmt = kw_let let_target compound_op expr ;
|
|
164
|
+
let_target = identifier assn_path_suffix ;
|
|
376
165
|
```
|
|
377
166
|
|
|
378
|
-
|
|
167
|
+
**Input:** `LET x% = 1`
|
|
379
168
|
|
|
380
|
-
|
|
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
|
-
**
|
|
171
|
+
**Required:** `let_stmt` children include `let_target` wrapping `identifier` (and optional `assn_path_suffix`).
|
|
385
172
|
|
|
386
|
-
|
|
173
|
+
EduBASIC workaround: [`assignment-helpers.ts`](../src/lang/parsing/ast-bridge/assignment-helpers.ts) `buildLetAssignmentTarget` accepts bare `identifier` at statement root.
|
|
387
174
|
|
|
388
|
-
###
|
|
175
|
+
### 3c. `print_list` / `print_segment`
|
|
389
176
|
|
|
390
177
|
**Grammar:**
|
|
391
178
|
|
|
392
179
|
```ebnf
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
|
|
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
|
-
**
|
|
402
|
-
|
|
403
|
-
---
|
|
187
|
+
**Required:** stable list shape matching expression `flatten` output (same as `expr_list`).
|
|
404
188
|
|
|
405
|
-
|
|
189
|
+
### Proposed semantics
|
|
406
190
|
|
|
407
|
-
|
|
191
|
+
When a CST node is a **non-terminal production** `P` and `P` has **no** `transform` rule:
|
|
408
192
|
|
|
409
|
-
|
|
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
|
-
|
|
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 "
|
|
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
|
-
|
|
209
|
+
integer = /[0-9]+/ ;
|
|
422
210
|
|
|
423
|
-
start
|
|
211
|
+
start line ;
|
|
424
212
|
|
|
425
213
|
grammar
|
|
426
|
-
|
|
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
|
-
|
|
219
|
+
comparison_op =
|
|
220
|
+
#less [tok]:less
|
|
221
|
+
;
|
|
430
222
|
|
|
431
223
|
transform
|
|
432
|
-
|
|
224
|
+
comparison_op ->
|
|
225
|
+
#less comparison_op.#less(tok) ;
|
|
433
226
|
```
|
|
434
227
|
|
|
435
|
-
**Input:** `
|
|
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
|
-
|
|
230
|
+
**Assert:** `case_selector` child at index 1 has `symbol === "comparison_op"`, not `less`.
|
|
442
231
|
|
|
443
232
|
---
|
|
444
233
|
|
|
445
|
-
##
|
|
234
|
+
## 4. `table validate` must work on edu-basic grammar (medium)
|
|
446
235
|
|
|
447
|
-
|
|
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
|
-
|
|
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
|
-
|
|
462
|
-
|
|
240
|
+
```bash
|
|
241
|
+
npx parser-lr table validate -g src/lang/parsing/grammars/edu-basic.grammar
|
|
463
242
|
```
|
|
464
243
|
|
|
465
|
-
|
|
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
|
-
|
|
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
|
-
|
|
250
|
+
`--strict` should be usable in CI.
|
|
472
251
|
|
|
473
252
|
---
|
|
474
253
|
|
|
475
|
-
##
|
|
254
|
+
## 5. Document transform contract (medium)
|
|
476
255
|
|
|
477
|
-
|
|
256
|
+
Update [`docs/grammar.md`](https://github.com/adamrmoss/parser-lr/blob/main/docs/grammar.md) with explicit rules:
|
|
478
257
|
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
267
|
+
---
|
|
489
268
|
|
|
490
|
-
|
|
269
|
+
## 6. Regression fixtures to add in parser-lr (medium)
|
|
491
270
|
|
|
492
|
-
|
|
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
|
-
|
|
280
|
+
Each fixture: `.grammar`, input line(s), expected AST JSON golden file.
|
|
495
281
|
|
|
496
282
|
---
|
|
497
283
|
|
|
498
|
-
##
|
|
284
|
+
## EduBASIC verification (run after parser-lr release)
|
|
499
285
|
|
|
500
|
-
|
|
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
|
-
|
|
295
|
+
Spot checks:
|
|
510
296
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
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
|
-
|
|
319
|
+
Then remove from edu-basic:
|
|
522
320
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
-
|
|
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
|
-
|
|
337
|
+
Target: **parser-lr 0.5.1** (or 0.6.0) with tarball acceptance tests for §1 and §2.
|