<?php declare(strict_types=1);

namespace Phan\AST;

use AssertionError;
use ast;
use ast\flags;
use ast\Node;
use function array_map;
use function array_merge;
use function array_pop;
use function count;
use function in_array;

/**
 * This simplifies a PHP AST into a form which is easier to analyze,
 * and returns the new Node.
 * The original \ast\Node objects are not modified.
 *
 * @phan-file-suppress PhanPartialTypeMismatchArgumentInternal
 */
class ASTSimplifier
{
    public function __construct()
    {
    }

    /**
     * @param Node $node
     * @return array<int,Node> (Equivalent list of nodes to [$node], possibly a clone with modifications)
     */
    private static function apply(Node $node) : array
    {
        switch ($node->kind) {
            case \ast\AST_FUNC_DECL:
            case \ast\AST_METHOD:
            case \ast\AST_CLOSURE:
            case \ast\AST_CLASS:
            case \ast\AST_DO_WHILE:
            case \ast\AST_FOREACH:
                return [self::applyToStmts($node)];
            case \ast\AST_FOR:
                return self::normalizeForStatement($node);
            case \ast\AST_WHILE:
                return self::normalizeWhileStatement($node);
            //case \ast\AST_BREAK:
            //case \ast\AST_CONTINUE:
            //case \ast\AST_RETURN:
            //case \ast\AST_THROW:
            //case \ast\AST_EXIT:
            default:
                return [$node];
            case \ast\AST_STMT_LIST:
                return [self::applyToStatementList($node)];
        // Conditional blocks:
            case \ast\AST_IF:
                return self::normalizeIfStatement($node);
            case \ast\AST_TRY:
                return [self::normalizeTryStatement($node)];
        }
    }

    /**
     * @param Node $node - A node which will have its child statements simplified.
     * @return Node - The same node, or an equivalent simplified node
     */
    private static function applyToStmts(Node $node) : Node
    {
        $stmts = $node->children['stmts'];
        // Can be null, a single statement, or (possibly) a scalar instead of a node?
        if (!($stmts instanceof Node)) {
            return $node;
        }
        $new_stmts = self::applyToStatementList($stmts);
        if ($new_stmts === $stmts) {
            return $node;
        }
        $new_node = clone($node);
        $new_node->children['stmts'] = $new_stmts;
        return $new_node;
    }

    /**
     * @param Node $statement_list - The statement list to simplify
     * @return Node - an equivalent statement list (Identical, or a clone)
     */
    private static function applyToStatementList(Node $statement_list) : Node
    {
        if ($statement_list->kind !== \ast\AST_STMT_LIST) {
            $statement_list = self::buildStatementList($statement_list->lineno ?? 0, $statement_list);
        }
        $new_children = [];
        foreach ($statement_list->children as $child_node) {
            if ($child_node instanceof Node) {
                foreach (self::apply($child_node) as $new_child_node) {
                    $new_children[] = $new_child_node;
                }
            } else {
                $new_children[] = $child_node;
            }
        }
        list($new_children, $modified) = self::normalizeStatementList($new_children);
        if (!$modified && $new_children === $statement_list->children) {
            return $statement_list;
        }
        $clone_node = clone($statement_list);
        $clone_node->children = $new_children;
        return $clone_node;
    }

    /**
     * Creates a new node with kind \ast\AST_STMT_LIST from a list of 0 or more child nodes.
     */
    private static function buildStatementList(int $lineno, Node ...$child_nodes) : Node
    {
        return new Node(
            \ast\AST_STMT_LIST,
            0,
            $child_nodes,
            $lineno
        );
    }

    /**
     * @param array<int,?Node|?float|?int|?string|?float|?bool> $statements
     * @return array{0:array<int,Node>,1:bool} - [New/old list, bool $modified] An equivalent list after simplifying (or the original list)
     */
    private static function normalizeStatementList(array $statements) : array
    {
        $modified = false;
        $new_statements = [];
        foreach ($statements as $stmt) {
            $new_statements[] = $stmt;
            if (!($stmt instanceof Node)) {
                continue;
            }
            if ($stmt->kind !== \ast\AST_IF) {
                continue;
            }
            // Run normalizeIfStatement again.
            \array_pop($new_statements);
            \array_push($new_statements, ...self::normalizeIfStatement($stmt));
            $modified = $modified || \end($new_statements) !== $stmt;
            continue;
        }
        return [$modified ? $new_statements : $statements, $modified];
    }

