@scinorandex/sparse 0.0.8 → 0.1.2
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/.github/workflows/ci.yml +41 -0
- package/AGENTS.md +60 -0
- package/README.md +286 -30
- package/dist/cli.js +113 -24
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +9 -2
- package/dist/generator.js +179 -50
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +34 -1
- package/dist/index.js.map +1 -1
- package/dist/meta/common.d.ts +17 -0
- package/dist/meta/common.js +66 -1
- package/dist/meta/common.js.map +1 -1
- package/dist/meta/selfhosted.d.ts +4 -5
- package/dist/meta/selfhosted.js +51 -38
- package/dist/meta/selfhosted.js.map +1 -1
- package/dist/parser.d.ts +56 -33
- package/dist/parser.js +146 -18
- package/dist/parser.js.map +1 -1
- package/dist/table/selfhosted.d.ts +10 -1
- package/dist/table/selfhosted.js +32 -12
- package/dist/table/selfhosted.js.map +1 -1
- package/dist/table/validate.d.ts +17 -0
- package/dist/table/validate.js +84 -0
- package/dist/table/validate.js.map +1 -0
- package/dist/utils/Stack.d.ts +4 -0
- package/dist/utils/Stack.js +16 -1
- package/dist/utils/Stack.js.map +1 -1
- package/dist/utils/errorWindowBuilder.js +11 -11
- package/dist/utils/errorWindowBuilder.js.map +1 -1
- package/dist/utils/loadFiles.d.ts +8 -0
- package/dist/utils/loadFiles.js +47 -0
- package/dist/utils/loadFiles.js.map +1 -0
- package/dist/utils/reducers.d.ts +9 -0
- package/dist/utils/reducers.js +56 -0
- package/dist/utils/reducers.js.map +1 -0
- package/example/LoLang/example.ts +42 -18
- package/example/LoLang/tablelalr.txt +381 -0
- package/example/kleene-test/example.ts +35 -17
- package/example/math/example.ts +12 -7
- package/example/selfhosted/example.ts +30 -24
- package/opencode.json +18 -0
- package/package.json +9 -3
- package/src/cli.ts +132 -23
- package/src/generator.ts +237 -67
- package/src/index.ts +56 -3
- package/src/meta/common.ts +116 -0
- package/src/meta/selfhosted.ts +42 -11
- package/src/parser.ts +259 -47
- package/src/table/selfhosted.ts +48 -12
- package/src/table/validate.ts +143 -0
- package/src/utils/Stack.ts +22 -2
- package/src/utils/errorWindowBuilder.ts +15 -13
- package/src/utils/loadFiles.ts +54 -0
- package/src/utils/reducers.ts +93 -0
- package/test/cli.test.ts +144 -0
- package/test/codegen.test.ts +75 -0
- package/test/grammar.test.ts +243 -0
- package/test/helpers.ts +59 -0
- package/test/lalr.test.ts +114 -0
- package/test/parser.test.ts +392 -0
- package/test/table.test.ts +144 -0
- package/tsconfig.json +1 -1
- package/example/complicated/grammar.txt +0 -37
package/src/generator.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { TableState } from "./parser";
|
|
2
|
-
import { GrammarToken, Production } from "./meta/common";
|
|
2
|
+
import { GrammarToken, Production, ProductionWarning, validateProductions } from "./meta/common";
|
|
3
3
|
import { Result } from "./utils/Result";
|
|
4
4
|
|
|
5
5
|
const xContainsAllOfY = <T>(xs: Set<T>, ys: Set<T>) => [...ys].every((x) => xs.has(x));
|
|
@@ -20,6 +20,13 @@ function computeFirstSets(allProductions: Production[]): Result<Map<string, Set<
|
|
|
20
20
|
const toBeModified = ret.get(production.identifier)!;
|
|
21
21
|
|
|
22
22
|
const firstRhsToken = production.rhs[0];
|
|
23
|
+
if (firstRhsToken == null)
|
|
24
|
+
return {
|
|
25
|
+
success: false,
|
|
26
|
+
reason: `Production "${production.identifier}" has an empty right hand side, which Sparse does not support`,
|
|
27
|
+
token: production.lhs,
|
|
28
|
+
};
|
|
29
|
+
|
|
23
30
|
if (firstRhsToken.type === "variable") {
|
|
24
31
|
const add = ret.get(firstRhsToken.identifier);
|
|
25
32
|
|
|
@@ -121,10 +128,26 @@ function computeFollowSets(
|
|
|
121
128
|
return { success: true, value: { firstSets, followSets: ret } };
|
|
122
129
|
}
|
|
123
130
|
|
|
124
|
-
|
|
131
|
+
const KEY_SEP = "\x00";
|
|
132
|
+
const FIELD_SEP = "\x01";
|
|
133
|
+
|
|
134
|
+
type Item = Production & { dot: number; lookahead: string[]; prodIndex: number; key: string };
|
|
125
135
|
type State = { itemSet: Item[]; kernel: Item; count: number };
|
|
126
136
|
|
|
127
|
-
export
|
|
137
|
+
export type GeneratorOptions = {
|
|
138
|
+
mode?: "lr1" | "lalr1";
|
|
139
|
+
onWarning?: (warning: ProductionWarning) => void;
|
|
140
|
+
/** Called every time a new state is discovered, useful to show progress on big grammars. */
|
|
141
|
+
onProgress?: (statesGenerated: number) => void;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export const generateStates = (
|
|
145
|
+
productions: Production[],
|
|
146
|
+
options: GeneratorOptions = {},
|
|
147
|
+
): Result<GeneratorResult> => {
|
|
148
|
+
const validationResult = validateProductions(productions, { onWarning: options.onWarning });
|
|
149
|
+
if (validationResult.success === false) return validationResult;
|
|
150
|
+
|
|
128
151
|
const followSetsResult = computeFollowSets(productions);
|
|
129
152
|
if (followSetsResult.success == false) return followSetsResult;
|
|
130
153
|
|
|
@@ -161,15 +184,45 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
161
184
|
}
|
|
162
185
|
};
|
|
163
186
|
|
|
187
|
+
const productionIndicesByLhs = new Map<string, number[]>();
|
|
188
|
+
for (let i = 0; i < productions.length; i++) {
|
|
189
|
+
const indexList = productionIndicesByLhs.get(productions[i].identifier);
|
|
190
|
+
if (indexList == undefined) productionIndicesByLhs.set(productions[i].identifier, [i]);
|
|
191
|
+
else indexList.push(i);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rhsEntrySignature = (rhsToken: Production["rhs"][number]) =>
|
|
195
|
+
`${rhsToken.type}${FIELD_SEP}${rhsToken.identifier}${FIELD_SEP}${rhsToken.name ?? ""}${FIELD_SEP}${rhsToken.token.line}${FIELD_SEP}${rhsToken.token.column}`;
|
|
196
|
+
|
|
197
|
+
const reduceValue: number[] = [];
|
|
198
|
+
{
|
|
199
|
+
const lowestIndexBySignature = new Map<string, number>();
|
|
200
|
+
for (let i = 0; i < productions.length; i++) {
|
|
201
|
+
const production = productions[i];
|
|
202
|
+
const signature = `${production.identifier}${FIELD_SEP}${production.rhs.map(rhsEntrySignature).join(FIELD_SEP)}`;
|
|
203
|
+
const lowest = lowestIndexBySignature.get(signature);
|
|
204
|
+
if (lowest == undefined) lowestIndexBySignature.set(signature, i);
|
|
205
|
+
reduceValue[i] = lowestIndexBySignature.get(signature)!;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const itemKey = (prodIndex: number, dot: number, lookahead: string[]) =>
|
|
210
|
+
`${prodIndex}${KEY_SEP}${dot}${KEY_SEP}${lookahead.join(FIELD_SEP)}`;
|
|
211
|
+
|
|
212
|
+
const itemSetSignature = (itemSet: Item[]) => itemSet.map((item) => item.key).join(FIELD_SEP);
|
|
213
|
+
|
|
164
214
|
// This fnuction creates the initial item set based on the
|
|
165
215
|
// production provided. It creating the initial item and expands it
|
|
166
|
-
function generateInitialItemSet(production: Production) {
|
|
216
|
+
function generateInitialItemSet(production: Production, prodIndex: number) {
|
|
217
|
+
const lookahead = [EOF_STRING];
|
|
167
218
|
const initialItem = {
|
|
168
219
|
lhs: production.lhs,
|
|
169
220
|
identifier: production.identifier,
|
|
170
221
|
rhs: production.rhs,
|
|
171
222
|
dot: 0,
|
|
172
|
-
lookahead
|
|
223
|
+
lookahead,
|
|
224
|
+
prodIndex,
|
|
225
|
+
key: itemKey(prodIndex, 0, lookahead),
|
|
173
226
|
} as Item;
|
|
174
227
|
|
|
175
228
|
return expandItemSet([initialItem]);
|
|
@@ -178,20 +231,19 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
178
231
|
// This function expands the item sets provided to it
|
|
179
232
|
function expandItemSet(items: Item[]): Result<{ itemSet: Item[]; kernel: Item }> {
|
|
180
233
|
const itemSet: Item[] = [...items];
|
|
234
|
+
const knownItems = new Set(items.map((item) => item.key));
|
|
181
235
|
|
|
182
|
-
// Create a queue of unprocessed items and keep
|
|
236
|
+
// Create a queue of unprocessed items and keep going until there are no more items left
|
|
183
237
|
const unprocesssedItems: Item[] = [...items];
|
|
184
|
-
|
|
185
|
-
|
|
238
|
+
let queuePosition = 0;
|
|
239
|
+
while (queuePosition < unprocesssedItems.length) {
|
|
240
|
+
const currentItem = unprocesssedItems[queuePosition++];
|
|
186
241
|
|
|
187
242
|
// Check if the symbol after the dot is a non terminal
|
|
188
243
|
const after = currentItem.rhs[currentItem.dot];
|
|
189
244
|
if (after == null) continue; // TODO: check if this should be here
|
|
190
245
|
|
|
191
246
|
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
247
|
// Compute the lookahead for the new productions to be added to the item set
|
|
196
248
|
const rest = currentItem.rhs.slice(currentItem.dot + 1);
|
|
197
249
|
|
|
@@ -199,19 +251,22 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
199
251
|
if (lookaheadResult.success === false) return lookaheadResult;
|
|
200
252
|
const lookahead = lookaheadResult.value;
|
|
201
253
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
254
|
+
const productionIndices = productionIndicesByLhs.get(after.identifier) ?? [];
|
|
255
|
+
for (const prodIndex of productionIndices) {
|
|
256
|
+
const newProduction = productions[prodIndex];
|
|
257
|
+
const key = itemKey(prodIndex, 0, lookahead);
|
|
258
|
+
|
|
259
|
+
if (knownItems.has(key) == false) {
|
|
260
|
+
knownItems.add(key);
|
|
261
|
+
const newItem = {
|
|
262
|
+
lhs: newProduction.lhs,
|
|
263
|
+
identifier: newProduction.identifier,
|
|
264
|
+
rhs: newProduction.rhs,
|
|
265
|
+
dot: 0,
|
|
266
|
+
lookahead,
|
|
267
|
+
prodIndex,
|
|
268
|
+
key,
|
|
269
|
+
} as Item;
|
|
215
270
|
unprocesssedItems.push(newItem);
|
|
216
271
|
itemSet.push(newItem);
|
|
217
272
|
}
|
|
@@ -223,7 +278,7 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
223
278
|
return { success: true, value: { itemSet, kernel: items[0] } };
|
|
224
279
|
}
|
|
225
280
|
|
|
226
|
-
const initialItemSetResult = generateInitialItemSet(productions[0]);
|
|
281
|
+
const initialItemSetResult = generateInitialItemSet(productions[0], 0);
|
|
227
282
|
if (initialItemSetResult.success === false) return initialItemSetResult;
|
|
228
283
|
const initialItemSet = initialItemSetResult.value;
|
|
229
284
|
|
|
@@ -242,9 +297,14 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
242
297
|
// array of maps whose keys is a terminal is a value of either to shift or reduce
|
|
243
298
|
const ActionTable = [] as Map<string, { action: "shift" | "reduce"; value: number }>[];
|
|
244
299
|
|
|
300
|
+
// map from an item set signature to the index of the state holding it
|
|
301
|
+
const stateIndexBySignature = new Map<string, number>();
|
|
302
|
+
stateIndexBySignature.set(itemSetSignature(initialState.itemSet), 0);
|
|
303
|
+
|
|
245
304
|
// While there is an unprocessed state, visit it
|
|
246
|
-
|
|
247
|
-
|
|
305
|
+
let statePosition = 0;
|
|
306
|
+
while (statePosition < unprocessedStates.length) {
|
|
307
|
+
const currentState = unprocessedStates[statePosition++];
|
|
248
308
|
const nextStates_Goto = new Map<string, Item[]>();
|
|
249
309
|
const nextStates_Shift = new Map<string, Item[]>();
|
|
250
310
|
|
|
@@ -256,25 +316,21 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
256
316
|
|
|
257
317
|
// no next symbol is available, therefore we need to create a reduction
|
|
258
318
|
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
319
|
// for each lookahead in the item, create a new entry in the action table to reduce to the production found
|
|
268
320
|
for (const lookahead of item.lookahead) {
|
|
269
321
|
if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
|
|
270
|
-
ActionTable[currentState.count]!.set(lookahead, { action: "reduce", value:
|
|
322
|
+
ActionTable[currentState.count]!.set(lookahead, { action: "reduce", value: reduceValue[item.prodIndex] });
|
|
271
323
|
}
|
|
272
324
|
|
|
273
325
|
continue calculateNextItem;
|
|
274
326
|
}
|
|
275
327
|
|
|
276
328
|
// Create the next item by shifting the dot by 1 place
|
|
277
|
-
const newItem: Item = {
|
|
329
|
+
const newItem: Item = {
|
|
330
|
+
...item,
|
|
331
|
+
dot: item.dot + 1,
|
|
332
|
+
key: itemKey(item.prodIndex, item.dot + 1, item.lookahead),
|
|
333
|
+
};
|
|
278
334
|
|
|
279
335
|
// Determine if we're going to create a GOTO or SHIFT based on the type of the symbol after the dot
|
|
280
336
|
if (nextSymbol.type === "variable") {
|
|
@@ -300,19 +356,11 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
300
356
|
const expandedItems = expandedItemsResult.value;
|
|
301
357
|
|
|
302
358
|
// 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
|
-
});
|
|
359
|
+
const signature = itemSetSignature(expandedItems.itemSet);
|
|
360
|
+
let existingStateIndex = stateIndexBySignature.get(signature);
|
|
313
361
|
|
|
314
362
|
if (GotoTable[currentState.count] === undefined) GotoTable[currentState.count] = new Map();
|
|
315
|
-
if (
|
|
363
|
+
if (existingStateIndex == undefined) {
|
|
316
364
|
const newState: State = {
|
|
317
365
|
itemSet: expandedItems.itemSet,
|
|
318
366
|
kernel: expandedItems.kernel,
|
|
@@ -320,10 +368,12 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
320
368
|
};
|
|
321
369
|
states.push(newState);
|
|
322
370
|
unprocessedStates.push(newState);
|
|
323
|
-
|
|
371
|
+
stateIndexBySignature.set(signature, newState.count);
|
|
372
|
+
existingStateIndex = newState.count;
|
|
324
373
|
}
|
|
325
374
|
|
|
326
|
-
|
|
375
|
+
options.onProgress?.(states.length);
|
|
376
|
+
GotoTable[currentState.count]!.set(gotoTransition, existingStateIndex);
|
|
327
377
|
}
|
|
328
378
|
|
|
329
379
|
// For each SHIFT transition from the current state, expand the item set that it points to
|
|
@@ -336,22 +386,14 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
336
386
|
const expandedItems = expandedItemsResult.value;
|
|
337
387
|
|
|
338
388
|
// 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
|
-
});
|
|
389
|
+
const signature = itemSetSignature(expandedItems.itemSet);
|
|
390
|
+
let existingStateIndex = stateIndexBySignature.get(signature);
|
|
349
391
|
|
|
350
392
|
// If there is no entry in the ActionTable for the current state, create one
|
|
351
393
|
if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
|
|
352
394
|
|
|
353
395
|
// If no such state exists, create a new state and add it to the list of states
|
|
354
|
-
if (
|
|
396
|
+
if (existingStateIndex == undefined) {
|
|
355
397
|
const newState: State = {
|
|
356
398
|
itemSet: expandedItems.itemSet,
|
|
357
399
|
kernel: expandedItems.kernel,
|
|
@@ -359,10 +401,12 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
359
401
|
};
|
|
360
402
|
states.push(newState);
|
|
361
403
|
unprocessedStates.push(newState);
|
|
362
|
-
|
|
404
|
+
stateIndexBySignature.set(signature, newState.count);
|
|
405
|
+
existingStateIndex = newState.count;
|
|
363
406
|
}
|
|
364
407
|
|
|
365
|
-
|
|
408
|
+
options.onProgress?.(states.length);
|
|
409
|
+
ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value: existingStateIndex });
|
|
366
410
|
}
|
|
367
411
|
}
|
|
368
412
|
|
|
@@ -373,9 +417,117 @@ export const generateStates = (productions: Production[]): Result<GeneratorResul
|
|
|
373
417
|
const StatesResult = generateStates(initialItemSet);
|
|
374
418
|
if (StatesResult.success === false) return StatesResult;
|
|
375
419
|
const { states, ActionTable, GotoTable } = StatesResult.value;
|
|
420
|
+
|
|
421
|
+
if (options.mode === "lalr1") {
|
|
422
|
+
const merged = mergeStatesForLALR(states, ActionTable, GotoTable);
|
|
423
|
+
if (merged.success === false) return merged;
|
|
424
|
+
return {
|
|
425
|
+
success: true,
|
|
426
|
+
value: new GeneratorResult(merged.value.states, merged.value.ActionTable, merged.value.GotoTable),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
376
430
|
return { success: true, value: new GeneratorResult(states, ActionTable, GotoTable) };
|
|
377
431
|
};
|
|
378
432
|
|
|
433
|
+
// Merges the LR(1) states into LALR(1) states by grouping states that have the same
|
|
434
|
+
// "same-core" item set (items compared without their lookahead sets) and unioning
|
|
435
|
+
// their item sets, action tables and goto tables. Returns an error if merging any
|
|
436
|
+
// group produces a shift/reduce, reduce/reduce, shift or goto conflict, which
|
|
437
|
+
// means the grammar is not LALR(1).
|
|
438
|
+
function mergeStatesForLALR(
|
|
439
|
+
states: State[],
|
|
440
|
+
ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[],
|
|
441
|
+
GotoTable: Map<string, number>[],
|
|
442
|
+
): Result<{
|
|
443
|
+
states: State[];
|
|
444
|
+
ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[];
|
|
445
|
+
GotoTable: Map<string, number>[];
|
|
446
|
+
}> {
|
|
447
|
+
type Action = { action: "shift" | "reduce"; value: number };
|
|
448
|
+
|
|
449
|
+
const itemSkeleton = (item: Item) => `${item.prodIndex}${KEY_SEP}${item.dot}`;
|
|
450
|
+
const stateSkeleton = (state: State) => state.itemSet.map(itemSkeleton).sort().join(FIELD_SEP);
|
|
451
|
+
|
|
452
|
+
// Group states by same-core signature, assigning merged indices in first-occurrence order
|
|
453
|
+
const groupIndices: number[][] = [];
|
|
454
|
+
const groupBySkeleton = new Map<string, number>();
|
|
455
|
+
const oldToNew = new Array<number>(states.length);
|
|
456
|
+
for (let i = 0; i < states.length; i++) {
|
|
457
|
+
const skeleton = stateSkeleton(states[i]);
|
|
458
|
+
let group = groupBySkeleton.get(skeleton);
|
|
459
|
+
if (group == undefined) {
|
|
460
|
+
group = groupIndices.length;
|
|
461
|
+
groupBySkeleton.set(skeleton, group);
|
|
462
|
+
groupIndices.push([]);
|
|
463
|
+
}
|
|
464
|
+
groupIndices[group].push(i);
|
|
465
|
+
oldToNew[i] = group;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const describeAction = (a: Action) =>
|
|
469
|
+
a.action === "reduce" ? `reduce by production ${a.value}` : `shift to state ${a.value}`;
|
|
470
|
+
const conflictToken = (members: number[]) => states[members[0]].kernel.lhs;
|
|
471
|
+
const conflict = (g: number, members: number[], reason: string): Result<never> => ({
|
|
472
|
+
success: false,
|
|
473
|
+
reason: `LALR(1) conflict in state ${g} on ${reason} between LR(1) states ${members.join(", ")}: the grammar is not LALR(1)`,
|
|
474
|
+
token: conflictToken(members),
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
const newStates: State[] = [];
|
|
478
|
+
const newActionTable: Map<string, Action>[] = [];
|
|
479
|
+
const newGotoTable: Map<string, number>[] = [];
|
|
480
|
+
|
|
481
|
+
for (let g = 0; g < groupIndices.length; g++) {
|
|
482
|
+
const members = groupIndices[g];
|
|
483
|
+
const actions = new Map<string, Action>();
|
|
484
|
+
const gotos = new Map<string, number>();
|
|
485
|
+
|
|
486
|
+
for (const member of members) {
|
|
487
|
+
// A state whose items only move to other states (no shifts, no completed items) has no
|
|
488
|
+
// entry in the action table at all.
|
|
489
|
+
for (const [key, entry] of (ActionTable[member] ?? new Map()).entries()) {
|
|
490
|
+
const value: Action = {
|
|
491
|
+
action: entry.action,
|
|
492
|
+
value: entry.action === "shift" ? oldToNew[entry.value] : entry.value,
|
|
493
|
+
};
|
|
494
|
+
const existing = actions.get(key);
|
|
495
|
+
if (existing == undefined) actions.set(key, value);
|
|
496
|
+
else if (existing.action !== value.action || existing.value !== value.value)
|
|
497
|
+
return conflict(g, members, `"${key}": ${describeAction(existing)} conflicts with ${describeAction(value)}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (GotoTable[member] !== undefined)
|
|
501
|
+
for (const [key, target] of GotoTable[member]!.entries()) {
|
|
502
|
+
const value = oldToNew[target];
|
|
503
|
+
const existing = gotos.get(key);
|
|
504
|
+
if (existing == undefined) gotos.set(key, value);
|
|
505
|
+
else if (existing !== value)
|
|
506
|
+
return conflict(g, members, `"${key}": goto to state ${existing} conflicts with goto to state ${value}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Union the item sets of all members, merging the lookahead sets of
|
|
511
|
+
// items that differ only in their lookahead
|
|
512
|
+
const itemsBySkeleton = new Map<string, Item>();
|
|
513
|
+
for (const member of members)
|
|
514
|
+
for (const item of states[member].itemSet) {
|
|
515
|
+
const skeleton = itemSkeleton(item);
|
|
516
|
+
const existing = itemsBySkeleton.get(skeleton);
|
|
517
|
+
if (existing == undefined) itemsBySkeleton.set(skeleton, { ...item, lookahead: [...item.lookahead] });
|
|
518
|
+
else
|
|
519
|
+
for (const lookahead of item.lookahead)
|
|
520
|
+
if (!existing.lookahead.includes(lookahead)) existing.lookahead.push(lookahead);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
newStates.push({ itemSet: [...itemsBySkeleton.values()], kernel: states[members[0]].kernel, count: g });
|
|
524
|
+
newActionTable.push(actions);
|
|
525
|
+
newGotoTable.push(gotos);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return { success: true, value: { states: newStates, ActionTable: newActionTable, GotoTable: newGotoTable } };
|
|
529
|
+
}
|
|
530
|
+
|
|
379
531
|
export class GeneratorResult {
|
|
380
532
|
constructor(
|
|
381
533
|
public readonly states: State[],
|
|
@@ -384,7 +536,15 @@ export class GeneratorResult {
|
|
|
384
536
|
) {}
|
|
385
537
|
|
|
386
538
|
toTable(): string {
|
|
387
|
-
|
|
539
|
+
const actionless = this.ActionTable.findIndex((actions) => actions === undefined || actions.size === 0);
|
|
540
|
+
if (actionless !== -1)
|
|
541
|
+
throw new Error(
|
|
542
|
+
`State ${actionless} of this grammar has no actions, so it cannot be written in the parsing table format. ` +
|
|
543
|
+
`Use Sparse.fromProductions to keep the states in memory instead.`,
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
// Array.from, not map: the action table is sparse and map would keep the holes.
|
|
547
|
+
return Array.from(this.ActionTable, (actions, idx) => {
|
|
388
548
|
return [
|
|
389
549
|
...[...actions.entries()].map(([k, { action, value }]) => `${k}=${action === "reduce" ? "r" : "s"}${value}`),
|
|
390
550
|
...(this.GotoTable[idx] === undefined ? [] : [...this.GotoTable[idx].entries()].map(([k, v]) => `${k}=${v}`)),
|
|
@@ -393,14 +553,24 @@ export class GeneratorResult {
|
|
|
393
553
|
}
|
|
394
554
|
|
|
395
555
|
toStates(): TableState[] {
|
|
396
|
-
|
|
556
|
+
// Array.from, not map: a state without actions leaves a hole in the action table, and map would
|
|
557
|
+
// hand back an array that is missing that state and shifts every later state number.
|
|
558
|
+
return Array.from(this.ActionTable, (actions, idx) => {
|
|
397
559
|
const tableState = new TableState();
|
|
398
560
|
|
|
399
|
-
|
|
561
|
+
// See toTable(): a state can legitimately have no actions, it just cannot be serialized.
|
|
562
|
+
if (actions === undefined) return tableState;
|
|
563
|
+
|
|
564
|
+
for (const [k, { action, value }] of actions.entries()) {
|
|
400
565
|
tableState.actions.set(k, { type: action === "reduce" ? "reduce" : "shift", value });
|
|
566
|
+
tableState.rawActions.set(k, `${action === "reduce" ? "r" : "s"}${value}`);
|
|
567
|
+
}
|
|
401
568
|
|
|
402
569
|
if (this.GotoTable[idx] !== undefined)
|
|
403
|
-
for (const [k, value] of this.GotoTable[idx].entries())
|
|
570
|
+
for (const [k, value] of this.GotoTable[idx].entries()) {
|
|
571
|
+
tableState.actions.set(k, { type: "goto", value });
|
|
572
|
+
tableState.rawActions.set(k, `${value}`);
|
|
573
|
+
}
|
|
404
574
|
|
|
405
575
|
return tableState;
|
|
406
576
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,66 @@
|
|
|
1
1
|
export { GeneratorResult, generateStates } from "./generator";
|
|
2
|
-
export {
|
|
2
|
+
export type { GeneratorOptions } from "./generator";
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
buildProductions,
|
|
6
|
+
tryBuildProductions,
|
|
7
|
+
grammarLexerGenerator,
|
|
8
|
+
getSelfHostedParserGenerator,
|
|
9
|
+
} from "./meta/selfhosted";
|
|
10
|
+
export type { LexerInterface } from "./meta/selfhosted";
|
|
11
|
+
|
|
3
12
|
export { Result } from "./utils/Result";
|
|
4
13
|
export {
|
|
5
14
|
ParserRecoveryFunction,
|
|
6
15
|
Sparse,
|
|
16
|
+
SparseGrammarError,
|
|
7
17
|
LR1ParserGraveError,
|
|
8
18
|
LR1StackSymbol,
|
|
19
|
+
LR1Parser,
|
|
9
20
|
ParserResult,
|
|
10
21
|
ParserError,
|
|
22
|
+
TableAction,
|
|
23
|
+
TableState,
|
|
11
24
|
} from "./parser";
|
|
12
|
-
export {
|
|
13
|
-
|
|
25
|
+
export type { ReducerType, SparseOptions, FromProductionsOptions } from "./parser";
|
|
26
|
+
|
|
27
|
+
export { buildStates, tryBuildStates } from "./table/selfhosted";
|
|
28
|
+
export type { BuildStatesOptions } from "./table/selfhosted";
|
|
29
|
+
export { validateTable, validateTableStates } from "./table/validate";
|
|
30
|
+
export type { ValidateTableOptions, ValidateTableStatesOptions, TableFormatWarning } from "./table/validate";
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
dehydateProduction,
|
|
34
|
+
hydrateProduction,
|
|
35
|
+
isVariableIdentifier,
|
|
36
|
+
syntheticGrammarToken,
|
|
37
|
+
terminalIdentifier,
|
|
38
|
+
validateProductions,
|
|
39
|
+
variableIdentifier,
|
|
40
|
+
GrammarTokenType,
|
|
41
|
+
} from "./meta/common";
|
|
42
|
+
export type {
|
|
43
|
+
GrammarToken,
|
|
44
|
+
GrammarTokenMetadata,
|
|
45
|
+
Production,
|
|
46
|
+
ProductionRhsItem,
|
|
47
|
+
ProductionWarning,
|
|
48
|
+
ProductionWarningKind,
|
|
49
|
+
ValidateProductionsOptions,
|
|
50
|
+
} from "./meta/common";
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
assertReducersCoverGrammar,
|
|
54
|
+
defineReducers,
|
|
55
|
+
enumToString,
|
|
56
|
+
missingReducerMessage,
|
|
57
|
+
missingReducerNames,
|
|
58
|
+
namedProductions,
|
|
59
|
+
KLEENE_REDUCER_NAME,
|
|
60
|
+
} from "./utils/reducers";
|
|
61
|
+
export type { ReducerMap } from "./utils/reducers";
|
|
62
|
+
|
|
63
|
+
export { loadGrammar, loadTable } from "./utils/loadFiles";
|
|
64
|
+
export type { LoadGrammarOptions, LoadTableOptions } from "./utils/loadFiles";
|
|
65
|
+
export { buildErrorWindow } from "./utils/errorWindowBuilder";
|
|
66
|
+
export { Stack } from "./utils/Stack";
|
package/src/meta/common.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ColumnAndRow, Token } from "@scinorandex/slex";
|
|
2
|
+
import { Result } from "../utils/Result";
|
|
2
3
|
|
|
3
4
|
export type Production = {
|
|
4
5
|
lhs: GrammarToken;
|
|
@@ -8,6 +9,8 @@ export type Production = {
|
|
|
8
9
|
rhs: { type: "terminal" | "variable"; token: GrammarToken; identifier: string; name: string | null }[];
|
|
9
10
|
};
|
|
10
11
|
|
|
12
|
+
export type ProductionRhsItem = Production["rhs"][number];
|
|
13
|
+
|
|
11
14
|
const dehydateGrammarToken = ({ column, lexeme, line, type }: GrammarToken) => {
|
|
12
15
|
return { column, lexeme, line, type };
|
|
13
16
|
};
|
|
@@ -50,6 +53,119 @@ export const hydrateProduction = (production: ReturnType<typeof dehydateProducti
|
|
|
50
53
|
};
|
|
51
54
|
|
|
52
55
|
export type GrammarTokenMetadata = {};
|
|
56
|
+
|
|
57
|
+
/** A readable one-liner for an unknown thrown value, used to turn it into a `Result` failure. */
|
|
58
|
+
export const describeThrowable = (err: unknown) =>
|
|
59
|
+
err instanceof Error ? err.message : `Unexpected error: ${String(err)}`;
|
|
60
|
+
|
|
61
|
+
/** A stand-in token used to report errors that do not belong to a specific place in a grammar. */
|
|
62
|
+
export const syntheticGrammarToken = () =>
|
|
63
|
+
new Token(GrammarTokenType.EOF, "", new ColumnAndRow(0, 0), {}) as GrammarToken;
|
|
64
|
+
|
|
65
|
+
export type ProductionWarningKind = "duplicate-rhs-name" | "unreachable-production";
|
|
66
|
+
|
|
67
|
+
export type ProductionWarning = {
|
|
68
|
+
kind: ProductionWarningKind;
|
|
69
|
+
reason: string;
|
|
70
|
+
token: GrammarToken;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type ValidateProductionsOptions = {
|
|
74
|
+
/** Called once per warning. Warnings never stop the parser generator, they only flag suspicious grammars. */
|
|
75
|
+
onWarning?: (warning: ProductionWarning) => void;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const at = (token: GrammarToken) => `${token.line}:${token.column}`;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Checks a set of unrolled productions for the mistakes that would otherwise surface as an
|
|
82
|
+
* inscrutable `TypeError` deep inside state generation or parsing:
|
|
83
|
+
*
|
|
84
|
+
* - a variable used on the right hand side that no production defines
|
|
85
|
+
* - a production with an empty right hand side (Sparse has no support for empty productions)
|
|
86
|
+
* - the start symbol being used on the right hand side (it must only appear on the left hand side)
|
|
87
|
+
*
|
|
88
|
+
* Suspicious-but-legal grammars (duplicate symbol names, unreachable productions) are reported
|
|
89
|
+
* through `onWarning` instead of failing.
|
|
90
|
+
*/
|
|
91
|
+
export const validateProductions = (
|
|
92
|
+
productions: Production[],
|
|
93
|
+
options: ValidateProductionsOptions = {},
|
|
94
|
+
): Result<Production[]> => {
|
|
95
|
+
const warn = (kind: ProductionWarningKind, reason: string, token: GrammarToken) => {
|
|
96
|
+
options.onWarning?.({ kind, reason, token });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
if (productions.length === 0)
|
|
100
|
+
return {
|
|
101
|
+
success: false,
|
|
102
|
+
reason: "The grammar does not contain any productions. Is the grammar file empty?",
|
|
103
|
+
token: syntheticGrammarToken(),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const lhsIdentifiers = new Set(productions.map((production) => production.identifier));
|
|
107
|
+
const referencedIdentifiers = new Set<string>();
|
|
108
|
+
|
|
109
|
+
for (const production of productions) {
|
|
110
|
+
if (production.rhs.length === 0)
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
reason: `Production "${production.identifier}" (line ${at(production.lhs)}) has an empty right hand side. Sparse does not support empty productions, so a lone "(...)? " group such as "<X: x>: ([A])?;" is not a valid production.`,
|
|
114
|
+
token: production.lhs,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const seenNames = new Set<string>();
|
|
118
|
+
for (const rhsItem of production.rhs) {
|
|
119
|
+
if (rhsItem.type === "variable") {
|
|
120
|
+
referencedIdentifiers.add(rhsItem.identifier);
|
|
121
|
+
if (lhsIdentifiers.has(rhsItem.identifier) === false)
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
reason: `Variable "${rhsItem.identifier}" is used on the right hand side of production "${production.identifier}" (line ${at(production.lhs)}) but no production defines it as its left hand side.`,
|
|
125
|
+
token: rhsItem.token,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (rhsItem.name != null) {
|
|
130
|
+
if (seenNames.has(rhsItem.name))
|
|
131
|
+
warn(
|
|
132
|
+
"duplicate-rhs-name",
|
|
133
|
+
`Production "${production.identifier}" (line ${at(production.lhs)}) names more than one symbol "${rhsItem.name}". Only the last one ends up on the reducer's "bag", use the positional input to reach the others.`,
|
|
134
|
+
rhsItem.token,
|
|
135
|
+
);
|
|
136
|
+
seenNames.add(rhsItem.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The first production is the accept production: its left hand side is the start symbol, which
|
|
142
|
+
// by definition cannot be referenced from the right hand side of anything.
|
|
143
|
+
const startProduction = productions[0];
|
|
144
|
+
if (referencedIdentifiers.has(startProduction.identifier))
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
reason: `"${startProduction.identifier}" is the start symbol of the grammar, so it cannot be used on the right hand side of a production. It is used at line ${at(startProduction.lhs)}.`,
|
|
148
|
+
token: startProduction.lhs,
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
for (let i = 1; i < productions.length; i++) {
|
|
152
|
+
const production = productions[i];
|
|
153
|
+
if (referencedIdentifiers.has(production.identifier)) continue;
|
|
154
|
+
warn(
|
|
155
|
+
"unreachable-production",
|
|
156
|
+
`Production "${production.identifier}" (line ${at(production.lhs)}) is never used on the right hand side of another production, so the parser can never reach it.`,
|
|
157
|
+
production.lhs,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { success: true, value: productions };
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** True when the identifier is a variable (`<FOO>`) rather than a terminal (`[FOO]`). */
|
|
165
|
+
export const isVariableIdentifier = (identifier: string) => identifier.startsWith("<") && identifier.endsWith(">");
|
|
166
|
+
|
|
167
|
+
export const terminalIdentifier = (name: string) => `[${name}]`;
|
|
168
|
+
export const variableIdentifier = (name: string) => `<${name}>`;
|
|
53
169
|
export enum GrammarTokenType {
|
|
54
170
|
IDENTIFIER,
|
|
55
171
|
L_ANGLE,
|