view src/jdk/nashorn/internal/codegen/Lower.java @ 143:4be452026847

8010652: Eliminate non-child references in Block/FunctionNode, and make few node types immutable Reviewed-by: jlaskey, lagergren
author attila
date Sat, 23 Mar 2013 00:58:39 +0100
parents 60684aeba89c
children
line wrap: on
line source

/*
 * Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package jdk.nashorn.internal.codegen;

import static jdk.nashorn.internal.codegen.CompilerConstants.ARGUMENTS;
import static jdk.nashorn.internal.codegen.CompilerConstants.CALLEE;
import static jdk.nashorn.internal.codegen.CompilerConstants.EVAL;
import static jdk.nashorn.internal.codegen.CompilerConstants.SCOPE;
import static jdk.nashorn.internal.codegen.CompilerConstants.SCRIPT_RETURN;
import static jdk.nashorn.internal.codegen.CompilerConstants.THIS;
import static jdk.nashorn.internal.codegen.CompilerConstants.VARARGS;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import jdk.nashorn.internal.ir.BaseNode;
import jdk.nashorn.internal.ir.BinaryNode;
import jdk.nashorn.internal.ir.Block;
import jdk.nashorn.internal.ir.BreakNode;
import jdk.nashorn.internal.ir.CallNode;
import jdk.nashorn.internal.ir.CaseNode;
import jdk.nashorn.internal.ir.CatchNode;
import jdk.nashorn.internal.ir.ContinueNode;
import jdk.nashorn.internal.ir.DoWhileNode;
import jdk.nashorn.internal.ir.EmptyNode;
import jdk.nashorn.internal.ir.ExecuteNode;
import jdk.nashorn.internal.ir.ForNode;
import jdk.nashorn.internal.ir.FunctionNode;
import jdk.nashorn.internal.ir.FunctionNode.CompilationState;
import jdk.nashorn.internal.ir.IdentNode;
import jdk.nashorn.internal.ir.IfNode;
import jdk.nashorn.internal.ir.LabelNode;
import jdk.nashorn.internal.ir.LabeledNode;
import jdk.nashorn.internal.ir.LexicalContext;
import jdk.nashorn.internal.ir.LineNumberNode;
import jdk.nashorn.internal.ir.LiteralNode;
import jdk.nashorn.internal.ir.Node;
import jdk.nashorn.internal.ir.ReturnNode;
import jdk.nashorn.internal.ir.SwitchNode;
import jdk.nashorn.internal.ir.Symbol;
import jdk.nashorn.internal.ir.ThrowNode;
import jdk.nashorn.internal.ir.TryNode;
import jdk.nashorn.internal.ir.UnaryNode;
import jdk.nashorn.internal.ir.VarNode;
import jdk.nashorn.internal.ir.WhileNode;
import jdk.nashorn.internal.ir.WithNode;
import jdk.nashorn.internal.ir.visitor.NodeOperatorVisitor;
import jdk.nashorn.internal.ir.visitor.NodeVisitor;
import jdk.nashorn.internal.parser.Token;
import jdk.nashorn.internal.parser.TokenType;
import jdk.nashorn.internal.runtime.DebugLogger;
import jdk.nashorn.internal.runtime.ScriptRuntime;
import jdk.nashorn.internal.runtime.Source;

/**
 * Lower to more primitive operations. After lowering, an AST still has no symbols
 * and types, but several nodes have been turned into more low level constructs
 * and control flow termination criteria have been computed.
 *
 * We do things like code copying/inlining of finallies here, as it is much
 * harder and context dependent to do any code copying after symbols have been
 * finalized.
 */

final class Lower extends NodeOperatorVisitor {

    /**
     * Nesting level stack. Currently just used for loops to avoid the problem
     * with terminal bodies that end with throw/return but still do continues to
     * outer loops or same loop.
     */
    private final Deque<Node> nesting;

    private static final DebugLogger LOG = new DebugLogger("lower");

