parser-lr 0.2.4 → 0.3.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 (58) hide show
  1. package/README.md +82 -0
  2. package/bin/parser-lr.js +336 -171
  3. package/bin/parser-lr.js.map +1 -1
  4. package/dist/lib/parse-table/analysis/first-follow.d.ts +6 -0
  5. package/dist/lib/parse-table/analysis/first-follow.d.ts.map +1 -1
  6. package/dist/lib/parse-table/analysis/first-follow.js +13 -0
  7. package/dist/lib/parse-table/analysis/first-follow.js.map +1 -1
  8. package/dist/lib/parse-table/bnf/desugar-ebnf.d.ts.map +1 -1
  9. package/dist/lib/parse-table/bnf/desugar-ebnf.js +2 -0
  10. package/dist/lib/parse-table/bnf/desugar-ebnf.js.map +1 -1
  11. package/dist/lib/parse-table/build-lr-table.d.ts +1 -0
  12. package/dist/lib/parse-table/build-lr-table.d.ts.map +1 -1
  13. package/dist/lib/parse-table/build-lr-table.js +11 -1
  14. package/dist/lib/parse-table/build-lr-table.js.map +1 -1
  15. package/dist/lib/parse-table/index.d.ts +1 -1
  16. package/dist/lib/parse-table/index.d.ts.map +1 -1
  17. package/dist/lib/parse-table/index.js +1 -1
  18. package/dist/lib/parse-table/index.js.map +1 -1
  19. package/dist/lib/parse-table/lr0/lr0-item-set.d.ts +26 -2
  20. package/dist/lib/parse-table/lr0/lr0-item-set.d.ts.map +1 -1
  21. package/dist/lib/parse-table/lr0/lr0-item-set.js +92 -54
  22. package/dist/lib/parse-table/lr0/lr0-item-set.js.map +1 -1
  23. package/dist/lib/parse-table/lr1/lr1-item-set.d.ts +31 -2
  24. package/dist/lib/parse-table/lr1/lr1-item-set.d.ts.map +1 -1
  25. package/dist/lib/parse-table/lr1/lr1-item-set.js +95 -60
  26. package/dist/lib/parse-table/lr1/lr1-item-set.js.map +1 -1
  27. package/dist/lib/parse-table/parse-table.d.ts +13 -3
  28. package/dist/lib/parse-table/parse-table.d.ts.map +1 -1
  29. package/dist/lib/parse-table/parse-table.js +19 -8
  30. package/dist/lib/parse-table/parse-table.js.map +1 -1
  31. package/dist/lib/parse-table/table/index.d.ts +1 -1
  32. package/dist/lib/parse-table/table/index.d.ts.map +1 -1
  33. package/dist/lib/parse-table/table/index.js +1 -1
  34. package/dist/lib/parse-table/table/index.js.map +1 -1
  35. package/dist/lib/parse-table/table/lr-parse-table.d.ts +14 -2
  36. package/dist/lib/parse-table/table/lr-parse-table.d.ts.map +1 -1
  37. package/dist/lib/parse-table/table/lr-parse-table.js +38 -3
  38. package/dist/lib/parse-table/table/lr-parse-table.js.map +1 -1
  39. package/dist/lib/parse-table/table/parse-action.d.ts +3 -1
  40. package/dist/lib/parse-table/table/parse-action.d.ts.map +1 -1
  41. package/dist/lib/parse-table/table/parse-action.js.map +1 -1
  42. package/dist/lib/parse-table/table/table-builder-base.d.ts +5 -4
  43. package/dist/lib/parse-table/table/table-builder-base.d.ts.map +1 -1
  44. package/dist/lib/parse-table/table/table-builder-base.js +28 -8
  45. package/dist/lib/parse-table/table/table-builder-base.js.map +1 -1
  46. package/dist/lib/shift-reduce/shift-reduce-engine.d.ts +10 -0
  47. package/dist/lib/shift-reduce/shift-reduce-engine.d.ts.map +1 -1
  48. package/dist/lib/shift-reduce/shift-reduce-engine.js +11 -0
  49. package/dist/lib/shift-reduce/shift-reduce-engine.js.map +1 -1
  50. package/dist/lib/transform/binding-map.d.ts +6 -0
  51. package/dist/lib/transform/binding-map.d.ts.map +1 -1
  52. package/dist/lib/transform/binding-map.js +17 -0
  53. package/dist/lib/transform/binding-map.js.map +1 -1
  54. package/dist/lib/transform/cst-transformer.d.ts +6 -0
  55. package/dist/lib/transform/cst-transformer.d.ts.map +1 -1
  56. package/dist/lib/transform/cst-transformer.js +24 -2
  57. package/dist/lib/transform/cst-transformer.js.map +1 -1
  58. package/package.json +3 -1
package/bin/parser-lr.js CHANGED
@@ -3781,7 +3781,7 @@ const {
3781
3781
  Help,
3782
3782
  } = commander;
3783
3783
 
3784
- var version$1 = "0.2.4";
3784
+ var version$1 = "0.3.0";
3785
3785
  var packageDefinition = {
3786
3786
  version: version$1};
3787
3787
 
