@scinorandex/sparse
Version:
Yet another parser generator
208 lines (176 loc) • 8.07 kB
text/typescript
import { Slex, Token } from "@scinorandex/slex";
import { generateStates } from "./generator";
import { Production } from "./grammarParser";
import { Stack } from "./utils/Stack";
import { Result } from "./utils/Result";
export type TableAction = { type: "shift" | "reduce" | "goto"; value: number };
export class TableState {
public readonly actions: Map<string, TableAction>;
constructor() {
this.actions = new Map();
}
public getTerminalAction(terminal: string): TableAction | undefined {
return this.actions.get(`[${terminal}]`);
}
public getVariableAction(variable: string): TableAction | undefined {
return this.actions.get(variable);
}
}
export type LR1StackSymbol<TokenType, Metadata, Node> =
| { type: "token"; token: Token<TokenType, Metadata> }
| { type: "node"; node: Node };
export class Sparse<TokenType, Metadata, Node> {
public constructor(
public readonly options: {
productions: Production[];
states: TableState[];
toStringifiedTokenType: (tokenType: TokenType) => string;
}
) {}
public static tryFromProductions<TokenType, Metadata, Node>(options: {
productions: Production[];
toStringifiedTokenType: (tokenType: TokenType) => string;
}): Result<Sparse<TokenType, Metadata, Node>> {
const statesResult = generateStates(options.productions);
if (statesResult.success === false) return statesResult;
const states = statesResult.value;
const sparse = new Sparse<TokenType, Metadata, Node>({
productions: options.productions,
states: states.toStates(),
toStringifiedTokenType: options.toStringifiedTokenType,
});
return { success: true, value: sparse };
}
public static fromProductions<TokenType, Metadata, Node>(options: {
productions: Production[];
toStringifiedTokenType: (tokenType: TokenType) => string;
}) {
const result = Sparse.tryFromProductions<TokenType, Metadata, Node>(options);
if (result.success === false)
throw new Error(`Encountered error "${result.reason}" at ${result.token.line}:${result.token.column}`);
return result.value;
}
public generate(
lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>,
options: {
reducer: (input: LR1StackSymbol<TokenType, Metadata, Node>[], productionIndex: number) => Node;
recover?: ParserRecoveryFunction<TokenType, Metadata, Node>;
}
) {
return new LR1Parser<TokenType, Metadata, Node>(this.options, options.reducer, options.recover ?? null, lexer);
}
}
export type ParserRecoveryFunction<TokenType, Metadata, Node> = (options: {
lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>;
statesStack: Stack<number>;
symbolsStack: Stack<LR1StackSymbol<TokenType, Metadata, Node>>;
isSafe: () => boolean;
addError: (reason: string) => void;
crash: (reason: string) => { success: false; reason: string };
finish: (options?: { newToken: Token<TokenType, Metadata> }) => {
success: true;
token?: Token<TokenType, Metadata>;
};
states: TableState[];
}) => { success: true; token?: Token<TokenType, Metadata> } | { success: false; reason: string };
export type ParserError<TokenType, Metadata> = { token: Token<TokenType, Metadata>; message: string };
export type ParserResult<TokenType, Metadata, Node> = {
result: Node | null;
errors: ParserError<TokenType, Metadata>[];
};
class LR1Parser<TokenType, Metadata, Node> {
statesStack: Stack<number> = new Stack([0]);
symbolsStack: Stack<LR1StackSymbol<TokenType, Metadata, Node>> = new Stack();
exceptions: ParserError<TokenType, Metadata>[] = [];
public constructor(
public readonly options: {
productions: Production[];
states: TableState[];
toStringifiedTokenType: (tokenType: TokenType) => string;
},
public readonly reducer: (input: LR1StackSymbol<TokenType, Metadata, Node>[], productionIndex: number) => Node,
public readonly recover: ParserRecoveryFunction<TokenType, Metadata, Node> | null,
public readonly lexer: ReturnType<Slex<TokenType, Metadata>["generate"]>
) {}
public parse(): ParserResult<TokenType, Metadata, Node> {
const { productions, states, toStringifiedTokenType } = this.options;
while (true) {
let currentState = this.statesStack.peek();
let token = this.lexer.peekNextToken();
let action = states[currentState].getTerminalAction(toStringifiedTokenType(token.type));
if (action == null) {
if (this.recover === null)
throw new LR1ParserGraveError(
`Invalid syntax. Expected ${[...states[currentState].actions.keys()].join(
","
)} but got ${toStringifiedTokenType(token.type)}`,
token
);
const recoveryResult = this.recover({
lexer: this.lexer,
states,
statesStack: this.statesStack,
symbolsStack: this.symbolsStack,
addError: (reason: string) => this.exceptions.push({ message: reason, token: this.lexer.peekNextToken() }),
crash: (reason: string) => ({ success: false, reason }),
finish: (options?: { newToken: Token<TokenType, Metadata> }) => ({ success: true, token: options?.newToken }),
isSafe: () =>
states[this.statesStack.peek()].actions.has(`[${toStringifiedTokenType(this.lexer.peekNextToken().type)}]`),
});
if (recoveryResult.success === false) throw new LR1ParserGraveError(recoveryResult.reason, token);
token = recoveryResult.token ?? this.lexer.peekNextToken();
currentState = this.statesStack.peek();
action = states[currentState].getTerminalAction(toStringifiedTokenType(token.type));
}
const fixedAction = action as TableAction;
if (fixedAction.type === "shift") {
// add current token to the stack and push the next state
token = this.lexer.getNextToken();
this.statesStack.push(fixedAction.value);
this.symbolsStack.push({ type: "token", token: token });
}
// handle reductions
else if (fixedAction.type === "reduce") {
if (fixedAction.value === 0) break;
const production = productions[fixedAction.value];
// pop the stack and reduce by this production
const popped: LR1StackSymbol<TokenType, Metadata, Node>[] = [];
for (let i = 0; i < production.rhs.length; i++) {
popped.unshift(this.symbolsStack.pop());
this.statesStack.pop();
}
// try to perform the reduction
try {
this.symbolsStack.push({ type: "node", node: this.reducer(popped, fixedAction.value) });
} catch (err) {
const input = JSON.stringify(popped, null, 2);
throw new LR1ParserGraveError(
`Error while performing reduction for production: ${fixedAction.value}. Reduction input: ${input}`,
token
);
}
// get the top node and figure out what state to add to state stack
const topNode = this.statesStack.peek();
const gotoAction = states[topNode].getVariableAction(production.lhs.lexeme);
if (gotoAction == undefined)
throw new LR1ParserGraveError(
`No GOTO action for for state: ${topNode} and production: ${production.lhs}. Actions: Reduce by production ${fixedAction.value}`,
token
);
else if (gotoAction.type !== "goto")
throw new LR1ParserGraveError(
`Expected GOTO action for state: ${topNode} and production: ${production.lhs}. Actions: Reduce by production ${fixedAction.value}. Received: ${gotoAction.type}`,
token
);
this.statesStack.push(gotoAction.value);
}
}
const top = this.symbolsStack.peek();
return { result: top.type === "node" ? top.node : null, errors: this.exceptions };
}
}
export class LR1ParserGraveError<TokenType, Metadata> extends Error {
constructor(public readonly reason: string, public readonly currentToken: Token<TokenType, Metadata>, err?: Error) {
super(reason);
}
}