    private Node lastStatement;

    private List<Node> statements;

    private LexicalContext lexicalContext = new LexicalContext();

    /**
     * Constructor.
     *
     * @param compiler the compiler
     */
    Lower() {
        this.nesting    = new ArrayDeque<>();
        this.statements = new ArrayList<>();
    }

    @Override
    public Node enterBlock(final Block block) {
        final Node       savedLastStatement = lastStatement;
        final List<Node> savedStatements    = statements;
        lexicalContext.push(block);
        try {
            this.statements = new ArrayList<>();
            NodeVisitor visitor = this;
            for (final Node statement : block.getStatements()) {
                statement.accept(visitor);
                /*
                 * This is slightly unsound, for example if we have a loop with
                 * a guarded statement like if (x) continue in the body and the
                 * body ends with TERMINAL, e.g. return; we removed the continue
                 * before we had the loop stack, as all we cared about was a
                 * return last in the loop.
                 *
                 * @see NASHORN-285
                 */
                if (lastStatement != null && lastStatement.isTerminal()) {
                    copyTerminal(block, lastStatement);
                    visitor = new DeadCodeVarDeclarationVisitor();
                }
            }
            block.setStatements(statements);

        } finally {
            this.statements = savedStatements;
            this.lastStatement = savedLastStatement;
            lexicalContext.pop(block);
        }

        return null;
    }

    @Override
    public Node enterBreakNode(final BreakNode breakNode) {
        return enterBreakOrContinue(breakNode);
    }

    @Override
    public Node enterCallNode(final CallNode callNode) {
        final Node function = markerFunction(callNode.getFunction());
        callNode.setFunction(function);
        checkEval(callNode); //check if this is an eval call and store the information
        return callNode;
    }

    @Override
    public Node leaveCaseNode(final CaseNode caseNode) {
        caseNode.copyTerminalFlags(caseNode.getBody());
        return caseNode;
    }

    @Override
    public Node leaveCatchNode(final CatchNode catchNode) {
        catchNode.copyTerminalFlags(catchNode.getBody());
        addStatement(catchNode);
        return catchNode;
    }

    @Override
    public Node enterContinueNode(final ContinueNode continueNode) {
        return enterBreakOrContinue(continueNode);
    }

    @Override
    public Node enterDoWhileNode(final DoWhileNode doWhileNode) {
        return enterWhileNode(doWhileNode);
    }

    @Override
    public Node leaveDoWhileNode(final DoWhileNode doWhileNode) {
        return leaveWhileNode(doWhileNode);
    }

    @Override
    public Node enterEmptyNode(final EmptyNode emptyNode) {
        return null;
    }

    @Override
    public Node leaveExecuteNode(final ExecuteNode executeNode) {
        final Node expr = executeNode.getExpression();

        if (getCurrentFunctionNode().isProgram()) {
            if (!(expr instanceof Block) || expr instanceof FunctionNode) { // it's not a block, but can be a function
                if (!isInternalExpression(expr) && !isEvalResultAssignment(expr)) {
                    executeNode.setExpression(new BinaryNode(executeNode.getSource(), Token.recast(executeNode.getToken(), TokenType.ASSIGN),
                            getCurrentFunctionNode().getResultNode(),
                            expr));
                }
            }
        }

        copyTerminal(executeNode, executeNode.getExpression());
        addStatement(executeNode);

        return executeNode;
    }

    @Override
    public Node enterForNode(final ForNode forNode) {
        nest(forNode);
        return forNode;
    }

    @Override
    public Node leaveForNode(final ForNode forNode) {
        final Node  test = forNode.getTest();
        final Block body = forNode.getBody();

        if (!forNode.isForIn() && test == null) {
            setHasGoto(forNode);
        }

        final boolean escapes = controlFlowEscapes(body);
        if (escapes) {
            setTerminal(body, false);
        }

        // pop the loop from the loop context
        unnest(forNode);

        if (!forNode.isForIn() && conservativeAlwaysTrue(test)) {
            forNode.setTest(null);
            setHasGoto(forNode);
            setTerminal(forNode, !escapes);
        }

        addStatement(forNode);

        return forNode;
    }

