@scinorandex/sparse 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +132 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +47 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/generator.d.ts +28 -0
  6. package/dist/generator.js +290 -0
  7. package/dist/generator.js.map +1 -0
  8. package/dist/grammarParser.d.ts +27 -0
  9. package/dist/grammarParser.js +73 -0
  10. package/dist/grammarParser.js.map +1 -0
  11. package/dist/index.d.ts +5 -0
  12. package/dist/index.js +15 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/parser.d.ts +102 -0
  15. package/dist/parser.js +125 -0
  16. package/dist/parser.js.map +1 -0
  17. package/dist/tableParser.d.ts +16 -0
  18. package/dist/tableParser.js +59 -0
  19. package/dist/tableParser.js.map +1 -0
  20. package/dist/utils/Result.d.ts +9 -0
  21. package/dist/utils/Result.js +3 -0
  22. package/dist/utils/Result.js.map +1 -0
  23. package/dist/utils/Stack.d.ts +7 -0
  24. package/dist/utils/Stack.js +26 -0
  25. package/dist/utils/Stack.js.map +1 -0
  26. package/dist/utils/errorWindowBuilder.d.ts +2 -0
  27. package/dist/utils/errorWindowBuilder.js +20 -0
  28. package/dist/utils/errorWindowBuilder.js.map +1 -0
  29. package/example/LoLang/Features_Array_Methods.lol +31 -0
  30. package/example/LoLang/example.ts +214 -0
  31. package/example/LoLang/grammar.txt +149 -0
  32. package/example/LoLang/table.txt +497 -0
  33. package/example/math/example.ts +49 -0
  34. package/example/math/grammar.txt +11 -0
  35. package/example/math/table.txt +22 -0
  36. package/package.json +36 -0
  37. package/src/cli.ts +49 -0
  38. package/src/generator.ts +394 -0
  39. package/src/grammarParser.ts +97 -0
  40. package/src/index.ts +12 -0
  41. package/src/parser.ts +207 -0
  42. package/src/tableParser.ts +69 -0
  43. package/src/utils/Result.ts +3 -0
  44. package/src/utils/Stack.ts +23 -0
  45. package/src/utils/errorWindowBuilder.ts +23 -0
  46. package/tsconfig.json +30 -0
