@scinorandex/sparse
Version:
Yet another parser generator
242 lines (203 loc) • 9.14 kB
text/typescript
import { Slex, Token } from "@scinorandex/slex";
import { generateStates } from "./generator";
import { Stack } from "./utils/Stack";
import { Result } from "./utils/Result";
import { Production } from "./meta/common";
export type TableAction = { type: "shift" | "reduce" | "goto"; value: number };
export class TableState {
public readonly actions: Map<string, TableAction>;
constructor(actions = new Map<string, TableAction>()) {
this.actions = actions;
}
public getTerminalAction(terminal: string): TableAction | undefined {
return this.actions.get(`[${terminal}]`);
}
public getVariableAction(variable: string): TableAction | undefined {
return this.actions.get(variable);
}
toJSObject() {
return Object.fromEntries(this.actions);
}
static fromJSObject(json: ReturnType<TableState["toJSObject"]>) {
return new TableState(new Map(Object.entries(json)));
}
}
type ReducerType<TokenType, Metadata, Node> = (
newInput: { bag: any; name: string | null },
oldInput: { input: LR1StackSymbol<TokenType, Metadata, Node>[]; index: number },
) => Node;
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: ReducerType<TokenType, Metadata, 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: ReducerType<TokenType, Metadata, 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
const productionIndex = production.originalProductionIndex;
try {
// prepare the object that you're going to send to the reducer function
const bag = {} as { [key: string]: Node | Token<TokenType, Metadata> };
for (let i = 0; i < production.rhs.length; i++) {
const rhsItem = production.rhs[i];
const poppedItem = popped[i];
if (rhsItem.name != null)
bag[rhsItem.name] = poppedItem.type === "token" ? poppedItem.token : poppedItem.node;
}
const newInput = { bag, name: production.name };
const oldInput = { input: popped, index: productionIndex };
const node = this.reducer(newInput, oldInput);
this.symbolsStack.push({ type: "node", node });
} catch (err) {
const input = JSON.stringify(popped, null, 2);
console.log(err);
throw new LR1ParserGraveError(
`Error while performing reduction for production: ${productionIndex}. 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.identifier);
if (gotoAction == undefined)
throw new LR1ParserGraveError(
`No GOTO action for for state: ${topNode} and production: ${production.identifier}. Actions: Reduce by production ${productionIndex}`,
token,
);
else if (gotoAction.type !== "goto")
throw new LR1ParserGraveError(
`Expected GOTO action for state: ${topNode} and production: ${production.identifier}. Actions: Reduce by production ${productionIndex}. 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);
}
}