@scinorandex/sparse
Version:
Yet another parser generator
395 lines (334 loc) • 17.6 kB
text/typescript
import { GrammarToken, Production } from "./grammarParser";
import { TableState } from "./parser";
import { Result } from "./utils/Result";
const xContainsAllOfY = <T>(xs: Set<T>, ys: Set<T>) => [...ys].every((x) => xs.has(x));
const EOF_STRING = "[EOF]";
// Creates a list of first sets for each production
function computeFirstSets(allProductions: Production[]): Result<Map<string, Set<string>>> {
// Initialize the hashmap
const ret = new Map<string, Set<string>>();
for (const production of allProductions) ret.set(production.lhs.lexeme, new Set());
// Keep iterating through all productions until no edits are made
let wasEdited = true;
while (wasEdited) {
wasEdited = false;
for (const production of allProductions) {
const toBeModified = ret.get(production.lhs.lexeme)!;
const firstRhsToken = production.rhs[0];
if (firstRhsToken.type === "variable") {
const add = ret.get(firstRhsToken.token.lexeme);
if (add == null) {
return {
success: false,
reason: `Variable "${firstRhsToken.token.lexeme}" doesn't have a corresponding left hand side`,
token: firstRhsToken.token,
};
}
// check if the hashmap of the current production already contains all the items to be added
// if not then add them and set wasEdited to true so we iterate one more time
if (!xContainsAllOfY(toBeModified, add)) {
wasEdited = true;
ret.set(production.lhs.lexeme, new Set([...toBeModified, ...add]));
}
} else if (firstRhsToken.type === "terminal") {
// add terminal to toBeModified and set wasEdited to true
if (toBeModified.has(firstRhsToken.token.lexeme) == false) {
wasEdited = true;
ret.set(production.lhs.lexeme, new Set([...toBeModified, firstRhsToken.token.lexeme]));
}
}
}
}
return { success: true, value: ret };
}
function computeFollowSets(
allProductions: Production[]
): Result<{ firstSets: Map<string, Set<string>>; followSets: Map<string, Set<string>> }> {
const firstSetsResult = computeFirstSets(allProductions);
if (firstSetsResult.success === false) return firstSetsResult;
const firstSets = firstSetsResult.value;
// create a list of follow sets and add EOF to the initial production
const ret = new Map<string, Set<string>>();
for (const production of allProductions) ret.set(production.lhs.lexeme, new Set());
ret.get(allProductions[0].lhs.lexeme)!.add(EOF_STRING);
let wasEdited = true;
while (wasEdited) {
wasEdited = false;
for (const variable of ret.keys()) {
for (const currentProduction of allProductions) {
// check the rhs of current production for variable
const rhs = currentProduction.rhs;
for (let i = 0; i < rhs.length; i++) {
const currentRhsToken = rhs[i];
if (currentRhsToken.type === "variable" && currentRhsToken.token.lexeme === variable) {
if (i === rhs.length - 1) {
// we are at the end of rhs, so whatever is in currentProduction
// we need to also need to add to variable
const toBeAdded = ret.get(currentProduction.lhs.lexeme)!;
const receiver = ret.get(variable)!;
if (!xContainsAllOfY(receiver, toBeAdded)) {
wasEdited = true;
ret.set(variable, new Set([...toBeAdded, ...receiver]));
}
} else {
// not at the end of rhs, so we need to add the FIRST set of the next rhs token
const nextRhsToken = rhs[i + 1];
if (nextRhsToken.type === "terminal") {
// next token is a terminal, check if its in existng follow set and add if it doesn't exist
const existing = ret.get(variable)!;
if (!existing.has(nextRhsToken.token.lexeme)) {
wasEdited = true;
ret.set(variable, new Set([...existing, nextRhsToken.token.lexeme]));
}
} else if (nextRhsToken.type === "variable") {
// next token is a variable, get the first set of the variable
// check if it's not in the followset and add if not
const firstSet = firstSets.get(nextRhsToken.token.lexeme);
if (firstSet == null)
return {
success: false,
reason: `Variable "${nextRhsToken.token.lexeme}" doesn't have a corresponding left hand side`,
token: nextRhsToken.token,
};
if (!xContainsAllOfY(ret.get(variable)!, firstSet)) {
wasEdited = true;
ret.set(variable, new Set([...ret.get(variable)!, ...firstSet]));
}
}
}
}
}
}
}
}
return { success: true, value: { firstSets, followSets: ret } };
}
type Item = Production & { dot: number; lookahead: string[] };
type State = { itemSet: Item[]; kernel: Item; count: number };
export const generateStates = (productions: Production[]): Result<GeneratorResult> => {
const followSetsResult = computeFollowSets(productions);
if (followSetsResult.success == false) return followSetsResult;
const { firstSets, followSets } = followSetsResult.value;
// This funciton takes a token and determines what the next lookahead should be
// The implementation of this function ensures that the table is an LR(1) parsing table
const determineNextLookAhead = (
lhs: GrammarToken,
array: { type: "terminal" | "variable"; token: GrammarToken }[]
): Result<string[]> => {
if (array.length === 0) {
const followSet = followSets.get(lhs.lexeme);
if (followSet != undefined) return { success: true, value: [...followSet] };
return {
success: false,
reason: `Variable ${lhs.lexeme} not present in computed follow sets`,
token: lhs,
};
} else {
const next = array[0];
if (next.type === "terminal") return { success: true, value: [next.token.lexeme] };
const testing = firstSets.get(next.token.lexeme);
if (testing !== undefined) return { success: true, value: [...testing] };
return {
success: false,
reason: `Variable ${next.token.lexeme} not present in computed first sets`,
token: next.token,
};
}
};
// This fnuction creates the initial item set based on the
// production provided. It creating the initial item and expands it
function generateInitialItemSet(production: Production) {
const initialItem = { lhs: production.lhs, rhs: production.rhs, dot: 0, lookahead: [EOF_STRING] } as Item;
return expandItemSet([initialItem]);
}
// This function expands the item sets provided to it
function expandItemSet(items: Item[]): Result<{ itemSet: Item[]; kernel: Item }> {
const itemSet: Item[] = [...items];
// Create a queue of unprocessed items and keep shifting until there are no more items left
const unprocesssedItems: Item[] = [...items];
while (unprocesssedItems.length > 0) {
const currentItem = unprocesssedItems.shift()!;
// Check if the symbol after the dot is a non terminal
const after = currentItem.rhs[currentItem.dot];
if (after == null) continue; // TODO: check if this should be here
if (after.type === "variable") {
// Find prodctions whose left hand side is the symbol after the dot
const newProductions = productions.filter((p) => p.lhs.lexeme === after.token.lexeme);
// Compute the lookahead for the new productions to be added to the item set
const rest = currentItem.rhs.slice(currentItem.dot + 1);
const lookaheadResult = determineNextLookAhead(currentItem.lhs, rest);
if (lookaheadResult.success === false) return lookaheadResult;
const lookahead = lookaheadResult.value;
for (const newProduction of newProductions) {
// Create the new item and check if it already exists in the item set
// If it doesn't exist, add it to the item set and the queue of unprocessed items
const newItem = { lhs: newProduction.lhs, rhs: newProduction.rhs, dot: 0, lookahead } as Item;
const encoding = JSON.stringify(newItem);
if (itemSet.some((i) => JSON.stringify(i) === encoding) == false) {
unprocesssedItems.push(newItem);
itemSet.push(newItem);
}
}
}
}
// Return the item set and the kernel
return { success: true, value: { itemSet, kernel: items[0] } };
}
const initialItemSetResult = generateInitialItemSet(productions[0]);
if (initialItemSetResult.success === false) return initialItemSetResult;
const initialItemSet = initialItemSetResult.value;
function generateStates(_initialState: { itemSet: Item[]; kernel: Item }): Result<{
states: State[];
GotoTable: Map<string, number>[];
ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[];
}> {
const initialState = { ..._initialState, count: 0 };
// Create a list of states to be returned and another list of states that have not been vistited
const states = [initialState] as State[];
const unprocessedStates = [initialState] as State[];
// array of maps whose keys is a variable and the value are the state to goto next
const GotoTable = [] as Map<string, number>[];
// array of maps whose keys is a terminal is a value of either to shift or reduce
const ActionTable = [] as Map<string, { action: "shift" | "reduce"; value: number }>[];
// While there is an unprocessed state, visit it
while (unprocessedStates.length > 0) {
const currentState = unprocessedStates.shift()!;
const nextStates_Goto = new Map<string, Item[]>();
const nextStates_Shift = new Map<string, Item[]>();
// determine transitions out of the current state by iterating
// through all the items in the item set of the state
calculateNextItem: for (const item of currentState.itemSet) {
// check next symbol after the dot
const nextSymbol = item.rhs[item.dot];
// no next symbol is available, therefore we need to create a reduction
if (nextSymbol == null) {
// Find the prodction that this item reduces to using the symbols on its right hand side
// TODO: check if the find can fail, honestly this might be invariant
const productionToReduceTo = productions
.map((p, i) => [p, i] as const)
.find(
([p, _]) => JSON.stringify(p.rhs) === JSON.stringify(item.rhs) && p.lhs.lexeme === item.lhs.lexeme
)![1];
// for each lookahead in the item, create a new entry in the action table to reduce to the production found
for (const lookahead of item.lookahead) {
if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
ActionTable[currentState.count]!.set(lookahead, { action: "reduce", value: productionToReduceTo });
}
continue calculateNextItem;
}
// Create the next item by shifting the dot by 1 place
const newItem: Item = { ...item, dot: item.dot + 1 };
// Determine if we're going to create a GOTO or SHIFT based on the type of the symbol after the dot
if (nextSymbol.type === "variable") {
// Next symbol is a variable so we need to add it to the GOTO table
// Check if the GOTO table already has an entry for the next symbola and either push or create the array
if (nextStates_Goto.has(nextSymbol.token.lexeme)) nextStates_Goto.get(nextSymbol.token.lexeme)!.push(newItem);
else nextStates_Goto.set(nextSymbol.token.lexeme, [newItem]);
} else if (nextSymbol.type === "terminal") {
// Next symbol is a terminal so we need to add it to the SHIFT table
// Check if the SHIFT table already has an entry for the next symbola and either push or create the array
if (nextStates_Shift.has(nextSymbol.token.lexeme))
nextStates_Shift.get(nextSymbol.token.lexeme)!.push(newItem);
else nextStates_Shift.set(nextSymbol.token.lexeme, [newItem]);
}
}
// For each GOTO transition from the current state, expand the item set that it points to
// and check if a state exist with the same itemset already exists
// If it doesn't exist, create a new state and add it to the list of states
// Add the existing / created state to the GOTO table
for (const [gotoTransition, gotoItems] of nextStates_Goto.entries()) {
const expandedItemsResult = expandItemSet(gotoItems);
if (expandedItemsResult.success === false) return expandedItemsResult;
const expandedItems = expandedItemsResult.value;
// check if a state exist with the same item sets
let existingState = states.find((s) => {
return (
s.kernel.lhs === expandedItems.kernel.lhs &&
s.kernel.rhs.length === expandedItems.kernel.rhs.length &&
JSON.stringify(s.kernel.rhs) === JSON.stringify(expandedItems.kernel.rhs) &&
s.kernel.lookahead.length === expandedItems.kernel.lookahead.length &&
JSON.stringify(s.kernel.lookahead) === JSON.stringify(expandedItems.kernel.lookahead) &&
JSON.stringify(s.itemSet) === JSON.stringify(expandedItems.itemSet)
);
});
if (GotoTable[currentState.count] === undefined) GotoTable[currentState.count] = new Map();
if (existingState == undefined) {
const newState: State = {
itemSet: expandedItems.itemSet,
kernel: expandedItems.kernel,
count: states.length,
};
states.push(newState);
unprocessedStates.push(newState);
existingState = newState;
}
GotoTable[currentState.count]!.set(gotoTransition, existingState.count);
}
// For each SHIFT transition from the current state, expand the item set that it points to
// and check if a state exist with the same itemset already exists
// If it doesn't exist, create a new state and add it to the list of states
// Add the existing / created state to the SHIFT table
for (const [shiftTransition, shiftItems] of nextStates_Shift.entries()) {
const expandedItemsResult = expandItemSet(shiftItems);
if (expandedItemsResult.success === false) return expandedItemsResult;
const expandedItems = expandedItemsResult.value;
// check if a state exist with the same item sets
let existingState = states.find((s) => {
return (
s.kernel.lhs === expandedItems.kernel.lhs &&
s.kernel.rhs.length === expandedItems.kernel.rhs.length &&
JSON.stringify(s.kernel.rhs) === JSON.stringify(expandedItems.kernel.rhs) &&
s.kernel.lookahead.length === expandedItems.kernel.lookahead.length &&
JSON.stringify(s.kernel.lookahead) === JSON.stringify(expandedItems.kernel.lookahead) &&
JSON.stringify(s.itemSet) === JSON.stringify(expandedItems.itemSet)
);
});
// If there is no entry in the ActionTable for the current state, create one
if (ActionTable[currentState.count] === undefined) ActionTable[currentState.count] = new Map();
// If no such state exists, create a new state and add it to the list of states
if (existingState == undefined) {
const newState: State = {
itemSet: expandedItems.itemSet,
kernel: expandedItems.kernel,
count: states.length,
};
states.push(newState);
unprocessedStates.push(newState);
existingState = newState;
}
ActionTable[currentState.count]!.set(shiftTransition, { action: "shift", value: existingState.count });
}
}
// return the set of states, the GOTO table and the ACTION table
return { success: true, value: { states, GotoTable, ActionTable } };
}
const StatesResult = generateStates(initialItemSet);
if (StatesResult.success === false) return StatesResult;
const { states, ActionTable, GotoTable } = StatesResult.value;
return { success: true, value: new GeneratorResult(states, ActionTable, GotoTable) };
};
export class GeneratorResult {
constructor(
public readonly states: State[],
public readonly ActionTable: Map<string, { action: "shift" | "reduce"; value: number }>[],
public readonly GotoTable: Map<string, number>[]
) {}
toTable(): string {
return this.ActionTable.map((actions, idx) => {
return [
...[...actions.entries()].map(([k, { action, value }]) => `${k}=${action === "reduce" ? "r" : "s"}${value}`),
...(this.GotoTable[idx] === undefined ? [] : [...this.GotoTable[idx].entries()].map(([k, v]) => `${k}=${v}`)),
].join(", ");
}).join("\n");
}
toStates(): TableState[] {
return this.ActionTable.map((actions, idx) => {
const tableState = new TableState();
for (const [k, { action, value }] of actions.entries())
tableState.actions.set(k, { type: action === "reduce" ? "reduce" : "shift", value });
if (this.GotoTable[idx] !== undefined)
for (const [k, value] of this.GotoTable[idx].entries()) tableState.actions.set(k, { type: "goto", value });
return tableState;
});
}
}