    /**
     * Replaces the last node in a list with a list of 0 or more nodes
     * @param array<int,Node> $nodes
     * @param Node ...$new_statements
     * @return void
     */
    private static function replaceLastNodeWithNodeList(array &$nodes, Node... $new_statements)
    {
        if (\array_pop($nodes) === false) {
            throw new AssertionError("Saw an unexpected empty node list");
        }
        foreach ($new_statements as $stmt) {
            $nodes[] = $stmt;
        }
    }

    const NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS = [
        flags\BINARY_BOOL_XOR,
        flags\BINARY_IS_IDENTICAL,
        flags\BINARY_IS_NOT_IDENTICAL,
        flags\BINARY_IS_EQUAL,
        flags\BINARY_IS_NOT_EQUAL,
        flags\BINARY_IS_SMALLER,
        flags\BINARY_IS_SMALLER_OR_EQUAL,
        flags\BINARY_IS_GREATER,
        flags\BINARY_IS_GREATER_OR_EQUAL,
        flags\BINARY_SPACESHIP,
    ];

    /**
     * If this returns true, the expression has no side effects, and can safely be reordered.
     * (E.g. returns true for `MY_CONST` or `false` in `if (MY_CONST === ($x = y))`
     *
     * @param Node|string|float|int $node
     * @internal the way this behaves may change
     */
    public static function isExpressionWithoutSideEffects($node) : bool
    {
        if (!($node instanceof Node)) {
            return true;
        }
        switch ($node->kind) {
            case \ast\AST_CONST:
            case \ast\AST_MAGIC_CONST:
            case \ast\AST_NAME:
                return true;
            case \ast\AST_UNARY_OP:
                return self::isExpressionWithoutSideEffects($node->children['expr']);
            case \ast\AST_BINARY_OP:
                return self::isExpressionWithoutSideEffects($node->children['left']) &&
                    self::isExpressionWithoutSideEffects($node->children['right']);
            case \ast\AST_CLASS_CONST:
                return self::isExpressionWithoutSideEffects($node->children['class']);
            default:
                return false;
        }
    }

