@scinorandex/sparse 0.1.1 → 0.1.3
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 +31 -11
- package/README.md +289 -29
- package/dist/cli.js +113 -24
- package/dist/cli.js.map +1 -1
- package/dist/generator.d.ts +3 -1
- package/dist/generator.js +29 -5
- package/dist/generator.js.map +1 -1
- package/dist/index.d.ts +16 -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 +49 -36
- package/dist/meta/selfhosted.js.map +1 -1
- package/dist/parser.d.ts +55 -35
- package/dist/parser.js +141 -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 +16 -15
- 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 +1 -1
- package/package.json +2 -2
- package/src/cli.ts +132 -23
- package/src/generator.ts +49 -8
- package/src/index.ts +55 -3
- package/src/meta/common.ts +116 -0
- package/src/meta/selfhosted.ts +42 -11
- package/src/parser.ts +243 -51
- 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 +110 -0
- package/test/parser.test.ts +377 -0
- package/test/table.test.ts +144 -0
- package/tsconfig.json +1 -1
- package/example/LoLang/table2.txt +0 -497
- package/example/complicated/grammar.txt +0 -37
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { Slex, Token as SlexToken } from "@scinorandex/slex";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
LR1StackSymbol,
|
|
4
|
+
Sparse,
|
|
5
|
+
assertReducersCoverGrammar,
|
|
6
|
+
defineReducers,
|
|
7
|
+
enumToString,
|
|
8
|
+
loadGrammar,
|
|
9
|
+
} from "../../src/index";
|
|
10
|
+
import path from "path";
|
|
4
11
|
|
|
5
12
|
// prettier-ignore
|
|
6
13
|
enum TokenType {
|
|
@@ -52,32 +59,43 @@ class KleeneNode<T> extends Node {
|
|
|
52
59
|
}
|
|
53
60
|
}
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
62
|
+
// Every reducer receives (newInput, oldInput): newInput is { bag, name } (the bag holds the named
|
|
63
|
+
// symbols of the right hand side), oldInput is { input, index } (the raw symbols in order).
|
|
64
|
+
type Reducer = (
|
|
65
|
+
newInput: { bag: any; name: string | null },
|
|
66
|
+
oldInput: { input: LR1StackSymbol<TokenType, Metadata, Node>[]; index: number },
|
|
67
|
+
) => Node;
|
|
68
|
+
|
|
69
|
+
const reducers: Record<string, Reducer> = {
|
|
70
|
+
// Every "*" and "+" group produces a list production named "autogenerated-kleene", which has to
|
|
71
|
+
// flatten the list it is handed: the first reduction has no "rest", later ones do.
|
|
72
|
+
"autogenerated-kleene": ({ bag }, { input }) => {
|
|
58
73
|
const { rest, ...otherNodes } = bag;
|
|
59
74
|
if (rest == null) return new KleeneNode(input, otherNodes);
|
|
60
75
|
else if (rest instanceof KleeneNode) return rest.add(input, otherNodes);
|
|
61
|
-
// You should not be able to reach this because this function always returns a KleeneNode
|
|
62
76
|
else throw new Error("Invalid reducer");
|
|
63
77
|
},
|
|
64
78
|
|
|
65
|
-
program: (
|
|
66
|
-
return new Node(input);
|
|
67
|
-
},
|
|
79
|
+
program: (_, { input }) => new Node(input),
|
|
68
80
|
};
|
|
69
81
|
|
|
82
|
+
const root = path.join(__dirname, "..", "..");
|
|
83
|
+
|
|
70
84
|
async function main() {
|
|
71
|
-
const
|
|
72
|
-
|
|
85
|
+
const productionsResult = await loadGrammar(path.join(root, "example/kleene-test/grammar.txt"));
|
|
86
|
+
if (productionsResult.success === false) throw new Error(productionsResult.reason);
|
|
87
|
+
|
|
88
|
+
// Fails loudly here if a production in the grammar has no reducer, instead of halfway through a parse.
|
|
89
|
+
assertReducersCoverGrammar(productionsResult.value, reducers);
|
|
90
|
+
|
|
91
|
+
// No table file for this example, so the states are generated from the grammar at startup.
|
|
92
|
+
const parserGenerator = Sparse.fromProductions<TokenType, Metadata, Node>({
|
|
93
|
+
productions: productionsResult.value,
|
|
94
|
+
toStringifiedTokenType: enumToString(TokenType),
|
|
95
|
+
});
|
|
73
96
|
|
|
74
|
-
const parserGenerator = Sparse.fromProductions<TokenType, Metadata, Node>({ productions, toStringifiedTokenType });
|
|
75
97
|
const parser = parserGenerator.generate(lexer, {
|
|
76
|
-
reducer: (
|
|
77
|
-
const reducer = reducers[name ?? ""];
|
|
78
|
-
if (reducer == null) throw new Error(`No reducer for ${name}`);
|
|
79
|
-
return reducer(bag, input);
|
|
80
|
-
},
|
|
98
|
+
reducer: defineReducers(reducers),
|
|
81
99
|
});
|
|
82
100
|
|
|
83
101
|
console.log(parser.parse().result!);
|
package/example/math/example.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Slex } from "@scinorandex/slex";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { LR1StackSymbol, Sparse, enumToString } from "../../src/index";
|
|
3
|
+
import path from "path";
|
|
4
4
|
|
|
5
5
|
// prettier-ignore
|
|
6
6
|
enum TokenType {
|
|
@@ -33,14 +33,19 @@ class Node {
|
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const root = path.join(__dirname, "..", "..");
|
|
37
|
+
|
|
36
38
|
async function main() {
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
// fromGrammarFile reads the grammar, reads the prebuilt table, and checks that the two match.
|
|
40
|
+
const parserGenerator = await Sparse.fromGrammarFile<TokenType, Metadata, Node>({
|
|
41
|
+
grammarPath: path.join(root, "example/math/grammar.txt"),
|
|
42
|
+
tablePath: path.join(root, "example/math/table.txt"),
|
|
43
|
+
toStringifiedTokenType: enumToString(TokenType),
|
|
44
|
+
onWarning: ({ reason, token }) => console.warn(`${token.line}:${token.column} ${reason}`),
|
|
45
|
+
});
|
|
41
46
|
|
|
42
47
|
const parser = parserGenerator.generate(lexer, { reducer: (_, { input }) => new Node(input) });
|
|
43
48
|
console.log(parser.parse().result!.toObject());
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
main();
|
|
51
|
+
main();
|
|
@@ -1,39 +1,45 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { buildErrorWindow, generateStates, tryBuildProductions } from "../../src";
|
|
4
|
+
import { grammarLexerGenerator } from "../../src/meta/selfhosted";
|
|
4
5
|
import { dehydateProduction } from "../../src/meta/common";
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
const grammar = await fs.readFile("./example/selfhosted/01-meta-grammar.txt", "utf8");
|
|
7
|
+
const root = path.join(__dirname, "..", "..");
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export const productions = ${JSON.stringify(productions.map(dehydateProduction), null, 2)};
|
|
18
|
-
export const selfhosted = { states, productions };`,
|
|
19
|
-
);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function codegenTableGrammar() {
|
|
23
|
-
const grammar = await fs.readFile("./example/selfhosted/02-table-grammar.txt", "utf8");
|
|
9
|
+
/**
|
|
10
|
+
* Regenerates the LR(1) states that Sparse uses to read its own grammar and table file formats.
|
|
11
|
+
* `src/meta/states.ts` and `src/table/states.ts` are byte-identical copies of the two files this
|
|
12
|
+
* writes, so after running this you must copy them over.
|
|
13
|
+
*/
|
|
14
|
+
async function codegen(name: string, grammarName: string, outputPath: string) {
|
|
15
|
+
const grammarPath = path.join(root, "example/selfhosted", grammarName);
|
|
16
|
+
const grammar = await fs.readFile(grammarPath, "utf8");
|
|
24
17
|
|
|
25
18
|
const lexer = grammarLexerGenerator.generate(grammar, () => ({}));
|
|
26
|
-
const
|
|
19
|
+
const productionsResult = tryBuildProductions(lexer);
|
|
20
|
+
if (productionsResult.success === false) {
|
|
21
|
+
console.error(`${productionsResult.reason}\n${buildErrorWindow(grammar, productionsResult.token)}`);
|
|
22
|
+
process.exitCode = 1;
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const productions = productionsResult.value;
|
|
26
|
+
|
|
27
27
|
const statesResult = generateStates(productions);
|
|
28
|
-
if (statesResult.success === false)
|
|
28
|
+
if (statesResult.success === false) {
|
|
29
|
+
console.error(`${statesResult.reason}\n${buildErrorWindow(grammar, statesResult.token)}`);
|
|
30
|
+
process.exitCode = 1;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
29
33
|
|
|
30
34
|
await fs.writeFile(
|
|
31
|
-
"
|
|
35
|
+
path.join(root, "example/selfhosted", outputPath),
|
|
32
36
|
`export const states = ${JSON.stringify(statesResult.value.toJSObject(), null, 2)};
|
|
33
37
|
export const productions = ${JSON.stringify(productions.map(dehydateProduction), null, 2)};
|
|
34
38
|
export const selfhosted = { states, productions };`,
|
|
35
39
|
);
|
|
40
|
+
|
|
41
|
+
console.log(`${name}: ${productions.length} productions, ${statesResult.value.states.length} states -> ${outputPath}`);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
codegen("meta grammar", "01-meta-grammar.txt", "codegen-meta.ts");
|
|
45
|
+
codegen("table grammar", "02-table-grammar.txt", "codegen-table.ts");
|
package/opencode.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scinorandex/sparse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Yet another parser generator",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/minimist": "^1.2.5",
|
|
33
33
|
"@types/node": "^26.0.0",
|
|
34
|
-
"@scinorandex/slex": "^0.
|
|
34
|
+
"@scinorandex/slex": "^0.1.2",
|
|
35
35
|
"typescript": "^5.6.0",
|
|
36
36
|
"vitest": "^3.0.0"
|
|
37
37
|
},
|
package/src/cli.ts
CHANGED
|
@@ -1,49 +1,158 @@
|
|
|
1
1
|
#! /usr/bin/env node
|
|
2
2
|
import fs from "fs/promises";
|
|
3
|
-
import { tryBuildProductions } from "./index";
|
|
4
|
-
import { generateStates } from "./generator";
|
|
5
|
-
import minimist from "minimist";
|
|
6
3
|
import path from "path";
|
|
7
|
-
import
|
|
4
|
+
import minimist from "minimist";
|
|
5
|
+
import { tryBuildProductions, tryBuildStates, generateStates, validateTable, buildErrorWindow } from "./index";
|
|
6
|
+
import type { GeneratorResult, ProductionWarning } from "./index";
|
|
8
7
|
|
|
9
8
|
const args = minimist(process.argv.slice(2));
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
const BINARY_NAME = "@scinorandex/sparse";
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage: npx @scinorandex/sparse --input=<grammar> --output=<table> [options]
|
|
13
|
+
|
|
14
|
+
Generates an LR(1) parsing table from a grammar file.
|
|
15
|
+
|
|
16
|
+
Required:
|
|
17
|
+
--input=<file> Grammar file to read
|
|
18
|
+
--output=<file> Where to write the parsing table (created if the directory is missing)
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
--lalr Generate LALR(1) states instead of LR(1) (smaller table, ~35% faster parsing)
|
|
22
|
+
--check Do not write anything: verify that <output> already matches the grammar
|
|
23
|
+
--stdout Print the table instead of writing it to a file
|
|
24
|
+
--quiet Do not print warnings about suspicious grammar rules
|
|
25
|
+
--help Show this message
|
|
26
|
+
|
|
27
|
+
Run with npx ${BINARY_NAME} (the package is @scinorandex/sparse).`;
|
|
28
|
+
|
|
29
|
+
function fail(message: string) {
|
|
30
|
+
console.error(message);
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Serializing can fail for grammars the table file format cannot express; report that, not a stack trace. */
|
|
35
|
+
function toTableOrFail(result: GeneratorResult): string | null {
|
|
36
|
+
try {
|
|
37
|
+
return result.toTable();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
17
42
|
}
|
|
18
43
|
|
|
19
44
|
async function main() {
|
|
20
|
-
if (
|
|
21
|
-
console.log(
|
|
45
|
+
if (args.help === true || args.h === true) {
|
|
46
|
+
console.log(USAGE);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (typeof args.input !== "string") {
|
|
51
|
+
fail(`Missing --input.\n\n${USAGE}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const useStdout = args.stdout === true;
|
|
56
|
+
const check = args.check === true;
|
|
57
|
+
if (typeof args.output !== "string" && !useStdout) {
|
|
58
|
+
fail(`Missing --output.\n\n${USAGE}`);
|
|
22
59
|
return;
|
|
23
60
|
}
|
|
24
61
|
|
|
25
62
|
const inputFile = path.resolve(args.input);
|
|
26
|
-
const outputFile = path.resolve(args.output);
|
|
27
|
-
|
|
63
|
+
const outputFile = typeof args.output === "string" ? path.resolve(args.output) : undefined;
|
|
64
|
+
|
|
65
|
+
let grammar: string;
|
|
66
|
+
try {
|
|
67
|
+
grammar = await fs.readFile(inputFile, "utf8");
|
|
68
|
+
} catch {
|
|
69
|
+
fail(`Cannot read grammar file ${inputFile}: it does not exist.`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
28
72
|
|
|
29
|
-
|
|
73
|
+
const onWarning = (warning: ProductionWarning) => {
|
|
74
|
+
if (args.quiet === true) return;
|
|
75
|
+
console.error(`Warning in ${inputFile} at ${warning.token.line}:${warning.token.column}: ${warning.reason}`);
|
|
76
|
+
};
|
|
30
77
|
|
|
31
|
-
const
|
|
32
|
-
const productionsResult = tryBuildProductions(grammar);
|
|
78
|
+
const productionsResult = tryBuildProductions(grammar, { onWarning });
|
|
33
79
|
if (productionsResult.success === false) {
|
|
34
|
-
|
|
35
|
-
|
|
80
|
+
fail(`${productionsResult.reason}\n\n${buildErrorWindow(grammar, productionsResult.token)}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const productions = productionsResult.value;
|
|
84
|
+
|
|
85
|
+
if (check) {
|
|
86
|
+
if (outputFile == null) {
|
|
87
|
+
fail(`--check needs --output to know which table to check.\n\n${USAGE}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let existingTable: string;
|
|
92
|
+
try {
|
|
93
|
+
existingTable = await fs.readFile(outputFile, "utf8");
|
|
94
|
+
} catch {
|
|
95
|
+
fail(`Cannot check ${outputFile}: it does not exist.`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const statesResult = tryBuildStates(existingTable, { productions });
|
|
100
|
+
if (statesResult.success === false) {
|
|
101
|
+
fail(`The existing table is not a valid parsing table:\n${statesResult.reason}`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const tableCheckResult = validateTable(productions, statesResult.value, { source: outputFile });
|
|
106
|
+
if (tableCheckResult.success === false)
|
|
107
|
+
fail(`The existing table does not match the grammar:\n${tableCheckResult.reason}`);
|
|
108
|
+
|
|
109
|
+
const expected = generateStates(productions, { mode: args.lalr === true ? "lalr1" : "lr1" });
|
|
110
|
+
if (expected.success === false) {
|
|
111
|
+
fail(`${expected.reason}\n\n${buildErrorWindow(grammar, expected.token)}`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const expectedTable = toTableOrFail(expected.value);
|
|
116
|
+
if (expectedTable == null) return;
|
|
117
|
+
const actualTable = existingTable.trim();
|
|
118
|
+
|
|
119
|
+
if (expectedTable !== actualTable) {
|
|
120
|
+
fail(
|
|
121
|
+
`${outputFile} is out of date with respect to ${inputFile}.\n` +
|
|
122
|
+
`Re-run without --check to regenerate it${args.lalr === true ? " (with --lalr)" : ""}.`,
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log(
|
|
128
|
+
`${outputFile} is up to date with respect to ${inputFile} (${expectedTable.split("\n").length} states).`,
|
|
129
|
+
);
|
|
36
130
|
return;
|
|
37
131
|
}
|
|
38
132
|
|
|
39
|
-
const generatorResult = generateStates(
|
|
133
|
+
const generatorResult = generateStates(productions, { mode: args.lalr === true ? "lalr1" : "lr1" });
|
|
40
134
|
if (generatorResult.success === false) {
|
|
41
|
-
|
|
42
|
-
console.log(buildErrorWindow(grammar, generatorResult.token));
|
|
135
|
+
fail(`${generatorResult.reason}\n\n${buildErrorWindow(grammar, generatorResult.token)}`);
|
|
43
136
|
return;
|
|
44
137
|
}
|
|
45
138
|
|
|
46
|
-
|
|
139
|
+
const table = toTableOrFail(generatorResult.value);
|
|
140
|
+
if (table == null) return;
|
|
141
|
+
|
|
142
|
+
if (useStdout) {
|
|
143
|
+
console.log(table);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (outputFile == null) return;
|
|
148
|
+
|
|
149
|
+
await fs.mkdir(path.dirname(outputFile), { recursive: true });
|
|
150
|
+
await fs.writeFile(outputFile, table, { encoding: "utf-8" });
|
|
151
|
+
|
|
152
|
+
const stateCount = table === "" ? 0 : table.split("\n").length;
|
|
153
|
+
console.log(
|
|
154
|
+
`Wrote ${stateCount} ${args.lalr === true ? "LALR(1)" : "LR(1)"} states from ${inputFile} to ${outputFile}.`,
|
|
155
|
+
);
|
|
47
156
|
}
|
|
48
157
|
|
|
49
158
|
main();
|
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
|
|
|
@@ -127,8 +134,20 @@ const FIELD_SEP = "\x01";
|
|
|
127
134
|
type Item = Production & { dot: number; lookahead: string[]; prodIndex: number; key: string };
|
|
128
135
|
type State = { itemSet: Item[]; kernel: Item; count: number };
|
|
129
136
|
|
|
130
|
-
export type GeneratorOptions = {
|
|
131
|
-
|
|
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
|
+
|
|
132
151
|
const followSetsResult = computeFollowSets(productions);
|
|
133
152
|
if (followSetsResult.success == false) return followSetsResult;
|
|
134
153
|
|
|
@@ -353,6 +372,7 @@ export const generateStates = (productions: Production[], options: GeneratorOpti
|
|
|
353
372
|
existingStateIndex = newState.count;
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
options.onProgress?.(states.length);
|
|
356
376
|
GotoTable[currentState.count]!.set(gotoTransition, existingStateIndex);
|
|
357
377
|
}
|
|
358
378
|
|
|
@@ -385,6 +405,7 @@ export const generateStates = (productions: Production[], options: GeneratorOpti
|
|
|
385
405
|
existingStateIndex = newState.count;
|
|
386
406
|
}
|
|
387
407
|
|
|
408
|
+
options.onProgress?.(states.length);
|
|
388
409
|
ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value: existingStateIndex });
|
|
389
410
|
}
|
|
390
411
|
}
|
|
@@ -463,7 +484,9 @@ function mergeStatesForLALR(
|
|
|
463
484
|
const gotos = new Map<string, number>();
|
|
464
485
|
|
|
465
486
|
for (const member of members) {
|
|
466
|
-
|
|
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()) {
|
|
467
490
|
const value: Action = {
|
|
468
491
|
action: entry.action,
|
|
469
492
|
value: entry.action === "shift" ? oldToNew[entry.value] : entry.value,
|
|
@@ -513,7 +536,15 @@ export class GeneratorResult {
|
|
|
513
536
|
) {}
|
|
514
537
|
|
|
515
538
|
toTable(): string {
|
|
516
|
-
|
|
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) => {
|
|
517
548
|
return [
|
|
518
549
|
...[...actions.entries()].map(([k, { action, value }]) => `${k}=${action === "reduce" ? "r" : "s"}${value}`),
|
|
519
550
|
...(this.GotoTable[idx] === undefined ? [] : [...this.GotoTable[idx].entries()].map(([k, v]) => `${k}=${v}`)),
|
|
@@ -522,14 +553,24 @@ export class GeneratorResult {
|
|
|
522
553
|
}
|
|
523
554
|
|
|
524
555
|
toStates(): TableState[] {
|
|
525
|
-
|
|
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) => {
|
|
526
559
|
const tableState = new TableState();
|
|
527
560
|
|
|
528
|
-
|
|
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()) {
|
|
529
565
|
tableState.actions.set(k, { type: action === "reduce" ? "reduce" : "shift", value });
|
|
566
|
+
tableState.rawActions.set(k, `${action === "reduce" ? "r" : "s"}${value}`);
|
|
567
|
+
}
|
|
530
568
|
|
|
531
569
|
if (this.GotoTable[idx] !== undefined)
|
|
532
|
-
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
|
+
}
|
|
533
574
|
|
|
534
575
|
return tableState;
|
|
535
576
|
});
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,66 @@
|
|
|
1
1
|
export { GeneratorResult, generateStates } from "./generator";
|
|
2
2
|
export type { GeneratorOptions } from "./generator";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
buildProductions,
|
|
6
|
+
tryBuildProductions,
|
|
7
|
+
grammarLexerGenerator,
|
|
8
|
+
getSelfHostedParserGenerator,
|
|
9
|
+
} from "./meta/selfhosted";
|
|
10
|
+
export type { LexerInterface } from "./meta/selfhosted";
|
|
11
|
+
|
|
4
12
|
export { Result } from "./utils/Result";
|
|
5
13
|
export {
|
|
6
14
|
ParserRecoveryFunction,
|
|
7
15
|
Sparse,
|
|
16
|
+
SparseGrammarError,
|
|
8
17
|
LR1ParserGraveError,
|
|
9
18
|
LR1StackSymbol,
|
|
19
|
+
LR1Parser,
|
|
10
20
|
ParserResult,
|
|
11
21
|
ParserError,
|
|
22
|
+
TableAction,
|
|
23
|
+
TableState,
|
|
12
24
|
} from "./parser";
|
|
13
|
-
export {
|
|
14
|
-
|
|
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";
|