    @Override
    public Node enterFunctionNode(final FunctionNode functionNode) {
        LOG.info("START FunctionNode: " + functionNode.getName());

        if (functionNode.isLazy()) {
            LOG.info("LAZY: " + functionNode.getName());
            return null;
        }
        lexicalContext.push(functionNode);
        initFunctionNode(functionNode);

        nest(functionNode);

        /*
         * As we are evaluating a nested structure, we need to store the
         * statement list for the surrounding block and restore it when the
         * function is done
         */
        final List<Node> savedStatements = statements;
        final Node savedLastStatement = lastStatement;

        statements    = new ArrayList<>();
        lastStatement = null;

        if (functionNode.needsSelfSymbol()) {
            //function needs to start with var funcIdent = __callee_;
            statements.add(functionNode.getSelfSymbolInit().accept(this));
        }

        NodeVisitor visitor = this;
        try {
            //do the statements - this fills the block with code
            boolean needsInitialEvalResult = functionNode.isProgram();
            for (final Node statement : functionNode.getStatements()) {
                // If this function is a program, then insert an assignment to the initial eval result after all
                // function declarations.
                if(needsInitialEvalResult && !(statement instanceof LineNumberNode || (statement instanceof VarNode && ((VarNode)statement).isFunctionDeclaration()))) {
                    addInitialEvalResult(functionNode);
                    needsInitialEvalResult = false;
                }
                statement.accept(visitor);
                //If there are unused terminated endpoints in the function, we need
                // to add a "return undefined" in those places for correct semantics
                LOG.info("Checking lastStatement="+lastStatement+" for terminal flags");
                if (lastStatement != null && lastStatement.hasTerminalFlags()) {
                    copyTerminal(functionNode, lastStatement);
                    assert !needsInitialEvalResult;
                    visitor = new DeadCodeVarDeclarationVisitor();
                }
            }
            if(needsInitialEvalResult) {
                addInitialEvalResult(functionNode);
            }
            functionNode.setStatements(statements);

            if (!functionNode.isTerminal()) {
                guaranteeReturn(functionNode);
            }
        } finally {
            statements    = savedStatements;
            lastStatement = savedLastStatement;
        }

        LOG.info("END FunctionNode: " + functionNode.getName());
        unnest(functionNode);
        lexicalContext.pop(functionNode);

        functionNode.setState(CompilationState.LOWERED);

        return null;
    }

    /**
     * This visitor is used to go over statements after a terminal statement. Those statements are dead code, but the
     * var declarations in them still have the effect of declaring a local variable on the function level. Therefore,
     * they aren't really dead code and must be preserved. Note that they're only preserved as no-op declarations; their
     * initializers are wiped out as those are, in fact, dead code.
     */
    private class DeadCodeVarDeclarationVisitor extends NodeOperatorVisitor {
        DeadCodeVarDeclarationVisitor() {
        }

        @Override
        public Node enterVarNode(VarNode varNode) {
            // Can't ever see a function declaration, as this visitor is only ever used after a terminal statement was
            // encountered, and all function declarations precede any terminal statements.
            assert !varNode.isFunctionDeclaration();
            if(varNode.getInit() == null) {
                // No initializer, just pass it to Lower.
                return varNode.accept(Lower.this);
            }
            // Wipe out the initializer and then pass it to Lower.
            return varNode.setInit(null).accept(Lower.this);
        }
    }

    private void addInitialEvalResult(final FunctionNode functionNode) {
        new ExecuteNode(functionNode.getSource(), functionNode.getFirstToken(), functionNode.getFinish(),
                getInitialEvalResult(functionNode)).accept(this);
    }

