parser-lr 0.4.1 → 0.6.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 (203) hide show
  1. package/README.md +39 -1
  2. package/bin/parser-lr.js +8373 -7960
  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/index.d.ts +3 -0
  13. package/dist/lib/grammar/index.d.ts.map +1 -1
  14. package/dist/lib/grammar/index.js +2 -0
  15. package/dist/lib/grammar/index.js.map +1 -1
  16. package/dist/lib/grammar/meta-grammar-table.d.ts +12 -0
  17. package/dist/lib/grammar/meta-grammar-table.d.ts.map +1 -1
  18. package/dist/lib/grammar/meta-grammar-table.js +33 -3
  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 +258 -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 +21 -0
  58. package/dist/lib-cjs/grammar/index.js.map +1 -0
  59. package/dist/lib-cjs/grammar/meta-grammar-table.js +68 -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 +262 -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 +113 -0
  173. package/docs/parser-lr-enhancement-proposals.md +337 -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/nested-production-repro/input.txt +1 -0
  188. package/grammars/fixtures/nested-production-repro/nested-production-repro.grammar +26 -0
  189. package/grammars/fixtures/pass-preserves-production/bare-terminal/bare-terminal.grammar +24 -0
  190. package/grammars/fixtures/pass-preserves-production/bare-terminal/input.txt +1 -0
  191. package/grammars/fixtures/pass-preserves-production/labeled-variant/input-end-if.txt +1 -0
  192. package/grammars/fixtures/pass-preserves-production/labeled-variant/input-end.txt +1 -0
  193. package/grammars/fixtures/pass-preserves-production/labeled-variant/labeled-variant.grammar +24 -0
  194. package/grammars/fixtures/pass-preserves-production/optional-absent/input.txt +1 -0
  195. package/grammars/fixtures/pass-preserves-production/optional-absent/optional-absent.grammar +24 -0
  196. package/grammars/fixtures/pass-preserves-production/optional-present/input.txt +1 -0
  197. package/grammars/fixtures/pass-preserves-production/optional-present/optional-present.grammar +24 -0
  198. package/grammars/fixtures/table-only-import/calc.table.json +190 -0
  199. package/grammars/fixtures/table-only-import/table-only-smoke.cjs +38 -0
  200. package/grammars/fixtures/table-only-import/table-only-smoke.mjs +18 -0
  201. package/grammars/grammar.grammar +428 -0
  202. package/grammars/lisp.grammar +95 -0
  203. package/package.json +10 -4
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fixtureGrammarPath = fixtureGrammarPath;
4
+ exports.findChildrenBySymbol = findChildrenBySymbol;
5
+ exports.findChildBySymbol = findChildBySymbol;
6
+ exports.parseFixture = parseFixture;
7
+ exports.discoverFixtures = discoverFixtures;
8
+ exports.runFixtureCase = runFixtureCase;
9
+ const node_fs_1 = require("node:fs");
10
+ const node_path_1 = require("node:path");
11
+ const parse_context_js_1 = require("../parse-context.js");
12
+ /**
13
+ * Returns the `.grammar` file for one fixture directory.
14
+ *
15
+ * Fixture grammars are named after their directory (for example
16
+ * `bare-terminal/bare-terminal.grammar`). The meta-grammar keeps the reserved
17
+ * name `grammar.grammar` at the repo root only.
18
+ *
19
+ * @param fixtureDir - Directory containing a fixture grammar and input files.
20
+ */
21
+ function fixtureGrammarPath(fixtureDir) {
22
+ const grammarPath = (0, node_path_1.join)(fixtureDir, `${(0, node_path_1.basename)(fixtureDir)}.grammar`);
23
+ return (0, node_fs_1.existsSync)(grammarPath) ? grammarPath : null;
24
+ }
25
+ /**
26
+ * Returns child AST nodes matching a symbol name.
27
+ *
28
+ * @param node - Parent AST node.
29
+ * @param symbol - Child symbol to find.
30
+ */
31
+ function findChildrenBySymbol(node, symbol) {
32
+ return node.children.filter((child) => child.symbol === symbol);
33
+ }
34
+ /**
35
+ * Returns the first child AST node matching a symbol name.
36
+ *
37
+ * @param node - Parent AST node.
38
+ * @param symbol - Child symbol to find.
39
+ */
40
+ function findChildBySymbol(node, symbol) {
41
+ return node.children.find((child) => child.symbol === symbol) ?? null;
42
+ }
43
+ /**
44
+ * Parses one fixture grammar and input file into an AST.
45
+ *
46
+ * @param fixtureDir - Directory containing a named fixture grammar and an input file.
47
+ * @param inputFileName - Input file name within the fixture directory.
48
+ */
49
+ function parseFixture(fixtureDir, inputFileName = 'input.txt') {
50
+ const grammarPath = fixtureGrammarPath(fixtureDir);
51
+ if (grammarPath === null) {
52
+ throw new Error(`fixture grammar not found in ${fixtureDir}`);
53
+ }
54
+ const grammarSource = (0, node_fs_1.readFileSync)(grammarPath, 'utf8');
55
+ const input = (0, node_fs_1.readFileSync)((0, node_path_1.join)(fixtureDir, inputFileName), 'utf8').trimEnd();
56
+ const context = parse_context_js_1.ParseContext.fromGrammar(grammarSource, 'lr1');
57
+ return context.parseSource(input);
58
+ }
59
+ /**
60
+ * Discovers fixture directories containing a named `.grammar` file under a root path.
61
+ *
62
+ * @param rootDir - Root directory to scan recursively.
63
+ */
64
+ function discoverFixtures(rootDir) {
65
+ const fixtures = [];
66
+ const visit = (directory) => {
67
+ const grammarPath = fixtureGrammarPath(directory);
68
+ if (grammarPath !== null) {
69
+ const inputFiles = (0, node_fs_1.readdirSync)(directory)
70
+ .filter((name) => name.startsWith('input') && name.endsWith('.txt'))
71
+ .sort();
72
+ for (const inputFile of inputFiles) {
73
+ const baseName = inputFile.replace(/^input/, '').replace(/\.txt$/, '');
74
+ const expectedName = baseName === '' ? 'expected.json' : `expected${baseName}.json`;
75
+ const expectedPath = (0, node_path_1.join)(directory, expectedName);
76
+ fixtures.push({
77
+ name: `${directory.split('/').slice(-2).join('/')}/${inputFile}`,
78
+ grammarPath,
79
+ inputPath: (0, node_path_1.join)(directory, inputFile),
80
+ expectedPath: (0, node_fs_1.existsSync)(expectedPath) ? expectedPath : null,
81
+ });
82
+ }
83
+ }
84
+ for (const entry of (0, node_fs_1.readdirSync)(directory, { withFileTypes: true })) {
85
+ if (entry.isDirectory()) {
86
+ visit((0, node_path_1.join)(directory, entry.name));
87
+ }
88
+ }
89
+ };
90
+ visit(rootDir);
91
+ return fixtures;
92
+ }
93
+ /**
94
+ * Parses a fixture case and compares to expected JSON when present.
95
+ *
96
+ * @param fixture - Fixture metadata.
97
+ */
98
+ function runFixtureCase(fixture, expectFn) {
99
+ const grammarSource = (0, node_fs_1.readFileSync)(fixture.grammarPath, 'utf8');
100
+ const input = (0, node_fs_1.readFileSync)(fixture.inputPath, 'utf8').trimEnd();
101
+ const context = parse_context_js_1.ParseContext.fromGrammar(grammarSource, 'lr1');
102
+ const ast = context.parseSource(input);
103
+ if (fixture.expectedPath !== null) {
104
+ const expected = JSON.parse((0, node_fs_1.readFileSync)(fixture.expectedPath, 'utf8'));
105
+ expectFn(ast).toEqual(expected);
106
+ }
107
+ return ast;
108
+ }
109
+ //# sourceMappingURL=grammar-fixture.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"grammar-fixture.js","sourceRoot":"","sources":["../../../src/lib/transform/grammar-fixture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.transformCst = exports.describeProductionSlots = exports.CstTransformer = exports.referenceSlotIndex = exports.productionSlots = exports.parseRhsSymbolKey = void 0;
4
+ var binding_map_js_1 = require("./binding-map.js");
5
+ Object.defineProperty(exports, "parseRhsSymbolKey", { enumerable: true, get: function () { return binding_map_js_1.parseRhsSymbolKey; } });
6
+ Object.defineProperty(exports, "productionSlots", { enumerable: true, get: function () { return binding_map_js_1.productionSlots; } });
7
+ Object.defineProperty(exports, "referenceSlotIndex", { enumerable: true, get: function () { return binding_map_js_1.referenceSlotIndex; } });
8
+ var cst_transformer_js_1 = require("./cst-transformer.js");
9
+ Object.defineProperty(exports, "CstTransformer", { enumerable: true, get: function () { return cst_transformer_js_1.CstTransformer; } });
10
+ Object.defineProperty(exports, "describeProductionSlots", { enumerable: true, get: function () { return cst_transformer_js_1.describeProductionSlots; } });
11
+ Object.defineProperty(exports, "transformCst", { enumerable: true, get: function () { return cst_transformer_js_1.transformCst; } });
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/transform/index.ts"],"names":[],"mappings":";;;AAAA,mDAA0F;AAAjF,mHAAA,iBAAiB,OAAA;AAAE,iHAAA,eAAe,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAE/D,2DAA6F;AAApF,oHAAA,cAAc,OAAA;AAAE,6HAAA,uBAAuB,OAAA;AAAE,kHAAA,YAAY,OAAA"}
package/docs/grammar.md CHANGED
@@ -155,6 +155,118 @@ transform
155
155
  ;
