UNPKG

@fauton/cfg

Version:

A package to work with context free grammars and LL1 parsers

245 lines (244 loc) 14.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertToCnf = exports.checkForSelfReference = exports.processSubstitutionsOfLengthTwo = exports.findSubstitutionOfLengthTwo = exports.processLongSubstitutions = exports.findLongSubstitution = exports.findSubstitution = void 0; const simplifyCfg_1 = require("./simplifyCfg"); const generateNewVariable_1 = require("./utils/generateNewVariable"); const populateCfg_1 = require("./utils/populateCfg"); /** * * @param productionRuleEntries An array of tuples (production rule variable, production rule substitutions) * **Example**: `["A", ["a b A", "b a b"]]` * @param callBackFn A callback function that will be called for each production rule substitution * @returns A tuple ([production rule variable, production rule substitution tokens, production rule substitution index ]) if a rule was found else null */ function findSubstitution(productionRuleEntries, // eslint-disable-next-line callBackFn) { // Loop through all the production rule entries for (let productionRuleEntriesIndex = 0; productionRuleEntriesIndex < productionRuleEntries.length; productionRuleEntriesIndex += 1) { // Get the producing variable and its substitutions from the entries const [productionRuleVariable, productionRuleSubstitutions] = productionRuleEntries[productionRuleEntriesIndex]; // Loop through all the substitutions for (let productionRuleSubstitutionsIndex = 0; productionRuleSubstitutionsIndex < productionRuleSubstitutions.length; productionRuleSubstitutionsIndex += 1) { // Split the substitutions to get its tokens const productionRuleSubstitution = productionRuleSubstitutions[productionRuleSubstitutionsIndex]; const tokens = productionRuleSubstitution.split(' '); // If the callback returns a truthy boolean value then this is the rule that must be returned if (callBackFn(tokens)) { // Return the tuple of production rule variable, the substitution tokens and the substitution number return [productionRuleVariable, tokens, productionRuleSubstitutionsIndex]; } } } return null; } exports.findSubstitution = findSubstitution; /** * Returns the first production rule whose `tokens.length > 2` * @param productionRuleEntries An array of tuples (production rule variable, production rule substitutions) * **Example**: `["A", ["a b A", "b a b"]]` * @returns A tuple ([production rule variable, production rule substitution tokens, production rule substitution index ]) if a long rule was found else null */ function findLongSubstitution(productionRuleEntries) { return findSubstitution(productionRuleEntries, (tokens) => tokens.length > 2); } exports.findLongSubstitution = findLongSubstitution; /** * Removes all the rules that have more than 2 tokens and add them as new production rules * Which will result in all substitutions to have chunk length of at-most 2 * @param cfg production rules record and variables array of cfg */ function processLongSubstitutions(cfg) { const { productionRules } = cfg; // Only the current entries will have substitution tokens greater than 2, so no need to recompute it every time const productionRuleEntries = Object.entries(productionRules); // A record to keep track of which substitution got mapped to which variable, // As we dont want to generate new variables for an existing substitution const generatedVariablesSubstitutionRecord = {}; function processLongSubstitution() { // Find a long rule first const longRule = findLongSubstitution(productionRuleEntries); // If no long rules exist then return false, this will break the loop below if (!longRule) { return false; } const [productionRuleVariable, tokens, productionRuleSubstitutionsIndex] = longRule; // Only keep the first substitution chunk and create a separate variable to store the rest // Example tokens = ["a", "A", "c"] => ["a", "<new_variable>"] new_variable = ["A", "c"] const restOfSubstitutionTokens = tokens.slice(0, tokens.length - 1); const restOfSubstitution = restOfSubstitutionTokens.join(' '); // Check if we have already generated a variable for this substitution const generatedVariableSubstitution = generatedVariablesSubstitutionRecord[restOfSubstitution]; // If not generate a new variable const newVariable = generatedVariableSubstitution !== null && generatedVariableSubstitution !== void 0 ? generatedVariableSubstitution : (0, generateNewVariable_1.generateNewVariable)(cfg.variables); // Remove the production rule substitution index as we will be adding a separate substitution with length 2 productionRules[productionRuleVariable].splice(productionRuleSubstitutionsIndex, 1); // Create a new substitution with the first chunk and the new variable productionRules[productionRuleVariable].push(`${newVariable} ${tokens[tokens.length - 1]}`); // If we haven't added the substitution to the record, add the substitution as the key and the generated variable as the value if (!generatedVariableSubstitution) { generatedVariablesSubstitutionRecord[restOfSubstitution] = newVariable; } // Add the new variable to the production rule, if it already exists it will be replaced productionRules[newVariable] = [restOfSubstitution]; return true; } while (processLongSubstitution()) { // } } exports.processLongSubstitutions = processLongSubstitutions; /** * Returns the first production rule whose `tokens.length === 2` * @param productionRuleEntries An array of tuples (production rule variable, production rule substitutions) * **Example**: `["A", ["a b A", "b a b"]]` * @returns A tuple ([production rule variable, production rule substitution tokens, production rule substitution index ]) if a substitution was found else null */ function findSubstitutionOfLengthTwo(productionRuleEntries, variables) { const variablesSet = new Set(variables); return findSubstitution(productionRuleEntries, // If there are two tokens and both of them are not variables (tokens) => { if (tokens.length === 2) { const [leftToken, rightToken] = tokens; const isLeftTokenVariable = variablesSet.has(leftToken); // Check if the right chunk is terminal const isRightTokenVariable = variablesSet.has(rightToken); return !isRightTokenVariable || !isLeftTokenVariable; } else { return false; } }); } exports.findSubstitutionOfLengthTwo = findSubstitutionOfLengthTwo; /** * Processes all substitutions of chunk length two * @param cfg Production rules record, variables and terminals array of cfg */ function processSubstitutionsOfLengthTwo(cfg) { const { productionRules, terminals, variables } = cfg; const terminalsSet = new Set(terminals); // A record to keep track of which terminals map to which variables const generatedVariablesTerminalRecord = {}; // Loop through all variables to see which variables produces a single terminal variables.forEach((variable) => { const terminal = productionRules[variable][0]; if (productionRules[variable].length === 1 && terminalsSet.has(terminal)) { generatedVariablesTerminalRecord[terminal] = variable; } }); function processSubstitutionOfLengthTwo() { var _a, _b, _c, _d; // Find the first substitution of length two const productionRuleEntries = Object.entries(productionRules); const lengthTwoRule = findSubstitutionOfLengthTwo(productionRuleEntries, variables); // If it doesn't exist return false, this will break the loop beneath if (!lengthTwoRule) { return false; } const [productionRuleVariable, tokens, productionRuleSubstitutionsIndex] = lengthTwoRule; // Get the left and right chunk from the tokens, as its guaranteed to be of length two const [leftToken, rightToken] = tokens; // Check if the left chunk is terminal const isLeftTokenTerminal = terminalsSet.has(leftToken); // Check if the right chunk is terminal const isRightTokenTerminal = terminalsSet.has(rightToken); // Steps:- // 1. Check which tokens (left, right) are terminals // 2. Check if we've already generated a variable for the chunk, if not generate new variable // 3. Update the record so that the key is the chunk and value is the terminal // 4. Update the production rules to point to the terminals // 5. Remove the previous substitution by its index // 6. Push the new substitution to the producing variable // If both left and right tokens are terminals // Example: ["a", "b"] => ["A", "B"] => A = ["a"], B = ["b"] if (isLeftTokenTerminal && isRightTokenTerminal) { const newVariable1 = (_a = generatedVariablesTerminalRecord[leftToken]) !== null && _a !== void 0 ? _a : (0, generateNewVariable_1.generateNewVariable)(variables); if (!generatedVariablesTerminalRecord[leftToken]) { generatedVariablesTerminalRecord[leftToken] = newVariable1; } const newVariable2 = (_b = generatedVariablesTerminalRecord[rightToken]) !== null && _b !== void 0 ? _b : (0, generateNewVariable_1.generateNewVariable)(variables); if (!generatedVariablesTerminalRecord[rightToken]) { generatedVariablesTerminalRecord[rightToken] = newVariable2; } productionRules[newVariable1] = [leftToken]; productionRules[newVariable2] = [rightToken]; productionRules[productionRuleVariable].splice(productionRuleSubstitutionsIndex, 1); productionRules[productionRuleVariable].push(`${newVariable1} ${newVariable2}`); } // If only the left chunk is terminal // Example: ["a", "C"] => ["A", "C"] => A = ["a"] else if (isLeftTokenTerminal) { const newVariable = (_c = generatedVariablesTerminalRecord[leftToken]) !== null && _c !== void 0 ? _c : (0, generateNewVariable_1.generateNewVariable)(variables); productionRules[newVariable] = [leftToken]; productionRules[productionRuleVariable].splice(productionRuleSubstitutionsIndex, 1); // The right chunk is not a terminal so keep it as it is productionRules[productionRuleVariable].push(`${newVariable} ${rightToken}`); if (!generatedVariablesTerminalRecord[leftToken]) { generatedVariablesTerminalRecord[leftToken] = newVariable; } } // If only the right chunk is terminal // Example: ["A", "b"] => ["A", "B"] => B = ["b"] else if (isRightTokenTerminal) { const newVariable = (_d = generatedVariablesTerminalRecord[rightToken]) !== null && _d !== void 0 ? _d : (0, generateNewVariable_1.generateNewVariable)(variables); productionRules[newVariable] = [rightToken]; productionRules[productionRuleVariable].splice(productionRuleSubstitutionsIndex, 1); productionRules[productionRuleVariable].push(`${leftToken} ${newVariable}`); if (!generatedVariablesTerminalRecord[rightToken]) { generatedVariablesTerminalRecord[rightToken] = newVariable; } } return true; } // While there are substitutions of length two to be processes, process them while (processSubstitutionOfLengthTwo()) { // } } exports.processSubstitutionsOfLengthTwo = processSubstitutionsOfLengthTwo; /** * Check if a variable references itself in its production rule * @param rules Production rules for the variable * @param variable Producing variable */ function checkForSelfReference(rules, variable) { return rules.some((rule) => { const tokens = new Set(rule.split(' ')); return tokens.has(variable); }); } exports.checkForSelfReference = checkForSelfReference; /** * Converts a cfg to cnf * @param cfg Input cfg to convert to cnf * @returns Resultant cfg converted to cnf */ function convertToCnf(inputCfg) { const cfg = (0, populateCfg_1.populateCfg)(inputCfg); // Make a deep clone of the CFG so as not to modify the input cfg const duplicateCfg = JSON.parse(JSON.stringify(cfg)); // Check if the start variable references itself const doesStartVariableReferencesItself = checkForSelfReference(duplicateCfg.productionRules[duplicateCfg.startVariable], duplicateCfg.startVariable); // Only create a new start variable if the current start variable produces itself if (doesStartVariableReferencesItself) { // Generate a new start variable const newStartStateVariable = (0, generateNewVariable_1.generateNewVariable)(duplicateCfg.variables); // The new start variable should contain all the substitutions of the previous start variable duplicateCfg.productionRules[newStartStateVariable] = [duplicateCfg.startVariable]; // Update the startVariable to point to the newly created one duplicateCfg.startVariable = newStartStateVariable; duplicateCfg.variables.push(newStartStateVariable); } // Simplify the cfg first (0, simplifyCfg_1.simplifyCfg)(duplicateCfg); // Some variables might have been removed after simplification duplicateCfg.variables = Object.keys(duplicateCfg.productionRules); // Process substitutions of tokens.length > 2 first processLongSubstitutions(duplicateCfg); // Process substitutions of tokens.length === 2 next processSubstitutionsOfLengthTwo(duplicateCfg); return duplicateCfg; } exports.convertToCnf = convertToCnf;