@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/AGENTS.md +40 -0
- package/README.md +6 -5
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +6 -1
- package/dist/generator.js +151 -46
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/meta/common.d.ts +12 -10
- package/dist/meta/common.js +12 -10
- package/dist/meta/common.js.map +1 -1
- package/dist/meta/selfhosted.js +68 -17
- package/dist/meta/selfhosted.js.map +1 -1
- package/dist/meta/states.d.ts +108 -0
- package/dist/meta/states.js +213 -47
- package/dist/meta/states.js.map +1 -1
- package/dist/parser.d.ts +2 -0
- package/dist/parser.js +1 -1
- package/dist/parser.js.map +1 -1
- package/example/LoLang/example.ts +27 -4
- package/example/LoLang/table2.txt +497 -0
- package/example/LoLang/tablelalr.txt +381 -0
- package/example/kleene-test/example.ts +86 -0
- package/example/kleene-test/grammar.txt +2 -0
- package/example/selfhosted/01-meta-grammar.txt +1 -1
- package/example/selfhosted/codegen-meta.ts +213 -47
- package/opencode.json +18 -0
- package/package.json +9 -3
- package/src/cli.ts +2 -2
- package/src/generator.ts +191 -62
- package/src/index.ts +1 -0
- package/src/meta/common.ts +2 -0
- package/src/meta/selfhosted.ts +97 -33
- package/src/meta/states.ts +213 -47
- package/src/parser.ts +3 -1
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
|
-
|
|
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
|
|
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
|
|
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
|
|
217
|
+
// Create a queue of unprocessed items and keep going until there are no more items left
|
|
183
218
|
const unprocesssedItems: Item[] = [...items];
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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:
|
|
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 = {
|
|
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
|
-
|
|
304
|
-
|
|
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 (
|
|
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
|
-
|
|
352
|
+
stateIndexBySignature.set(signature, newState.count);
|
|
353
|
+
existingStateIndex = newState.count;
|
|
324
354
|
}
|
|
325
355
|
|
|
326
|
-
GotoTable[currentState.count]!.set(gotoTransition,
|
|
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
|
-
|
|
340
|
-
|
|
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 (
|
|
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
|
-
|
|
384
|
+
stateIndexBySignature.set(signature, newState.count);
|
|
385
|
+
existingStateIndex = newState.count;
|
|
363
386
|
}
|
|
364
387
|
|
|
365
|
-
ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value:
|
|
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
package/src/meta/common.ts
CHANGED
package/src/meta/selfhosted.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
production.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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 =
|
|
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(
|
|
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
|
|
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),
|