    /**
     * Result of initial result of evaluating a particular program, which is either the last function it declares, or
     * undefined if it doesn't declare any functions.
     * @param program
     * @return the initial result of evaluating the program
     */
    private static Node getInitialEvalResult(final FunctionNode program) {
        IdentNode lastFnName = null;
        for (final FunctionNode fn : program.getDeclaredFunctions()) {
            assert fn.isDeclared();
            final IdentNode fnName = fn.getIdent();
            if(fnName != null) {
                lastFnName = fnName;
            }
        }
        return lastFnName != null ? new IdentNode(lastFnName) : LiteralNode.newInstance(program, ScriptRuntime.UNDEFINED);
    }

    @Override
    public Node enterIfNode(final IfNode ifNode) {
        return nest(ifNode);
    }

    @Override
    public Node leaveIfNode(final IfNode ifNode) {
        final Node pass = ifNode.getPass();
        final Node fail = ifNode.getFail();

        if (pass.isTerminal() && fail != null && fail.isTerminal()) {
            setTerminal(ifNode,  true);
        }

        addStatement(ifNode);
        unnest(ifNode);

        return ifNode;
    }

    @Override
    public Node enterLabelNode(LabelNode labelNode) {
        final Block body = labelNode.getBody();
        body.accept(this);
        copyTerminal(labelNode, body);
        addStatement(labelNode);
        return null;
    }

    @Override
    public Node enterLineNumberNode(final LineNumberNode lineNumberNode) {
        addStatement(lineNumberNode, false); // don't put it in lastStatement cache
        return null;
    }

    @Override
    public Node enterReturnNode(final ReturnNode returnNode) {
        final TryNode tryNode = returnNode.getTryChain();
        final Node    expr    = returnNode.getExpression();

        if (tryNode != null) {
            //we are inside a try block - we don't necessarily have a result node yet. attr will do that.
            if (expr != null) {
                final Source source = getCurrentFunctionNode().getSource();

                //we need to evaluate the result of the return in case it is complex while
                //still in the try block, store it in a result value and return it afterwards
                final long token        = returnNode.getToken();
                final Node resultNode   = new IdentNode(getCurrentFunctionNode().getResultNode());
                final Node assignResult = new BinaryNode(source, Token.recast(token, TokenType.ASSIGN), resultNode, expr);

                //add return_in_try = expr; to try block
                new ExecuteNode(source, token, Token.descPosition(token), assignResult).accept(this);

                //splice in the finally code, inlining it here
                if (copyFinally(tryNode, null)) {
                    return null;
                }

                //make sure that the return node now returns 'return_in_try'
                returnNode.setExpression(resultNode);
            } else if (copyFinally(tryNode, null)) {
                return null;
            }
        } else if (expr != null) {
            returnNode.setExpression(expr.accept(this));
        }

        addStatement(returnNode);

        return null;
    }

    @Override
    public Node leaveReturnNode(final ReturnNode returnNode) {
        addStatement(returnNode); //ReturnNodes are always terminal, marked as such in constructor
        return returnNode;
    }

    @Override
    public Node enterSwitchNode(final SwitchNode switchNode) {
        nest(switchNode);
        return switchNode;
    }

    @Override
    public Node leaveSwitchNode(final SwitchNode switchNode) {
        unnest(switchNode);

        final List<CaseNode> cases       = switchNode.getCases();
        final CaseNode       defaultCase = switchNode.getDefaultCase();

        boolean allTerminal = !cases.isEmpty();
        for (final CaseNode caseNode : switchNode.getCases()) {
            allTerminal &= caseNode.isTerminal();
        }

        if (allTerminal && defaultCase != null && defaultCase.isTerminal()) {
            setTerminal(switchNode, true);
        }

        addStatement(switchNode);

        return switchNode;
    }

    @Override
    public Node leaveThrowNode(final ThrowNode throwNode) {
        addStatement(throwNode); //ThrowNodes are always terminal, marked as such in constructor
        return throwNode;
    }