package/src/parser.ts ADDED
@@ -0,0 +1,207 @@
1
+ import { Slex, Token } from "@scinorandex/slex";
2
+ import { generateStates } from "./generator";
3
+ import { Production } from "./grammarParser";
4
+ import { Stack } from "./utils/Stack";
5
+ import { Result } from "./utils/Result";
6
+
7
+ export type TableAction = { type: "shift" | "reduce" | "goto"; value: number };
8
+
9
+ export class TableState {
10
+ public readonly actions: Map<string, TableAction>;
11
+
12
+ constructor() {
13
+ this.actions = new Map();
14
+ }
15
+
16
+ public getTerminalAction(terminal: string): TableAction | undefined {
17
+ return this.actions.get(`[${terminal}]`);
18
+ }
19
+
20
+ public getVariableAction(variable: string): TableAction | undefined {
21
+ return this.actions.get(variable);
22
+ }
23
+ }
24
+
25
+ export type LR1StackSymbol<TokenType, Metadata, Node> =
26
+ | { type: "token"; token: Token<TokenType, Metadata> }
27
+ | { type: "node"; node: Node };
28
+
29
+ export class Sparse<TokenType, Metadata, Node> {
30
+ public constructor(
31
+ public readonly options: {
32
+ productions: Production[];
33
+ states: TableState[];
34
+ toStringifiedTokenType: (tokenType: TokenType) => string;
35
+ }
36
+ ) {}
37
+
38
+ public static tryFromProductions<TokenType, Metadata, Node>(options: {
39
+ productions: Production[];
40
+ toStringifiedTokenType: (tokenType: TokenType) => string;
41
+ }): Result<Sparse<TokenType, Metadata, Node>> {
42
+ const statesResult = generateStates(options.productions);
43
+ if (statesResult.success === false) return statesResult;
44
+ const states = statesResult.value;
45
+
46
+ const sparse = new Sparse<TokenType, Metadata, Node>({
47
+ productions: options.productions,
48
+ states: states.toStates(),
49
+ toStringifiedTokenType: options.toStringifiedTokenType,
50
+ });
51
+
52
+ return { success: true, value: sparse };
53
+ }
54
+
55
+ public static fromProductions<TokenType, Metadata, Node>(options: {
56
+ productions: Production[];
57
+ toStringifiedTokenType: (tokenType: TokenType) => string;
58
+ }) {
59
+ const result = Sparse.tryFromProductions<TokenType, Metadata, Node>(options);
60
+ if (result.success === false)
61
+ throw new Error(`Encountered error "${result.reason}" at ${result.token.line}:${result.token.column}`);
62
+ return result.value;
63
+ }
64
+
65
+ public generate(
66
+ lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>,
67
+ options: {
68
+ reducer: (input: LR1StackSymbol<TokenType, Metadata, Node>[], productionIndex: number) => Node;
69
+ recover?: ParserRecoveryFunction<TokenType, Metadata, Node>;
70
+ }
71
+ ) {
72
+ return new LR1Parser<TokenType, Metadata, Node>(this.options, options.reducer, options.recover ?? null, lexer);
73
+ }
74
+ }
75
+
76
+ export type ParserRecoveryFunction<TokenType, Metadata, Node> = (options: {
77
+ lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>;
78
+ statesStack: Stack<number>;
79
+ symbolsStack: Stack<LR1StackSymbol<TokenType, Metadata, Node>>;
80
+ isSafe: () => boolean;
81
+ addError: (reason: string) => void;
82
+ crash: (reason: string) => { success: false; reason: string };
83
+ finish: (options?: { newToken: Token<TokenType, Metadata> }) => {
84
+ success: true;
85
+ token?: Token<TokenType, Metadata>;
86
+ };
87
+ states: TableState[];
88
+ }) => { success: true; token?: Token<TokenType, Metadata> } | { success: false; reason: string };
89
+
90
+ export type ParserError<TokenType, Metadata> = { token: Token<TokenType, Metadata>; message: string };
91
+ export type ParserResult<TokenType, Metadata, Node> = {
92
+ result: Node | null;
93
+ errors: ParserError<TokenType, Metadata>[];
94
+ };
95
+
96
+ class LR1Parser<TokenType, Metadata, Node> {
97
+ statesStack: Stack<number> = new Stack([0]);
98
+ symbolsStack: Stack<LR1StackSymbol<TokenType, Metadata, Node>> = new Stack();
99
+ exceptions: ParserError<TokenType, Metadata>[] = [];
100
+
101
+ public constructor(
102
+ public readonly options: {
103
+ productions: Production[];
104
+ states: TableState[];
105
+ toStringifiedTokenType: (tokenType: TokenType) => string;
106
+ },
107
+ public readonly reducer: (input: LR1StackSymbol<TokenType, Metadata, Node>[], productionIndex: number) => Node,
108
+ public readonly recover: ParserRecoveryFunction<TokenType, Metadata, Node> | null,
109
+ public readonly lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>
110
+ ) {}
111
+
112
+ public parse(): ParserResult<TokenType, Metadata, Node> {
113
+ const { productions, states, toStringifiedTokenType } = this.options;
114
+
115
+ while (true) {
116
+ let currentState = this.statesStack.peek();
117
+ let token = this.lexer.peekNextToken();
118
+ let action = states[currentState].getTerminalAction(toStringifiedTokenType(token.type));
119
+
120
+ if (action == null) {
121
+ if (this.recover === null)
122
+ throw new LR1ParserGraveError(
123
+ `Invalid syntax. Expected ${[...states[currentState].actions.keys()].join(
124
+ ","
125
+ )} but got ${toStringifiedTokenType(token.type)}`,
126
+ token
127
+ );
128
+
129
+ const recoveryResult = this.recover({
130
+ lexer: this.lexer,
131
+ states,
132
+ statesStack: this.statesStack,
133
+ symbolsStack: this.symbolsStack,
134
+ addError: (reason: string) => this.exceptions.push({ message: reason, token: this.lexer.peekNextToken() }),
135
+ crash: (reason: string) => ({ success: false, reason }),
136
+ finish: (options?: { newToken: Token<TokenType, Metadata> }) => ({ success: true, token: options?.newToken }),
137
+ isSafe: () =>
138
+ states[this.statesStack.peek()].actions.has(`[${toStringifiedTokenType(this.lexer.peekNextToken().type)}]`),
139
+ });
140
+
141
+ if (recoveryResult.success === false) throw new LR1ParserGraveError(recoveryResult.reason, token);
142
+
143
+ token = recoveryResult.token ?? this.lexer.peekNextToken();
144
+ currentState = this.statesStack.peek();
145
+ action = states[currentState].getTerminalAction(toStringifiedTokenType(token.type));
146
+ }
147
+
148
+ const fixedAction = action as TableAction;
149
+ if (fixedAction.type === "shift") {
150
+ // add current token to the stack and push the next state
151
+ token = this.lexer.getNextToken();
152
+ this.statesStack.push(fixedAction.value);
153
+ this.symbolsStack.push({ type: "token", token: token });
154
+ }
155
+
156
+ // handle reductions
157
+ else if (fixedAction.type === "reduce") {
158
+ if (fixedAction.value === 0) break;
159
+ const production = productions[fixedAction.value];
160
+
161
+ // pop the stack and reduce by this production
162
+ const popped: LR1StackSymbol<TokenType, Metadata, Node>[] = [];
163
+ for (let i = 0; i < production.rhs.length; i++) {
164
+ popped.unshift(this.symbolsStack.pop());
165
+ this.statesStack.pop();
166
+ }
167
+
168
+ // try to perform the reduction
169
+ try {
170
+ this.symbolsStack.push({ type: "node", node: this.reducer(popped, fixedAction.value) });
171
+ } catch (err) {
172
+ const input = JSON.stringify(popped, null, 2);
173
+ throw new LR1ParserGraveError(
174
+ `Error while performing reduction for production: ${fixedAction.value}. Reduction input: ${input}`,
175
+ token
176
+ );
177
+ }
178
+
179
+ // get the top node and figure out what state to add to state stack
180
+ const topNode = this.statesStack.peek();
181
+ const gotoAction = states[topNode].getVariableAction(production.lhs.lexeme);
182
+
183
+ if (gotoAction == undefined)
184
+ throw new LR1ParserGraveError(
185
+ `No GOTO action for for state: ${topNode} and production: ${production.lhs}. Actions: Reduce by production ${fixedAction.value}`,
186
+ token
187
+ );
188
+ else if (gotoAction.type !== "goto")
189
+ throw new LR1ParserGraveError(
190
+ `Expected GOTO action for state: ${topNode} and production: ${production.lhs}. Actions: Reduce by production ${fixedAction.value}. Received: ${gotoAction.type}`,
191
+ token
192
+ );
193
+
194
+ this.statesStack.push(gotoAction.value);
195
+ }
196
+ }
197
+
198
+ const top = this.symbolsStack.peek();
199
+ return { result: top.type === "node" ? top.node : null, errors: this.exceptions };
200
+ }
201
+ }
202
+
203
+ export class LR1ParserGraveError<TokenType, Metadata> extends Error {
204
+ constructor(public readonly reason: string, public readonly currentToken: Token<TokenType, Metadata>, err?: Error) {
205
+ super(reason);
206
+ }
207
+ }
@@ -0,0 +1,69 @@
1
+ import { Slex } from "@scinorandex/slex";
2
+ import { TableAction, TableState } from "./parser";
3
+
4
+ type TableTokenMetadata = {};
5
+ export enum TableTokenType {
6
+ PRODUCTION_NAME,
7
+ TOKEN_NAME,
8
+ COMMA,
9
+ EQUALS,
10
+ SHIFT_ACTION,
11
+ REDUCE_ACTION,
12
+ GOTO_ACTION,
13
+ EOF,
14
+ }
15
+
16
+ export const tableLexerGenerator = new Slex<TableTokenType, TableTokenMetadata>({
17
+ EOF_TYPE: TableTokenType.EOF,
18
+ isHigherPrecedence: () => false,
19
+ });
20
+
21
+ tableLexerGenerator.addRule(
22
+ "lowercase",
23
+ "a | b | c | d | e | f | g | h | i | j | k | l | m | n | o | p | q | r | s | t | u | v | w | x | y | z"
24
+ );
25
+ tableLexerGenerator.addRule(
26
+ "uppercase",
27
+ "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q | R | S | T | U | V | W | X | Y | Z"
28
+ );
29
+ tableLexerGenerator.addRule("letter", "${lowercase} | ${uppercase}");
30
+ tableLexerGenerator.addRule("digit", "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9");
31
+ tableLexerGenerator.addRule("alphanumeric", "${letter} | ${digit}");
32
+ tableLexerGenerator.addRule("identifier", "(${letter} | ${digit} | $_)*");
33
+ tableLexerGenerator.addRule("comma", "$,", TableTokenType.COMMA);
34
+ tableLexerGenerator.addRule("equals", "$=", TableTokenType.EQUALS);
35
+ tableLexerGenerator.addRule("production_name", "$< ${identifier} $>", TableTokenType.PRODUCTION_NAME);
36
+ tableLexerGenerator.addRule("token_name", "$[ ${identifier} $]", TableTokenType.TOKEN_NAME);
37
+ tableLexerGenerator.addRule("shift_action", "s(${digit})*", TableTokenType.SHIFT_ACTION);
38
+ tableLexerGenerator.addRule("reduce_action", "r(${digit})*", TableTokenType.REDUCE_ACTION);
39
+ tableLexerGenerator.addRule("goto_action", "${digit}(${digit})*", TableTokenType.GOTO_ACTION);
40
+
41
+ export const buildStates = (grammar: string) =>
42
+ grammar.split("\n").map((line) => {
43
+ const lexer = tableLexerGenerator.generate(line, () => ({}));
44
+ const state = new TableState();
45
+
46
+ const expect = (type: TableTokenType) => {
47
+ const token = lexer.peekNextToken();
48
+ if (token.type !== type) throw new Error(`Expected ${TableTokenType[type]}, got ${TableTokenType[token.type]}`);
49
+ lexer.getNextToken();
50
+ };
51
+
52
+ while (lexer.hasNextToken()) {
53
+ const token = lexer.getNextToken();
54
+ expect(TableTokenType.EQUALS);
55
+ const value = lexer.getNextToken();
56
+
57
+ const setting: TableAction =
58
+ token.type === TableTokenType.PRODUCTION_NAME
59
+ ? { type: "goto", value: parseInt(value.lexeme) }
60
+ : value.type === TableTokenType.REDUCE_ACTION
61
+ ? { type: "reduce", value: parseInt(value.lexeme.substring(1)) }
62
+ : { type: "shift", value: parseInt(value.lexeme.substring(1)) };
63
+
64
+ state.actions.set(token.lexeme, setting);
65
+ if (lexer.peekNextToken().type == TableTokenType.COMMA) lexer.getNextToken();
66
+ }
67
+
68
+ return state;
69
+ });
@@ -0,0 +1,3 @@
1
+ import { GrammarToken } from "src/grammarParser";
2
+
3
+ export type Result<T> = { success: true; value: T } | { success: false; reason: string; token: GrammarToken };
@@ -0,0 +1,23 @@
1
+ export class Stack<T> {
2
+ private items: T[] = [];
3
+
4
+ public push(item: T) {
5
+ this.items.push(item);
6
+ }
7
+
8
+ public pop(): T {
9
+ const item = this.items.pop();
10
+ if (item === undefined) throw new Error("Stack is empty");
11
+ return item;
12
+ }
13
+
14
+ public peek(): T {
15
+ const ret = this.items[this.items.length - 1];
16
+ if (ret === undefined) throw new Error("Stack is empty");
17
+ return ret;
18
+ }
19
+
20
+ constructor(items: T[] = []) {
21
+ this.items = items;
22
+ }
23
+ }
@@ -0,0 +1,23 @@
1
+ import type { GrammarToken } from "src/grammarParser";
2
+
3
+ export const buildErrorWindow = (file: string, token: GrammarToken) => {
4
+ const lines = file.split("\n");
5
+
6
+ const length = Math.max(`${token.line}`.length, `${token.column}`.length);
7
+ let second = "",
8
+ third = "";
9
+
10
+ for (let i = 0; i < token.column + length + 3; i++) {
11
+ second += " ";
12
+ third += "-";
13
+ }
14
+ second += "|";
15
+ third += "┘";
16
+
17
+ let window = `${token.line.toString().padStart(length)} | ${
18
+ lines.length <= token.line - 1 ? "" : lines[token.line - 1]
19
+ }\n${second}\n${third}\n`;
20
+
21
+ if (token.line < lines.length) window += `${(token.line + 1).toString().padStart(length)} | ${lines[token.line]}\n`;
22
+ return window;
23
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2017",
4
+ "module": "commonjs",
5
+ "lib": ["dom", "es6", "es2021", "esnext.asynciterable"],
6
+ "skipLibCheck": true,
7
+ "sourceMap": true,
8
+ "outDir": "./dist",
9
+ "moduleResolution": "node",
10
+ "removeComments": true,
11
+ "noImplicitAny": true,
12
+ "strict": true,
13
+ "strictNullChecks": true,
14
+ "strictFunctionTypes": true,
15
+ "noImplicitThis": true,
16
+ // "noUnusedLocals": true,
17
+ // "noUnusedParameters": true,
18
+ "noImplicitReturns": true,
19
+ "noFallthroughCasesInSwitch": true,
20
+ "allowSyntheticDefaultImports": true,
21
+ "esModuleInterop": true,
22
+ "emitDecoratorMetadata": true,
23
+ "experimentalDecorators": true,
24
+ "resolveJsonModule": true,
25
+ "baseUrl": ".",
26
+ "declaration": true
27
+ },
28
+ "exclude": ["node_modules"],
29
+ "include": ["./src/**/*.ts"]
30
+ }