@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
|
@@ -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
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://opencode.ai/config.json",
|
|
3
|
+
"enabled_providers": ["llama", "openrouter"],
|
|
4
|
+
"provider": {
|
|
5
|
+
"llama": {
|
|
6
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
7
|
+
"name": "llama.cpp",
|
|
8
|
+
"options": { "baseURL": "http://192.168.0.78:8080/v1" },
|
|
9
|
+
"models": {
|
|
10
|
+
"your-model-id": {
|
|
11
|
+
"id": "Qwen3.8-27B-GGUF:UD-IQ3_XXS",
|
|
12
|
+
"name": "Qwen3.8-27B-GGUF:UD-IQ3_XXS",
|
|
13
|
+
"limit": { "context": 163840, "output": 8192 }
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scinorandex/sparse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Yet another parser generator",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "tsc -p tsconfig.build.json",
|
|
8
|
-
"watch": "tsc -w"
|
|
8
|
+
"watch": "tsc -w",
|
|
9
|
+
"tc": "tsc --noEmit -p tsconfig.json",
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"bench": "vitest bench"
|
|
9
13
|
},
|
|
10
14
|
"bin": {
|
|
11
15
|
"sparse": "dist/cli.js"
|
|
@@ -27,7 +31,9 @@
|
|
|
27
31
|
"devDependencies": {
|
|
28
32
|
"@types/minimist": "^1.2.5",
|
|
29
33
|
"@types/node": "^26.0.0",
|
|
30
|
-
"@scinorandex/slex": "^0.
|
|
34
|
+
"@scinorandex/slex": "^0.1.2",
|
|
35
|
+
"typescript": "^5.6.0",
|
|
36
|
+
"vitest": "^3.0.0"
|
|
31
37
|
},
|
|
32
38
|
"peerDependencies": {
|
|
33
39
|
"@scinorandex/slex": "*"
|
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();
|