    @Override
    public Node enterTryNode(final TryNode tryNode) {
        final Block  finallyBody = tryNode.getFinallyBody();
        final long   token       = tryNode.getToken();
        final int    finish      = tryNode.getFinish();

        nest(tryNode);

        if (finallyBody == null) {
            //do nothing if no finally exists
            return tryNode;
        }

        /*
         * We have a finally clause.
         *
         * Transform to do finally tail duplication as follows:
         *
         * <pre>
         *  try {
         *    try_body
         *  } catch e1 {
         *    catchbody_1
         *  }
         *  ...
         *  } catch en {
         *    catchbody_n
         *  } finally {
         *    finally_body
         *  }
         *
         *  (where e1 ... en are optional)
         *
         *  turns into
         *
         *  try {
         *    try {
         *      try_body
         *    } catch e1 {
         *      catchbody1
         *      //nothing inlined explicitly here, return, break other
         *      //terminals may inline the finally body
         *      ...
         *    } catch en {
         *      catchbody2
         *      //nothing inlined explicitly here, return, break other
         *      //terminals may inline the finally body
         *    }
         *  } catch all ex {
         *      finally_body_inlined
         *      rethrow ex
         *  }
         *  finally_body_inlined
         * </pre>
         *
         * If tries are catches are terminal, visitors for return, break &
         * continue will handle the tail duplications. Throw needs to be
         * treated specially with the catchall as described in the above
         * ASCII art.
         *
         * If the try isn't terminal we do the finally_body_inlined at the
         * end. If the try is terminated with continue/break/return the
         * existing visitor logic will inline the finally before that
         * operation. if the try is terminated with a throw, the catches e1
         * ... en will have a chance to process the exception. If the
         * appropriate catch e1..en is non terminal we fall through to the
         * last finally_body_inlined. if the catch e1...en IS terminal with
         * continue/break/return existing visitor logic will fix it. If they
         * are terminal with another throw it goes to the catchall and the
         * finally_body_inlined marked (*) will fix it before rethrowing
         * whatever problem there was for identical semantic.
         */
        final Source source = getCurrentFunctionNode().getSource();

        // if try node does not contain a catch we can skip creation of a new
        // try node and just append our synthetic catch to the existing try node.
        if (!tryNode.getCatchBlocks().isEmpty()) {
            // insert an intermediate try-catch* node, where we move the body and all catch blocks.
            // the original try node become a try-finally container for the new try-catch* node.
            // because we don't clone (to avoid deep copy), we have to fix the block chain in the end.
            final TryNode innerTryNode;
            innerTryNode = new TryNode(source, token, finish, tryNode.getNext());
            innerTryNode.setBody(tryNode.getBody());
            innerTryNode.setCatchBlocks(tryNode.getCatchBlocks());

            // set outer tryNode's body to innerTryNode
            final Block outerBody;
            outerBody = new Block(source, token, finish);
            outerBody.setStatements(new ArrayList<Node>(Arrays.asList(innerTryNode)));
            tryNode.setBody(outerBody);
            tryNode.setCatchBlocks(null);
        }

        // create a catch-all that inlines finally and rethrows

        final Block catchBlock      = new Block(source, token, finish);
        //this catch block should get define symbol

        final Block catchBody       = new Block(source, token, finish);
        final Node  catchAllFinally = finallyBody.copy();

        catchBody.addStatement(new ExecuteNode(source, finallyBody.getToken(), finallyBody.getFinish(), catchAllFinally));
        setTerminal(catchBody, true);

        final CatchNode catchAllNode;
        final IdentNode exception;

        exception    = new IdentNode(source, token, finish, getCurrentFunctionNode().uniqueName("catch_all"));
        catchAllNode = new CatchNode(source, token, finish, new IdentNode(exception), null, catchBody);
        catchAllNode.setIsSyntheticRethrow();

        catchBlock.addStatement(catchAllNode);

        // replace all catches of outer tryNode with the catch-all
        tryNode.setCatchBlocks(new ArrayList<>(Arrays.asList(catchBlock)));

        /*
         * We leave the finally block for the original try in place for now
         * so that children visitations will work. It is removed and placed
         * afterwards in the else case below, after all children are visited
         */

        return tryNode;
    }