156
156
  ```
157
157
 
158
+ ### Choosing `pass`, `build`, and `flatten`
159
+
160
+ | Transform | Use when |
161
+ |-----------|----------|
162
+ | `pass(slot)` | Lift one bound child unchanged. `pass(boundSlot)` preserves the bound production symbol and its `#` variant. `pass(terminalName)` at the same rule may collapse to that terminal. |
163
+ | `build(type.#variant, …)` | Construct an AST node with a fixed shape. Omit absent optional bindings from the argument list. |
164
+ | `flatten(type.#variant, head, tail)` | Turn a `{ repeat }` list into one AST node with ordered children. Use the repeat non-terminal (for example `list$repeat_0`) as `tail` on the head production. |
165
+
166
+ **Production symbols are preserved.** When a CST node has parse-table metadata, transforms keep its production name even if it has only one terminal child. Bare `CLS` through `pass(stmt)` where `[stmt]:cls_stmt` yields an `cls_stmt` node, not `kw_cls`.
167
+
168
+ ### Transform contract
169
+
170
+ The transformer applies these rules to every CST node:
171
+
172
+ | Situation | Behavior |
173
+ |-----------|----------|
174
+ | `pass(slot)` | Preserve the bound production's symbol and `#` variant; never collapse to its sole terminal child. |
175
+ | No transform rule for production `P` | Emit an identity node `{ symbol: P, variant, children }`. A named non-terminal is kept even when it has a single child; the child is not hoisted in its place. |
176
+ | `build(type.#variant, …)` | Absent optional bindings are omitted from `children`, not shifted into other slots. |
177
+ | `flatten(type.#list, head, tail)` | Repeat tails produce no empty placeholder nodes and drop no items. |
178
+
179
+ **Nested non-terminals stay wrapped.** A production referenced inside another production keeps its own node, so consumers match the grammar non-terminal (for example `comparison_op`) rather than a lexer token name (`less`). To retain a token *inside* that wrapper, bind it: write `comparison_op = #less [tok]:less` and reference `tok` from the transform. Without the binding, `build(comparison_op.#less, tok)` has no slot to read and the child is dropped.
180
+
181
+ Before / after for `CASE IS < 0` with `case_selector = #relational kw_is comparison_op integer`:
182
+
183
+ ```json
184
+ {
185
+ "symbol": "case_selector",
186
+ "variant": "relational",
187
+ "children": [
188
+ { "symbol": "kw_is" },
189
+ { "symbol": "comparison_op", "variant": "less", "children": [ { "symbol": "less" } ] },
190
+ { "symbol": "integer" }
191
+ ]
192
+ }
193
+ ```
194
+
195
+ Run `parser-lr table validate -g mylang.grammar` to check `ast` / `transform` consistency. The validator:
196
+
197
+ - warns when `pass(boundSlot)` targets a production that can match a single terminal and has no transform rule;
198
+ - errors when a `build` / `fold` / `flatten` references a `type.#variant` not declared in `ast`;
199
+ - warns when a production, `ast` type, or `transform` rule is declared more than once (the later definition silently overrides the earlier one).
200
+
201
+ Add `--strict` to fail on warnings in CI.
202
+
203
+ ## Statement authoring
204
+
205
+ Statements follow the same `grammar` / `ast` / `transform` pattern as expressions. Dispatch on stable `*_stmt` production names instead of keyword tokens.
206
+
207
+ ```ebnf
208
+ grammar
209
+ statement =
210
+ #cls pass(stmt)
211
+ | #print pass(stmt)
212
+ | #end pass(stmt)
213
+ ;
214
+
215
+ cls_stmt =
216
+ #bare kw_cls
217
+ | #with kw_cls kw_with [bg]:expr
218
+ ;
219
+
220
+ print_stmt =
221
+ #bare kw_print
222
+ | #items kw_print [list]:print_list
223
+ ;
224
+
225
+ end_stmt =
226
+ #program kw_end
227
+ | #if kw_end kw_if
228
+ ;
229
+
230
+ ast
231
+ cls_stmt =
232
+ #bare kw_cls
233
+ | #with kw_cls kw_with [bg]:expr
234
+ ;
235
+
236
+ print_stmt =
237
+ #bare kw_print
238
+ | #items kw_print [list]:print_list
239
+ ;
240
+
241
+ end_stmt =
242
+ #program kw_end
243
+ | #if kw_end kw_if
244
+ ;
245
+
246
+ transform
247
+ statement ->
248
+ #cls pass(stmt)
249
+ | #print pass(stmt)
250
+ | #end pass(stmt)
251
+ ;
252
+
253
+ cls_stmt ->
254
+ #bare cls_stmt.#bare(kw_cls)
255
+ | #with cls_stmt.#with(kw_cls, kw_with, bg) ;
256
+
257
+ print_stmt ->
258
+ #bare print_stmt.#bare(kw_print)
259
+ | #items print_stmt.#items(kw_print, list) ;
260
+
261
+ end_stmt ->
262
+ #program end_stmt.#program(kw_end)
263
+ | #if end_stmt.#if(kw_end, kw_if) ;
264
+ ```
265
+
266
+ After `pass(stmt)`, consumers see `cls_stmt`, `print_stmt`, or `end_stmt` at the statement root. Add `ast` + `transform` rules per statement production rather than scanning for keyword terminals in bridge code.
267
+
268
+ See [`docs/parser-lr-enhancement-proposals.md`](parser-lr-enhancement-proposals.md) for the EduBASIC migration that motivated these conventions.
269
+
158
270
  ## Complete example