@@ -5264,6 +5264,7 @@ async function lexChunkStreamAsync(grammar, chunks) {
5264
5264
  * Returns the ACTION table key for a lexer token.
5265
5265
  *
5266
5266
  * @param token - Current input token.
5267
+ * @returns The ACTION table key for the token, or `$eof` for end-of-input.
5267
5268
  */
5268
5269
  function tokenActionKey(token) {
5269
5270
  if (isEofToken(token)) {
@@ -5275,6 +5276,7 @@ function tokenActionKey(token) {
5275
5276
  * Builds a terminal CST node from a shifted token.
5276
5277
  *
5277
5278
  * @param token - Lexer token that was shifted.
5279
+ * @returns A terminal CST node carrying the token name, text, and location.
5278
5280
  */
5279
5281
  function astNodeFromToken(token) {
5280
5282
  return AstNode.terminal(token.name, token.text, token.location);
@@ -5283,10 +5285,12 @@ function astNodeFromToken(token) {
5283
5285
  * Merges child spans into one source location.
5284
5286
  *
5285
5287
  * @param children - Reduced child subtrees.
5288
+ * @returns The smallest span covering all child locations, or null when none exist.
5286
5289
  */
5287
5290
  function mergeChildLocations(children) {
5288
5291
  let offset = null;
5289
5292
  let end = null;
5293
+ // Walk children to find the earliest offset and latest end position.
5290
5294
  for (const child of children) {
5291
5295
  if (child.location === null) {
5292
5296
  continue;
@@ -5346,6 +5350,7 @@ class ShiftReduceEngine {
5346
5350
  const stateStack = [0];
5347
5351
  const valueStack = [];
5348
5352
  let tokenIndex = 0;
5353
+ // Drive the parser until accept, error, or an unrecoverable reduce failure.
5349
5354
  while (true) {
5350
5355
  const state = stateStack[stateStack.length - 1];
5351
5356
  const currentToken = tokens[tokenIndex];
@@ -5402,6 +5407,7 @@ class ShiftReduceEngine {
5402
5407
  * @param productionId - Production to reduce by.
5403
5408
  * @param stateStack - Parser state stack.
5404
5409
  * @param valueStack - Semantic value stack.
5410
+ * @returns The reduced subtree pushed onto the value stack, or null on stack underflow.
5405
5411
  */
5406
5412
  static reduce(table, productionId, stateStack, valueStack) {
5407
5413
  const production = table.production(productionId);
@@ -5410,6 +5416,7 @@ class ShiftReduceEngine {
5410
5416
  }
5411
5417
  const rhsLength = production.rhs.length;
5412
5418
  const children = [];
5419
+ // Pop one state and value per right-hand side symbol.
5413
5420
  for (let index = 0; index < rhsLength; index += 1) {
5414
5421
  if (stateStack.length === 0 || valueStack.length === 0) {
5415
5422
  return null;
@@ -5424,6 +5431,7 @@ class ShiftReduceEngine {
5424
5431
  if (stateStack.length === 0) {
5425
5432
  return null;
5426
5433
  }
5434
+ // Follow GOTO for the production left-hand side and push the new subtree.
5427
5435
  const gotoState = table.goto(stateStack[stateStack.length - 1], production.name);
5428
5436
  if (gotoState === null) {
5429
5437
  return null;
@@ -5438,6 +5446,7 @@ class ShiftReduceEngine {
5438
5446
  *
5439
5447
  * @param production - Reduced production metadata.
5440
5448
  * @param children - Child subtrees matching the production right-hand side.
5449
+ * @returns A rule CST node spanning the reduced children.
5441
5450
  */
5442
5451
  static nodeFromProduction(production, children) {
5443
5452
  return AstNode.rule(production.name, children, mergeChildLocations(children), production.variant, production.id, production.origin);
@@ -5448,6 +5457,7 @@ class ShiftReduceEngine {
5448
5457
  *
5449
5458
  * @param table - Parse table with ACTION, GOTO, and production metadata.
5450
5459
  * @param tokens - Token stream ending with `$eof`.
5460
+ * @returns The start symbol parse tree, or null on syntax error.
5451
5461
  */
5452
5462
  function parseWithTable(table, tokens) {
5453
5463
  return ShiftReduceEngine.parse(table, tokens);
@@ -5457,6 +5467,7 @@ function parseWithTable(table, tokens) {
5457
5467
  *
5458
5468
  * @param table - Parse table with ACTION, GOTO, and production metadata.
5459
5469
  * @param tokens - Token stream ending with `$eof`.
5470
+ * @returns Parse tree on success, or error offset and message on failure.
5460
5471
  */
5461
5472
  function parseWithTableResult(table, tokens) {
5462
5473
  return ShiftReduceEngine.parseWithResult(table, tokens);
@@ -5683,6 +5694,7 @@ class EbnfDesugarer {
5683
5694
  */
5684
5695
  desugarProduction(production) {
5685
5696
  const alternatives = this.expandExpression(production.name, production.expression, null);
5697
+ // Record one BNF production per desugared alternative.
5686
5698
  for (const alternative of alternatives) {
5687
5699
  this.addProduction(production.name, alternative.symbols, alternative.variant, production.name);
5688
5700
  }
@@ -5760,6 +5772,7 @@ class EbnfDesugarer {
5760
5772
  */
5761
5773
  expandSequence(ownerName, elements, variant) {
5762
5774
  let results = [{ symbols: [], variant }];
5775
+ // Cartesian product: combine each prefix alternative with each next-element alternative.
5763
5776
  for (const element of elements) {
5764
5777
  const elementAlternatives = this.expandExpression(ownerName, element, variant);
5765
5778
  const combined = [];
@@ -5882,6 +5895,7 @@ class GrammarAnalysis {
5882
5895
  * Returns whether a non-terminal can derive the empty string.
5883
5896
  *
5884
5897
  * @param name - Non-terminal name to test.
5898
+ * @returns True when the non-terminal can derive ε.
5885
5899
  */
5886
5900
  isNullable(name) {
5887
5901
  return this.nullable.has(name);
@@ -5906,10 +5920,12 @@ class GrammarAnalysis {
5906
5920
  * Returns FIRST symbols for a symbol sequence.
5907
5921
  *
5908
5922
  * @param symbols - Right-hand side prefix or full sequence.
5923
+ * @returns Encoded terminal keys in FIRST of the sequence.
5909
5924
  */
5910
5925
  firstOfSequence(symbols) {
5911
5926
  const result = new Set();
5912
5927
  let allNullable = true;
5928
+ // Accumulate FIRST of each prefix symbol until a non-nullable symbol stops propagation.
5913
5929
  for (const symbol of symbols) {
5914
5930
  const symbolFirst = this.firstOfSymbol(symbol);
5915
5931
  GrammarAnalysis.addAll(result, symbolFirst);
@@ -5918,6 +5934,7 @@ class GrammarAnalysis {
5918
5934
  break;
5919
5935
  }
5920
5936
  }
5937
+ // Every symbol is nullable, so end-of-input is in FIRST.
5921
5938
  if (allNullable) {
5922
5939
  result.add(EOF_TOKEN_NAME);
5923
5940
  }
@@ -5949,10 +5966,12 @@ class GrammarAnalysis {
5949
5966
  * Computes nullable non-terminals by fixed-point iteration.
5950
5967
  *
5951
5968
  * @param grammar - Plain BNF grammar to analyze.
5969
+ * @returns The set of non-terminal names that derive ε.
5952
5970
  */
5953
5971
  static computeNullable(grammar) {
5954
5972
  const nullable = new Set();
5955
5973
  let changed = true;
5974
+ // Fixed-point: mark A nullable when every production A → α has nullable α.
5956
5975
  while (changed) {
5957
5976
  changed = false;
5958
5977
  for (const production of grammar.productions) {
@@ -5976,6 +5995,7 @@ class GrammarAnalysis {
5976
5995
  *
5977
5996
  * @param grammar - Plain BNF grammar to analyze.
5978
5997
  * @param nullable - Nullable non-terminal names.
5998
+ * @returns FIRST set for every non-terminal name.
5979
5999
  */
5980
6000
  static computeFirst(grammar, nullable) {
5981
6001
  const firstByNonTerminal = new Map();
@@ -5983,6 +6003,7 @@ class GrammarAnalysis {
5983
6003
  firstByNonTerminal.set(name, new Set());
5984
6004
  }
5985
6005
  let changed = true;
6006
+ // Fixed-point: add FIRST(α) into FIRST(A) for each production A → α.
5986
6007
  while (changed) {
5987
6008
  changed = false;
5988
6009
  for (const production of grammar.productions) {
@@ -6003,16 +6024,19 @@ class GrammarAnalysis {
6003
6024
  * @param grammar - Plain BNF grammar to analyze.
6004
6025
  * @param firstByNonTerminal - Precomputed FIRST sets.
6005
6026
  * @param nullable - Nullable non-terminal names.
6027
+ * @returns FOLLOW set for every non-terminal name.
6006
6028
  */
6007
6029
  static computeFollow(grammar, firstByNonTerminal, nullable) {
6008
6030
  const followByNonTerminal = new Map();
6009
6031
  for (const name of grammar.nonTerminalNames()) {
6010
6032
  followByNonTerminal.set(name, new Set());
6011
6033
  }
6034
+ // Seed FOLLOW of the start symbol with end-of-input.
6012
6035
  const startFollow = followByNonTerminal.get(grammar.startSymbol) ?? new Set();
6013
6036
  startFollow.add(EOF_TOKEN_NAME);
6014
6037
  followByNonTerminal.set(grammar.startSymbol, startFollow);
6015
6038
  let changed = true;
6039
+ // Fixed-point: propagate FOLLOW through every production A → α B β.
6016
6040
  while (changed) {
6017
6041
  changed = false;
6018
6042
  for (const production of grammar.productions) {
@@ -6026,6 +6050,7 @@ class GrammarAnalysis {
6026
6050
  const beta = production.rhs.slice(index + 1);
6027
6051
  const betaFirst = GrammarAnalysis.firstOfSequenceStatic(beta, firstByNonTerminal, nullable);
6028
6052
  GrammarAnalysis.addAll(follow, betaFirst);
6053
+ // When β is empty or nullable, inherit FOLLOW of the production lhs.
6029
6054
  if (beta.length === 0 || GrammarAnalysis.isSequenceNullableStatic(beta, nullable)) {
6030
6055
  const lhsFollow = followByNonTerminal.get(production.name) ?? new Set();
6031
6056
  GrammarAnalysis.addAll(follow, lhsFollow);
@@ -6107,6 +6132,7 @@ class GrammarAnalysis {
6107
6132
  * Computes nullable, FIRST, and FOLLOW sets for a BNF grammar.
6108
6133
  *
6109
6134
  * @param grammar - Plain BNF grammar to analyze.
6135
+ * @returns Nullable, FIRST, and FOLLOW analysis for the grammar.
6110
6136
  */
6111
6137
  function analyzeGrammar(grammar) {
6112
6138
  return new GrammarAnalysis(grammar);
@@ -6125,38 +6151,60 @@ function sortLr0Items(items) {
6125
6151
  return left.dot - right.dot;
6126
6152
  });
6127
6153
  }
6154
+ /**
6155
+ * Returns encoded symbol keys that appear immediately after the dot in an item set.
6156
+ *
6157
+ * @param grammar - BNF grammar containing referenced productions.
6158
+ * @param items - Closed item set.
6159
+ * @returns Sorted terminal and non-terminal keys eligible for GOTO from this state.
6160
+ */
6161
+ function symbolsAfterDot(grammar, items) {
6162
+ const keys = new Set();
6163
+ for (const item of items) {
6164
+ const production = grammar.production(item.productionId);
6165
+ if (production === null) {
6166
+ continue;
6167
+ }
6168
+ const nextSymbol = production.rhs[item.dot];
6169
+ if (nextSymbol === undefined) {
6170
+ continue;
6171
+ }
6172
+ keys.add(bnfParserSymbolKey(nextSymbol));
6173
+ }
6174
+ return [...keys].sort();
6175
+ }
6128
6176
  /**
6129
6177
  * Computes the LR(0) closure of an item set.
6130
6178
  *
6131
6179
  * @param grammar - Plain or augmented BNF grammar.
6132
6180
  * @param items - Seed items.
6181
+ * @returns The seed items plus all items implied by dotted non-terminal prefixes.
6133
6182
  */
6134
6183
  function lr0Closure(grammar, items) {
6135
6184
  const result = sortLr0Items(items);
6136
6185
  const itemKeys = new Set(result.map((item) => Lr0ItemSetBuilder.itemIdentity(item)));
6137
- let changed = true;
6138
- while (changed) {
6139
- changed = false;
6140
- for (const item of [...result]) {
6141
- const production = grammar.production(item.productionId);
6142
- if (production === null) {
6143
- continue;
6144
- }
6145
- const nextSymbol = production.rhs[item.dot];
6146
- if (nextSymbol === undefined || nextSymbol.kind !== 'nonTerminal') {
6147
- continue;
6148
- }
6149
- for (const candidate of grammar.productionsFor(nextSymbol.name)) {
6150
- const closureItem = {
6151
- productionId: candidate.id,
6152
- dot: 0,
6153
- };
6154
- const identity = Lr0ItemSetBuilder.itemIdentity(closureItem);
6155
- if (!itemKeys.has(identity)) {
6156
- itemKeys.add(identity);
6157
- result.push(closureItem);
6158
- changed = true;
6159
- }
6186
+ const pendingIndices = result.map((_, index) => index);
6187
+ // Expand closure from each newly discovered item only.
6188
+ while (pendingIndices.length > 0) {
6189
+ const item = result[pendingIndices.pop()];
6190
+ const production = grammar.production(item.productionId);
6191
+ if (production === null) {
6192
+ continue;
6193
+ }
6194
+ const nextSymbol = production.rhs[item.dot];
6195
+ if (nextSymbol === undefined || nextSymbol.kind !== 'nonTerminal') {
6196
+ continue;
6197
+ }
6198
+ for (const candidate of grammar.productionsFor(nextSymbol.name)) {
6199
+ const closureItem = {
6200
+ productionId: candidate.id,
6201
+ dot: 0,
6202
+ };
6203
+ const identity = Lr0ItemSetBuilder.itemIdentity(closureItem);
6204
+ if (!itemKeys.has(identity)) {
6205
+ itemKeys.add(identity);
6206
+ pendingIndices.push(result.length);
6207
+ result.push(closureItem);
6160
6208
  }
6161
6209
  }
6162
6210
  }
@@ -6168,9 +6216,11 @@ function lr0Closure(grammar, items) {
6168
6216
  * @param grammar - Plain or augmented BNF grammar.
6169
6217
  * @param items - Closed item set.
6170
6218
  * @param symbolKey - Encoded terminal or non-terminal symbol.
6219
+ * @returns The closed item set reached after advancing the dot past `symbolKey`.
6171
6220
  */
6172
6221
  function lr0Goto(grammar, items, symbolKey) {
6173
6222
  const moved = [];
6223
+ // Advance the dot on every item expecting `symbolKey`.
6174
6224
  for (const item of items) {
6175
6225
  const production = grammar.production(item.productionId);
6176
6226
  if (production === null) {
@@ -6196,15 +6246,30 @@ function lr0Goto(grammar, items, symbolKey) {
6196
6246
  class Lr0ItemSetCollection {
6197
6247
  grammar;
6198
6248
  itemSets;
6249
+ indexByIdentity;
6199
6250
  /**
6200
6251
  * Creates a collection from an ordered list of closed item sets.
6201
6252
  *
6202
6253
  * @param grammar - Grammar the item sets were built from.
6203
6254
  * @param itemSets - Closed LR(0) item sets in discovery order.
6255
+ * @param indexByIdentity - Optional precomputed identity-to-state map.
6204
6256
  */
6205
- constructor(grammar, itemSets) {
6257
+ constructor(
6258
+ /** Grammar analyzed when the item sets were constructed. */
6259
+ grammar,
6260
+ /** Closed LR(0) item sets indexed by parser state number. */
6261
+ itemSets, indexByIdentity) {
6206
6262
  this.grammar = grammar;
6207
6263
  this.itemSets = itemSets;
6264
+ if (indexByIdentity !== undefined) {
6265
+ this.indexByIdentity = indexByIdentity;
6266
+ return;
6267
+ }
6268
+ const builtIndex = new Map();
6269
+ for (let index = 0; index < itemSets.length; index += 1) {
6270
+ builtIndex.set(Lr0ItemSetBuilder.setIdentity(itemSets[index] ?? [], true), index);
6271
+ }
6272
+ this.indexByIdentity = builtIndex;
6208
6273
  }
6209
6274
  /**
6210
6275
  * Returns the index of an item set matching `items`, or null when absent.
@@ -6212,13 +6277,8 @@ class Lr0ItemSetCollection {
6212
6277
  * @param items - Closed item set to locate.
6213
6278
  */
6214
6279
  indexOf(items) {
6215
- const target = Lr0ItemSetBuilder.setIdentity(items);
6216
- for (let index = 0; index < this.itemSets.length; index += 1) {
6217
- if (Lr0ItemSetBuilder.setIdentity(this.itemSets[index]) === target) {
6218
- return index;
6219
- }
6220
- }
6221
- return null;
6280
+ const target = Lr0ItemSetBuilder.setIdentity(items, true);
6281
+ return this.indexByIdentity.get(target) ?? null;
6222
6282
  }
6223
6283
  }
6224
6284
  /**
@@ -6229,42 +6289,42 @@ class Lr0ItemSetBuilder {
6229
6289
  * Builds the canonical LR(0) collection for a grammar.
6230
6290
  *
6231
6291
  * @param grammar - Augmented or plain BNF grammar.
6292
+ * @returns The canonical LR(0) item set collection for the grammar.
6232
6293
  */
6233
6294
  static build(grammar) {
6234
6295
  const startProductions = grammar.productionsFor(grammar.startSymbol);
6235
6296
  if (startProductions.length === 0) {
6236
6297
  return new Lr0ItemSetCollection(grammar, []);
6237
6298
  }
6299
+ // Seed state 0 from the start production at dot zero.
6238
6300
  const initial = lr0Closure(grammar, [{
6239
6301
  productionId: startProductions[0].id,
6240
6302
  dot: 0,
6241
6303
  }]);
6242
6304
  const itemSets = [[...initial]];
6243
- const itemSetKeys = new Map([
6244
- [Lr0ItemSetBuilder.setIdentity(initial), 0],
6305
+ const indexByIdentity = new Map([
6306
+ [Lr0ItemSetBuilder.setIdentity(initial, true), 0],
6245
6307
  ]);
6246
- const symbols = Lr0ItemSetBuilder.grammarSymbolKeys(grammar);
6247
- let changed = true;
6248
- while (changed) {
6249
- changed = false;
6250
- for (const itemSet of [...itemSets]) {
6251
- for (const symbolKey of symbols) {
6252
- const gotoSet = lr0Goto(grammar, itemSet, symbolKey);
6253
- if (gotoSet.length === 0) {
6254
- continue;
6255
- }
6256
- const gotoKey = Lr0ItemSetBuilder.setIdentity(gotoSet);
6257
- const existingIndex = itemSetKeys.get(gotoKey);
6258
- if (existingIndex === undefined) {
6259
- const nextIndex = itemSets.length;
6260
- itemSetKeys.set(gotoKey, nextIndex);
6261
- itemSets.push([...gotoSet]);
6262
- changed = true;
6263
- }
6308
+ const pendingStates = [0];
6309
+ // Discover new states from each state exactly once.
6310
+ while (pendingStates.length > 0) {
6311
+ const stateIndex = pendingStates.pop();
6312
+ const itemSet = itemSets[stateIndex] ?? [];
6313
+ for (const symbolKey of symbolsAfterDot(grammar, itemSet)) {
6314
+ const gotoSet = lr0Goto(grammar, itemSet, symbolKey);
6315
+ if (gotoSet.length === 0) {
6316
+ continue;
6317
+ }
6318
+ const gotoKey = Lr0ItemSetBuilder.setIdentity(gotoSet, true);
6319
+ if (!indexByIdentity.has(gotoKey)) {
6320
+ const nextIndex = itemSets.length;
6321
+ indexByIdentity.set(gotoKey, nextIndex);
6322
+ itemSets.push([...gotoSet]);
6323
+ pendingStates.push(nextIndex);
6264
6324
  }
6265
6325
  }
6266
6326
  }
6267
- return new Lr0ItemSetCollection(grammar, itemSets);
6327
+ return new Lr0ItemSetCollection(grammar, itemSets, indexByIdentity);
6268
6328
  }
6269
6329
  /**
6270
6330
  * Returns every terminal and non-terminal key that can appear in GOTO.
@@ -6290,9 +6350,11 @@ class Lr0ItemSetBuilder {
6290
6350
  * Returns a stable identity string for an item set.
6291
6351
  *
6292
6352
  * @param items - Closed item set.
6353
+ * @param alreadySorted - When true, skips re-sorting because items are canonical.
6293
6354
  */
6294
- static setIdentity(items) {
6295
- return sortLr0Items(items)
6355
+ static setIdentity(items, alreadySorted = false) {
6356
+ const sorted = alreadySorted ? items : sortLr0Items(items);
6357
+ return sorted
6296
6358
  .map((item) => Lr0ItemSetBuilder.itemIdentity(item))
6297
6359
  .join('|');
6298
6360
  }
@@ -6301,6 +6363,7 @@ class Lr0ItemSetBuilder {
6301
6363
  * Builds the canonical LR(0) item set collection for a grammar.
6302
6364
  *
6303
6365
  * @param grammar - Augmented or plain BNF grammar.
6366
+ * @returns The canonical LR(0) item set collection for the grammar.
6304
6367
  */
6305
6368
  function buildLr0ItemSets(grammar) {
6306
6369
  return Lr0ItemSetBuilder.build(grammar);
@@ -6325,21 +6388,6 @@ function parseActionsEqual(left, right) {
6325
6388
  return true;
6326
6389
  }
6327
6390
  }
6328
- /**
6329
- * Returns a readable parse action label for tests and diagnostics.
6330
- *
6331
- * @param action - Parse action to format.
6332
- */
6333
- function formatParseAction(action) {
6334
- switch (action.kind) {
6335
- case 'shift':
6336
- return `s${String(action.state)}`;
6337
- case 'reduce':
6338
- return `r${String(action.productionId)}`;
6339
- case 'accept':
6340
- return 'acc';
6341
- }
6342
- }
6343
6391
  /**
6344
6392
  * Classifies a conflict between two distinct parse actions.
6345
6393
  *
@@ -6365,13 +6413,14 @@ class TableBuilderBase {
6365
6413
  * @param grammar - Augmented BNF grammar.
6366
6414
  * @param itemSets - Canonical item set collection.
6367
6415
  * @param gotoItems - Computes the GOTO item set for one state and symbol.
6416
+ * @returns Map from `state:symbol` keys to target state indices.
6368
6417
  */
6369
6418
  static buildGotoTargets(grammar, itemSets, indexOf, gotoItems) {
6370
6419
  const targets = new Map();
6371
- const symbols = Lr0ItemSetBuilder.grammarSymbolKeys(grammar);
6420
+ // Precompute GOTO for every state and symbol reachable from its items.
6372
6421
  for (let state = 0; state < itemSets.length; state += 1) {
6373
6422
  const itemSet = itemSets[state] ?? [];
6374
- for (const symbolKey of symbols) {
6423
+ for (const symbolKey of symbolsAfterDot(grammar, itemSet)) {
6375
6424
  const gotoSet = gotoItems(grammar, itemSet, symbolKey);
6376
6425
  if (gotoSet.length === 0) {
6377
6426
  continue;
@@ -6394,9 +6443,10 @@ class TableBuilderBase {
6394
6443
  * @param gotoTargets - Precomputed GOTO target indices.
6395
6444
  * @param stateActions - ACTION map for the state.
6396
6445
  * @param stateGotos - GOTO map for the state.
6397
- * @param conflicts - Conflict list to append to when actions disagree.
6446
+ * @param conflicts - Resolved conflict list to append to when actions disagree.
6398
6447
  */
6399
6448
  static fillShiftsAndGotos(grammar, state, itemSet, gotoTargets, stateActions, stateGotos, conflicts) {
6449
+ // Derive shift and GOTO entries from every incomplete item in the state.
6400
6450
  for (const item of itemSet) {
6401
6451
  const production = grammar.production(item.productionId);
6402
6452
  if (production === null) {
@@ -6411,6 +6461,7 @@ class TableBuilderBase {
6411
6461
  if (targetState === undefined) {
6412
6462
  continue;
6413
6463
  }
6464
+ // Non-terminal symbols populate GOTO; terminals populate ACTION shift entries.
6414
6465
  if (nextSymbol.kind === 'nonTerminal') {
6415
6466
  stateGotos.set(nextSymbol.name, targetState);
6416
6467
  continue;
@@ -6424,7 +6475,7 @@ class TableBuilderBase {
6424
6475
  * @param state - Parser state index.
6425
6476
  * @param productionName - Left-hand side of the completed production.
6426
6477
  * @param stateActions - ACTION map for the state.
6427
- * @param conflicts - Conflict list to append to when actions disagree.
6478
+ * @param conflicts - Resolved conflict list to append to when actions disagree.
6428
6479
  */
6429
6480
  static setAcceptAction(state, productionName, stateActions, conflicts) {
6430
6481
  if (productionName !== AUGMENTED_START_SYMBOL) {
@@ -6442,10 +6493,10 @@ class TableBuilderBase {
6442
6493
  return `${String(state)}:${symbolKey}`;
6443
6494
  }
6444
6495
  /**
6445
- * Inserts one ACTION entry, recording a conflict when the slot is already occupied.
6496
+ * Inserts one ACTION entry, resolving shift/reduce and reduce/reduce conflicts.
6446
6497
  *
6447
6498
  * @param stateActions - ACTION map for the current state.
6448
- * @param conflicts - Conflict list to append to when actions disagree.
6499
+ * @param conflicts - Resolved conflict list to append to when actions disagree.
6449
6500
  * @param state - Parser state index.
6450
6501
  * @param symbol - Encoded terminal or `$eof` key.
6451
6502
  * @param incoming - Candidate parse action.
@@ -6456,15 +6507,32 @@ class TableBuilderBase {
6456
6507
  stateActions.set(symbol, incoming);
6457
6508
  return;
6458
6509
  }
6510
+ // Identical re-insertions are ignored; disagreeing actions are resolved by conflict kind.
6459
6511
  if (parseActionsEqual(existing, incoming)) {
6460
6512
  return;
6461
6513
  }
6514
+ const conflictKind = classifyParseConflict(existing, incoming);
6515
+ if (conflictKind === 'shift-reduce') {
6516
+ const shiftAction = existing.kind === 'shift' ? existing : incoming;
6517
+ stateActions.set(symbol, shiftAction);
6518
+ conflicts.push({
6519
+ kind: 'shift-reduce',
6520
+ state,
6521
+ symbol,
6522
+ existing,
6523
+ incoming,
6524
+ resolution: 'shift',
6525
+ });
6526
+ return;
6527
+ }
6528
+ // Reduce/reduce: keep the first reduce action already in the table.
6462
6529
  conflicts.push({
6463
- kind: classifyParseConflict(existing, incoming),
6530
+ kind: 'reduce-reduce',
6464
6531
  state,
6465
6532
  symbol,
6466
6533
  existing,
6467
6534
  incoming,
6535
+ resolution: 'reduce',
6468
6536
  });
6469
6537
  }
6470
6538
  /**
@@ -6483,6 +6551,7 @@ class TableBuilderBase {
6483
6551
  * Sorts LR(1) items for canonical set comparison.
6484
6552
  *
6485
6553
  * @param items - Items to sort.
6554
+ * @returns A new array sorted by production id, dot, then lookahead.
6486
6555
  */
6487
6556
  function sortLr1Items(items) {
6488
6557
  return [...items].sort((left, right) => {
@@ -6499,6 +6568,7 @@ function sortLr1Items(items) {
6499
6568
  * Returns the LR(0) core key for one LR(1) item.
6500
6569
  *
6501
6570
  * @param item - LR(1) item to encode.
6571
+ * @returns A string `productionId:dot` shared by all lookaheads at the same dot position.
6502
6572
  */
6503
6573
  function lr1CoreKey(item) {
6504
6574
  return `${String(item.productionId)}:${String(item.dot)}`;
@@ -6507,6 +6577,7 @@ function lr1CoreKey(item) {
6507
6577
  * Returns a stable identity string for one LR(1) item.
6508
6578
  *
6509
6579
  * @param item - Item to encode.
6580
+ * @returns A string `productionId:dot:lookahead` for set membership tests.
6510
6581
  */
6511
6582
  function lr1ItemIdentity(item) {
6512
6583
  return `${lr1CoreKey(item)}:${item.lookahead}`;
@@ -6515,9 +6586,12 @@ function lr1ItemIdentity(item) {
6515
6586
  * Returns a stable identity string for an LR(1) item set.
6516
6587
  *
6517
6588
  * @param items - Closed item set.
6589
+ * @param alreadySorted - When true, skips re-sorting because items are canonical.
6590
+ * @returns A pipe-separated, sorted identity string for canonical set comparison.
6518
6591
  */
6519
- function lr1SetIdentity(items) {
6520
- return sortLr1Items(items)
6592
+ function lr1SetIdentity(items, alreadySorted = false) {
6593
+ const sorted = alreadySorted ? items : sortLr1Items(items);
6594
+ return sorted
6521
6595
  .map((item) => lr1ItemIdentity(item))
6522
6596
  .join('|');
6523
6597
  }
@@ -6525,6 +6599,7 @@ function lr1SetIdentity(items) {
6525
6599
  * Returns the LR(0) core identity for an LR(1) item set.
6526
6600
  *
6527
6601
  * @param items - LR(1) item set.
6602
+ * @returns A pipe-separated, sorted string of LR(0) cores with lookaheads removed.
6528
6603
  */
6529
6604
  function lr1CoreSetIdentity(items) {
6530
6605
  const cores = new Set(items.map((item) => lr1CoreKey(item)));
@@ -6536,9 +6611,11 @@ function lr1CoreSetIdentity(items) {
6536
6611
  * @param beta - Remaining right-hand side symbols after a non-terminal.
6537
6612
  * @param lookahead - Lookahead terminal following β.
6538
6613
  * @param analysis - Nullable and FIRST analysis for the grammar.
6614
+ * @returns Terminal keys that may follow the dotted non-terminal in this item.
6539
6615
  */
6540
6616
  function firstAfterSymbols(beta, lookahead, analysis) {
6541
6617
  const result = new Set();
6618
+ // Accumulate FIRST of each prefix symbol until a non-nullable symbol stops propagation.
6542
6619
  for (const symbol of beta) {
6543
6620
  for (const terminal of analysis.firstOfSymbol(symbol)) {
6544
6621
  result.add(terminal);
@@ -6547,6 +6624,7 @@ function firstAfterSymbols(beta, lookahead, analysis) {
6547
6624
  return result;
6548
6625
  }
6549
6626
  }
6627
+ // Every symbol in β is nullable, so the item's own lookahead is reachable.
6550
6628
  result.add(lookahead);
6551
6629
  return result;
6552
6630
  }
@@ -6556,36 +6634,37 @@ function firstAfterSymbols(beta, lookahead, analysis) {
6556
6634
  * @param grammar - Augmented BNF grammar.
6557
6635
  * @param analysis - Nullable and FIRST analysis for the grammar.
6558
6636
  * @param items - Seed items.
6637
+ * @returns The seed items plus all items implied by dotted non-terminal prefixes.
6559
6638
  */
6560
6639
  function lr1Closure(grammar, analysis, items) {
6561
6640
  const result = sortLr1Items(items);
6562
6641
  const itemKeys = new Set(result.map((item) => lr1ItemIdentity(item)));
6563
- let changed = true;
6564
- while (changed) {
6565
- changed = false;
6566
- for (const item of [...result]) {
6567
- const production = grammar.production(item.productionId);
6568
- if (production === null) {
6569
- continue;
6570
- }
6571
- const nextSymbol = production.rhs[item.dot];
6572
- if (nextSymbol === undefined || nextSymbol.kind !== 'nonTerminal') {
6573
- continue;
6574
- }
6575
- const beta = production.rhs.slice(item.dot + 1);
6576
- for (const candidate of grammar.productionsFor(nextSymbol.name)) {
6577
- for (const lookahead of firstAfterSymbols(beta, item.lookahead, analysis)) {
6578
- const closureItem = {
6579
- productionId: candidate.id,
6580
- dot: 0,
6581
- lookahead,
6582
- };
6583
- const identity = lr1ItemIdentity(closureItem);
6584
- if (!itemKeys.has(identity)) {
6585
- itemKeys.add(identity);
6586
- result.push(closureItem);
6587
- changed = true;
6588
- }
6642
+ const pendingIndices = result.map((_, index) => index);
6643
+ // Expand closure from each newly discovered item only.
6644
+ while (pendingIndices.length > 0) {
6645
+ const item = result[pendingIndices.pop()];
6646
+ const production = grammar.production(item.productionId);
6647
+ if (production === null) {
6648
+ continue;
6649
+ }
6650
+ const nextSymbol = production.rhs[item.dot];
6651
+ if (nextSymbol === undefined || nextSymbol.kind !== 'nonTerminal') {
6652
+ continue;
6653
+ }
6654
+ const beta = production.rhs.slice(item.dot + 1);
6655
+ // For each production of the non-terminal after the dot, propagate lookaheads.
6656
+ for (const candidate of grammar.productionsFor(nextSymbol.name)) {
6657
+ for (const lookahead of firstAfterSymbols(beta, item.lookahead, analysis)) {
6658
+ const closureItem = {
6659
+ productionId: candidate.id,
6660
+ dot: 0,
6661
+ lookahead,
6662
+ };
6663
+ const identity = lr1ItemIdentity(closureItem);
6664
+ if (!itemKeys.has(identity)) {
6665
+ itemKeys.add(identity);
6666
+ pendingIndices.push(result.length);
6667
+ result.push(closureItem);
6589
6668
  }
6590
6669
  }
6591
6670
  }
@@ -6599,9 +6678,11 @@ function lr1Closure(grammar, analysis, items) {
6599
6678
  * @param analysis - Nullable and FIRST analysis for the grammar.
6600
6679
  * @param items - Closed item set.
6601
6680
  * @param symbolKey - Encoded terminal or non-terminal symbol.
6681
+ * @returns The closed item set reached after advancing the dot past `symbolKey`.
6602
6682
  */
6603
6683
  function lr1Goto(grammar, analysis, items, symbolKey) {
6604
6684
  const moved = [];
6685
+ // Advance the dot on every item expecting `symbolKey`, preserving lookaheads.
6605
6686
  for (const item of items) {
6606
6687
  const production = grammar.production(item.productionId);
6607
6688
  if (production === null) {
@@ -6628,34 +6709,45 @@ function lr1Goto(grammar, analysis, items, symbolKey) {
6628
6709
  class Lr1ItemSetCollection {
6629
6710
  grammar;
6630
6711
  itemSets;
6712
+ indexByIdentity;
6631
6713
  /**
6632
6714
  * Creates a collection from an ordered list of closed item sets.
6633
6715
  *
6634
6716
  * @param grammar - Grammar the item sets were built from.
6635
6717
  * @param itemSets - Closed LR(1) item sets in discovery order.
6718
+ * @param indexByIdentity - Optional precomputed identity-to-state map.
6636
6719
  */
6637
- constructor(grammar, itemSets) {
6720
+ constructor(
6721
+ /** Grammar analyzed when the item sets were constructed. */
6722
+ grammar,
6723
+ /** Closed LR(1) item sets indexed by parser state number. */
6724
+ itemSets, indexByIdentity) {
6638
6725
  this.grammar = grammar;
6639
6726
  this.itemSets = itemSets;
6727
+ if (indexByIdentity !== undefined) {
6728
+ this.indexByIdentity = indexByIdentity;
6729
+ return;
6730
+ }
6731
+ const builtIndex = new Map();
6732
+ for (let index = 0; index < itemSets.length; index += 1) {
6733
+ builtIndex.set(lr1SetIdentity(itemSets[index] ?? [], true), index);
6734
+ }
6735
+ this.indexByIdentity = builtIndex;
6640
6736
  }
6641
6737
  /**
6642
6738
  * Returns the index of an item set matching `items`, or null when absent.
6643
6739
  *
6644
6740
  * @param items - Closed item set to locate.
6741
+ * @returns State index, or null when no set has the same canonical identity.
6645
6742
  */
6646
6743
  indexOf(items) {
6647
- const target = lr1SetIdentity(items);
6648
- for (let index = 0; index < this.itemSets.length; index += 1) {
6649
- if (lr1SetIdentity(this.itemSets[index]) === target) {
6650
- return index;
6651
- }
6652
- }
6653
- return null;
6744
+ return this.indexByIdentity.get(lr1SetIdentity(items, true)) ?? null;
6654
6745
  }
6655
6746
  /**
6656
6747
  * Returns LR(0) items derived from one LR(1) item set.
6657
6748
  *
6658
6749
  * @param items - LR(1) item set.
6750
+ * @returns Deduplicated LR(0) cores with lookaheads stripped.
6659
6751
  */
6660
6752
  static lr0Core(items) {
6661
6753
  const cores = new Map();
@@ -6678,43 +6770,43 @@ class Lr1ItemSetBuilder {
6678
6770
  * @param grammar - Augmented BNF grammar.
6679
6771
  * @param analysis - Nullable and FIRST analysis for the grammar.
6680
6772
  * @param startLookahead - Lookahead terminal for the initial item.
6773
+ * @returns The canonical LR(1) item set collection for the grammar.
6681
6774
  */
6682
6775
  static build(grammar, analysis, startLookahead) {
6683
6776
  const startProductions = grammar.productionsFor(grammar.startSymbol);
6684
6777
  if (startProductions.length === 0) {
6685
6778
  return new Lr1ItemSetCollection(grammar, []);
6686
6779
  }
6780
+ // Seed state 0 from the augmented start production and its initial lookahead.
6687
6781
  const initial = lr1Closure(grammar, analysis, [{
6688
6782
  productionId: startProductions[0].id,
6689
6783
  dot: 0,
6690
6784
  lookahead: startLookahead,
6691
6785
  }]);
6692
6786
  const itemSets = [[...initial]];
6693
- const itemSetKeys = new Map([
6694
- [lr1SetIdentity(initial), 0],
6787
+ const indexByIdentity = new Map([
6788
+ [lr1SetIdentity(initial, true), 0],
6695
6789
  ]);
6696
- const symbols = grammar.terminalKeys().concat(grammar.nonTerminalNames()).sort();
6697
- let changed = true;
6698
- while (changed) {
6699
- changed = false;
6700
- for (const itemSet of [...itemSets]) {
6701
- for (const symbolKey of symbols) {
6702
- const gotoSet = lr1Goto(grammar, analysis, itemSet, symbolKey);
6703
- if (gotoSet.length === 0) {
6704
- continue;
6705
- }
6706
- const gotoKey = lr1SetIdentity(gotoSet);
6707
- const existingIndex = itemSetKeys.get(gotoKey);
6708
- if (existingIndex === undefined) {
6709
- const nextIndex = itemSets.length;
6710
- itemSetKeys.set(gotoKey, nextIndex);
6711
- itemSets.push([...gotoSet]);
6712
- changed = true;
6713
- }
6790
+ const pendingStates = [0];
6791
+ // Discover new states from each state exactly once.
6792
+ while (pendingStates.length > 0) {
6793
+ const stateIndex = pendingStates.pop();
6794
+ const itemSet = itemSets[stateIndex] ?? [];
6795
+ for (const symbolKey of symbolsAfterDot(grammar, itemSet)) {
6796
+ const gotoSet = lr1Goto(grammar, analysis, itemSet, symbolKey);
6797
+ if (gotoSet.length === 0) {
6798
+ continue;
6799
+ }
6800
+ const gotoKey = lr1SetIdentity(gotoSet, true);
6801
+ if (!indexByIdentity.has(gotoKey)) {
6802
+ const nextIndex = itemSets.length;
6803
+ indexByIdentity.set(gotoKey, nextIndex);
6804
+ itemSets.push([...gotoSet]);
6805
+ pendingStates.push(nextIndex);
6714
6806
  }
6715
6807
  }
6716
6808
  }
6717
- return new Lr1ItemSetCollection(grammar, itemSets);
6809
+ return new Lr1ItemSetCollection(grammar, itemSets, indexByIdentity);
6718
6810
  }
6719
6811
  }
6720
6812
  /**
@@ -6723,6 +6815,7 @@ class Lr1ItemSetBuilder {
6723
6815
  * @param grammar - Augmented BNF grammar.
6724
6816
  * @param analysis - Nullable and FIRST analysis for the grammar.
6725
6817
  * @param startLookahead - Lookahead terminal for the initial item.
6818
+ * @returns The canonical LR(1) item set collection for the grammar.
6726
6819
  */
6727
6820
  function buildLr1ItemSets(grammar, analysis, startLookahead) {
6728
6821
  return Lr1ItemSetBuilder.build(grammar, analysis, startLookahead);
@@ -6731,11 +6824,13 @@ function buildLr1ItemSets(grammar, analysis, startLookahead) {
6731
6824
  * Merges LR(1) item sets that share the same LR(0) core into LALR item sets.
6732
6825
  *
6733
6826
  * @param collection - Canonical LR(1) item set collection.
6827
+ * @returns A new collection with LR(1) states merged by LR(0) core.
6734
6828
  */
6735
6829
  function mergeLalrItemSets(collection) {
6736
6830
  const coreToLalrIndex = new Map();
6737
6831
  const lr1ToLalr = new Map();
6738
6832
  const mergedItemSets = [];
6833
+ // Assign each LR(1) state to an LALR state keyed by its LR(0) core identity.
6739
6834
  for (let lr1Index = 0; lr1Index < collection.itemSets.length; lr1Index += 1) {
6740
6835
  const itemSet = collection.itemSets[lr1Index] ?? [];
6741
6836
  const core = lr1CoreSetIdentity(itemSet);
@@ -6759,17 +6854,18 @@ function mergeLalrItemSets(collection) {
6759
6854
  * @param lr1Collection - Canonical LR(1) item set collection.
6760
6855
  * @param lalrCollection - Merged LALR item set collection.
6761
6856
  * @param lr1ToLalr - LR(1) state index to LALR state index map.
6857
+ * @returns Map from `state:symbol` keys to LALR GOTO target state indices.
6762
6858
  */
6763
6859
  function buildLalrGotoTargets(grammar, analysis, lr1Collection, lr1ToLalr) {
6764
6860
  const targets = new Map();
6765
- const symbols = grammar.terminalKeys().concat(grammar.nonTerminalNames()).sort();
6861
+ // Recompute LR(1) GOTOs and remap each target through the LR(1) → LALR index map.
6766
6862
  for (let lr1State = 0; lr1State < lr1Collection.itemSets.length; lr1State += 1) {
6767
6863
  const lalrState = lr1ToLalr.get(lr1State);
6768
6864
  if (lalrState === undefined) {
6769
6865
  continue;
6770
6866
  }
6771
6867
  const itemSet = lr1Collection.itemSets[lr1State] ?? [];
6772
- for (const symbolKey of symbols) {
6868
+ for (const symbolKey of symbolsAfterDot(grammar, itemSet)) {
6773
6869
  const gotoSet = lr1Goto(grammar, analysis, itemSet, symbolKey);
6774
6870
  if (gotoSet.length === 0) {
6775
6871
  continue;
@@ -6796,9 +6892,11 @@ function buildLalrGotoTargets(grammar, analysis, lr1Collection, lr1ToLalr) {
6796
6892
  *
6797
6893
  * @param left - Existing merged item set.
6798
6894
  * @param right - Additional LR(1) item set to merge in.
6895
+ * @returns Items with lookaheads unioned for each shared LR(0) core.
6799
6896
  */
6800
6897
  function mergeItems(left, right) {
6801
6898
  const lookaheadsByCore = new Map();
6899
+ // Collect every lookahead grouped by dotted-production core.
6802
6900
  for (const item of [...left, ...right]) {
6803
6901
  const core = lr1CoreKey(item);
6804
6902
  const lookaheads = lookaheadsByCore.get(core) ?? new Set();
@@ -6806,6 +6904,7 @@ function buildLalrGotoTargets(grammar, analysis, lr1Collection, lr1ToLalr) {
6806
6904
  lookaheadsByCore.set(core, lookaheads);
6807
6905
  }
6808
6906
  const merged = [];
6907
+ // Expand each core into one item per distinct lookahead.
6809
6908
  for (const [core, lookaheads] of lookaheadsByCore.entries()) {
6810
6909
  const [productionIdText, dotText] = core.split(':');
6811
6910
  const productionId = Number(productionIdText);
@@ -6826,13 +6925,16 @@ function buildLalrGotoTargets(grammar, analysis, lr1Collection, lr1ToLalr) {
6826
6925
  *
6827
6926
  * @param lr1Collection - Canonical LR(1) item set collection.
6828
6927
  * @param lalrCollection - Merged LALR item set collection.
6928
+ * @returns Map from LR(1) state index to merged LALR state index.
6829
6929
  */
6830
6930
  function buildLr1ToLalrMap(lr1Collection, lalrCollection) {
6831
6931
  const coreToLalrIndex = new Map();
6932
+ // Index LALR states by their LR(0) core identity.
6832
6933
  for (let lalrIndex = 0; lalrIndex < lalrCollection.itemSets.length; lalrIndex += 1) {
6833
6934
  coreToLalrIndex.set(lr1CoreSetIdentity(lalrCollection.itemSets[lalrIndex] ?? []), lalrIndex);
6834
6935
  }
6835
6936
  const lr1ToLalr = new Map();
6937
+ // Map each LR(1) state to the LALR state sharing its core.
6836
6938
  for (let lr1Index = 0; lr1Index < lr1Collection.itemSets.length; lr1Index += 1) {
6837
6939
  const core = lr1CoreSetIdentity(lr1Collection.itemSets[lr1Index] ?? []);
6838
6940
  const lalrIndex = coreToLalrIndex.get(core);
@@ -6865,7 +6967,7 @@ class LrParseTable {
6865
6967
  * @param stateCount - Number of parser states in the item set collection.
6866
6968
  * @param actions - ACTION entries keyed by state and terminal symbol.
6867
6969
  * @param gotos - GOTO entries keyed by state and non-terminal name.
6868
- * @param conflicts - Shift/reduce and reduce/reduce conflicts encountered while building.
6970
+ * @param conflicts - Shift/reduce and reduce/reduce conflicts resolved during construction.
6869
6971
  */
6870
6972
  constructor(algorithm, grammar, productions, stateCount, actions, gotos, conflicts) {
6871
6973
  this.algorithm = algorithm;
@@ -6877,7 +6979,7 @@ class LrParseTable {
6877
6979
  this.conflicts = conflicts;
6878
6980
  }
6879
6981
  /**
6880
- * Whether the table has no unresolved conflicts.
6982
+ * Whether the table was built without conflicts.
6881
6983
  */
6882
6984
  get isConflictFree() {
6883
6985
  return this.conflicts.length === 0;
@@ -6902,14 +7004,31 @@ class LrParseTable {
6902
7004
  }
6903
7005
  }
6904
7006
  /**
6905
- * Returns readable conflict labels for tests and diagnostics.
7007
+ * Formats one resolved conflict as a warning line.
6906
7008
  *
6907
- * @param table - LR parse table to format.
7009
+ * @param conflict - Resolved shift/reduce or reduce/reduce conflict.
6908
7010
  */
6909
- function formatLrConflicts(table) {
6910
- return table.conflicts.map((conflict) => `${String(conflict.state)}/${conflict.symbol} `
6911
- + `${formatParseAction(conflict.existing)} vs ${formatParseAction(conflict.incoming)} `
6912
- + `(${conflict.kind})`);
7011
+ function formatParseConflictWarning(conflict) {
7012
+ const tokenLabel = formatConflictTokenLabel(conflict.symbol);
7013
+ if (conflict.kind === 'shift-reduce') {
7014
+ return `state ${String(conflict.state)}: shift/reduce conflict on ${tokenLabel} resolved as shift`;
7015
+ }
7016
+ const keptProduction = conflict.existing.kind === 'reduce'
7017
+ ? conflict.existing.productionId
7018
+ : conflict.incoming.productionId;
7019
+ return `state ${String(conflict.state)}: reduce/reduce conflict on ${tokenLabel} `
7020
+ + `resolved using rule ${String(keptProduction)}`;
7021
+ }
7022
+ /**
7023
+ * Returns a readable terminal label for conflict warning text.
7024
+ *
7025
+ * @param symbol - Encoded terminal or `$eof` key from the ACTION table.
7026
+ */
7027
+ function formatConflictTokenLabel(symbol) {
7028
+ if (symbol === '$eof') {
7029
+ return 'token $eof';
7030
+ }
7031
+ return `token ${symbol}`;
6913
7032
  }
6914
7033
  /**
6915
7034
  * Returns encoded right-hand side symbols for one production.
@@ -6925,6 +7044,7 @@ function encodeProductionRhs(production) {
6925
7044
  *
6926
7045
  * @param grammar - Plain BNF grammar to analyze.
6927
7046
  * @param algorithm - LR table construction algorithm.
7047
+ * @returns A populated LR parse table, possibly recording conflicts.
6928
7048
  */
6929
7049
  function buildLrTable(grammar, algorithm) {
6930
7050
  const augmented = grammar.augment();
@@ -6948,7 +7068,7 @@ class LrTableBuilder {
6948
7068
  * Builds an LR(0) parse table.
6949
7069
  *
6950
7070
  * @param grammar - Augmented BNF grammar.
6951
- * @param analysis - Nullable, FIRST, and FOLLOW analysis for the grammar.
7071
+ * @returns An LR(0) parse table using all terminals as reduce lookaheads.
6952
7072
  */
6953
7073
  static buildLr0(grammar) {
6954
7074
  const itemSets = buildLr0ItemSets(grammar);
@@ -6965,6 +7085,7 @@ class LrTableBuilder {
6965
7085
  *
6966
7086
  * @param grammar - Augmented BNF grammar.
6967
7087
  * @param analysis - Nullable, FIRST, and FOLLOW analysis for the grammar.
7088
+ * @returns An SLR parse table using FOLLOW sets as reduce lookaheads.
6968
7089
  */
6969
7090
  static buildSlr(grammar, analysis) {
6970
7091
  const itemSets = buildLr0ItemSets(grammar);
@@ -6980,6 +7101,7 @@ class LrTableBuilder {
6980
7101
  *
6981
7102
  * @param grammar - Augmented BNF grammar.
6982
7103
  * @param analysis - Nullable, FIRST, and FOLLOW analysis for the grammar.
7104
+ * @returns An LR(1) parse table with per-item lookaheads.
6983
7105
  */
6984
7106
  static buildLr1(grammar, analysis) {
6985
7107
  const itemSets = buildLr1ItemSets(grammar, analysis, EOF_TOKEN_NAME);
@@ -6990,6 +7112,7 @@ class LrTableBuilder {
6990
7112
  *
6991
7113
  * @param grammar - Augmented BNF grammar.
6992
7114
  * @param analysis - Nullable, FIRST, and FOLLOW analysis for the grammar.
7115
+ * @returns An LALR parse table merged from the canonical LR(1) collection.
6993
7116
  */
6994
7117
  static buildLalr(grammar, analysis) {
6995
7118
  const lr1ItemSets = buildLr1ItemSets(grammar, analysis, EOF_TOKEN_NAME);
@@ -7005,12 +7128,14 @@ class LrTableBuilder {
7005
7128
  * @param grammar - Augmented BNF grammar.
7006
7129
  * @param itemSets - Canonical LR(0) item set collection.
7007
7130
  * @param reduceLookaheads - Returns terminal keys for one completed production.
7131
+ * @returns A parse table with ACTION, GOTO, and any recorded conflicts.
7008
7132
  */
7009
7133
  static buildFromLr0ItemSets(algorithm, grammar, itemSets, reduceLookaheads) {
7010
7134
  const gotoTargets = TableBuilderBase.buildGotoTargets(grammar, itemSets.itemSets, (items) => itemSets.indexOf(items), TableBuilderBase.lr0GotoItems);
7011
7135
  const actions = new Map();
7012
7136
  const gotos = new Map();
7013
7137
  const conflicts = [];
7138
+ // Fill ACTION and GOTO rows for every LR(0) state.
7014
7139
  for (let state = 0; state < itemSets.itemSets.length; state += 1) {
7015
7140
  const itemSet = itemSets.itemSets[state] ?? [];
7016
7141
  const stateActions = new Map();
@@ -7018,6 +7143,7 @@ class LrTableBuilder {
7018
7143
  actions.set(state, stateActions);
7019
7144
  gotos.set(state, stateGotos);
7020
7145
  TableBuilderBase.fillShiftsAndGotos(grammar, state, itemSet, gotoTargets, stateActions, stateGotos, conflicts);
7146
+ // Record reduce and accept actions for every completed item in the state.
7021
7147
  for (const item of itemSet) {
7022
7148
  const production = grammar.production(item.productionId);
7023
7149
  if (production === null || item.dot !== production.rhs.length) {
@@ -7042,12 +7168,14 @@ class LrTableBuilder {
7042
7168
  * @param itemSets - LR(1) or merged LALR item set collection used for ACTION/GOTO rows.
7043
7169
  * @param lr1Collection - Canonical LR(1) collection used for LALR GOTO remapping.
7044
7170
  * @param gotoTargetsOverride - Optional precomputed GOTO indices for LALR.
7171
+ * @returns A parse table with ACTION, GOTO, and any recorded conflicts.
7045
7172
  */
7046
7173
  static buildFromLr1ItemSets(algorithm, grammar, analysis, itemSets, gotoTargetsOverride) {
7047
7174
  const gotoTargets = gotoTargetsOverride ?? TableBuilderBase.buildGotoTargets(grammar, itemSets.itemSets, (items) => itemSets.indexOf(items), (grammarArg, items, symbolKey) => lr1Goto(grammarArg, analysis, items, symbolKey));
7048
7175
  const actions = new Map();
7049
7176
  const gotos = new Map();
7050
7177
  const conflicts = [];
7178
+ // Fill ACTION and GOTO rows for every LR(1) or merged LALR state.
7051
7179
  for (let state = 0; state < itemSets.itemSets.length; state += 1) {
7052
7180
  const itemSet = itemSets.itemSets[state] ?? [];
7053
7181
  const stateActions = new Map();
@@ -7055,6 +7183,7 @@ class LrTableBuilder {
7055
7183
  actions.set(state, stateActions);
7056
7184
  gotos.set(state, stateGotos);
7057
7185
  TableBuilderBase.fillShiftsAndGotos(grammar, state, itemSet, gotoTargets, stateActions, stateGotos, conflicts);
7186
+ // Record reduce or accept for each completed item using its lookahead.
7058
7187
  for (const item of itemSet) {
7059
7188
  const production = grammar.production(item.productionId);
7060
7189
  if (production === null || item.dot !== production.rhs.length) {
@@ -7071,25 +7200,6 @@ class LrTableBuilder {
7071
7200
  }
7072
7201
  }
7073
7202
 
7074
- /**
7075
- * Thrown when LR table construction encounters unresolved conflicts.
7076
- */
7077
- class ParseTableBuildError extends ParserLrError {
7078
- algorithm;
7079
- conflicts;
7080
- /**
7081
- * Creates a parse table build error from detected conflicts.
7082
- *
7083
- * @param algorithm - LR algorithm that was attempted.
7084
- * @param conflicts - Readable conflict descriptions.
7085
- */
7086
- constructor(algorithm, conflicts) {
7087
- super(`LR table build failed for ${algorithm}: ${conflicts.join('; ')}`);
7088
- this.algorithm = algorithm;
7089
- this.conflicts = conflicts;
7090
- }
7091
- }
7092
-
7093
7203
  /**
7094
7204
  * Thrown when serialized parse table JSON cannot be loaded.
7095
7205
  */
@@ -7129,6 +7239,7 @@ class ParseTable {
7129
7239
  states;
7130
7240
  algorithm;
7131
7241
  parserStateCount;
7242
+ conflicts;
7132
7243
  actionByState;
7133
7244
  gotoByState;
7134
7245
  productionById;
@@ -7146,8 +7257,9 @@ class ParseTable {
7146
7257
  * @param productions - Flat BNF production metadata for reduce actions.
7147
7258
  * @param actions - ACTION entries keyed by state and terminal symbol.
7148
7259
  * @param gotos - GOTO entries keyed by state and non-terminal name.
7260
+ * @param conflicts - Shift/reduce and reduce/reduce conflicts resolved during construction.
7149
7261
  */
7150
- constructor(grammarName, startSymbol, tokens, tokenRules, skipRules, states, algorithm, parserStateCount = 0, productions = [], actions = new Map(), gotos = new Map()) {
7262
+ constructor(grammarName, startSymbol, tokens, tokenRules, skipRules, states, algorithm, parserStateCount = 0, productions = [], actions = new Map(), gotos = new Map(), conflicts = []) {
7151
7263
  this.grammarName = grammarName;
7152
7264
  this.startSymbol = startSymbol;
7153
7265
  this.tokens = tokens;
@@ -7156,6 +7268,7 @@ class ParseTable {
7156
7268
  this.states = states;
7157
7269
  this.algorithm = algorithm;
7158
7270
  this.parserStateCount = parserStateCount;
7271
+ this.conflicts = conflicts;
7159
7272
  this.actionByState = actions;
7160
7273
  this.gotoByState = gotos;
7161
7274
  const productionById = new Map();
@@ -7170,6 +7283,18 @@ class ParseTable {
7170
7283
  get hasParserTable() {
7171
7284
  return this.parserStateCount > 0 || this.actionByState.size > 0;
7172
7285
  }
7286
+ /**
7287
+ * Whether the table was built without conflicts.
7288
+ */
7289
+ get isConflictFree() {
7290
+ return this.conflicts.length === 0;
7291
+ }
7292
+ /**
7293
+ * Returns conflict warning lines for this table.
7294
+ */
7295
+ formatConflictWarnings() {
7296
+ return this.conflicts.map((conflict) => formatParseConflictWarning(conflict));
7297
+ }
7173
7298
  /**
7174
7299
  * Builds a parse table from a parsed grammar.
7175
7300
  *
@@ -7180,9 +7305,6 @@ class ParseTable {
7180
7305
  static fromGrammar(grammar, algorithm = 'lr1') {
7181
7306
  const bnf = desugarEbnf(grammar);
7182
7307
  const lrTable = buildLrTable(bnf, algorithm);
7183
- if (!lrTable.isConflictFree) {
7184
- throw new ParseTableBuildError(algorithm, formatLrConflicts(lrTable));
7185
- }
7186
7308
  return ParseTable.fromLrParseTable(grammar, lrTable);
7187
7309
  }
7188
7310
  /**
@@ -7298,7 +7420,7 @@ class ParseTable {
7298
7420
  * Creates a {@link ParseTable} from a built LR table and source grammar.
7299
7421
  *
7300
7422
  * @param grammar - Parsed `.grammar` file model.
7301
- * @param lrTable - Conflict-free LR parse table.
7423
+ * @param lrTable - Built LR parse table, including any resolved conflict warnings.
7302
7424
  */
7303
7425
  static fromLrParseTable(grammar, lrTable) {
7304
7426
  const productions = lrTable.productions.map((production) => ({
@@ -7308,7 +7430,7 @@ class ParseTable {
7308
7430
  variant: production.variant,
7309
7431
  origin: production.origin,
7310
7432
  }));
7311
- return new ParseTable(grammar.name, grammar.startSymbol, tokenInventory(grammar), [...grammar.tokenRules], [...grammar.skipRules], [...grammar.states], lrTable.algorithm, lrTable.stateCount, productions, lrTable.actions, lrTable.gotos);
7433
+ return new ParseTable(grammar.name, grammar.startSymbol, tokenInventory(grammar), [...grammar.tokenRules], [...grammar.skipRules], [...grammar.states], lrTable.algorithm, lrTable.stateCount, productions, lrTable.actions, lrTable.gotos, [...lrTable.conflicts]);
7312
7434
  }
7313
7435
  /**
7314
7436
  * Converts ACTION maps into JSON rows.
@@ -15535,6 +15657,15 @@ function productionSlots(production) {
15535
15657
  };
15536
15658
  });
15537
15659
  }
15660
+ /**
15661
+ * Returns the shared `$repeat` prefix for a numbered synthetic repeat symbol or reference.
15662
+ *
15663
+ * @param name - Repeat non-terminal or transform reference name.
15664
+ */
15665
+ function repeatSymbolPrefix(name) {
15666
+ const match = /^(.+\$repeat)_\d+$/.exec(name);
15667
+ return match?.[1] ?? null;
15668
+ }
15538
15669
  /**
15539
15670
  * Finds the child index for a transform reference name.
15540
15671
  *
@@ -15552,6 +15683,14 @@ function referenceSlotIndex(production, reference) {
15552
15683
  return slot.index;
15553
15684
  }
15554
15685
  }
15686
+ const repeatPrefix = repeatSymbolPrefix(reference);
15687
+ if (repeatPrefix !== null) {
15688
+ for (const slot of productionSlots(production)) {
15689
+ if (slot.binding === null && repeatSymbolPrefix(slot.symbol) === repeatPrefix) {
15690
+ return slot.index;
15691
+ }
15692
+ }
15693
+ }
15555
15694
  return null;
15556
15695
  }
15557
15696
 
@@ -15610,7 +15749,7 @@ class CstTransformer {
15610
15749
  ? this.transformNode(node.children[0])
15611
15750
  : null;
15612
15751
  }
15613
- const rule = this.schema.rule(node.symbol) ?? (node.origin === null ? null : this.schema.rule(node.origin));
15752
+ const rule = this.resolveTransformRule(node);
15614
15753
  if (rule === null) {
15615
15754
  return this.defaultTransform(node);
15616
15755
  }
@@ -15857,6 +15996,28 @@ class CstTransformer {
15857
15996
  }
15858
15997
  return null;
15859
15998
  }
15999
+ /**
16000
+ * Resolves a transform rule for a CST node, aliasing numbered repeat symbols to `$repeat_0`.
16001
+ *
16002
+ * @param node - CST node being transformed.
16003
+ */
16004
+ resolveTransformRule(node) {
16005
+ const candidates = [node.symbol];
16006
+ if (node.origin !== null && node.origin !== node.symbol) {
16007
+ candidates.push(node.origin);
16008
+ }
16009
+ const repeatPrefix = repeatSymbolPrefix(node.symbol);
16010
+ if (repeatPrefix !== null) {
16011
+ candidates.push(`${repeatPrefix}_0`);
16012
+ }
16013
+ for (const candidate of candidates) {
16014
+ const rule = this.schema.rule(candidate);
16015
+ if (rule !== null) {
16016
+ return rule;
16017
+ }
16018
+ }
16019
+ return null;
16020
+ }
15860
16021
  /**
15861
16022
  * Returns whether a CST node represents an epsilon production.
15862
16023
  *
@@ -16305,6 +16466,10 @@ function registerTableCommands(program) {
16305
16466
  algorithm: options.algorithm,
16306
16467
  });
16307
16468
  const json = context.table.toJsonString();
16469
+ // Write conflict warnings to stderr after table generation.
16470
+ for (const warning of context.table.formatConflictWarnings()) {
16471
+ process.stderr.write(`${warning}\n`);
16472
+ }
16308
16473
  // Write JSON to disk or stdout.
16309
16474
  if (options.output !== undefined) {
16310
16475
  await writeTextFile(options.output, json);