diff --git a/decompiler/pipeline/controlflowanalysis/__init__.py b/decompiler/pipeline/controlflowanalysis/__init__.py index ebbc4b673..685e19ac6 100644 --- a/decompiler/pipeline/controlflowanalysis/__init__.py +++ b/decompiler/pipeline/controlflowanalysis/__init__.py @@ -1,4 +1,4 @@ -from .expression_simplification import ExpressionSimplification +from .expression_simplification_rules import ExpressionSimplificationRulesAst, ExpressionSimplificationRulesCfg from .instruction_length_handler import InstructionLengthHandler from .readability_based_refinement import ReadabilityBasedRefinement from .variable_name_generation import VariableNameGeneration diff --git a/decompiler/pipeline/controlflowanalysis/expression_simplification.py b/decompiler/pipeline/controlflowanalysis/expression_simplification.py deleted file mode 100644 index 5064e58a3..000000000 --- a/decompiler/pipeline/controlflowanalysis/expression_simplification.py +++ /dev/null @@ -1,142 +0,0 @@ -"""Module implementing basic simplifications for expressions.""" -from typing import Optional - -from decompiler.pipeline.stage import PipelineStage -from decompiler.structures.ast.ast_nodes import CodeNode -from decompiler.structures.pseudo.expressions import Constant, Expression -from decompiler.structures.pseudo.instructions import Instruction -from decompiler.structures.pseudo.operations import BinaryOperation, Operation, OperationType, UnaryOperation -from decompiler.structures.pseudo.typing import Float, Integer -from decompiler.task import DecompilerTask - - -class ExpressionSimplification(PipelineStage): - """The ExpressionSimplification makes various simplifications to expressions on the AST, like a + 0 = a.""" - - name = "expression-simplification" - - def __init__(self): - self.HANDLERS = { - OperationType.plus: self._simplify_addition, - OperationType.minus: self._simplify_subtraction, - OperationType.multiply: self._simplify_multiplication, - OperationType.divide: self._simplify_division, - OperationType.divide_us: self._simplify_division, - OperationType.divide_float: self._simplify_division, - OperationType.dereference: self._simplify_dereference, - } - - def run(self, task: DecompilerTask): - """Run the task expression simplification on each instruction of the AST.""" - if task.syntax_tree is None: - for instruction in task.graph.instructions: - self.simplify(instruction) - else: - for node in task.syntax_tree.topological_order(): - if not isinstance(node, CodeNode): - continue - for instruction in node.instructions: - self.simplify(instruction) - - def simplify(self, instruction: Instruction): - """Simplify all subexpressions of the given instruction recursively.""" - todo = list(instruction) - while todo and (expression := todo.pop()): - if self.simplify_expression(expression, instruction): - todo = list(instruction) - else: - todo.extend(expression) - - def simplify_expression(self, expression: Expression, parent: Instruction) -> Optional[Expression]: - """Simplify the given instruction utilizing the registered OperationType handlers.""" - if isinstance(expression, Operation) and expression.operation in self.HANDLERS: - if simplified := self.HANDLERS[expression.operation](expression): - parent.substitute(expression, simplified) - return simplified - - def _simplify_addition(self, expression: BinaryOperation) -> Optional[Expression]: - """ - Simplifies the given addition in the given instruction. - - -> Simplifies a+0, 0+a, a-0 and -0 + a to a - """ - if any(self.is_zero_constant(zero := op) for op in expression.operands): - return self.get_other_operand(expression, zero).copy() - - def _simplify_subtraction(self, expression: BinaryOperation) -> Optional[Expression]: - """ - Simplifies the given subtraction in the given instruction. - - -> Simplifies a-0, a-(-0) to a - -> Simplifies 0-a, -0-a to -a - """ - if self.is_zero_constant(expression.operands[1]): - return expression.operands[0].copy() - if self.is_zero_constant(expression.operands[0]): - return self.negate_expression(expression.operands[1]) - - def _simplify_multiplication(self, expression: BinaryOperation) -> Optional[Expression]: - """ - Simplifies the given multiplication in the given instruction. - - -> Simplifies a*0, 0*a, a*(-0) and (-0) * a to 0 - -> Simplifies a*1, 1*a to a - -> Simplifies a*(-1), (-1)*a to -a - """ - if any(self.is_zero_constant(zero := op) for op in expression.operands): - return zero.copy() - if any(self.is_one_constant(one := op) for op in expression.operands): - return self.get_other_operand(expression, one).copy() - if any(self.is_minus_one_constant(minus_one := op) for op in expression.operands): - return self.negate_expression(self.get_other_operand(expression, minus_one)) - - def _simplify_division(self, expression: BinaryOperation) -> Optional[Expression]: - """ - Simplifies the given division in the given instruction. - - -> Simplifies a/1 to a and a/(-1) to -a - """ - if self.is_one_constant(expression.operands[1]): - return expression.operands[0].copy() - if self.is_minus_one_constant(expression.operands[1]): - return self.negate_expression(expression.operands[0]) - - def _simplify_dereference(self, expression: UnaryOperation) -> Optional[Expression]: - """ - Simplifies dereference expression with nested address-of expressions. - - -> Simplifies *(&(x)) to x - """ - if isinstance(expression.operand, UnaryOperation) and expression.operand.operation == OperationType.address: - return expression.operand.operand.copy() - - @staticmethod - def is_zero_constant(expression: Expression) -> bool: - """Checks whether the given expression is 0.""" - return isinstance(expression, Constant) and expression.value == 0 - - @staticmethod - def is_one_constant(expression: Expression) -> bool: - """Checks whether the given expression is 1.""" - return isinstance(expression, Constant) and expression.value == 1 - - @staticmethod - def is_minus_one_constant(expression: Expression) -> bool: - """Checks whether the given expression is -1.""" - return isinstance(expression, Constant) and expression.value == -1 - - @staticmethod - def negate_expression(expression: Expression) -> Expression: - """Negate the given expression and return it.""" - match expression: - case Constant(value=0): return expression - case UnaryOperation(operation=OperationType.negate): return expression.operand - case Constant(type=Integer(is_signed=True) | Float()): return Constant(-expression.value, expression.type) - case _: return UnaryOperation(OperationType.negate, [expression]) - - @staticmethod - def get_other_operand(binary_operation: BinaryOperation, expression: Expression) -> Expression: - """Returns the operand that is not equal to expression.""" - if binary_operation.operands[0] == expression: - return binary_operation.operands[1] - return binary_operation.operands[0] diff --git a/decompiler/pipeline/controlflowanalysis/expression_simplification/rules/rule.py b/decompiler/pipeline/controlflowanalysis/expression_simplification/rules/rule.py new file mode 100644 index 000000000..a4f70334a --- /dev/null +++ b/decompiler/pipeline/controlflowanalysis/expression_simplification/rules/rule.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod + +from decompiler.structures.pseudo import Expression, Operation + + +class SimplificationRule(ABC): + """ + This class defines the interface for simplification rules that can be applied to expressions. + """ + + @abstractmethod + def apply(self, operation: Operation) -> list[tuple[Expression, Expression]]: + """ + Apply the simplification rule to the given operation. + + :param operation: The operation to which the simplification rule should be applied. + :return: A list of tuples, each containing a pair of expressions representing the original + and simplified versions resulting from applying the simplification rule to the given operation. + """ + pass diff --git a/decompiler/pipeline/controlflowanalysis/expression_simplification_rules.py b/decompiler/pipeline/controlflowanalysis/expression_simplification_rules.py new file mode 100644 index 000000000..997711a34 --- /dev/null +++ b/decompiler/pipeline/controlflowanalysis/expression_simplification_rules.py @@ -0,0 +1,117 @@ +import logging +from abc import ABC, abstractmethod + +from decompiler.backend.cexpressiongenerator import CExpressionGenerator +from decompiler.pipeline.controlflowanalysis.expression_simplification.rules.rule import SimplificationRule +from decompiler.pipeline.stage import PipelineStage +from decompiler.structures.ast.ast_nodes import CodeNode +from decompiler.structures.pseudo import Instruction, Operation +from decompiler.structures.visitors.substitute_visitor import SubstituteVisitor +from decompiler.task import DecompilerTask + + +class _ExpressionSimplificationRulesBase(PipelineStage, ABC): + + def run(self, task: DecompilerTask): + max_iterations = task.options.getint("expression-simplification.max_iterations") + simplify_instructions(self._get_instructions(task), max_iterations) + + @abstractmethod + def _get_instructions(self, task: DecompilerTask) -> list[Instruction]: + pass + + +class ExpressionSimplificationRulesCfg(_ExpressionSimplificationRulesBase): + """ + Pipeline stage that simplifies cfg expressions by applying a set of simplification rules. + """ + + name = "expression-simplification-rules-cfg" + + def _get_instructions(self, task: DecompilerTask) -> list[Instruction]: + return list(task.graph.instructions) + + +class ExpressionSimplificationRulesAst(_ExpressionSimplificationRulesBase): + """ + Pipeline stage that simplifies ast expressions by applying a set of simplification rules. + """ + + name = "expression-simplification-rules-ast" + + def _get_instructions(self, task: DecompilerTask) -> list[Instruction]: + instructions = [] + for node in task.syntax_tree.topological_order(): + if isinstance(node, CodeNode): + instructions.extend(node.instructions) + + return instructions + + +_pre_rules: list[SimplificationRule] = [] +_rules: list[SimplificationRule] = [] +_post_rules: list[SimplificationRule] = [] + + +def simplify_instructions(instructions: list[Instruction], max_iterations: int): + rule_sets = [ + ("pre-rules", _pre_rules), + ("rules", _rules), + ("post-rules", _post_rules) + ] + for rule_name, rule_set in rule_sets: + iteration_count = _simplify_instructions_with_rule_set(instructions, rule_set, max_iterations) + if iteration_count <= max_iterations: + logging.info(f"Expression simplification took {iteration_count} iterations for {rule_name}") + else: + logging.warning(f"Exceeded max iteration count for {rule_name}") + + +def _simplify_instructions_with_rule_set( + instructions: list[Instruction], + rule_set: list[SimplificationRule], + max_iterations: int +) -> int: + iteration_count = 0 + + changes = True + while changes: + changes = False + + for rule in rule_set: + for instruction in instructions: + for expression in instruction.subexpressions(): + while True: + if expression is None: + break + if not isinstance(expression, Operation): + break + + substitutions = rule.apply(expression) + if not substitutions: + break + + changes = True + iteration_count += 1 + + if iteration_count > max_iterations: + logging.warning("Took to many iterations for rule set to finish") + return iteration_count + + for i, (replacee, replacement) in enumerate(substitutions): + expression_gen = CExpressionGenerator() + logging.debug( + f"[{rule.__class__.__name__}] {i}. Substituting: '{replacee.accept(expression_gen)}'" + f" with '{replacement.accept(expression_gen)}' in '{expression.accept(expression_gen)}'" + ) + instruction.accept(SubstituteVisitor.identity(replacee, replacement)) + + # This is modifying the expression tree, while we are iterating over it. + # This works because we are iterating depth first and only + # modifying already visited nodes. + + # if expression got replaced, we need to update the reference + if replacee == expression: + expression = replacement + + return iteration_count diff --git a/decompiler/pipeline/default.py b/decompiler/pipeline/default.py index cfce108ec..08091eb3c 100644 --- a/decompiler/pipeline/default.py +++ b/decompiler/pipeline/default.py @@ -1,7 +1,8 @@ """Module defining the available pipelines.""" from decompiler.pipeline.controlflowanalysis import ( - ExpressionSimplification, + ExpressionSimplificationRulesAst, + ExpressionSimplificationRulesCfg, InstructionLengthHandler, ReadabilityBasedRefinement, VariableNameGeneration, @@ -35,10 +36,15 @@ IdentityElimination, CommonSubexpressionElimination, ArrayAccessDetection, - ExpressionSimplification, + ExpressionSimplificationRulesCfg, DeadComponentPruner, GraphExpressionFolding, EdgePruner, ] -AST_STAGES = [ReadabilityBasedRefinement, ExpressionSimplification, InstructionLengthHandler, VariableNameGeneration] +AST_STAGES = [ + ReadabilityBasedRefinement, + ExpressionSimplificationRulesAst, + InstructionLengthHandler, + VariableNameGeneration +] diff --git a/decompiler/util/default.json b/decompiler/util/default.json index 074555644..9c63b780b 100644 --- a/decompiler/util/default.json +++ b/decompiler/util/default.json @@ -581,6 +581,16 @@ "is_hidden_from_gui": true, "is_hidden_from_cli": false, "argument_name": "--loop_break_in_cases" + }, + { + "dest": "expression-simplification.max_iterations", + "default": 10000, + "type": "number", + "title": "The maximum number of iterations the expression simplification is allowed to take", + "description": "Stop simplifying after this number of iterations is exceeded, even if more possible simplifications are possible", + "is_hidden_from_gui": false, + "is_hidden_from_cli": false, + "argument_name": "--max_expression_simplification_iterations" } ] }, @@ -598,7 +608,7 @@ "dead-code-elimination", "expression-propagation-memory", "expression-propagation-function-call", - "expression-simplification", + "expression-simplification-rules-cfg", "dead-code-elimination", "redundant-casts-elimination", "identity-elimination", @@ -618,7 +628,7 @@ "dest": "pipeline.ast_stages", "default": [ "readability-based-refinement", - "expression-simplification", + "expression-simplification-rules-ast", "instruction-length-handler", "variable-name-generation" ], diff --git a/tests/pipeline/controlflowanalysis/test_expression_simplification.py b/tests/pipeline/controlflowanalysis/test_expression_simplification.py deleted file mode 100644 index 11a9a7042..000000000 --- a/tests/pipeline/controlflowanalysis/test_expression_simplification.py +++ /dev/null @@ -1,244 +0,0 @@ -from typing import Optional - -import pytest -from decompiler.pipeline.controlflowanalysis import ExpressionSimplification -from decompiler.structures.ast.ast_nodes import CodeNode -from decompiler.structures.ast.syntaxtree import AbstractSyntaxTree -from decompiler.structures.graphs.cfg import BasicBlock, ControlFlowGraph -from decompiler.structures.logic.logic_condition import LogicCondition -from decompiler.structures.pseudo.expressions import Constant, ImportedFunctionSymbol, Variable -from decompiler.structures.pseudo.instructions import Assignment, Instruction, Return -from decompiler.structures.pseudo.operations import BinaryOperation, Call, ListOperation, OperationType, UnaryOperation -from decompiler.structures.pseudo.typing import Integer -from decompiler.task import DecompilerTask - - -def _task(ast: Optional[AbstractSyntaxTree] = None, cfg: Optional[ControlFlowGraph] = None) -> DecompilerTask: - cfg = ControlFlowGraph() if cfg is None else cfg - task = DecompilerTask("test_function", cfg, ast) - return task - - -x = Variable("x") -y = Variable("y") -const_0 = Constant(0, Integer(32, signed=True)) -const_m0 = Constant(-0, Integer(32, signed=True)) -const_1 = Constant(1, Integer(32, signed=True)) -const_m1 = Constant(-1, Integer(32, signed=True)) - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.plus, [x, const_0])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.plus, [const_0, x])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.plus, [const_0, const_0])), Assignment(y, const_0)), - (Assignment(y, BinaryOperation(OperationType.plus, [x, const_m0])), Assignment(y, x)), - ( - Assignment(y, BinaryOperation(OperationType.plus, [x, BinaryOperation(OperationType.plus, [const_0, const_0])])), - Assignment(y, x), - ), - ( - Assignment(y, BinaryOperation(OperationType.plus, [const_0, BinaryOperation(OperationType.plus, [const_0, x])])), - Assignment(y, x), - ), - ], -) -def test_easy_simplification_with_zero_addition(instruction, result): - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict())) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.multiply, [x, const_0])), Assignment(y, const_0)), - (Assignment(y, BinaryOperation(OperationType.multiply, [const_0, x])), Assignment(y, const_0)), - (Assignment(y, BinaryOperation(OperationType.multiply, [const_0, const_0])), Assignment(y, const_0)), - (Assignment(y, BinaryOperation(OperationType.multiply, [x, const_m0])), Assignment(y, const_0)), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [x, BinaryOperation(OperationType.multiply, [x, const_0])])), - Assignment(y, const_0), - ), - ], -) -def test_simplification_with_zero_multiplication(instruction, result): - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict())) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.minus, (x, const_0))), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.minus, (const_0, x))), Assignment(y, UnaryOperation(OperationType.negate, [x]))), - (Assignment(y, BinaryOperation(OperationType.minus, [const_0, UnaryOperation(OperationType.negate, [x])])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.minus, [const_0, const_m1])), Assignment(y, const_1)), - (Assignment(y, BinaryOperation(OperationType.minus, (x, const_m0))), Assignment(y, x)), - ], -) -def test_simplification_with_zero_subtraction(instruction, result): - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict())) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - ( - Assignment(y, BinaryOperation(OperationType.plus, [x, BinaryOperation(OperationType.multiply, [x, const_0])])), - Assignment(y, x), - ), - ( - Assignment(y, BinaryOperation(OperationType.minus, [y, BinaryOperation(OperationType.minus, [x, const_0])])), - Assignment(y, BinaryOperation(OperationType.minus, [y, x])), - ), - ], -) -def test_simplification_with_zero_mix(instruction, result): - cfg = ControlFlowGraph() - cfg.add_node(BasicBlock(1, [instruction])) - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict()), cfg) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.multiply, [x, const_1])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.multiply, [const_1, x])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.multiply, [const_1, const_1])), Assignment(y, const_1)), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [x, const_m1])), - Assignment(y, UnaryOperation(OperationType.negate, [x])), - ), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [Constant(2, Integer(32, signed=False)), const_m1])), - Assignment(y, UnaryOperation(OperationType.negate, [Constant(2, Integer(32, signed=False))])), - ), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [Constant(2, Integer(32, signed=True)), const_m1])), - Assignment(y, Constant(-2, Integer(32, signed=True))), - ), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [Constant(-2, Integer(32, signed=True)), const_m1])), - Assignment(y, Constant(2, Integer(32, signed=True))), - ), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [x, BinaryOperation(OperationType.multiply, [const_1, const_1])])), - Assignment(y, x), - ), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [const_1, BinaryOperation(OperationType.multiply, [x, const_1])])), - Assignment(y, x), - ), - ], -) -def test_simplification_with_one_multiplication(instruction, result): - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict())) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.divide, [x, const_1])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.divide, [const_1, const_1])), Assignment(y, const_1)), - (Assignment(y, BinaryOperation(OperationType.divide, [const_m1, const_1])), Assignment(y, const_m1)), - (Assignment(y, BinaryOperation(OperationType.divide, [const_1, const_m1])), Assignment(y, const_m1)), - (Assignment(y, BinaryOperation(OperationType.divide, [const_m1, const_m1])), Assignment(y, const_1)), - (Assignment(y, BinaryOperation(OperationType.divide, [x, const_m1])), Assignment(y, UnaryOperation(OperationType.negate, [x]))), - ( - Assignment(y, BinaryOperation(OperationType.divide, [const_0, const_1])), - Assignment(y, const_0), - ), - ( - Assignment(y, BinaryOperation(OperationType.divide, [const_0, const_m1])), - Assignment(y, const_0), - ), - ( - Assignment(y, BinaryOperation(OperationType.divide, [Constant(2, Integer(32, signed=False)), const_m1])), - Assignment(y, UnaryOperation(OperationType.negate, [Constant(2, Integer(32, signed=False))])), - ), - ], -) -def test_simplification_with_one_division(instruction, result): - true_value = LogicCondition.initialize_true(LogicCondition.generate_new_context()) - task = _task(AbstractSyntaxTree(CodeNode([instruction], true_value.copy()), dict())) - ExpressionSimplification().run(task) - assert task.syntax_tree.root == CodeNode([result], true_value.copy()) - - -@pytest.mark.parametrize( - "instruction, result", - [ - ( - Assignment(UnaryOperation(OperationType.dereference, [UnaryOperation(OperationType.address, [Variable("x")])]), Constant(0)), - Assignment(Variable("x"), Constant(0)), - ), - ( - Assignment( - ListOperation([]), - Call( - ImportedFunctionSymbol("foo", 0x42), - [ - BinaryOperation( - OperationType.minus, - [ - UnaryOperation(OperationType.dereference, [UnaryOperation(OperationType.address, [Variable("x")])]), - Constant(2), - ], - ) - ], - ), - ), - Assignment( - ListOperation([]), - Call(ImportedFunctionSymbol("foo", 0x42), [BinaryOperation(OperationType.minus, [Variable("x"), Constant(2)])]), - ), - ), - ( - Return([UnaryOperation(OperationType.dereference, [UnaryOperation(OperationType.address, [Variable("y")])])]), - Return([Variable("y")]), - ), - ], -) -def test_simplification_of_dereference_operations(instruction: Instruction, result: Instruction): - """Check if dereference operations with address-of operands are simplified correctly.""" - ExpressionSimplification().simplify(instruction) - assert instruction == result - - -@pytest.mark.parametrize( - "instruction, result", - [ - (Assignment(y, BinaryOperation(OperationType.divide, [x, const_1])), Assignment(y, x)), - (Assignment(y, BinaryOperation(OperationType.divide, [const_1, const_1])), Assignment(y, const_1)), - (Assignment(y, BinaryOperation(OperationType.multiply, [const_1, const_1])), Assignment(y, const_1)), - ( - Assignment(y, BinaryOperation(OperationType.multiply, [x, const_m1])), - Assignment(y, UnaryOperation(OperationType.negate, [x])), - ), - ( - Assignment(y, BinaryOperation(OperationType.plus, [x, BinaryOperation(OperationType.multiply, [x, const_0])])), - Assignment(y, x), - ), - (Assignment(y, BinaryOperation(OperationType.plus, [x, const_m0])), Assignment(y, x)), - ], -) -def test_for_cfg(instruction, result): - cfg = ControlFlowGraph() - cfg.add_node(BasicBlock(0, [instruction])) - task = _task(cfg=cfg) - ExpressionSimplification().run(task) - assert list(task.graph.instructions) == [result]