    @Override
    public Node leaveTryNode(final TryNode tryNode) {
        final Block finallyBody   = tryNode.getFinallyBody();

        boolean allTerminal = tryNode.getBody().isTerminal() && (finallyBody == null || finallyBody.isTerminal());

        for (final Block catchBlock : tryNode.getCatchBlocks()) {
            allTerminal &= catchBlock.isTerminal();
        }

        tryNode.setIsTerminal(allTerminal);

        addStatement(tryNode);
        unnest(tryNode);

        // if finally body is present, place it after the tryNode
        if (finallyBody != null) {
            tryNode.setFinallyBody(null);
            addStatement(finallyBody);
        }

        return tryNode;
    }

    @Override
    public Node leaveVarNode(final VarNode varNode) {
        addStatement(varNode);
        return varNode;
    }

    @Override
    public Node enterWhileNode(final WhileNode whileNode) {
        return nest(whileNode);
    }

    @Override
    public Node leaveWhileNode(final WhileNode whileNode) {
        final Node test = whileNode.getTest();

        if (test == null) {
            setHasGoto(whileNode);
        }

        final Block   body    = whileNode.getBody();
        final boolean escapes = controlFlowEscapes(body);
        if (escapes) {
            setTerminal(body, false);
        }

        Node node = whileNode;

        if (body.isTerminal()) {
            if (whileNode instanceof DoWhileNode) {
                setTerminal(whileNode, true);
            } else if (conservativeAlwaysTrue(test)) {
                node = new ForNode(whileNode.getSource(), whileNode.getToken(), whileNode.getFinish());
                ((ForNode)node).setBody(body);
                node.accept(this);
                setTerminal(node, !escapes);
            }
        }

        // pop the loop from the loop context
        unnest(whileNode);
        addStatement(node);

        return node;
    }

    @Override
    public Node leaveWithNode(final WithNode withNode) {
        if (withNode.getBody().isTerminal()) {
            setTerminal(withNode,  true);
        }
        addStatement(withNode);

        return withNode;
    }

    @Override
    public Node leaveDELETE(final UnaryNode unaryNode) {
        final Node rhs = unaryNode.rhs();
        if (rhs instanceof IdentNode || rhs instanceof BaseNode) {
            return unaryNode;
        }
        addStatement(new ExecuteNode(rhs));
        return LiteralNode.newInstance(unaryNode, true);
    }

    /**
     * Given a function node that is a callee in a CallNode, replace it with
     * the appropriate marker function. This is used by {@link CodeGenerator}
     * for fast scope calls
     *
     * @param function function called by a CallNode
     * @return transformed node to marker function or identity if not ident/access/indexnode
     */
    private static Node markerFunction(final Node function) {
        if (function instanceof IdentNode) {
            return ((IdentNode)function).setIsFunction();
        } else if (function instanceof BaseNode) {
            return ((BaseNode)function).setIsFunction();
        }
        return function;
    }

    /**
     * Calculate a synthetic eval location for a node for the stacktrace, for example src#17<eval>
     * @param node a node
     * @return eval location
     */
    private static String evalLocation(final IdentNode node) {
        return new StringBuilder().
            append(node.getSource().getName()).
            append('#').
            append(node.getSource().getLine(node.position())).
            append("<eval>").
            toString();
    }

