@scinorandex/sparse 0.1.1 → 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.
Files changed (65) hide show
  1. package/.github/workflows/ci.yml +41 -0
  2. package/AGENTS.md +31 -11
  3. package/README.md +283 -29
  4. package/dist/cli.js +113 -24
  5. package/dist/cli.js.map +1 -1
  6. package/dist/generator.d.ts +3 -1
  7. package/dist/generator.js +29 -5
  8. package/dist/generator.js.map +1 -1
  9. package/dist/index.d.ts +16 -4
  10. package/dist/index.js +34 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/meta/common.d.ts +17 -0
  13. package/dist/meta/common.js +66 -1
  14. package/dist/meta/common.js.map +1 -1
  15. package/dist/meta/selfhosted.d.ts +4 -5
  16. package/dist/meta/selfhosted.js +49 -36
  17. package/dist/meta/selfhosted.js.map +1 -1
  18. package/dist/parser.d.ts +56 -35
  19. package/dist/parser.js +146 -18
  20. package/dist/parser.js.map +1 -1
  21. package/dist/table/selfhosted.d.ts +10 -1
  22. package/dist/table/selfhosted.js +32 -12
  23. package/dist/table/selfhosted.js.map +1 -1
  24. package/dist/table/validate.d.ts +17 -0
  25. package/dist/table/validate.js +84 -0
  26. package/dist/table/validate.js.map +1 -0
  27. package/dist/utils/Stack.d.ts +4 -0
  28. package/dist/utils/Stack.js +16 -1
  29. package/dist/utils/Stack.js.map +1 -1
  30. package/dist/utils/errorWindowBuilder.js +11 -11
  31. package/dist/utils/errorWindowBuilder.js.map +1 -1
  32. package/dist/utils/loadFiles.d.ts +8 -0
  33. package/dist/utils/loadFiles.js +47 -0
  34. package/dist/utils/loadFiles.js.map +1 -0
  35. package/dist/utils/reducers.d.ts +9 -0
  36. package/dist/utils/reducers.js +56 -0
  37. package/dist/utils/reducers.js.map +1 -0
  38. package/example/LoLang/example.ts +16 -15
  39. package/example/kleene-test/example.ts +35 -17
  40. package/example/math/example.ts +12 -7
  41. package/example/selfhosted/example.ts +30 -24
  42. package/opencode.json +1 -1
  43. package/package.json +2 -2
  44. package/src/cli.ts +132 -23
  45. package/src/generator.ts +49 -8
  46. package/src/index.ts +55 -3
  47. package/src/meta/common.ts +116 -0
  48. package/src/meta/selfhosted.ts +42 -11
  49. package/src/parser.ts +259 -49
  50. package/src/table/selfhosted.ts +48 -12
  51. package/src/table/validate.ts +143 -0
  52. package/src/utils/Stack.ts +22 -2
  53. package/src/utils/errorWindowBuilder.ts +15 -13
  54. package/src/utils/loadFiles.ts +54 -0
  55. package/src/utils/reducers.ts +93 -0
  56. package/test/cli.test.ts +144 -0
  57. package/test/codegen.test.ts +75 -0
  58. package/test/grammar.test.ts +243 -0
  59. package/test/helpers.ts +59 -0
  60. package/test/lalr.test.ts +114 -0
  61. package/test/parser.test.ts +392 -0
  62. package/test/table.test.ts +144 -0
  63. package/tsconfig.json +1 -1
  64. package/example/LoLang/table2.txt +0 -497
  65. package/example/complicated/grammar.txt +0 -37
@@ -1,6 +1,13 @@
1
1
  import { Slex, Token as SlexToken } from "@scinorandex/slex";
