@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.
- package/README.md +132 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +47 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator.d.ts +28 -0
- package/dist/generator.js +290 -0
- package/dist/generator.js.map +1 -0
- package/dist/grammarParser.d.ts +27 -0
- package/dist/grammarParser.js +73 -0
- package/dist/grammarParser.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +102 -0
- package/dist/parser.js +125 -0
- package/dist/parser.js.map +1 -0
- package/dist/tableParser.d.ts +16 -0
- package/dist/tableParser.js +59 -0
- package/dist/tableParser.js.map +1 -0
- package/dist/utils/Result.d.ts +9 -0
- package/dist/utils/Result.js +3 -0
- package/dist/utils/Result.js.map +1 -0
- package/dist/utils/Stack.d.ts +7 -0
- package/dist/utils/Stack.js +26 -0
- package/dist/utils/Stack.js.map +1 -0
- package/dist/utils/errorWindowBuilder.d.ts +2 -0
- package/dist/utils/errorWindowBuilder.js +20 -0
- package/dist/utils/errorWindowBuilder.js.map +1 -0
- package/example/LoLang/Features_Array_Methods.lol +31 -0
- package/example/LoLang/example.ts +214 -0
- package/example/LoLang/grammar.txt +149 -0
- package/example/LoLang/table.txt +497 -0
- package/example/math/example.ts +49 -0
- package/example/math/grammar.txt +11 -0
- package/example/math/table.txt +22 -0
- package/package.json +36 -0
- package/src/cli.ts +49 -0
- package/src/generator.ts +394 -0
- package/src/grammarParser.ts +97 -0
- package/src/index.ts +12 -0
- package/src/parser.ts +207 -0
- package/src/tableParser.ts +69 -0
- package/src/utils/Result.ts +3 -0
- package/src/utils/Stack.ts +23 -0
- package/src/utils/errorWindowBuilder.ts +23 -0
- 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,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
|
+
}
|