    /**
     * Check whether a call node may be a call to eval. In that case we
     * clone the args in order to create the following construct in
     * {@link CodeGenerator}
     *
     * <pre>
     * if (calledFuntion == buildInEval) {
     *    eval(cloned arg);
     * } else {
     *    cloned arg;
     * }
     * </pre>
     *
     * @param callNode call node to check if it's an eval
     */
    private void checkEval(final CallNode callNode) {
        if (callNode.getFunction() instanceof IdentNode) {

            final List<Node> args   = callNode.getArgs();
            final IdentNode  callee = (IdentNode)callNode.getFunction();

            // 'eval' call with at least one argument
            if (args.size() >= 1 && EVAL.tag().equals(callee.getName())) {
                final CallNode.EvalArgs evalArgs =
                    new CallNode.EvalArgs(
                        args.get(0).copy().accept(this), //clone as we use this for the "is eval case". original evaluated separately for "is not eval case"
                        getCurrentFunctionNode().getThisNode(),
                        evalLocation(callee),
                        getCurrentFunctionNode().isStrictMode());
                callNode.setEvalArgs(evalArgs);
            }
        }
    }

    private static boolean conservativeAlwaysTrue(final Node node) {
        return node == null || ((node instanceof LiteralNode) && Boolean.TRUE.equals(((LiteralNode<?>)node).getValue()));
    }

    /**
     * Helper that given a loop body makes sure that it is not terminal if it
     * has a continue that leads to the loop header or to outer loops' loop
     * headers. This means that, even if the body ends with a terminal
     * statement, we cannot tag it as terminal
     *
     * @param loopBody the loop body to check
     * @return true if control flow may escape the loop
     */
    private boolean controlFlowEscapes(final Node loopBody) {
        final List<Node> escapes = new ArrayList<>();

        loopBody.accept(new NodeVisitor() {
            @Override
            public Node leaveBreakNode(final BreakNode node) {
                escapes.add(node);
                return node;
            }

            @Override
            public Node leaveContinueNode(final ContinueNode node) {
                // all inner loops have been popped.
                if (nesting.contains(node.getTargetNode())) {
                    escapes.add(node);
                }
                return node;
            }
        });

        return !escapes.isEmpty();
    }

    private void guaranteeReturn(final FunctionNode functionNode) {
        Node resultNode;

        if (functionNode.isProgram()) {
            resultNode = functionNode.getResultNode(); // the eval result, symbol assigned in Attr
        } else {
            if (lastStatement != null && lastStatement.isTerminal() || lastStatement instanceof ReturnNode) {
                return; //already in place or not needed, as it should be for a non-undefined returning function
            }
            resultNode = LiteralNode.newInstance(functionNode, ScriptRuntime.UNDEFINED);
        }

        //create a return statement
        final Node returnNode = new ReturnNode(functionNode.getSource(), functionNode.getLastToken(), functionNode.getFinish(), resultNode, null);
        returnNode.accept(this);
    }


    private Node nest(final Node node) {
        LOG.info("Nesting: " + node);
        LOG.indent();
        nesting.push(node);
        return node;
    }

    private void unnest(final Node node) {
        LOG.unindent();
        assert nesting.getFirst() == node : "inconsistent nesting order : " + nesting.getFirst() + " != " + node;
        LOG.info("Unnesting: " + nesting);
        nesting.pop();
    }

    private static void setTerminal(final Node node, final boolean isTerminal) {
        LOG.info("terminal = " + isTerminal + " for " + node);
        node.setIsTerminal(isTerminal);
    }

    private static void setHasGoto(final Node node) { //, final boolean hasGoto) {
        LOG.info("hasGoto = true for " + node);
        node.setHasGoto();
    }

    private static void copyTerminal(final Node node, final Node sourceNode) {
        LOG.info("copy terminal flags " + sourceNode + " -> " + node);
        node.copyTerminalFlags(sourceNode);
    }

    private void addStatement(final Node statement, final boolean storeInLastStatement) {
        LOG.info("add statement = " + statement + " (lastStatement = " + lastStatement + ")");
        statements.add(statement);
        if (storeInLastStatement) {
            lastStatement = statement;
        }
    }

    private void addStatement(final Node statement) {
        addStatement(statement, true);
    }