159
271
 
160
272
  See [`grammars/calc.grammar`](../grammars/calc.grammar) for a small working grammar with `grammar`, `ast`, and `transform` sections. [`grammars/lisp.grammar`](../grammars/lisp.grammar) and [`grammars/6502.grammar`](../grammars/6502.grammar) show larger languages.
@@ -163,6 +275,7 @@ See [`grammars/calc.grammar`](../grammars/calc.grammar) for a small working gram
163
275
 
164
276
  ```bash
165
277
  parser-lr table generate -g mylang.grammar -o mylang.json
278
+ parser-lr table validate -g mylang.grammar
166
279
  parser-lr parse -i source.txt -g mylang.grammar
167
280
  parser-lr parse -i source.txt -t mylang.json
168
281
  ```
@@ -0,0 +1,337 @@
1
+ # parser-lr open requests (EduBASIC)
2
+
3
+ Audience: agents working on [parser-lr](https://github.com/adamrmoss/parser-lr).
4
+
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.
8
+
9
+ ---
10
+
11
+ ## Resolved in 0.5.0 (no further work)
12
+
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
+
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
+
21
+ ---
22
+
23
+ ## 1. CLI cannot find `grammar.json` (blocker)
24
+
25
+ ### Symptom
26
+
27
+ ```bash
28
+ npm run parser:table
29
+ # ENOENT: .../node_modules/parser-lr/bin/grammar.json
30
+ ```
31
+
32
+ Same failure for:
33
+
34
+ ```bash
35
+ npx parser-lr table validate -g src/lang/parsing/grammars/edu-basic.grammar
36
+ ```
37
+
38
+ ### Actual layout in published 0.5.0
39
+
40
+ | Path | Present? |
41
+ |------|----------|
42
+ | `dist/lib/grammar/grammar.json` | Yes |
43
+ | `dist/lib-cjs/grammar/grammar.json` | Yes |
44
+ | `bin/grammar.json` | **No** |
45
+
46
+ CLI bundle resolves meta-grammar relative to `bin/`, not `dist/lib/grammar/`.
47
+
48
+ ### Required fix
49
+
50
+ Bundle CLI against the same `grammarJsonDirectory()` helper the library uses, or copy `grammar.json` beside `bin/parser-lr.js` in `prepack`.
51
+
52
+ ### Acceptance test (parser-lr repo)
53
+
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
+ ```
62
+
63
+ Both commands must exit 0 on a clean install from the tarball.
64
+
65
+ ---
66
+
67
+ ## 2. Table-only import must not load meta-grammar (blocker for Jest)
68
+
69
+ ### Symptom
70
+
71
+ EduBASIC tests import `ParseContext` / `AstNode` from `parser-lr` only to parse a pre-built table. Jest fails:
72
+
73
+ ```
74
+ SyntaxError: Cannot use 'import.meta' outside a module
75
+ at grammar-json-path.esm.js
76
+ ```
77
+
78
+ Import chain today:
79
+
80
+ ```
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)
86
+ ```
87
+
88
+ `readGrammar()` is not called; meta-grammar still loads because it sits on the public barrel.
89
+
90
+ ### Required fix (pick one)
91
+
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:
93
+
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`)
96
+
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`.
98
+
99
+ ### Acceptance test (parser-lr repo)
100
+
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);
107
+ ```
108
+
109
+ ```javascript
110
+ // table-only-smoke.mjs — same for ESM default entry
111
+ import { ParseContext } from 'parser-lr';
112
+ ```
113
+
114
+ Neither import may read `grammar.json` or evaluate `import.meta`.
115
+
116
+ ---
117
+
118
+ ## 3. Preserve intermediate production nodes (high)
119
+
120
+ ### Problem
121
+
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.
123
+
124
+ EduBASIC bridge code must scan for raw tokens or alternate shapes.
125
+
126
+ ### 3a. `comparison_op` inside `case_selector`
127
+
128
+ **Grammar (already in edu-basic):**
129
+
130
+ ```ebnf
131
+ case_selector =
132
+ #relational kw_is comparison_op expr
133
+ ;
134
+
135
+ comparison_op =
136
+ #less less
137
+ | #greater_equal greater_equal
138
+ | … ;
139
+ ```
140
+
141
+ `comparison_op` **has** expression-level transforms:
142
+
143
+ ```ebnf
144
+ comparison_op ->
145
+ #less pass(tok)
146
+ | #greaterEqual pass(tok)
147
+ | … ;
148
+ ```
149
+
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`
155
+
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`
159
+
160
+ **Grammar:**
161
+
162
+ ```ebnf
163
+ let_stmt = kw_let let_target compound_op expr ;
164
+ let_target = identifier assn_path_suffix ;
165
+ ```
166
+
167
+ **Input:** `LET x% = 1`
168
+
169
+ **Actual:** flat `identifier`, `equal`, `expr` under `let_stmt` (no `let_target` wrapper).
170
+
171
+ **Required:** `let_stmt` children include `let_target` wrapping `identifier` (and optional `assn_path_suffix`).
172
+
173
+ EduBASIC workaround: [`assignment-helpers.ts`](../src/lang/parsing/ast-bridge/assignment-helpers.ts) `buildLetAssignmentTarget` accepts bare `identifier` at statement root.
174
+
175
+ ### 3c. `print_list` / `print_segment`
176
+
177
+ **Grammar:**
178
+
179
+ ```ebnf
180
+ print_stmt = kw_print [ print_list ] ;
181
+ print_list = print_segment { print_segment } ;
182
+ print_segment = #bare expr | #trailingComma expr comma | … ;
183
+ ```
184
+
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`).
186
+
187
+ **Required:** stable list shape matching expression `flatten` output (same as `expr_list`).
188
+
189
+ ### Proposed semantics
190
+
191
+ When a CST node is a **non-terminal production** `P` and `P` has **no** `transform` rule:
192
+
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).
196
+
197
+ This generalizes the 0.5.0 `pass()` bound-slot fix to **all referenced non-terminals**.
198
+
199
+ ### Minimal reproducer grammar (add to parser-lr fixtures)
200
+
201
+ ```ebnf
202
+ name "nested-production-repro" ;
203
+
204
+ tokens
205
+ kw_is = /IS/i ;
206
+ kw_case = /CASE/i ;
207
+ less = /</ ;
208
+ ident = /[a-z]+/ ;
209
+ integer = /[0-9]+/ ;
210
+
211
+ start line ;
212
+
213
+ grammar
214
+ line = kw_case case_selector ;
215
+ case_selector = #relational kw_is comparison_op integer ;
216
+ comparison_op = #less less ;
217
+
218
+ ast
219
+ comparison_op =
220
+ #less [tok]:less
221
+ ;
222
+
223
+ transform
224
+ comparison_op ->
225
+ #less comparison_op.#less(tok) ;
226
+ ```
227
+
228
+ **Input:** `CASE IS < 0`
229
+
230
+ **Assert:** `case_selector` child at index 1 has `symbol === "comparison_op"`, not `less`.
231
+
232
+ ---
233
+
234
+ ## 4. `table validate` must work on edu-basic grammar (medium)
235
+
236
+ Depends on **§1** (CLI packaging).
237
+
238
+ Once the CLI runs, validate this consumer grammar:
239
+
240
+ ```bash
241
+ npx parser-lr table validate -g src/lang/parsing/grammars/edu-basic.grammar
242
+ ```
243
+
244
+ Useful warnings (already described in README) should include:
245
+
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).
249
+
250
+ `--strict` should be usable in CI.
251
+
252
+ ---
253
+
254
+ ## 5. Document transform contract (medium)
255
+
256
+ Update [`docs/grammar.md`](https://github.com/adamrmoss/parser-lr/blob/main/docs/grammar.md) with explicit rules:
257
+
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. |
264
+
265
+ Include before/after JSON for `CLS`, `CASE IS < 0`, and `LET x = 1`.
266
+
267
+ ---
268
+
269
+ ## 6. Regression fixtures to add in parser-lr (medium)
270
+
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 |
279
+
280
+ Each fixture: `.grammar`, input line(s), expected AST JSON golden file.
281
+
282
+ ---
283
+
284
+ ## EduBASIC verification (run after parser-lr release)
285
+
286
+ Run these locally after bumping `parser-lr` and regenerating the table:
287
+
288
+ ```bash
289
+ npm install parser-lr@<version>
290
+ npm run parser:table
291
+ npm run parser:check
292
+ npm test
293
+ ```
294
+
295
+ Spot checks:
296
+
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
+ ```
308
+
309
+ Expected after all items above:
310
+
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` |
318
+
319
+ Then remove from edu-basic:
320
+
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)
325
+
326
+ ---
327
+
328
+ ## Priority summary
329
+
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 |
336
+
337
+ Target: **parser-lr 0.5.1** (or 0.6.0) with tarball acceptance tests for §1 and §2.