    /**
     * Converts an if statement to one which is easier for phan to analyze
     * E.g. repeatedly makes these conversions
     * if (A && B) {X} -> if (A) { if (B) {X}}
     * if ($var = A) {X} -> $var = A; if ($var) {X}
     * @return array<int,Node> - One or more nodes created from $original_node.
     *        Will return [$original_node] if no modifications were made.
     */
    private static function normalizeIfStatement(Node $original_node) : array
    {
        $nodes = [$original_node];
        // Repeatedly apply these rules
        do {
            $old_nodes = $nodes;
            $node = $nodes[count($nodes) - 1];
            $node->flags = 0;
            $if_cond = $node->children[0]->children['cond'];
            if (!($if_cond instanceof Node)) {
                break;  // No transformation rules apply here.
            }

            if ($if_cond->kind === \ast\AST_UNARY_OP &&
                $if_cond->flags === flags\UNARY_BOOL_NOT) {
                $cond_node = $if_cond->children['expr'];
                if ($cond_node instanceof Node &&
                        $cond_node->kind === \ast\AST_UNARY_OP &&
                        $cond_node->flags === flags\UNARY_BOOL_NOT) {
                    self::replaceLastNodeWithNodeList($nodes, self::applyIfDoubleNegateReduction($node));
                    continue;
                }
                if (count($node->children) === 1) {
                    self::replaceLastNodeWithNodeList($nodes, self::applyIfNegatedToIfElseReduction($node));
                    continue;
                }
            }
            if ($if_cond->kind === \ast\AST_BINARY_OP && in_array($if_cond->flags, self::NON_SHORT_CIRCUITING_BINARY_OPERATOR_FLAGS, true)) {
                // if (($var = A) === B) {X} -> $var = A; if ($var === B) { X}
                $if_cond_children = $if_cond->children;
                if (in_array($if_cond_children['left']->kind ?? 0, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true) &&
                        ($if_cond_children['left']->children['var']->kind ?? 0) === \ast\AST_VAR &&
                        self::isExpressionWithoutSideEffects($if_cond_children['right'])) {
                    self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInLeftSideOfBinaryOpReduction($node));
                    continue;
                }
                if (in_array($if_cond_children['right']->kind ?? 0, [\ast\AST_ASSIGN, \ast\AST_ASSIGN_REF], true) &&
                        ($if_cond_children['right']->children['var']->kind ?? 0) === \ast\AST_VAR &&
                        self::isExpressionWithoutSideEffects($if_cond_children['left'])) {
                    self::replaceLastNodeWithNodeList($nodes, ...self::applyAssignInRightSideOfBinaryOpReduction($node));
                    continue;
                }
                // TODO: If the left-hand side is a constant or class constant or literal, that's safe to rearrange as well
                // (But `foo($y = something()) && $x = $y` is not safe to rearrange)
            }
            if (count($node->children) === 1) {
                if ($if_cond->kind === \ast\AST_BINARY_OP &&
                        $if_cond->flags === flags\BINARY_BOOL_AND) {
                    self::replaceLastNodeWithNodeList($nodes, self::applyIfAndReduction($node));
                    // if (A && B) {X} -> if (A) { if (B) {X}}
                    // Do this, unless there is an else statement that can be executed.
                    continue;
                }
            } elseif (count($node->children) === 2) {
                if ($if_cond->kind === \ast\AST_UNARY_OP &&
                        $if_cond->flags === flags\UNARY_BOOL_NOT &&
                        $node->children[1]->children['cond'] === null) {
                    self::replaceLastNodeWithNodeList($nodes, self::applyIfNegateReduction($node));
                    continue;
                }
            } elseif (count($node->children) >= 3) {
                self::replaceLastNodeWithNodeList($nodes, self::applyIfChainReduction($node));
                continue;
            }
            if ($if_cond->kind === \ast\AST_ASSIGN &&
                    ($if_cond->children['var']->kind ?? null) === \ast\AST_VAR) {
                // if ($var = A) {X} -> $var = A; if ($var) {X}
                // do this whether or not there is an else.
                // TODO: Could also reduce `if (($var = A) && B) {X} else if (C) {Y} -> $var = A; ....
                self::replaceLastNodeWithNodeList($nodes, ...self::applyIfAssignReduction($node));
                continue;
            }
        } while ($old_nodes !== $nodes);
        return $nodes;
    }

    /**
     * Converts a while statement to one which is easier for phan to analyze
     * E.g. repeatedly makes these conversions
     * while (A && B) {X} -> while (A) { if (!B) {break;} X}
     * while (!!A) {X} -> while (A) { X }
     * @return array{0:Node} - An array with a single while statement
     *        Will return [$original_node] if no modifications were made.
     */
    private static function normalizeWhileStatement(Node $original_node) : array
    {
        $node = $original_node;
        // Repeatedly apply these rules
        while (true) {
            $while_cond = $node->children['cond'];
            if (!($while_cond instanceof Node)) {
                break;  // No transformation rules apply here.
            }

            if ($while_cond->kind === \ast\AST_UNARY_OP &&
                $while_cond->flags === flags\UNARY_BOOL_NOT) {
                $cond_node = $while_cond->children['expr'];
                if ($cond_node instanceof Node &&
                        $cond_node->kind === \ast\AST_UNARY_OP &&
                        $cond_node->flags === flags\UNARY_BOOL_NOT) {
                    $node = self::applyWhileDoubleNegateReduction($node);
                    continue;
                }
                break;
            }
            if ($while_cond->kind === \ast\AST_BINARY_OP &&
                    $while_cond->flags === flags\BINARY_BOOL_AND) {
                // TODO: Also support `and` operator.
                $node = self::applyWhileAndReduction($node);
                // while (A && B) {X} -> while (A) { if (!B) {break;} X}
                // Do this, unless there is an else statement that can be executed.
                continue;
            }
            break;
        }

        return [$node];
    }

    /**
     * Converts a while statement to one which is easier for phan to analyze
     * E.g. repeatedly makes these conversions
     * while (A && B) {X} -> while (A) { if (!B) {break;} X}
     * while (!!A) {X} -> while (A) { X }
     * @return array{0:Node} - An array with a single while statement
     *        Will return [$node] if no modifications were made.
     */
    private static function normalizeForStatement(Node $node) : array
    {
        // Repeatedly apply these rules
        while (true) {
            $for_cond_list = $node->children['cond'];
            if (!($for_cond_list instanceof Node)) {
                break;  // No transformation rules apply here.
            }

            $for_cond = \end($for_cond_list->children);

            if (!($for_cond instanceof Node)) {
                break;
            }
            if ($for_cond->kind === \ast\AST_UNARY_OP &&
                $for_cond->flags === flags\UNARY_BOOL_NOT) {
                $cond_node = $for_cond->children['expr'];
                if ($cond_node instanceof Node &&
                        $cond_node->kind === \ast\AST_UNARY_OP &&
                        $cond_node->flags === flags\UNARY_BOOL_NOT) {
                    $node = self::applyForDoubleNegateReduction($node);
                    continue;
                }
                break;
            }
            if ($for_cond->kind === \ast\AST_BINARY_OP &&
                    $for_cond->flags === flags\BINARY_BOOL_AND) {
                // TODO: Also support `and` operator.
                $node = self::applyForAndReduction($node);
                // for (init ; A && B; loop) {X} -> for (init; A; loop) { if (!B) {break;} X}
                // Do this, unless there is an else statement that can be executed.
                continue;
            }
            break;
        }

        return [$node];
    }

    /**
     * if (($var = A) === B) {X} -> $var = A; if ($var === B) { X }
     *
     * @return array{0:Node,1:Node}
     */
    private static function applyAssignInLeftSideOfBinaryOpReduction(Node $node) : array
    {
        $inner_assign_statement = $node->children[0]->children['cond']->children['left'];
        if (!($inner_assign_statement instanceof Node)) {
            throw new AssertionError('Expected $inner_assign_statement instanceof Node');
        }
        $inner_assign_var = $inner_assign_statement->children['var'];

        if ($inner_assign_var->kind !== \ast\AST_VAR) {
            throw new AssertionError('Expected $inner_assign_var->kind === \ast\AST_VAR');
        }

        $new_node_elem = clone($node->children[0]);
        $new_node_elem->children['cond']->children['left'] = $inner_assign_var;
        $new_node_elem->flags = 0;
        $new_node = clone($node);
        $new_node->children[0] = $new_node_elem;
        $new_node->lineno = $new_node_elem->lineno ?? 0;
        $new_node->flags = 0;
        return [$inner_assign_statement, $new_node];
    }

    /**
     * if (B === ($var = A)) {X} -> $var = A; if (B === $var) { X }
     *
     * @return array{0:Node,1:Node}
     */
    private static function applyAssignInRightSideOfBinaryOpReduction(Node $node) : array
    {
        $inner_assign_statement = $node->children[0]->children['cond']->children['right'];
        $inner_assign_var = $inner_assign_statement->children['var'];

        $new_node_elem = clone($node->children[0]);
        $new_node_elem->children['cond']->children['right'] = $inner_assign_var;
        $new_node_elem->flags = 0;
        $new_node = clone($node);
        $new_node->children[0] = $new_node_elem;
        $new_node->lineno = $new_node_elem->lineno ?? 0;
        $new_node->flags = 0;
        return [$inner_assign_statement, $new_node];
    }

    /**
     * Creates a new node with kind \ast\AST_IF from two branches
     */
    private static function buildIfNode(Node $l, Node $r) : Node
    {
        return new Node(
            \ast\AST_IF,
            0,
            [$l, $r],
            $l->lineno ?? 0
        );
    }

    /**
     * maps if (A) {X} elseif (B) {Y} else {Z} -> if (A) {Y} else { if (B) {Y} else {Z}}
     */
    private static function applyIfChainReduction(Node $node) : Node
    {
        $children = $node->children;  // Copy of array of Nodes of type IF_ELEM
        if (count($children) <= 2) {
            return $node;
        }
        while (count($children) > 2) {
            $r = array_pop($children);
            $l = array_pop($children);
            if (!($l instanceof Node && $r instanceof Node)) {
                throw new AssertionError("Expected to have AST_IF_ELEM nodes");
            }
            $l->children['stmts']->flags = 0;
            $r->children['stmts']->flags = 0;
            $inner_if_node = self::buildIfNode($l, $r);
            $new_r = new Node(
                \ast\AST_IF_ELEM,
                0,
                [
                    'cond' => null,
                    'stmts' => self::buildStatementList($inner_if_node->lineno, ...(self::normalizeIfStatement($inner_if_node))),
                ],
                0
            );
            $children[] = $new_r;
        }
        // $children is an array of 2 nodes of type IF_ELEM
        return new Node(\ast\AST_IF, 0, $children, $node->lineno);
    }

    /**
     * Converts if (A && B) {X}` -> `if (A) { if (B){X}}`
     * @return Node simplified node logically equivalent to $node, with kind \ast\AST_IF.
     */
    private static function applyIfAndReduction(Node $node) : Node
    {
        if (count($node->children) != 1) {
            throw new AssertionError('Expected an if statement with no else/elseif statements');
        }
        $inner_node_elem = clone($node->children[0]);  // AST_IF_ELEM
        $inner_node_elem->children['cond'] = $inner_node_elem->children['cond']->children['right'];
        $inner_node_elem->flags = 0;
        $inner_node_lineno = $inner_node_elem->lineno ?? 0;

        // Normalize code such as `if (A && (B && C)) {...}` recursively.
        $inner_node_stmts = self::normalizeIfStatement(new Node(
            \ast\AST_IF,
            0,
            [$inner_node_elem],
            $inner_node_lineno
        ));

        $inner_node_stmt_list = new Node(\ast\AST_STMT_LIST, 0, $inner_node_stmts, $inner_node_lineno);
        $outer_node_elem = clone($node->children[0]);  // AST_IF_ELEM
        $outer_node_elem->children['cond'] = $node->children[0]->children['cond']->children['left'];
        $outer_node_elem->children['stmts'] = $inner_node_stmt_list;
        $outer_node_elem->flags = 0;
        return new Node(
            \ast\AST_IF,
            $node->lineno,
            [$outer_node_elem],
            0
        );
    }

    /**
     * Converts `while (A && B) {X}` -> `while (A) { if (!B) { break;} X}`
     * @return Node simplified node logically equivalent to $node, with kind \ast\AST_IF.
     */
    private static function applyWhileAndReduction(Node $node) : Node
    {
        $cond_node = $node->children['cond'];
        $right_node = $cond_node->children['right'];
        $lineno = $right_node->lineno ?? $cond_node->lineno;
        $conditional_break_elem = self::makeBreakWithNegatedConditional($right_node, $lineno);

        return new Node(
            ast\AST_WHILE,
            0,
            [
                'cond' => $cond_node->children['left'],
                'stmts' => new Node(
                    ast\AST_STMT_LIST,
                    $lineno,
                    array_merge([$conditional_break_elem], $node->children['stmts']->children),
                    0
                ),
            ],
            $node->lineno
        );
    }

    /**
     * Converts `for (INIT; A && B; LOOP) {X}` -> `for (INIT; A; LOOP) { if (!B) { break;} X}`
     * @return Node simplified node logically equivalent to $node, with kind \ast\AST_IF.
     */
    private static function applyForAndReduction(Node $node) : Node
    {
        $children = $node->children;
        $cond_node_array = $children['cond']->children;
        $cond_node = array_pop($cond_node_array);
        $right_node = $cond_node->children['right'];
        $lineno = $right_node->lineno ?? $cond_node->lineno;
        $conditional_break_elem = self::makeBreakWithNegatedConditional($right_node, $lineno);

        $cond_node_array[] = $cond_node->children['left'];

        $cond_node_list = new Node(
            ast\AST_EXPR_LIST,
            0,
            $cond_node_array,
            $children['cond']->lineno
        );

        return new Node(
            ast\AST_FOR,
            0,
            [
                'init' => $children['init'],
                'cond' => $cond_node_list,
                'loop' => $children['loop'],
                'stmts' => new Node(
                    ast\AST_STMT_LIST,
                    $lineno,
                    array_merge([$conditional_break_elem], $children['stmts']->children),
                    0
                ),
            ],
            $node->lineno
        );
    }

    /**
     * Creates a Node for `if (!COND) { break; }`
     * @param Node|string|int|float $cond_node
     */
    private static function makeBreakWithNegatedConditional($cond_node, int $lineno) : Node
    {
        $break_if_elem = new Node(
            ast\AST_IF_ELEM,
            0,
            [
                'cond' => new Node(
                    ast\AST_UNARY_OP,
                    flags\UNARY_BOOL_NOT,
                    ['expr' => $cond_node],
                    $lineno
                ),
                'stmts' => new Node(
                    ast\AST_STMT_LIST,
                    0,
                    [new Node(ast\AST_BREAK, $lineno, ['depth' => null], 0)],
                    $lineno
                ),
            ],
            $lineno
        );
        return new Node(
            ast\AST_IF,
            0,
            [$break_if_elem],
            $lineno
        );
    }

    /**
     * Converts if ($x = A) {Y} -> $x = A; if ($x) {Y}
     * This allows analyzing variables set in if blocks outside of the `if` block
     * @return array{0:Node,1:Node} [$outer_assign_statement, $new_node]
     */
    private static function applyIfAssignReduction(Node $node) : array
    {
        $outer_assign_statement = $node->children[0]->children['cond'];
        if (!($outer_assign_statement instanceof Node)) {
            throw new AssertionError('Expected condition of first if statement (with assignment as condition) to be a Node');
        }
        $new_node_elem = clone($node->children[0]);
        $new_node_elem->children['cond'] = $new_node_elem->children['cond']->children['var'];
        $new_node_elem->flags = 0;
        $new_node = clone($node);
        $new_node->children[0] = $new_node_elem;
        $new_node->lineno = $new_node_elem->lineno ?? 0;
        $new_node->flags = 0;
        return [$outer_assign_statement, $new_node];
    }

    /**
     * Converts if (!x) {Y} else {Z} -> if (x) {Z} else {Y}
     * This improves Phan's analysis for cases such as `if (!is_string($x))`.
     */
    private static function applyIfNegateReduction(Node $node) : Node
    {
        if (!(
            count($node->children) === 2 &&
            $node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT &&
            $node->children[1]->children['cond'] === null
        )) {
            throw new AssertionError('Failed precondition of ' . __METHOD__);
        }
        $new_node = clone($node);
        $new_node->children = [clone($new_node->children[1]), clone($new_node->children[0])];
        $new_node->children[0]->children['cond'] = $node->children[0]->children['cond']->children['expr'];
        $new_node->children[1]->children['cond'] = null;
        $new_node->flags = 0;
        return $new_node;
    }

    /**
     * Converts if (!!(x)) {Y} -> if (x) {Y}
     * This improves Phan's analysis for cases such as `if (!!x)`
     */
    private static function applyIfDoubleNegateReduction(Node $node) : Node
    {
        if (!(
            $node->children[0]->children['cond']->flags === flags\UNARY_BOOL_NOT &&
            $node->children[0]->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT
        )) {
            throw new AssertionError('Failed precondition of ' . __METHOD__);
        }

        $new_cond = $node->children[0]->children['cond']->children['expr']->children['expr'];
        $new_node = clone($node);
        $new_node->flags = 0;
        $new_node->children[0] = clone($node->children[0]);
        $new_node->children[0]->flags = 0;
        $new_node->children[0]->children['cond'] = $new_cond;

        return $new_node;
    }

    /**
     * Converts while (!!(x)) {Y} -> if (x) {Y}
     * This improves Phan's analysis for cases such as `if (!!x)`
     */
    private static function applyWhileDoubleNegateReduction(Node $node) : Node
    {
        if (!(
            $node->children['cond']->flags === flags\UNARY_BOOL_NOT &&
            $node->children['cond']->children['expr']->flags === flags\UNARY_BOOL_NOT
        )) {
            throw new AssertionError('Failed precondition of ' . __METHOD__);
        }

        return new Node(
            ast\AST_WHILE,
            0,
            [
                'cond' => $node->children['cond']->children['expr']->children['expr'],
                'stmts' => $node->children['stmts']
            ],
            $node->lineno
        );
    }

    /**
     * Converts for (INIT; !!(x); LOOP) {Y} -> if (INIT; x; LOOP) {Y}
     * This improves Phan's analysis for cases such as `if (!!x)`
     */
    private static function applyForDoubleNegateReduction(Node $node) : Node
    {
        $children = $node->children;
        $cond_node_list = $children['cond']->children;
        $cond_node = array_pop($cond_node_list);
        if (!(
            $cond_node->flags === flags\UNARY_BOOL_NOT &&
            $cond_node->children['expr']->flags === flags\UNARY_BOOL_NOT
        )) {
            throw new AssertionError('Failed precondition of ' . __METHOD__);
        }
        $cond_node_list[] = $cond_node->children['expr']->children['expr'];

        $children['cond'] = new ast\Node(
            ast\AST_EXPR_LIST,
            0,
            $cond_node_list,
            $children['cond']->lineno
        );

        return new Node(
            ast\AST_FOR,
            0,
            $children,
            $node->lineno
        );
    }

    private static function applyIfNegatedToIfElseReduction(Node $node) : Node
    {
        if (count($node->children) !== 1) {
            throw new AssertionError("Expected one child node");
        }
        $if_elem = $node->children[0];
        if ($if_elem->children['cond']->flags !== flags\UNARY_BOOL_NOT) {
            throw new AssertionError("Expected condition to begin with unary boolean negation operator");
        }
        $lineno = $if_elem->lineno;
        $new_else_elem = new Node(
            \ast\AST_IF_ELEM,
            0,
            [
                'cond' => null,
                'stmts' => $if_elem->children['stmts'],
            ],
            $lineno
        );
        $new_if_elem = new Node(
            \ast\AST_IF_ELEM,
            0,
            [
                'cond' => $if_elem->children['cond']->children['expr'],
                'stmts' => new Node(\ast\AST_STMT_LIST, 0, [], $if_elem->lineno),
            ],
            $lineno
        );
        return new Node(
            \ast\AST_IF,
            0,
            [$new_if_elem, $new_else_elem],
            $node->lineno
        );
    }

    /**
     * Recurses on a list of 0 or more catch statements. (as in try/catch)
     * Returns an equivalent list of catch AST nodes (or the original if no changes were made)
     */
    private static function normalizeCatchesList(Node $catches) : Node
    {
        $list = $catches->children;
        $new_list = array_map(
            /** @return mixed */
            static function (Node $node) {
                return self::applyToStmts($node);
            },
            // @phan-suppress-next-line PhanPartialTypeMismatchArgument should be impossible to be float
            $list
        );
        if ($new_list === $list) {
            return $catches;
        }
        $new_catches = clone($catches);
        $new_catches->children = $new_list;
        $new_catches->flags = 0;
        return $new_catches;
    }

    /**
     * Recurses on a try/catch/finally node, applying simplifications(catch/finally are optional)
     * Returns an equivalent try/catch/finally node (or the original if no changes were made)
     */
    private static function normalizeTryStatement(Node $node) : Node
    {
        $try = $node->children['try'];
        $catches = $node->children['catches'];
        $finally = $node->children['finally'] ?? null;
        $new_try = self::applyToStatementList($try);
        $new_catches = $catches ? self::normalizeCatchesList($catches) : $catches;
        $new_finally = $finally ? self::applyToStatementList($finally) : $finally;
        if ($new_try === $try && $new_catches === $catches && $new_finally === $finally) {
            return $node;
        }
        $new_node = clone($node);
        $new_node->children['try'] = $new_try;
        $new_node->children['catches'] = $new_catches;
        $new_node->children['finally'] = $new_finally;
        $new_node->flags = 0;
        return $new_node;
    }

    /**
     * Returns a Node that represents $node after all of the AST simplification steps.
     *
     * $node is not modified. This will reuse descendant nodes that didn't change.
     */
    public static function applyStatic(Node $node) : Node
    {
        $rewriter = new self();
        $nodes = $rewriter->apply($node);
        if (count($nodes) !== 1) {
            throw new AssertionError("Expected applying simplifier to a statement list would return an array with one statement list");
        }
        return $nodes[0];
    }
}
