@scinorandex/sparse 0.0.7 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/generator.ts CHANGED
@@ -121,10 +121,14 @@ function computeFollowSets(
121
121
  return { success: true, value: { firstSets, followSets: ret } };
122
122
  }
123
123
 
124
- type Item = Production & { dot: number; lookahead: string[] };
124
+ const KEY_SEP = "\x00";
125
+ const FIELD_SEP = "\x01";
126
+
127
+ type Item = Production & { dot: number; lookahead: string[]; prodIndex: number; key: string };
125
128
  type State = { itemSet: Item[]; kernel: Item; count: number };
126
129
 
127
- export const generateStates = (productions: Production[]): Result<GeneratorResult> => {
130
+ export type GeneratorOptions = { mode?: "lr1" | "lalr1" };
131
+ export const generateStates = (productions: Production[], options: GeneratorOptions = {}): Result<GeneratorResult> => {
128
132
  const followSetsResult = computeFollowSets(productions);
129
133
  if (followSetsResult.success == false) return followSetsResult;
130
134
 
@@ -161,15 +165,45 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
161
165
  }
162
166
  };
163
167
 
168
+ const productionIndicesByLhs = new Map<string, number[]>();
169
+ for (let i = 0; i < productions.length; i++) {
170
+ const indexList = productionIndicesByLhs.get(productions[i].identifier);
171
+ if (indexList == undefined) productionIndicesByLhs.set(productions[i].identifier, [i]);
172
+ else indexList.push(i);
173
+ }
174
+
175
+ const rhsEntrySignature = (rhsToken: Production["rhs"][number]) =>
176
+ `${rhsToken.type}${FIELD_SEP}${rhsToken.identifier}${FIELD_SEP}${rhsToken.name ?? ""}${FIELD_SEP}${rhsToken.token.line}${FIELD_SEP}${rhsToken.token.column}`;
177
+
178
+ const reduceValue: number[] = [];
179
+ {
180
+ const lowestIndexBySignature = new Map<string, number>();
181
+ for (let i = 0; i < productions.length; i++) {
182
+ const production = productions[i];
183
+ const signature = `${production.identifier}${FIELD_SEP}${production.rhs.map(rhsEntrySignature).join(FIELD_SEP)}`;
184
+ const lowest = lowestIndexBySignature.get(signature);
185
+ if (lowest == undefined) lowestIndexBySignature.set(signature, i);
186
+ reduceValue[i] = lowestIndexBySignature.get(signature)!;
187
+ }
188
+ }
189
+
190
+ const itemKey = (prodIndex: number, dot: number, lookahead: string[]) =>
191
+ `${prodIndex}${KEY_SEP}${dot}${KEY_SEP}${lookahead.join(FIELD_SEP)}`;
192
+
193
+ const itemSetSignature = (itemSet: Item[]) => itemSet.map((item) => item.key).join(FIELD_SEP);
194
+
164
195
  // This fnuction creates the initial item set based on the
165
196
  // production provided. It creating the initial item and expands it