    /**
     * Determine if Try block is inside target block.
     *
     * @param tryNode Try node to test.
     * @param target  Target block.
     *
     * @return true if try block is inside the target, false otherwise.
     */
    private boolean isNestedTry(final TryNode tryNode, final Block target) {
        for(Iterator<Block> blocks = lexicalContext.getBlocks(getCurrentBlock()); blocks.hasNext();) {
            final Block block = blocks.next();
            if(block == target) {
                return false;
            }
            if(tryNode.isChildBlock(block)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Clones the body of the try finallys up to the target block.
     *
     * @param node       first try node in the chain.
     * @param targetNode target block of the break/continue statement or null for return
     *
     * @return true if terminates.
     */
    private boolean copyFinally(final TryNode node, final Node targetNode) {
        Block target = null;

        if (targetNode instanceof Block) {
            target = (Block)targetNode;
        }

        for (TryNode tryNode = node; tryNode != null; tryNode = tryNode.getNext()) {
            if (target != null && !isNestedTry(tryNode, target)) {
                return false;
            }

            Block finallyBody = tryNode.getFinallyBody();
            if (finallyBody == null) {
                continue;
            }

            finallyBody = (Block)finallyBody.copy();
            final boolean hasTerminalFlags = finallyBody.hasTerminalFlags();

            new ExecuteNode(finallyBody.getSource(), finallyBody.getToken(), finallyBody.getFinish(), finallyBody).accept(this);

            if (hasTerminalFlags) {
                getCurrentBlock().copyTerminalFlags(finallyBody);
                return true;
            }
        }

        return false;
    }

    private Node enterBreakOrContinue(final LabeledNode labeledNode) {
        final TryNode tryNode = labeledNode.getTryChain();
        if (tryNode != null && copyFinally(tryNode, labeledNode.getTargetNode())) {
            return null;
        }
        addStatement(labeledNode);
        return null;
    }


    /**
     * An internal expression has a symbol that is tagged internal. Check if
     * this is such a node
     *
     * @param expression expression to check for internal symbol
     * @return true if internal, false otherwise
     */
    private static boolean isInternalExpression(final Node expression) {
        final Symbol symbol = expression.getSymbol();
        return symbol != null && symbol.isInternal();
    }

    /**
     * Is this an assignment to the special variable that hosts scripting eval
     * results?
     *
     * @param expression expression to check whether it is $evalresult = X
     * @return true if an assignment to eval result, false otherwise
     */
    private boolean isEvalResultAssignment(final Node expression) {
        Node e = expression;
        if (e.tokenType() == TokenType.DISCARD) {
            e = ((UnaryNode)expression).rhs();
        }
        final Node resultNode = getCurrentFunctionNode().getResultNode();
        return e instanceof BinaryNode && ((BinaryNode)e).lhs().equals(resultNode);
    }

    /**
     * Prepare special function nodes.
     * TODO : only create those that are needed.
     * TODO : make sure slot numbering is not hardcoded in {@link CompilerConstants} - now creation order is significant
     */
    private static void initFunctionNode(final FunctionNode functionNode) {
        final Source source = functionNode.getSource();
        final long token    = functionNode.getToken();
        final int  finish   = functionNode.getFinish();

        functionNode.setThisNode(new IdentNode(source, token, finish, THIS.tag()));
        functionNode.setScopeNode(new IdentNode(source, token, finish, SCOPE.tag()));
        functionNode.setResultNode(new IdentNode(source, token, finish, SCRIPT_RETURN.tag()));
        functionNode.setCalleeNode(new IdentNode(source, token, finish, CALLEE.tag()));
        if (functionNode.isVarArg()) {
            functionNode.setVarArgsNode(new IdentNode(source, token, finish, VARARGS.tag()));
            if (functionNode.needsArguments()) {
                functionNode.setArgumentsNode(new IdentNode(source, token, finish, ARGUMENTS.tag()));
            }
        }
    }

}