UNPKG

simc-ast-builder

Version:

Parser and AST generator for SimulationCraft files

883 lines 38.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConditionOptimizer = void 0; const deep_equal_1 = __importDefault(require("deep-equal")); const fieldMaps_1 = require("../parser/visitors/ast/utils/fieldMaps"); const types_1 = require("../types"); /** * A class that optimizes logical conditions in the AST before code generation. */ class ConditionOptimizer { /** * Create a new ConditionOptimizer * @param options Optimization options */ constructor(options = types_1.DEFAULT_OPTIMIZER_OPTIONS) { /** * Get the sort priority for a nodeType. Higher index = higher priority = earlier. * If not found, returns -1 (lowest priority). */ this.CONDITION_SORT_ORDER = [ "variable", "talent", "equipped", "set_bonus", "stat", "resource", "cooldown", "action", "active_dot", "buff", "debuff", "dot", ]; // Merge with default options this.options = Object.assign(Object.assign({}, types_1.DEFAULT_OPTIMIZER_OPTIONS), options); } /** * Optimize an expression node by applying logical transformations * @param node The expression node to optimize * @returns The optimized expression node */ optimize(node) { // If optimizations are disabled overall, return the node unchanged if (this.options.enabled === false) { return node; } return this.applyOptimizations(node); } /** * Apply absorption laws to simplify expressions * - A && (A || B) → A * - A || (A && B) → A * @param node The node to simplify * @returns The simplified node */ applyAbsorptionLaws(node) { // Process AND nodes if (this.isAndNode(node)) { const andNode = node; // Process both sides first const processedLeft = this.applyAbsorptionLaws(andNode.left); const processedRight = this.applyAbsorptionLaws(andNode.right); // Check for A && (A || B) → A // Check if right operand is an OR node if (processedRight.nodeType === "or") { const orNode = processedRight; // Check if left operand appears in the OR if ((0, deep_equal_1.default)(processedLeft, orNode.left) || (0, deep_equal_1.default)(processedLeft, orNode.right)) { return processedLeft; } } // Check for (A || B) && A → A // Check if left operand is an OR node if (processedLeft.nodeType === "or") { const orNode = processedLeft; // Check if right operand appears in the OR if ((0, deep_equal_1.default)(processedRight, orNode.left) || (0, deep_equal_1.default)(processedRight, orNode.right)) { return processedRight; } } // If no simplification applies, return the AND node with processed operands return Object.assign(Object.assign({}, andNode), { left: processedLeft, right: processedRight }); } // Process OR nodes if (this.isOrNode(node)) { const orNode = node; // Process both sides first const processedLeft = this.applyAbsorptionLaws(orNode.left); const processedRight = this.applyAbsorptionLaws(orNode.right); // Check for A || (A && B) → A // Check if right operand is an AND node if (processedRight.nodeType === "and") { const andNode = processedRight; // Check if left operand appears in the AND if ((0, deep_equal_1.default)(processedLeft, andNode.left) || (0, deep_equal_1.default)(processedLeft, andNode.right)) { return processedLeft; } } // Check for (A && B) || A → A // Check if left operand is an AND node if (processedLeft.nodeType === "and") { const andNode = processedLeft; // Check if right operand appears in the AND if ((0, deep_equal_1.default)(processedRight, andNode.left) || (0, deep_equal_1.default)(processedRight, andNode.right)) { return processedRight; } } // If no simplification applies, return the OR node with processed operands return Object.assign(Object.assign({}, orNode), { left: processedLeft, right: processedRight }); } // Process NOT nodes if (this.isNotNode(node)) { const notNode = node; // Process the argument const processedArgument = this.applyAbsorptionLaws(notNode.argument); return Object.assign(Object.assign({}, notNode), { argument: processedArgument }); } // For other node types, return as is return node; } /** * Apply De Morgan's law to NOT expressions * !(A && B) -> !A || !B * !(A || B) -> !A && !B * @param node The node to transform * @returns The transformed node */ applyDeMorgansLaw(node) { // If this is not a NOT node, recursively process its children if (!this.isNotNode(node)) { // For binary operators (AND, OR), process both sides if (node.nodeType === "and" || node.nodeType === "or") { const binaryNode = node; return Object.assign(Object.assign({}, binaryNode), { left: this.applyDeMorgansLaw(binaryNode.left), right: this.applyDeMorgansLaw(binaryNode.right) }); } // For other node types, return as is return node; } // This is a NOT node const notNode = node; const argument = notNode.argument; // Check if the argument is an AND or OR node if (argument.nodeType === "and" || argument.nodeType === "or") { const binaryNode = argument; // Apply De Morgan's law const newOperator = argument.nodeType === "and" ? "or" : "and"; // Create negated versions of the left and right operands const negatedLeft = { argument: binaryNode.left, expressionType: "boolean", kind: "expression", nodeType: "unary", operator: "not", }; const negatedRight = { argument: binaryNode.right, expressionType: "boolean", kind: "expression", nodeType: "unary", operator: "not", }; // Create a new binary node with the opposite operator and negated operands // If the new operator is "and", create an AND node if (newOperator === "and") { return { expressionType: "boolean", kind: "expression", left: this.applyDeMorgansLaw(negatedLeft), nodeType: "and", operator: "and", right: this.applyDeMorgansLaw(negatedRight), }; } // If the new operator is "or", create an OR node return { expressionType: "boolean", kind: "expression", left: this.applyDeMorgansLaw(negatedLeft), nodeType: "or", operator: "or", right: this.applyDeMorgansLaw(negatedRight), }; } // If not applicable, recursively process the argument return Object.assign(Object.assign({}, notNode), { argument: this.applyDeMorgansLaw(argument) }); } /** * Replace field with negatedName in NOT expressions (e.g., !buff.up → buff.down) * @param node The node to optimize * @returns The optimized node */ applyNegatedFieldOptimization(node) { if (this.isNotNode(node)) { const notNode = node; const processedArgument = this.applyNegatedFieldOptimization(notNode.argument); if (processedArgument["field"] !== undefined && processedArgument["expressionType"] === "boolean") { const fieldDef = (0, fieldMaps_1.getFieldDef)(processedArgument["field"].name); if (fieldDef && fieldDef.negatedName !== fieldDef.name) { const replaceFieldDef = (0, fieldMaps_1.getFieldDef)(fieldDef.negatedName); return Object.assign(Object.assign({}, processedArgument), { expressionType: "boolean", field: replaceFieldDef }); } } return Object.assign(Object.assign({}, notNode), { argument: processedArgument }); } // Recursively process children for AND/OR nodes if (node.nodeType === "and" || node.nodeType === "or") { const binaryNode = node; return Object.assign(Object.assign({}, binaryNode), { left: this.applyNegatedFieldOptimization(binaryNode.left), right: this.applyNegatedFieldOptimization(binaryNode.right) }); } return node; } /** * Apply all available optimizations to the node, respecting option flags * @param node The node to optimize * @returns The optimized node */ applyOptimizations(node) { // Apply optimizations in sequence, but only if enabled let optimizedNode = node; // Apply double negation simplification if (this.options.doubleNegation) { optimizedNode = this.simplifyDoubleNegation(optimizedNode); } // Apply De Morgan's law if (this.options.deMorgansLaw) { optimizedNode = this.applyDeMorgansLaw(optimizedNode); } // Apply constants and identities simplification if (this.options.constantsAndIdentities) { optimizedNode = this.simplifyConstantsAndIdentities(optimizedNode); } // Apply negated field optimization if (this.options.negatedFieldOptimization) { optimizedNode = this.applyNegatedFieldOptimization(optimizedNode); } // Apply complementary terms simplification if (this.options.complementaryTerms) { optimizedNode = this.simplifyComplementaryTerms(optimizedNode); } // Apply absorption laws if (this.options.absorptionLaws) { optimizedNode = this.applyAbsorptionLaws(optimizedNode); } // Flatten nested operations if (this.options.flattenNestedOperations) { optimizedNode = this.flattenNestedOperations(optimizedNode); } // Sort AND/OR conditions by priority (higher = earlier) if enabled if (this.options.conditionSorting) { optimizedNode = this.sortConditionsByPriority(optimizedNode); } // Eliminate common subexpressions if (this.options.commonSubexpressions) { optimizedNode = this.eliminateCommonSubexpressions(optimizedNode); } return optimizedNode; } /** * Check if two nodes are complementary terms (one is the negation of the other) * @param node1 First node to check * @param node2 Second node to check * @returns True if the nodes are complementary terms */ areComplementaryTerms(node1, node2) { // Check if node1 is a NOT node and its argument is equal to node2 if (this.isNotNode(node1)) { const notNode = node1; return (0, deep_equal_1.default)(notNode.argument, node2); } // Check if node2 is a NOT node and its argument is equal to node1 if (this.isNotNode(node2)) { const notNode = node2; return (0, deep_equal_1.default)(notNode.argument, node1); } // Not complementary terms return false; } /** * Create a node representing the boolean value 'false' as a number (0) * @returns A number node with value 0 */ createFalseNode() { return { expressionType: "numeric", kind: "expression", nodeType: "literal", value: "0", }; } /** * Create a node representing the boolean value 'true' as a number (1) * @returns A number node with value 1 */ createTrueNode() { return { expressionType: "numeric", kind: "expression", nodeType: "literal", value: "1", }; } /** * Identify and eliminate common subexpressions * - (A && B) || (A && C) → A && (B || C) * - (A || B) && (A || C) → A || (B && C) * @param node The node to simplify * @returns The simplified node */ eliminateCommonSubexpressions(node) { // Only process OR and AND nodes if (node.nodeType !== "or" && node.nodeType !== "and") { // For NOT nodes, process the argument if (this.isNotNode(node)) { const notNode = node; return Object.assign(Object.assign({}, notNode), { argument: this.eliminateCommonSubexpressions(notNode.argument) }); } // For other node types, return as is return node; } const binaryNode = node; // Process both sides first const processedLeft = this.eliminateCommonSubexpressions(binaryNode.left); const processedRight = this.eliminateCommonSubexpressions(binaryNode.right); // Check for common subexpressions if (node.nodeType === "or") { // Case: (A && B) || (A && C) → A && (B || C) if (processedLeft.nodeType === "and" && processedRight.nodeType === "and") { const leftAndNode = processedLeft; const rightAndNode = processedRight; // Check if there's a common term in both AND expressions if ((0, deep_equal_1.default)(leftAndNode.left, rightAndNode.left)) { // Common term is on the left side of both AND expressions return { expressionType: "boolean", kind: "expression", left: leftAndNode.left, nodeType: "and", operator: "and", right: { expressionType: "boolean", kind: "expression", left: leftAndNode.right, nodeType: "or", operator: "or", right: rightAndNode.right, }, }; } else if ((0, deep_equal_1.default)(leftAndNode.left, rightAndNode.right)) { // Common term is on the left side of first AND and right side of second AND return { expressionType: "boolean", kind: "expression", left: leftAndNode.left, nodeType: "and", operator: "and", right: { expressionType: "boolean", kind: "expression", left: leftAndNode.right, nodeType: "or", operator: "or", right: rightAndNode.left, }, }; } else if ((0, deep_equal_1.default)(leftAndNode.right, rightAndNode.left)) { // Common term is on the right side of first AND and left side of second AND return { expressionType: "boolean", kind: "expression", left: leftAndNode.right, nodeType: "and", operator: "and", right: { expressionType: "boolean", kind: "expression", left: leftAndNode.left, nodeType: "or", operator: "or", right: rightAndNode.right, }, }; } else if ((0, deep_equal_1.default)(leftAndNode.right, rightAndNode.right)) { // Common term is on the right side of both AND expressions return { expressionType: "boolean", kind: "expression", left: leftAndNode.right, nodeType: "and", operator: "and", right: { expressionType: "boolean", kind: "expression", left: leftAndNode.left, nodeType: "or", operator: "or", right: rightAndNode.left, }, }; } } } else if (node.nodeType === "and") { // Case: (A || B) && (A || C) → A || (B && C) if (processedLeft.nodeType === "or" && processedRight.nodeType === "or") { const leftOrNode = processedLeft; const rightOrNode = processedRight; // Check if there's a common term in both OR expressions if ((0, deep_equal_1.default)(leftOrNode.left, rightOrNode.left)) { // Common term is on the left side of both OR expressions return { expressionType: "boolean", kind: "expression", left: leftOrNode.left, nodeType: "or", operator: "or", right: { expressionType: "boolean", kind: "expression", left: leftOrNode.right, nodeType: "and", operator: "and", right: rightOrNode.right, }, }; } else if ((0, deep_equal_1.default)(leftOrNode.left, rightOrNode.right)) { // Common term is on the left side of first OR and right side of second OR return { expressionType: "boolean", kind: "expression", left: leftOrNode.left, nodeType: "or", operator: "or", right: { expressionType: "boolean", kind: "expression", left: leftOrNode.right, nodeType: "and", operator: "and", right: rightOrNode.left, }, }; } else if ((0, deep_equal_1.default)(leftOrNode.right, rightOrNode.left)) { // Common term is on the right side of first OR and left side of second OR return { expressionType: "boolean", kind: "expression", left: leftOrNode.right, nodeType: "or", operator: "or", right: { expressionType: "boolean", kind: "expression", left: leftOrNode.left, nodeType: "and", operator: "and", right: rightOrNode.right, }, }; } else if ((0, deep_equal_1.default)(leftOrNode.right, rightOrNode.right)) { // Common term is on the right side of both OR expressions return { expressionType: "boolean", kind: "expression", left: leftOrNode.right, nodeType: "or", operator: "or", right: { expressionType: "boolean", kind: "expression", left: leftOrNode.left, nodeType: "and", operator: "and", right: rightOrNode.left, }, }; } } } // If no common subexpressions found, return the node with processed operands return Object.assign(Object.assign({}, binaryNode), { left: processedLeft, right: processedRight }); } /** * Flatten nested AND/OR expressions * - A && (B && C) → A && B && C * - A || (B || C) → A || B || C * * Note: Since our AST doesn't support nodes with more than two operands, * we'll restructure the binary tree to achieve the same logical effect. * @param node The node to flatten * @returns The flattened node */ flattenNestedOperations(node) { // Process AND nodes if (node.nodeType === "and") { const andNode = node; // Process both sides first const processedLeft = this.flattenNestedOperations(andNode.left); const processedRight = this.flattenNestedOperations(andNode.right); // Check if right operand is also an AND node if (processedRight.nodeType === "and") { const rightAndNode = processedRight; // Restructure: A && (B && C) → (A && B) && C return { expressionType: "boolean", kind: "expression", left: { expressionType: "boolean", kind: "expression", left: processedLeft, nodeType: "and", operator: "and", right: rightAndNode.left, }, nodeType: "and", operator: "and", right: rightAndNode.right, }; } // If no flattening applies, return the AND node with processed operands return Object.assign(Object.assign({}, andNode), { left: processedLeft, right: processedRight }); } // Process OR nodes if (node.nodeType === "or") { const orNode = node; // Process both sides first const processedLeft = this.flattenNestedOperations(orNode.left); const processedRight = this.flattenNestedOperations(orNode.right); // Check if right operand is also an OR node if (processedRight.nodeType === "or") { const rightOrNode = processedRight; // Restructure: A || (B || C) → (A || B) || C return { expressionType: "boolean", kind: "expression", left: { expressionType: "boolean", kind: "expression", left: processedLeft, nodeType: "or", operator: "or", right: rightOrNode.left, }, nodeType: "or", operator: "or", right: rightOrNode.right, }; } // If no flattening applies, return the OR node with processed operands return Object.assign(Object.assign({}, orNode), { left: processedLeft, right: processedRight }); } // Process NOT nodes if (this.isNotNode(node)) { const notNode = node; // Process the argument const processedArgument = this.flattenNestedOperations(notNode.argument); return Object.assign(Object.assign({}, notNode), { argument: processedArgument }); } // For other node types, return as is return node; } getNodeTypePriority(nodeType) { const sortOrder = this.CONDITION_SORT_ORDER; const idx = sortOrder.indexOf(nodeType); if (idx === -1) { // If not found, return -1 (lowest priority) return -1; } return sortOrder.length - idx; } /** * Check if a node is an AND operator node * @param node The node to check * @returns True if the node is an AND node */ isAndNode(node) { return node.nodeType === "and"; } /** * Check if a node is a NOT operator node (unary with "not" operator) * @param node The node to check * @returns True if the node is a NOT node */ isNotNode(node) { return (node.nodeType === "unary" && node.operator === "not"); } /** * Check if a node is an OR operator node * @param node The node to check * @returns True if the node is an OR node */ isOrNode(node) { return node.nodeType === "or"; } /** * Log the structure of an expression node for debugging * @param node The expression node to log * @param depth Current recursion depth (for indentation) */ logNodeStructure(node, depth = 0) { const indent = " ".repeat(depth); console.log(`${indent}Node Type: ${node.nodeType}`); // Log specific properties based on node type switch (node.nodeType) { case "and": case "or": const binaryNode = node; console.log(`${indent}Operator: ${binaryNode.operator}`); console.log(`${indent}Left:`); this.logNodeStructure(binaryNode.left, depth + 1); console.log(`${indent}Right:`); this.logNodeStructure(binaryNode.right, depth + 1); break; case "unary": const unaryNode = node; console.log(`${indent}Operator: ${unaryNode.operator}`); console.log(`${indent}Argument:`); this.logNodeStructure(unaryNode.argument, depth + 1); break; default: console.log(`${indent}Expression Type: ${node.expressionType}`); break; } } /** * Simplify expressions with complementary terms * - A && !A → false * - A || !A → true * @param node The node to simplify * @returns The simplified node */ simplifyComplementaryTerms(node) { // Process AND nodes if (node.nodeType === "and") { const andNode = node; // Process both sides first const processedLeft = this.simplifyComplementaryTerms(andNode.left); const processedRight = this.simplifyComplementaryTerms(andNode.right); // Check if one operand is the negation of the other // A && !A → false or !A && A → false if (this.areComplementaryTerms(processedLeft, processedRight)) { return this.createFalseNode(); } // If no simplification applies, return the AND node with processed operands return Object.assign(Object.assign({}, andNode), { left: processedLeft, right: processedRight }); } // Process OR nodes if (node.nodeType === "or") { const orNode = node; // Process both sides first const processedLeft = this.simplifyComplementaryTerms(orNode.left); const processedRight = this.simplifyComplementaryTerms(orNode.right); // Check if one operand is the negation of the other // A || !A → true or !A || A → true if (this.areComplementaryTerms(processedLeft, processedRight)) { return this.createTrueNode(); } // If no simplification applies, return the OR node with processed operands return Object.assign(Object.assign({}, orNode), { left: processedLeft, right: processedRight }); } // Process NOT nodes if (this.isNotNode(node)) { const notNode = node; // Process the argument const processedArgument = this.simplifyComplementaryTerms(notNode.argument); return Object.assign(Object.assign({}, notNode), { argument: processedArgument }); } // For other node types, return as is return node; } /** * Simplify expressions involving boolean constants and identical operands * - true && A → A * - false && A → false * - true || A → true * - false || A → A * - !true → false * - !false → true * - A && A → A * - A || A → A * @param node The node to simplify * @returns The simplified node */ simplifyConstantsAndIdentities(node) { // Process NOT nodes with boolean literals if (this.isNotNode(node)) { const notNode = node; // Process the argument first const processedArgument = this.simplifyConstantsAndIdentities(notNode.argument); // Check if the argument is a number representing a boolean if (processedArgument.nodeType === "literal" && processedArgument.expressionType === "numeric") { // !0 → 1, !non-zero → 0 const value = processedArgument["value"]; // Only "0" is considered false, any other value is considered true return value === "0" ? this.createTrueNode() : this.createFalseNode(); } // If not a boolean literal, return the NOT node with processed argument return Object.assign(Object.assign({}, notNode), { argument: processedArgument }); } // Process AND nodes if (node.nodeType === "and") { const andNode = node; // Process both sides first const processedLeft = this.simplifyConstantsAndIdentities(andNode.left); const processedRight = this.simplifyConstantsAndIdentities(andNode.right); // Check for boolean literals // false && A → false if (processedLeft.nodeType === "literal" && processedLeft.expressionType === "numeric" && processedLeft["value"] === "0") { return this.createFalseNode(); } // A && false → false if (processedRight.nodeType === "literal" && processedRight.expressionType === "numeric" && processedRight["value"] === "0") { return this.createFalseNode(); } // true && A → A (any non-zero value is considered true) if (processedLeft.nodeType === "literal" && processedLeft.expressionType === "numeric" && processedLeft["value"] !== "0") { return processedRight; } // A && true → A (any non-zero value is considered true) if (processedRight.nodeType === "literal" && processedRight.expressionType === "numeric" && processedRight["value"] !== "0") { return processedLeft; } // A && A → A (if both sides are identical) if ((0, deep_equal_1.default)(processedLeft, processedRight)) { return processedLeft; } // If no simplification applies, return the AND node with processed operands return Object.assign(Object.assign({}, andNode), { left: processedLeft, right: processedRight }); } // Process OR nodes if (node.nodeType === "or") { const orNode = node; // Process both sides first const processedLeft = this.simplifyConstantsAndIdentities(orNode.left); const processedRight = this.simplifyConstantsAndIdentities(orNode.right); // Check for boolean literals // true || A → true (any non-zero value is considered true) if (processedLeft.nodeType === "literal" && processedLeft.expressionType === "numeric" && processedLeft["value"] !== "0") { return this.createTrueNode(); } // A || true → true (any non-zero value is considered true) if (processedRight.nodeType === "literal" && processedRight.expressionType === "numeric" && processedRight["value"] !== "0") { return this.createTrueNode(); } // false || A → A if (processedLeft.nodeType === "literal" && processedLeft.expressionType === "numeric" && processedLeft["value"] === "0") { return processedRight; } // A || false → A if (processedRight.nodeType === "literal" && processedRight.expressionType === "numeric" && processedRight["value"] === "0") { return processedLeft; } // A || A → A (if both sides are identical) if ((0, deep_equal_1.default)(processedLeft, processedRight)) { return processedLeft; } // If no simplification applies, return the OR node with processed operands return Object.assign(Object.assign({}, orNode), { left: processedLeft, right: processedRight }); } // For other node types, return as is return node; } /** * Simplify double negation (!!A -> A) * @param node The node to simplify * @returns The simplified node */ simplifyDoubleNegation(node) { // If this is not a NOT node, recursively process its children if (!this.isNotNode(node)) { // For binary operators (AND, OR), process both sides if (node.nodeType === "and" || node.nodeType === "or") { const binaryNode = node; return Object.assign(Object.assign({}, binaryNode), { left: this.simplifyDoubleNegation(binaryNode.left), right: this.simplifyDoubleNegation(binaryNode.right) }); } // For other node types, return as is return node; } // This is a NOT node const notNode = node; const argument = notNode.argument; // Check if the argument is also a NOT node (double negation) if (this.isNotNode(argument)) { const innerNotNode = argument; // Return the inner argument, effectively removing both NOT operations return this.simplifyDoubleNegation(innerNotNode.argument); } // If not a double negation, recursively process the argument return Object.assign(Object.assign({}, notNode), { argument: this.simplifyDoubleNegation(argument) }); } /** * Recursively sorts AND/OR conditions by nodeType priority (higher = earlier). * Only sorts if more than 2 operands, otherwise just recurses. */ sortConditionsByPriority(node) { if (node.nodeType !== "and" && node.nodeType !== "or") { return node; } const type = node.nodeType; // Flatten all nodes of the same type const nodes = []; const flatten = (n) => { if (n.nodeType === type) { // @ts-ignore flatten(n.left); // @ts-ignore flatten(n.right); } else { nodes.push(n); } }; flatten(node); if (nodes.length <= 2) { // No need to sort, just recursively process children // @ts-ignore return Object.assign(Object.assign({}, node), { left: this.sortConditionsByPriority(node["left"]), right: this.sortConditionsByPriority(node["right"]) }); } // Sort by priority descending (higher index = earlier) nodes.sort((a, b) => { const prioA = this.getNodeTypePriority(a.nodeType); const prioB = this.getNodeTypePriority(b.nodeType); // Higher priority first return prioB - prioA; }); // Recursively sort children const sorted = nodes.map((n) => this.sortConditionsByPriority(n)); // Rebuild left-deep binary tree let tree = { expressionType: "boolean", kind: "expression", left: sorted[0], nodeType: type, operator: type, right: sorted[1], }; for (let i = 2; i < sorted.length; i++) { tree = { expressionType: "boolean", kind: "expression", left: tree, nodeType: type, operator: type, right: sorted[i], }; } return tree; } } exports.ConditionOptimizer = ConditionOptimizer; //# sourceMappingURL=ConditionOptimizer.js.map