2
- import { buildProductions, LR1StackSymbol, Sparse } from "../../src/index";
3
- import fs from "fs/promises";
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
- type Reducer = (bag: any, input: LR1StackSymbol<TokenType, Metadata, Node>[]) => Node;
56
- const reducers: { [key: string]: Reducer } = {
57
- "autogenerated-kleene": (bag, input) => {
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: (bag: { first_number: Node; rest: KleeneNode<{ op: Token; extra_number: Token }> }, input) => {
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 toStringifiedTokenType = (type: TokenType) => TokenType[type];
72
- const productions = buildProductions(await fs.readFile("./example/kleene-test/grammar.txt", "utf8"));
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: ({ bag, name }, { input }) => {
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!);
@@ -1,6 +1,6 @@
1
1
  import { Slex } from "@scinorandex/slex";
2
- import { buildProductions, buildStates, LR1StackSymbol, Sparse } from "../../src/index";
3
- import fs from "fs/promises";
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
- const toStringifiedTokenType = (type: TokenType) => TokenType[type];
38
- const productions = buildProductions(await fs.readFile("./example/math/grammar.txt", "utf8"));
39
- const states = buildStates(await fs.readFile("./example/math/table.txt", "utf8"));
40
- const parserGenerator = new Sparse<TokenType, Metadata, Node>({ productions, states, toStringifiedTokenType });
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 { generateStates } from "../../src";
3
- import { buildProductions, grammarLexerGenerator } from "../../src/meta/selfhosted";
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
- async function codegenMetaGrammar() {
7
- const grammar = await fs.readFile("./example/selfhosted/01-meta-grammar.txt", "utf8");
7
+ const root = path.join(__dirname, "..", "..");
8
8
 
9
- const lexer = grammarLexerGenerator.generate(grammar, () => ({}));
10
- const productions = buildProductions(lexer);
11
- const statesResult = generateStates(productions);
12
- if (statesResult.success === false) throw new Error(statesResult.reason);
13
-
14
- await fs.writeFile(
15
- "./example/selfhosted/codegen-meta.ts",
16
- `export const states = ${JSON.stringify(statesResult.value.toJSObject(), null, 2)};
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 productions = buildProductions(lexer);
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) throw new Error(statesResult.reason);
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
- "./example/selfhosted/codegen-table.ts",
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
- codegenMetaGrammar();
39
- codegenTableGrammar();
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
- "enabled_providers": ["llama"],
3
+ "enabled_providers": ["llama", "openrouter"],
4
4
  "provider": {
5
5
  "llama": {
6
6
  "npm": "@ai-sdk/openai-compatible",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scinorandex/sparse",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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.0.4",
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 { buildErrorWindow } from "./utils/errorWindowBuilder";
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
- function checkFileExists(filepath: string) {
12
- return new Promise<boolean>((resolve, reject) => {
13
- fs.access(filepath, fs.constants.F_OK)
14
- .then(() => resolve(true))
15
- .catch(() => resolve(false));
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 (typeof args.input !== "string" || typeof args.output !== "string") {
21
- console.log("Usage: npx @scinorandex/sparse --input=<input> --output=<output> [--lalr]");
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
- console.log(`Reading grammar from ${inputFile} and outputting table to ${outputFile}\n`);
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
- if (!(await checkFileExists(inputFile))) return console.log(`File ${inputFile} does not exist`);
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 grammar = await fs.readFile(args.input, "utf8");
32
- const productionsResult = tryBuildProductions(grammar);
78
+ const productionsResult = tryBuildProductions(grammar, { onWarning });
33
79
  if (productionsResult.success === false) {
34
- console.log(productionsResult.reason);
35
- console.log(buildErrorWindow(grammar, productionsResult.token));
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(productionsResult.value, { mode: args.lalr === true ? "lalr1" : "lr1" });
133
+ const generatorResult = generateStates(productions, { mode: args.lalr === true ? "lalr1" : "lr1" });
40
134
  if (generatorResult.success === false) {
41
- console.log(generatorResult.reason);
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
- await fs.writeFile(outputFile, generatorResult.value.toTable(), { encoding: "utf-8" });
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 = { mode?: "lr1" | "lalr1" };
131
- export const generateStates = (productions: Production[], options: GeneratorOptions = {}): Result<GeneratorResult> => {
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
- for (const [key, entry] of ActionTable[member].entries()) {
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
- return this.ActionTable.map((actions, idx) => {
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
- return this.ActionTable.map((actions, idx) => {
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
- for (const [k, { action, value }] of actions.entries())
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()) tableState.actions.set(k, { type: "goto", value });
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
- export { buildProductions, tryBuildProductions } from "./meta/selfhosted";
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 { buildStates } from "./table/selfhosted";
14
- export { Production } from "./meta/common";
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";