dotlr
Version:
An LR(1) parser generator and visualizer created for educational purposes.
158 lines (139 loc) • 5.4 kB
text/typescript
import type {
Action,
AtomicPattern,
GrammarError,
Item,
ParserError,
ParsingError,
Rule,
Token,
Tree,
} from "./types";
export function stringifyToken(token: Token, noApostrophes = false) {
if (token.type === "Eof") return "$";
if (token.type === "Empty") return "ε";
if (token.type === "Regex") return `%${token.value}`;
if (token.type === "Constant")
return noApostrophes ? token.value : `'${token.value}'`;
return "";
}
export function stringifyAtom(atom: AtomicPattern, noApostrophes = false) {
if (atom.type === "Symbol") return atom.value;
if (atom.type === "Token") return stringifyToken(atom.value, noApostrophes);
return "";
}
export function stringifyItem(item: Item, noApostrophes = false) {
const children = item.rule.pattern.map((a) =>
stringifyAtom(a, noApostrophes),
);
//inserts the dot
children.splice(item.dot, 0, ".");
return `${item.rule.symbol} -> ${children.join(" ")}`;
}
export function stringifyRule(rule: Rule, noApostrophes = false) {
const children = rule.pattern.map((a) => stringifyAtom(a, noApostrophes));
return `${rule.symbol} -> ${children.join(" ")}`;
}
export function stringifyLookahead(item: Token[], noApostrophes = false) {
const children = item.map((t) => stringifyToken(t, noApostrophes));
return children.join(" ");
}
export function stringifyAction(action: Action) {
if (action.type === "Accept") return `a${action.value.rule_index + 1}`;
if (action.type === "Reduce") return `r${action.value.rule_index + 1}`;
if (action.type === "Shift") return `s${action.value.next_state}`;
return "";
}
export function stringifyActionVerbose(
action: Action,
rules: Rule[],
noApostrophes: boolean = false,
) {
if (action.type === "Shift") {
return `Shift ${action.value.next_state}`;
} else if (action.type === "Accept") {
return `Accept ${action.value.rule_index + 1} (${stringifyRule(rules[action.value.rule_index], noApostrophes)})`;
} else if (action.type === "Reduce") {
return `Reduce ${action.value.rule_index + 1} (${stringifyRule(rules[action.value.rule_index], noApostrophes)})`;
}
return "";
}
export function stringifyTreeStack(
tree: Tree[],
noApostrophes = false,
): string[] {
return tree.map((i) => {
if (i.type === "Terminal")
return stringifyToken(i.value.token, noApostrophes);
if (i.type === "NonTerminal") return i.value.symbol;
});
}
export function stringifyTree(
tree: Tree,
indent: string = "",
isLast: boolean = true,
): string {
const linePrefix = isLast ? "└─ " : "├─ ";
let result = "";
if (tree.type === "Terminal") {
const { token, slice } = tree.value;
if (token.type !== "Eof" && token.type !== "Empty") {
result += `${indent}${linePrefix}${token.value} [${slice}]\n`;
}
} else {
const { symbol, pattern } = tree.value;
result += `${indent}${linePrefix}${symbol}\n`;
const newIndent = indent + (isLast ? " " : "│ ");
pattern.forEach((child, index) => {
result += stringifyTree(child, newIndent, index === pattern.length - 1);
});
}
return result;
}
export function stringifyGrammarError(e: GrammarError) {
if (e.type === "UnexpectedToken") {
return `Unexpected token at ${stringifySpan(e.value)}, expected one of:\n${e.value.expected.map(maybeToken).join(", ")}`;
} else if (e.type === "UnexpectedEof") {
return `Unexpected end of input, expected one of:\n${e.value.expected.map(maybeToken).join(", ")}`;
} else if (e.type === "InvalidRegex") {
return `Invalid regular expression at ${stringifySpan(e.value)}\n${e.value.regex}`;
}
return "Unknown error";
}
function maybeToken(token: Token | string) {
return typeof token === "string" ? token : stringifyToken(token);
}
function stringifySpan(span: { column: number; line: number }) {
return `${span.line}:${span.column}`;
}
export function stringifyParsingError(error: ParsingError) {
if (error.type === "UnexpectedEof") {
return `Unexpected end of input at ${stringifySpan(error.value.span)}, expected one of:\n${error.value.expected.map(maybeToken).join(", ")}`;
} else if (error.type === "UnknownToken") {
return `Unknown token at ${stringifySpan(error.value.span)}: ${error.value.token}`;
} else if (error.type === "UnexpectedToken") {
return `Unexpected token at ${stringifySpan(error.value.span)}, expected one of:\n${error.value.expected.map(maybeToken).join(", ")}`;
}
return "Unknown error";
}
export function stringifyParserError(error: ParserError) {
if (error.type === "EmptyGrammar") return "Empty grammar";
if (error.type === "UndefinedSymbol")
return `Undefined symbol: ${error.value.symbol}`;
if (error.type === "UndefinedRegexToken")
return `Undefined regex token: ${error.value.regex_token}`;
if (error.type === "Conflict")
return `Conflict in state ${error.value.state} on token ${stringifyToken(error.value.token)}`;
return "Unknown error";
}
export function stringifyError(
error: GrammarError | ParsingError | ParserError,
) {
const s = stringifyGrammarError(error as GrammarError);
const s2 = stringifyParsingError(error as ParsingError);
const s3 = stringifyParserError(error as ParserError);
if ([s, s2, s3].every((s) => s === "Unknown error")) return "Unknown error";
if (s !== "Unknown error") return s;
if (s2 !== "Unknown error") return s2;
return s3;
}