@scinorandex/sparse
Version:
Yet another parser generator
273 lines (226 loc) • 9.75 kB
text/typescript
import { RegexEngine, Slex } from "@scinorandex/slex";
import { Result, Sparse } from "../index";
import { selfhosted } from "./states";
import { TableState } from "../parser";
import { GrammarToken, GrammarTokenMetadata, GrammarTokenType, hydrateProduction, Production } from "./common";
export const grammarLexerGenerator = new Slex<GrammarTokenType, GrammarTokenMetadata>({
EOF_TYPE: GrammarTokenType.EOF,
isHigherPrecedence: () => false,
});
grammarLexerGenerator.addRule(
"lowercase",
"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",
);
grammarLexerGenerator.addRule(
"uppercase",
"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",
);
grammarLexerGenerator.addRule("letter", "${lowercase} | ${uppercase}");
grammarLexerGenerator.addRule("digit", "0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9");
grammarLexerGenerator.addRule("alphanumeric", "${letter} | ${digit}");
grammarLexerGenerator.addRule(
"identifier",
"(${letter} | $_) (${letter} | ${digit} | $_)*",
GrammarTokenType.IDENTIFIER,
);
grammarLexerGenerator.addRule("colon", "$:", GrammarTokenType.COLON);
grammarLexerGenerator.addRule("semicolon", "$;", GrammarTokenType.SEMICOLON);
grammarLexerGenerator.addRule("langle", "$<", GrammarTokenType.L_ANGLE);
grammarLexerGenerator.addRule("rangle", "$>", GrammarTokenType.R_ANGLE);
grammarLexerGenerator.addRule("lbracket", "$[", GrammarTokenType.L_BRACKET);
grammarLexerGenerator.addRule("rbracket", "$]", GrammarTokenType.R_BRACKET);
grammarLexerGenerator.addRule("lparen", "$(", GrammarTokenType.L_PAREN);
grammarLexerGenerator.addRule("rparen", "$)", GrammarTokenType.R_PAREN);
grammarLexerGenerator.addRule("pipe", "$|", GrammarTokenType.PIPE);
grammarLexerGenerator.addRule("equals", "$=", GrammarTokenType.EQUALS);
grammarLexerGenerator.addRule("comma", "$,", GrammarTokenType.COMMA);
grammarLexerGenerator.addRule("question_mark", "$?", GrammarTokenType.QUESTION_MARK);
grammarLexerGenerator.addRule("number", "(${digit})+", GrammarTokenType.NUMBER);
export abstract class BaseNode {}
export class ListNode<T> extends BaseNode {
private items: T[];
constructor(items: T | T[]) {
super();
if (Array.isArray(items)) this.items = items;
else this.items = [items];
}
add(item: T) {
this.items.push(item);
return this;
}
getItems() {
return this.items;
}
getItemsReversed() {
return this.items.toReversed();
}
}
class ProductionNode extends BaseNode {
constructor(
public readonly lhs: GrammarToken,
public readonly rhs: ListNode<TokenNode | GroupedTokenNode>,
public originalProductionIndex: number,
public readonly name?: string | undefined,
) {
super();
}
toStruct(): Production {
if (this.originalProductionIndex === -1) throw new Error("ProductionNode has no originalProductionIndex");
const hasGrouped = this.rhs.getItemsReversed().some((node) => node instanceof GroupedTokenNode);
if (hasGrouped) throw new Error("Need to unroll before serializing");
return {
lhs: this.lhs,
identifier: `<${this.lhs.lexeme}>`,
name: this.name ?? null,
originalProductionIndex: this.originalProductionIndex,
rhs: this.rhs.getItemsReversed().map((node) => (node as TokenNode).toStruct()),
};
}
setOriginalProductionIndex(index: number) {
this.originalProductionIndex = index;
}
}
function expandProduction(production: ProductionNode): ProductionNode[] {
const rhs = production.rhs.getItemsReversed();
for (let i = 0; i < rhs.length; i++) {
const item = rhs[i];
const beforeItems = rhs.slice(0, i);
const afterItems = rhs.slice(i + 1);
if (item instanceof GroupedTokenNode) {
// need to create two new productions, one optional and the other not
// then apply unrolling on both
const withoutCurrentItem = new ProductionNode(
production.lhs,
new ListNode<TokenNode | GroupedTokenNode>([...beforeItems, ...afterItems].toReversed()),
production.originalProductionIndex,
production.name,
);
const withCurrentItem = new ProductionNode(
production.lhs,
new ListNode<TokenNode | GroupedTokenNode>(
[...beforeItems, ...item.inside.getItemsReversed(), ...afterItems].toReversed(),
),
production.originalProductionIndex,
production.name,
);
const newProductions: ProductionNode[] = [withoutCurrentItem, withCurrentItem];
return newProductions.flatMap((prod) => expandProduction(prod));
} else if (item instanceof TokenNode) {
if (item.variables.getItems().length > 1) {
// this token node has many variants and we should unroll it
const variables = item.variables.getItemsReversed();
const newProductions = variables.map((variable) => {
return new ProductionNode(
production.lhs,
new ListNode<TokenNode | GroupedTokenNode>([
...beforeItems,
new TokenNode("variable", new ListNode<GrammarToken>([variable]), item.name),
]),
production.originalProductionIndex,
production.name,
);
});
return newProductions.flatMap((prod) => expandProduction(prod));
}
} else {
throw new Error("shoudn't reach here, ProductionNode RHS is neither GroupedTokenNode nor TokenNode");
}
}
// if it reached here then its normal
return [production];
}
class TokenNode extends BaseNode {
constructor(
public readonly type: "terminal" | "variable",
public readonly variables: ListNode<GrammarToken>,
public readonly name: string | undefined,
) {
super();
}
toStruct() {
const items = this.variables.getItems();
if (items.length > 1) throw new Error("Not yet unrolled");
const lexeme = items[0].lexeme;
const identifier = this.type === "variable" ? `<${lexeme}>` : `[${lexeme}]`;
const name = this.name ?? null;
return { type: this.type, token: items[0], identifier, name };
}
}
class GroupedTokenNode extends BaseNode {
constructor(public readonly inside: ListNode<TokenNode>) {
super();
}
}
class ProgramNode extends BaseNode {
constructor(public readonly productions: ListNode<ProductionNode>) {
super();
}
unrollProductions() {
const productions = this.productions.getItemsReversed();
for (let i = 0; i < productions.length; i++) productions[i].setOriginalProductionIndex(i);
return productions.flatMap((node) => expandProduction(node));
}
getProductions() {
return this.unrollProductions().map((node) => node.toStruct());
}
}
export function getSelfHostedParserGenerator(e: typeof selfhosted = selfhosted) {
return new Sparse<GrammarTokenType, GrammarTokenMetadata, BaseNode>({
productions: e.productions.map((production) => hydrateProduction(production as any)),
states: e.states.map((state) => TableState.fromJSObject(state as any)),
toStringifiedTokenType: (type: GrammarTokenType) => GrammarTokenType[type],
});
}
type Reducer = (bag: any) => BaseNode;
const reducers: { [key: string]: Reducer } = {
program: (bag: { productions?: ListNode<ProductionNode> }) =>
new ProgramNode(bag.productions ?? new ListNode<ProductionNode>([])),
productions: (bag: { production: ProductionNode; rest?: ListNode<ProductionNode> }) => {
if (bag.rest == null) return new ListNode<ProductionNode>([bag.production]);
else return bag.rest.add(bag.production);
},
production: (bag: {
production_name: GrammarToken;
tokens: ListNode<TokenNode | GroupedTokenNode>;
uuid?: GrammarToken;
}) => new ProductionNode(bag.production_name, bag.tokens, -1, bag.uuid?.lexeme),
tokens: (bag: { token: TokenNode | GroupedTokenNode; rest?: ListNode<TokenNode | GroupedTokenNode> }) => {
if (bag.rest == null) return new ListNode<TokenNode | GroupedTokenNode>([bag.token]);
else return bag.rest.add(bag.token);
},
token: (bag: { token: TokenNode }) => bag.token,
grouped_token: (bag: { tokens: ListNode<TokenNode> }) => new GroupedTokenNode(bag.tokens),
variable: (bag: { inside: ListNode<GrammarToken>; token_name?: GrammarToken }) =>
new TokenNode("variable", bag.inside, bag.token_name?.lexeme),
terminal: (bag: { inside: ListNode<GrammarToken>; token_name?: GrammarToken }) =>
new TokenNode("terminal", bag.inside, bag.token_name?.lexeme),
inside: (bag: { identifier: GrammarToken; rest?: ListNode<GrammarToken> }) => {
if (bag.rest == null) return new ListNode<GrammarToken>([bag.identifier]);
else return bag.rest.add(bag.identifier);
},
};
export const tryBuildProductions = (lexer: LexerInterface | string): Result<Production[]> => {
if (typeof lexer === "string") lexer = grammarLexerGenerator.generate(lexer, () => ({}));
const parserGenerator = getSelfHostedParserGenerator();
const parser = parserGenerator.generate(lexer as RegexEngine<GrammarTokenType, GrammarTokenMetadata>, {
reducer: ({ bag, name }) => {
return reducers[name ?? ""](bag);
},
});
const parsingResult = parser.parse().result as ProgramNode;
const productions = parsingResult.getProductions();
return { success: true, value: productions };
};
interface LexerInterface {
peekNextToken(): GrammarToken;
getNextToken(): GrammarToken;
hasNextToken(): boolean;
}
export const buildProductions = (lexer: LexerInterface | string): Production[] => {
const result = tryBuildProductions(lexer);
if (result.success === false)
throw new Error(
`Encountered error "${result.reason}" while parsing grammar at ${result.token.line}:${result.token.column}`,
);
return result.value;
};