UNPKG

@scinorandex/sparse

Version:

Yet another parser generator

395 lines (334 loc) 17.6 kB
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; }); } }