166
- function generateInitialItemSet(production: Production) {
197
+ function generateInitialItemSet(production: Production, prodIndex: number) {
198
+ const lookahead = [EOF_STRING];
167
199
  const initialItem = {
168
200
  lhs: production.lhs,
169
201
  identifier: production.identifier,
170
202
  rhs: production.rhs,
171
203
  dot: 0,
172
- lookahead: [EOF_STRING],
204
+ lookahead,
205
+ prodIndex,
206
+ key: itemKey(prodIndex, 0, lookahead),
173
207
  } as Item;
174
208
 
175
209
  return expandItemSet([initialItem]);
@@ -178,20 +212,19 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
178
212
  // This function expands the item sets provided to it
179
213
  function expandItemSet(items: Item[]): Result<{ itemSet: Item[]; kernel: Item }> {
180
214
  const itemSet: Item[] = [...items];
215
+ const knownItems = new Set(items.map((item) => item.key));
181
216
 
182
- // Create a queue of unprocessed items and keep shifting until there are no more items left
217
+ // Create a queue of unprocessed items and keep going until there are no more items left
183
218
  const unprocesssedItems: Item[] = [...items];
184
- while (unprocesssedItems.length > 0) {
185
- const currentItem = unprocesssedItems.shift()!;
219
+ let queuePosition = 0;
220
+ while (queuePosition < unprocesssedItems.length) {
221
+ const currentItem = unprocesssedItems[queuePosition++];
186
222
 
187
223
  // Check if the symbol after the dot is a non terminal
188
224
  const after = currentItem.rhs[currentItem.dot];
189
225
  if (after == null) continue; // TODO: check if this should be here
190
226
 
191
227
  if (after.type === "variable") {
192
- // Find prodctions whose left hand side is the symbol after the dot
193
- const newProductions = productions.filter((p) => p.identifier === after.identifier);
194
-
195
228
  // Compute the lookahead for the new productions to be added to the item set
196
229
  const rest = currentItem.rhs.slice(currentItem.dot + 1);
197
230
 
@@ -199,19 +232,22 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
199
232
  if (lookaheadResult.success === false) return lookaheadResult;
200
233
  const lookahead = lookaheadResult.value;
201
234
 
202
- for (const newProduction of newProductions) {
203
- // Create the new item and check if it already exists in the item set
204
- // If it doesn't exist, add it to the item set and the queue of unprocessed items
205
- const newItem = {
206
- lhs: newProduction.lhs,
207
- identifier: newProduction.identifier,
208
- rhs: newProduction.rhs,
209
- dot: 0,
210
- lookahead,
211
- } as Item;
212
- const encoding = JSON.stringify(newItem);
213
-
214
- if (itemSet.some((i) => JSON.stringify(i) === encoding) == false) {
235
+ const productionIndices = productionIndicesByLhs.get(after.identifier) ?? [];
236
+ for (const prodIndex of productionIndices) {
237
+ const newProduction = productions[prodIndex];
238
+ const key = itemKey(prodIndex, 0, lookahead);
239
+
240
+ if (knownItems.has(key) == false) {
241
+ knownItems.add(key);
242
+ const newItem = {
243
+ lhs: newProduction.lhs,
244
+ identifier: newProduction.identifier,
245
+ rhs: newProduction.rhs,
246
+ dot: 0,
247
+ lookahead,
248
+ prodIndex,
249
+ key,
250
+ } as Item;
215
251
  unprocesssedItems.push(newItem);
216
252
  itemSet.push(newItem);
217
253
  }
@@ -223,7 +259,7 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
223
259
  return { success: true, value: { itemSet, kernel: items[0] } };
224
260
  }
225
261
 
226
- const initialItemSetResult = generateInitialItemSet(productions[0]);
262
+ const initialItemSetResult = generateInitialItemSet(productions[0], 0);
227
263
  if (initialItemSetResult.success === false) return initialItemSetResult;
228
264
  const initialItemSet = initialItemSetResult.value;
229
265
 
@@ -242,9 +278,14 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
242
278
  // array of maps whose keys is a terminal is a value of either to shift or reduce
243
279
  const ActionTable = [] as Map<string, { action: "shift" | "reduce"; value: number }>[];
244
280
 
281
+ // map from an item set signature to the index of the state holding it
282
+ const stateIndexBySignature = new Map<string, number>();
283
+ stateIndexBySignature.set(itemSetSignature(initialState.itemSet), 0);
284
+
245
285
  // While there is an unprocessed state, visit it
246
- while (unprocessedStates.length > 0) {
247
- const currentState = unprocessedStates.shift()!;
286
+ let statePosition = 0;
287
+ while (statePosition < unprocessedStates.length) {
288
+ const currentState = unprocessedStates[statePosition++];
248
289
  const nextStates_Goto = new Map<string, Item[]>();
249
290
  const nextStates_Shift = new Map<string, Item[]>();
250
291
 
@@ -256,25 +297,21 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
256
297
 
257
298
  // no next symbol is available, therefore we need to create a reduction
258
299
  if (nextSymbol == null) {
259
- // Find the prodction that this item reduces to using the symbols on its right hand side
260
- // TODO: check if the find can fail, honestly this might be invariant
261
- const productionToReduceTo = productions
262
- .map((p, i) => [p, i] as const)
263
- .find(
264
- ([p, _]) => JSON.stringify(p.rhs) === JSON.stringify(item.rhs) && p.identifier === item.identifier,
265
- )![1];
266
-
267
300
  // for each lookahead in the item, create a new entry in the action table to reduce to the production found
268
301
  for (const lookahead of item.lookahead) {
269
302
  if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
270
- ActionTable[currentState.count]!.set(lookahead, { action: "reduce", value: productionToReduceTo });
303
+ ActionTable[currentState.count]!.set(lookahead, { action: "reduce", value: reduceValue[item.prodIndex] });
271
304
  }
272
305
 
273
306
  continue calculateNextItem;
274
307
  }
275
308
 
276
309
  // Create the next item by shifting the dot by 1 place
277
- const newItem: Item = { ...item, dot: item.dot + 1 };
310
+ const newItem: Item = {
311
+ ...item,
312
+ dot: item.dot + 1,
313
+ key: itemKey(item.prodIndex, item.dot + 1, item.lookahead),
314
+ };
278
315
 
279
316
  // Determine if we're going to create a GOTO or SHIFT based on the type of the symbol after the dot
280
317
  if (nextSymbol.type === "variable") {
@@ -300,19 +337,11 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
300
337
  const expandedItems = expandedItemsResult.value;
301
338
 
302
339
  // check if a state exist with the same item sets
303
- let existingState = states.find((s) => {
304
- return (
305
- s.kernel.lhs === expandedItems.kernel.lhs &&
306
- s.kernel.rhs.length === expandedItems.kernel.rhs.length &&
307
- JSON.stringify(s.kernel.rhs) === JSON.stringify(expandedItems.kernel.rhs) &&
308
- s.kernel.lookahead.length === expandedItems.kernel.lookahead.length &&
309
- JSON.stringify(s.kernel.lookahead) === JSON.stringify(expandedItems.kernel.lookahead) &&
310
- JSON.stringify(s.itemSet) === JSON.stringify(expandedItems.itemSet)
311
- );
312
- });
340
+ const signature = itemSetSignature(expandedItems.itemSet);
341
+ let existingStateIndex = stateIndexBySignature.get(signature);
313
342
 
314
343
  if (GotoTable[currentState.count] === undefined) GotoTable[currentState.count] = new Map();
315
- if (existingState == undefined) {
344
+ if (existingStateIndex == undefined) {
316
345
  const newState: State = {
317
346
  itemSet: expandedItems.itemSet,
318
347
  kernel: expandedItems.kernel,
@@ -320,10 +349,11 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
320
349
  };
321
350
  states.push(newState);
322
351
  unprocessedStates.push(newState);
323
- existingState = newState;
352
+ stateIndexBySignature.set(signature, newState.count);
353
+ existingStateIndex = newState.count;
324
354
  }
325
355
 
326
- GotoTable[currentState.count]!.set(gotoTransition, existingState.count);
356
+ GotoTable[currentState.count]!.set(gotoTransition, existingStateIndex);
327
357
  }
328
358
 
329
359
  // For each SHIFT transition from the current state, expand the item set that it points to
@@ -336,22 +366,14 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
336
366
  const expandedItems = expandedItemsResult.value;
337
367
 
338
368
  // check if a state exist with the same item sets
339
- let existingState = states.find((s) => {
340
- return (
341
- s.kernel.lhs === expandedItems.kernel.lhs &&
342
- s.kernel.rhs.length === expandedItems.kernel.rhs.length &&
343
- JSON.stringify(s.kernel.rhs) === JSON.stringify(expandedItems.kernel.rhs) &&
344
- s.kernel.lookahead.length === expandedItems.kernel.lookahead.length &&
345
- JSON.stringify(s.kernel.lookahead) === JSON.stringify(expandedItems.kernel.lookahead) &&
346
- JSON.stringify(s.itemSet) === JSON.stringify(expandedItems.itemSet)
347
- );
348
- });
369
+ const signature = itemSetSignature(expandedItems.itemSet);
370
+ let existingStateIndex = stateIndexBySignature.get(signature);
349
371
 
350
372
  // If there is no entry in the ActionTable for the current state, create one
351
373
  if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
352
374
 
353
375
  // If no such state exists, create a new state and add it to the list of states
354
- if (existingState == undefined) {
376
+ if (existingStateIndex == undefined) {
355
377
  const newState: State = {
356
378
  itemSet: expandedItems.itemSet,
357
379
  kernel: expandedItems.kernel,
@@ -359,10 +381,11 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
359
381
  };
360
382
  states.push(newState);
361
383
  unprocessedStates.push(newState);
362
- existingState = newState;
384
+ stateIndexBySignature.set(signature, newState.count);
385
+ existingStateIndex = newState.count;
363
386
  }
364
387
 
365
- ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value: existingState.count });
388
+ ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value: existingStateIndex });
366
389
  }
367
390
  }
368
391
 
@@ -373,9 +396,115 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
373
396
  const StatesResult = generateStates(initialItemSet);
374
397
  if (StatesResult.success === false) return StatesResult;
375
398
  const { states, ActionTable, GotoTable } = StatesResult.value;
399
+
400
+ if (options.mode === "lalr1") {
401
+ const merged = mergeStatesForLALR(states, ActionTable, GotoTable);
402
+ if (merged.success === false) return merged;
403
+ return {
404
+ success: true,
405
+ value: new GeneratorResult(merged.value.states, merged.value.ActionTable, merged.value.GotoTable),
406
+ };
407
+ }
408
+
376
409
  return { success: true, value: new GeneratorResult(states, ActionTable, GotoTable) };
377
410
  };
378
411
 
412
+ // Merges the LR(1) states into LALR(1) states by grouping states that have the same
413
+ // "same-core" item set (items compared without their lookahead sets) and unioning
414
+ // their item sets, action tables and goto tables. Returns an error if merging any
415
+ // group produces a shift/reduce, reduce/reduce, shift or goto conflict, which
416
+ // means the grammar is not LALR(1).
417
+ function mergeStatesForLALR(
418
+ states: State[],
419
+ ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[],
420
+ GotoTable: Map<string, number>[],
421
+ ): Result<{
422
+ states: State[];
423
+ ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[];
424
+ GotoTable: Map<string, number>[];
425
+ }> {
426
+ type Action = { action: "shift" | "reduce"; value: number };
427
+
428
+ const itemSkeleton = (item: Item) => `${item.prodIndex}${KEY_SEP}${item.dot}`;
429
+ const stateSkeleton = (state: State) => state.itemSet.map(itemSkeleton).sort().join(FIELD_SEP);
430
+
431
+ // Group states by same-core signature, assigning merged indices in first-occurrence order
432
+ const groupIndices: number[][] = [];
433
+ const groupBySkeleton = new Map<string, number>();
434
+ const oldToNew = new Array<number>(states.length);
435
+ for (let i = 0; i < states.length; i++) {
436
+ const skeleton = stateSkeleton(states[i]);
437
+ let group = groupBySkeleton.get(skeleton);
438
+ if (group == undefined) {
439
+ group = groupIndices.length;
440
+ groupBySkeleton.set(skeleton, group);
441
+ groupIndices.push([]);
442
+ }
443
+ groupIndices[group].push(i);
444
+ oldToNew[i] = group;
445
+ }
446
+
447
+ const describeAction = (a: Action) =>
448
+ a.action === "reduce" ? `reduce by production ${a.value}` : `shift to state ${a.value}`;
449
+ const conflictToken = (members: number[]) => states[members[0]].kernel.lhs;
450
+ const conflict = (g: number, members: number[], reason: string): Result<never> => ({
451
+ success: false,
452
+ reason: `LALR(1) conflict in state ${g} on ${reason} between LR(1) states ${members.join(", ")}: the grammar is not LALR(1)`,
453
+ token: conflictToken(members),
454
+ });
455
+
456
+ const newStates: State[] = [];
457
+ const newActionTable: Map<string, Action>[] = [];
458
+ const newGotoTable: Map<string, number>[] = [];
459
+
460
+ for (let g = 0; g < groupIndices.length; g++) {
461
+ const members = groupIndices[g];
462
+ const actions = new Map<string, Action>();
463
+ const gotos = new Map<string, number>();
464
+
465
+ for (const member of members) {
466
+ for (const [key, entry] of ActionTable[member].entries()) {
467
+ const value: Action = {
468
+ action: entry.action,
469
+ value: entry.action === "shift" ? oldToNew[entry.value] : entry.value,
470
+ };
471
+ const existing = actions.get(key);
472
+ if (existing == undefined) actions.set(key, value);
473
+ else if (existing.action !== value.action || existing.value !== value.value)
474
+ return conflict(g, members, `"${key}": ${describeAction(existing)} conflicts with ${describeAction(value)}`);
475
+ }
476
+
477
+ if (GotoTable[member] !== undefined)
478
+ for (const [key, target] of GotoTable[member]!.entries()) {
479
+ const value = oldToNew[target];
480
+ const existing = gotos.get(key);
481
+ if (existing == undefined) gotos.set(key, value);
482
+ else if (existing !== value)
483
+ return conflict(g, members, `"${key}": goto to state ${existing} conflicts with goto to state ${value}`);
484
+ }
485
+ }
486
+
487
+ // Union the item sets of all members, merging the lookahead sets of
488
+ // items that differ only in their lookahead
489
+ const itemsBySkeleton = new Map<string, Item>();
490
+ for (const member of members)
491
+ for (const item of states[member].itemSet) {
492
+ const skeleton = itemSkeleton(item);
493
+ const existing = itemsBySkeleton.get(skeleton);
494
+ if (existing == undefined) itemsBySkeleton.set(skeleton, { ...item, lookahead: [...item.lookahead] });
495
+ else
496
+ for (const lookahead of item.lookahead)
497
+ if (!existing.lookahead.includes(lookahead)) existing.lookahead.push(lookahead);
498
+ }
499
+
500
+ newStates.push({ itemSet: [...itemsBySkeleton.values()], kernel: states[members[0]].kernel, count: g });
501
+ newActionTable.push(actions);
502
+ newGotoTable.push(gotos);
503
+ }
504
+
505
+ return { success: true, value: { states: newStates, ActionTable: newActionTable, GotoTable: newGotoTable } };
506
+ }
507
+
379
508
  export class GeneratorResult {
380
509
  constructor(
381
510
  public readonly states: State[],
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { GeneratorResult, generateStates } from "./generator";
2
+ export type { GeneratorOptions } from "./generator";
2
3
  export { buildProductions, tryBuildProductions } from "./meta/selfhosted";
3
4
  export { Result } from "./utils/Result";
4
5
  export {
@@ -60,6 +60,8 @@ export enum GrammarTokenType {
60
60
  R_PAREN,
61
61
  PIPE,
62
62
  QUESTION_MARK,
63
+ STAR,
64
+ PLUS,
63
65
  COLON,
64
66
  SEMICOLON,
65
67
  NUMBER,
@@ -38,6 +38,8 @@ grammarLexerGenerator.addRule("pipe", "$|", GrammarTokenType.PIPE);
38
38
  grammarLexerGenerator.addRule("equals", "$=", GrammarTokenType.EQUALS);
39
39
  grammarLexerGenerator.addRule("comma", "$,", GrammarTokenType.COMMA);
40
40
  grammarLexerGenerator.addRule("question_mark", "$?", GrammarTokenType.QUESTION_MARK);
41
+ grammarLexerGenerator.addRule("star", "$*", GrammarTokenType.STAR);
42
+ grammarLexerGenerator.addRule("plus", "$+", GrammarTokenType.PLUS);
41
43
  grammarLexerGenerator.addRule("number", "(${digit})+", GrammarTokenType.NUMBER);
42
44
 
43
45
  grammarLexerGenerator.addRule("single_line_comment", "$/$/(($\n)!)*", GrammarTokenType.SINGLE_LINE_COMMENT);
@@ -74,11 +76,16 @@ class ProductionNode extends BaseNode {
74
76
  public readonly lhs: GrammarToken,
75
77
  public readonly rhs: ListNode<TokenNode | GroupedTokenNode>,
76
78
  public originalProductionIndex: number,
79
+ public readonly identifier: string,
77
80
  public readonly name?: string | undefined,
78
81
  ) {
79
82
  super();
80
83
  }
81
84
 
85
+ fork(rhs: ListNode<TokenNode | GroupedTokenNode>) {
86
+ return new ProductionNode(this.lhs, rhs, this.originalProductionIndex, this.identifier, this.name);
87
+ }
88
+
82
89
  toStruct(): Production {
83
90
  if (this.originalProductionIndex === -1) throw new Error("ProductionNode has no originalProductionIndex");
84
91
 
@@ -87,7 +94,7 @@ class ProductionNode extends BaseNode {
87
94
 
88
95
  return {
89
96
  lhs: this.lhs,
90
- identifier: `<${this.lhs.lexeme}>`,
97
+ identifier: `<${this.identifier}>`,
91
98
  name: this.name ?? null,
92
99
  originalProductionIndex: this.originalProductionIndex,
93
100
  rhs: this.rhs.getItemsReversed().map((node) => (node as TokenNode).toStruct()),
@@ -99,7 +106,21 @@ class ProductionNode extends BaseNode {
99
106
  }
100
107
  }
101
108
 
102
- function expandProduction(production: ProductionNode): ProductionNode[] {
109
+ class ExpanderContext {
110
+ current = 0;
111
+
112
+ constructor(public currentProductionIndex: number) {}
113
+
114
+ getNextProductionIndex() {
115
+ return this.currentProductionIndex++;
116
+ }
117
+
118
+ getAutogeneratedId() {
119
+ return `autogen-${this.current++}`;
120
+ }
121
+ }
122
+
123
+ function expandProduction(production: ProductionNode, ctx: ExpanderContext): ProductionNode[] {
103
124
  const rhs = production.rhs.getItemsReversed();
104
125
 
105
126
  for (let i = 0; i < rhs.length; i++) {
@@ -109,35 +130,53 @@ function expandProduction(production: ProductionNode): ProductionNode[] {
109
130
  const afterItems = rhs.slice(i + 1);
110
131
 
111
132
  if (item instanceof GroupedTokenNode) {
112
- // need to create two new productions, one optional and the other not
113
- // then apply unrolling on both
114
-
115
- const withoutCurrentItem = new ProductionNode(
116
- production.lhs,
117
- new ListNode<TokenNode | GroupedTokenNode>([...beforeItems, ...afterItems].toReversed()),
118
- production.originalProductionIndex,
119
- production.name,
120
- );
121
-
122
- const withCurrentItem = new ProductionNode(
123
- production.lhs,
124
- new ListNode<TokenNode | GroupedTokenNode>(
125
- [...beforeItems, ...item.inside.getItemsReversed(), ...afterItems].toReversed(),
126
- ),
127
- production.originalProductionIndex,
128
- production.name,
129
- );
130
-
131
- const newProductions: ProductionNode[] = [withoutCurrentItem, withCurrentItem];
132
- return newProductions.flatMap((prod) => expandProduction(prod));
133
+ // The unrolling rules here depend on the modifier in the GroupedTokenNode
134
+
135
+ if (item.modifier === GrammarTokenType.QUESTION_MARK) {
136
+ const withoutCurrentItem = production.fork(
137
+ new ListNode<TokenNode | GroupedTokenNode>([...beforeItems, ...afterItems].toReversed()),
138
+ );
139
+
140
+ const withCurrentItem = production.fork(
141
+ new ListNode<TokenNode | GroupedTokenNode>(
142
+ [...beforeItems, ...item.inside.getItemsReversed(), ...afterItems].toReversed(),
143
+ ),
144
+ );
145
+
146
+ const newProductions: ProductionNode[] = [withoutCurrentItem, withCurrentItem];
147
+ return newProductions.flatMap((prod) => expandProduction(prod, ctx));
148
+ } else if (item.modifier === GrammarTokenType.STAR || item.modifier === GrammarTokenType.PLUS) {
149
+ // need to handle kleene-star and kleene-plus, which create entirely unique new productions
150
+ const inside = item.inside.getItemsReversed();
151
+ const newProductionIdentifier = ctx.getAutogeneratedId();
152
+
153
+ // create a dummy production that contains the contents of inside
154
+ const raw = new StubTokenNode("variable", new ListNode([production.lhs]), "rest", newProductionIdentifier);
155
+ const pump = new GroupedTokenNode(new ListNode([raw]), GrammarTokenType.QUESTION_MARK);
156
+
157
+ const stub = new ProductionNode(
158
+ production.lhs,
159
+ new ListNode<TokenNode | GroupedTokenNode>([...inside, pump].toReversed()),
160
+ ctx.getNextProductionIndex(),
161
+ newProductionIdentifier,
162
+ "autogenerated-kleene", // the user needs to implement a reducer named "autogenerated-kleene"
163
+ );
164
+
165
+ const currentReplacement = production.fork(
166
+ new ListNode(
167
+ [...beforeItems, item.modifier === GrammarTokenType.STAR ? pump : raw, ...afterItems].toReversed(),
168
+ ),
169
+ );
170
+
171
+ return [currentReplacement, stub].flatMap((prod) => expandProduction(prod, ctx));
172
+ } else throw new Error(`Unknown modifier ${GrammarTokenType[item.modifier]}`);
133
173
  } else if (item instanceof TokenNode) {
134
174
  if (item.variables.getItems().length > 1) {
135
175
  // this token node has many variants and we should unroll it
136
176
  const variables = item.variables.getItemsReversed();
137
177
 
138
178
  const newProductions = variables.map((variable) => {
139
- return new ProductionNode(
140
- production.lhs,
179
+ return production.fork(
141
180
  new ListNode<TokenNode | GroupedTokenNode>(
142
181
  [
143
182
  ...beforeItems,
@@ -145,12 +184,10 @@ function expandProduction(production: ProductionNode): ProductionNode[] {
145
184
  ...afterItems,
146
185
  ].toReversed(),
147
186
  ),
148
- production.originalProductionIndex,
149
- production.name,
150
187
  );
151
188
  });
152
189
 
153
- return newProductions.flatMap((prod) => expandProduction(prod));
190
+ return newProductions.flatMap((prod) => expandProduction(prod, ctx));
154
191
  }
155
192
  } else {
156
193
  throw new Error("shoudn't reach here, ProductionNode RHS is neither GroupedTokenNode nor TokenNode");
@@ -170,11 +207,17 @@ class TokenNode extends BaseNode {
170
207
  super();
171
208
  }
172
209
 
210
+ public getIdentifier() {
211
+ const items = this.variables.getItems();
212
+ if (items.length > 1) throw new Error("Not yet unrolled");
213
+ return items[0].lexeme;
214
+ }
215
+
173
216
  toStruct() {
174
217
  const items = this.variables.getItems();
175
218
  if (items.length > 1) throw new Error("Not yet unrolled");
176
219
 
177
- const lexeme = items[0].lexeme;
220
+ const lexeme = this.getIdentifier();
178
221
  const identifier = this.type === "variable" ? `<${lexeme}>` : `[${lexeme}]`;
179
222
  const name = this.name ?? null;
180
223
 
@@ -182,8 +225,26 @@ class TokenNode extends BaseNode {
182
225
  }
183
226
  }
184
227
 
228
+ class StubTokenNode extends TokenNode {
229
+ constructor(
230
+ public readonly type: "terminal" | "variable",
231
+ public readonly variables: ListNode<GrammarToken>,
232
+ public readonly name: string | undefined,
233
+ public readonly identifier: string,
234
+ ) {
235
+ super(type, variables, name);
236
+ }
237
+
238
+ public getIdentifier() {
239
+ return this.identifier;
240
+ }
241
+ }
242
+
185
243
  class GroupedTokenNode extends BaseNode {
186
- constructor(public readonly inside: ListNode<TokenNode>) {
244
+ constructor(
245
+ public readonly inside: ListNode<TokenNode>,
246
+ public readonly modifier: GrammarTokenType,
247
+ ) {
187
248
  super();
188
249
  }
189
250
  }
@@ -195,8 +256,10 @@ class ProgramNode extends BaseNode {
195
256
 
196
257
  unrollProductions() {
197
258
  const productions = this.productions.getItemsReversed();
259
+ const expanderContext = new ExpanderContext(this.productions.getItems().length);
260
+
198
261
  for (let i = 0; i < productions.length; i++) productions[i].setOriginalProductionIndex(i);
199
- return productions.flatMap((node) => expandProduction(node));
262
+ return productions.flatMap((node) => expandProduction(node, expanderContext));
200
263
  }
201
264
 
202
265
  getProductions() {
@@ -226,7 +289,7 @@ const reducers: { [key: string]: Reducer } = {
226
289
  production_name: GrammarToken;
227
290
  tokens: ListNode<TokenNode | GroupedTokenNode>;
228
291
  uuid?: GrammarToken;
229
- }) => new ProductionNode(bag.production_name, bag.tokens, -1, bag.uuid?.lexeme),
292
+ }) => new ProductionNode(bag.production_name, bag.tokens, -1, bag.production_name.lexeme, bag.uuid?.lexeme),
230
293
 
231
294
  tokens: (bag: { token: TokenNode | GroupedTokenNode; rest?: ListNode<TokenNode | GroupedTokenNode> }) => {
232
295
  if (bag.rest == null) return new ListNode<TokenNode | GroupedTokenNode>([bag.token]);
@@ -234,7 +297,8 @@ const reducers: { [key: string]: Reducer } = {
234
297
  },
235
298
 
236
299
  token: (bag: { token: TokenNode }) => bag.token,
237
- grouped_token: (bag: { tokens: ListNode<TokenNode> }) => new GroupedTokenNode(bag.tokens),
300
+ grouped_token: (bag: { tokens: ListNode<TokenNode>; modifier: GrammarToken }) =>
301
+ new GroupedTokenNode(bag.tokens, bag.modifier.type),
238
302
 
239
303
  variable: (bag: { inside: ListNode<GrammarToken>; token_name?: GrammarToken }) =>
240
304
  new TokenNode("variable", bag.inside, bag.token_name?.lexeme),