/*
 * 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.CALLEE;
import static jdk.nashorn.internal.codegen.CompilerConstants.SCOPE;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import jdk.nashorn.internal.codegen.types.Type;
import jdk.nashorn.internal.ir.AccessNode;
import jdk.nashorn.internal.ir.Assignment;
import jdk.nashorn.internal.ir.BinaryNode;
import jdk.nashorn.internal.ir.Block;
import jdk.nashorn.internal.ir.CallNode;
import jdk.nashorn.internal.ir.CaseNode;
import jdk.nashorn.internal.ir.CatchNode;
import jdk.nashorn.internal.ir.Expression;
import jdk.nashorn.internal.ir.ExpressionStatement;
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.IndexNode;
import jdk.nashorn.internal.ir.LexicalContext;
import jdk.nashorn.internal.ir.LiteralNode;
import jdk.nashorn.internal.ir.LiteralNode.ArrayLiteralNode;
import jdk.nashorn.internal.ir.Node;
import jdk.nashorn.internal.ir.ReturnNode;
import jdk.nashorn.internal.ir.RuntimeNode;
import jdk.nashorn.internal.ir.RuntimeNode.Request;
import jdk.nashorn.internal.ir.SwitchNode;
import jdk.nashorn.internal.ir.Symbol;
import jdk.nashorn.internal.ir.TemporarySymbols;
import jdk.nashorn.internal.ir.TernaryNode;
import jdk.nashorn.internal.ir.ThrowNode;
import jdk.nashorn.internal.ir.TypeOverride;
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.Debug;
import jdk.nashorn.internal.runtime.DebugLogger;
import jdk.nashorn.internal.runtime.JSType;

/**
 * Lower to more primitive operations. After lowering, an AST has symbols and
 * types. Lowering may also add specialized versions of methods to the script if
 * the optimizer is turned on.
 *
 * Any expression that requires temporary storage as part of computation will
 * also be detected here and give a temporary symbol
 *
 * For any op that we process in FinalizeTypes it is an absolute guarantee
 * that scope and slot information is correct. This enables e.g. AccessSpecialization
 * and frame optimizations
 */

final class FinalizeTypes extends NodeOperatorVisitor<LexicalContext> {

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

    private final TemporarySymbols temporarySymbols;

    FinalizeTypes(final TemporarySymbols temporarySymbols) {
        super(new LexicalContext());
        this.temporarySymbols = temporarySymbols;
    }

    @Override
    public Node leaveCallNode(final CallNode callNode) {
        // AccessSpecializer - call return type may change the access for this location
        final Node function = callNode.getFunction();
        if (function instanceof FunctionNode) {
            return setTypeOverride(callNode, ((FunctionNode)function).getReturnType());
        }
        return callNode;
    }

    private Node leaveUnary(final UnaryNode unaryNode) {
        return unaryNode.setRHS(convert(unaryNode.rhs(), unaryNode.getType()));
    }

    @Override
    public Node leaveADD(final UnaryNode unaryNode) {
        return leaveUnary(unaryNode);
    }

    @Override
    public Node leaveBIT_NOT(final UnaryNode unaryNode) {
        return leaveUnary(unaryNode);
    }

    @Override
    public Node leaveCONVERT(final UnaryNode unaryNode) {
        assert unaryNode.rhs().tokenType() != TokenType.CONVERT : "convert(convert encountered. check its origin and remove it";
        return unaryNode;
    }

    @Override
    public Node leaveDECINC(final UnaryNode unaryNode) {
        return specialize(unaryNode).node;
    }

    @Override
    public Node leaveNEW(final UnaryNode unaryNode) {
        assert unaryNode.getSymbol() != null && unaryNode.getSymbol().getSymbolType().isObject();
        return unaryNode.setRHS(((CallNode)unaryNode.rhs()).setIsNew());
    }

    @Override
    public Node leaveSUB(final UnaryNode unaryNode) {
        return leaveUnary(unaryNode);
    }

    /**
     * Add is a special binary, as it works not only on arithmetic, but for
     * strings etc as well.
     */
    @Override
    public Expression leaveADD(final BinaryNode binaryNode) {
        final Expression lhs = binaryNode.lhs();
        final Expression rhs = binaryNode.rhs();

        final Type type = binaryNode.getType();

        if (type.isObject()) {
            if (!isAddString(binaryNode)) {
                return new RuntimeNode(binaryNode, Request.ADD);
            }
        }

        return binaryNode.setLHS(convert(lhs, type)).setRHS(convert(rhs, type));
    }

    @Override
    public Node leaveAND(final BinaryNode binaryNode) {
        return binaryNode;
    }

    @Override
    public Node leaveASSIGN(final BinaryNode binaryNode) {
        final SpecializedNode specialized = specialize(binaryNode);
        final BinaryNode specBinaryNode = (BinaryNode)specialized.node;
        Type destType = specialized.type;
        if (destType == null) {
            destType = specBinaryNode.getType();
        }
        // Register assignments to this object in case this is used as constructor
        if (binaryNode.lhs() instanceof AccessNode) {
            AccessNode accessNode = (AccessNode) binaryNode.lhs();

            if (accessNode.getBase().getSymbol().isThis()) {
                lc.getCurrentFunction().addThisProperty(accessNode.getProperty().getName());
            }
        }
        return specBinaryNode.setRHS(convert(specBinaryNode.rhs(), destType));
    }

    @Override
    public Node leaveASSIGN_ADD(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_BIT_AND(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_BIT_OR(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_BIT_XOR(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_DIV(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_MOD(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_MUL(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_SAR(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_SHL(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_SHR(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    @Override
    public Node leaveASSIGN_SUB(final BinaryNode binaryNode) {
        return leaveASSIGN(binaryNode);
    }

    private boolean symbolIsInteger(final Expression node) {
        final Symbol symbol = node.getSymbol();
        assert symbol != null && symbol.getSymbolType().isInteger() : "int coercion expected: " + Debug.id(symbol) + " " + symbol + " " + lc.getCurrentFunction().getSource();
        return true;
    }

    @Override
    public Node leaveBIT_AND(final BinaryNode binaryNode) {
        assert symbolIsInteger(binaryNode);
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveBIT_OR(final BinaryNode binaryNode) {
        assert symbolIsInteger(binaryNode);
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveBIT_XOR(final BinaryNode binaryNode) {
        assert symbolIsInteger(binaryNode);
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveCOMMALEFT(final BinaryNode binaryNode) {
        assert binaryNode.getSymbol() != null;
        final BinaryNode newBinaryNode = binaryNode.setRHS(discard(binaryNode.rhs()));
        // AccessSpecializer - the type of lhs, which is the remaining value of this node may have changed
        // in that case, update the node type as well
        return propagateType(newBinaryNode, newBinaryNode.lhs().getType());
    }

    @Override
    public Node leaveCOMMARIGHT(final BinaryNode binaryNode) {
        assert binaryNode.getSymbol() != null;
        final BinaryNode newBinaryNode = binaryNode.setLHS(discard(binaryNode.lhs()));
        // AccessSpecializer - the type of rhs, which is the remaining value of this node may have changed
        // in that case, update the node type as well
        return propagateType(newBinaryNode, newBinaryNode.rhs().getType());
    }

    @Override
    public Node leaveDIV(final BinaryNode binaryNode) {
        return leaveBinaryArith(binaryNode);
    }


    @Override
    public Node leaveEQ(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.EQ);
    }

    @Override
    public Node leaveEQ_STRICT(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.EQ_STRICT);
    }

    @Override
    public Node leaveGE(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.GE);
    }

    @Override
    public Node leaveGT(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.GT);
    }

    @Override
    public Node leaveLE(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.LE);
    }

    @Override
    public Node leaveLT(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.LT);
    }

    @Override
    public Node leaveMOD(final BinaryNode binaryNode) {
        return leaveBinaryArith(binaryNode);
    }

    @Override
    public Node leaveMUL(final BinaryNode binaryNode) {
        return leaveBinaryArith(binaryNode);
    }

    @Override
    public Node leaveNE(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.NE);
    }

    @Override
    public Node leaveNE_STRICT(final BinaryNode binaryNode) {
        return leaveCmp(binaryNode, Request.NE_STRICT);
    }

    @Override
    public Node leaveOR(final BinaryNode binaryNode) {
        return binaryNode;
    }

    @Override
    public Node leaveSAR(final BinaryNode binaryNode) {
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveSHL(final BinaryNode binaryNode) {
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveSHR(final BinaryNode binaryNode) {
        assert binaryNode.getSymbol() != null && binaryNode.getSymbol().getSymbolType().isLong() : "long coercion expected: " + binaryNode.getSymbol();
        return leaveBinary(binaryNode, Type.INT, Type.INT);
    }

    @Override
    public Node leaveSUB(final BinaryNode binaryNode) {
        return leaveBinaryArith(binaryNode);
    }

    @Override
    public boolean enterBlock(final Block block) {
        updateSymbols(block);
        return true;
    }

    @Override
    public Node leaveCatchNode(final CatchNode catchNode) {
        final Expression exceptionCondition = catchNode.getExceptionCondition();
        if (exceptionCondition != null) {
            return catchNode.setExceptionCondition(convert(exceptionCondition, Type.BOOLEAN));
        }
        return catchNode;
    }

    @Override
    public Node leaveExpressionStatement(final ExpressionStatement expressionStatement) {
        temporarySymbols.reuse();
        return expressionStatement.setExpression(discard(expressionStatement.getExpression()));
    }

    @Override
    public Node leaveForNode(final ForNode forNode) {
        final Expression init   = forNode.getInit();
        final Expression test   = forNode.getTest();
        final Expression modify = forNode.getModify();

        if (forNode.isForIn()) {
            return forNode.setModify(lc, convert(forNode.getModify(), Type.OBJECT)); // NASHORN-400
        }
        assert test != null || forNode.hasGoto() : "forNode " + forNode + " needs goto and is missing it in " + lc.getCurrentFunction();

        return forNode.
            setInit(lc, init == null ? null : discard(init)).
            setTest(lc, test == null ? null : convert(test, Type.BOOLEAN)).
            setModify(lc, modify == null ? null : discard(modify));
    }

    @Override
    public boolean enterFunctionNode(final FunctionNode functionNode) {
        if (functionNode.isLazy()) {
            return false;
        }

        // If the function doesn't need a callee, we ensure its __callee__ symbol doesn't get a slot. We can't do
        // this earlier, as access to scoped variables, self symbol, etc. in previous phases can all trigger the
        // need for the callee.
        if (!functionNode.needsCallee()) {
            functionNode.compilerConstant(CALLEE).setNeedsSlot(false);
        }
        // Similar reasoning applies to __scope__ symbol: if the function doesn't need either parent scope and none of
        // its blocks create a scope, we ensure it doesn't get a slot, but we can't determine whether it needs a scope
        // earlier than this phase.
        if (!(functionNode.hasScopeBlock() || functionNode.needsParentScope())) {
            functionNode.compilerConstant(SCOPE).setNeedsSlot(false);
        }

        return true;
    }

    @Override
    public Node leaveFunctionNode(final FunctionNode functionNode) {
        return functionNode.setState(lc, CompilationState.FINALIZED);
    }

    @Override
    public Node leaveIfNode(final IfNode ifNode) {
        return ifNode.setTest(convert(ifNode.getTest(), Type.BOOLEAN));
    }

    @SuppressWarnings("rawtypes")
    @Override
    public boolean enterLiteralNode(final LiteralNode literalNode) {
        if (literalNode instanceof ArrayLiteralNode) {
            final ArrayLiteralNode arrayLiteralNode = (ArrayLiteralNode)literalNode;
            final Expression[]     array            = arrayLiteralNode.getValue();
            final Type             elementType      = arrayLiteralNode.getElementType();

            for (int i = 0; i < array.length; i++) {
                final Node element = array[i];
                if (element != null) {
                    array[i] = convert((Expression)element.accept(this), elementType);
                }
            }
        }

        return false;
    }

    @Override
    public Node leaveReturnNode(final ReturnNode returnNode) {
        final Expression expr = returnNode.getExpression();
        if (expr != null) {
            return returnNode.setExpression(convert(expr, lc.getCurrentFunction().getReturnType()));
        }
        return returnNode;
    }

    @Override
    public Node leaveRuntimeNode(final RuntimeNode runtimeNode) {
        final List<Expression> args = runtimeNode.getArgs();
        for (final Expression arg : args) {
            assert !arg.getType().isUnknown();
        }
        return runtimeNode;
    }

    @Override
    public Node leaveSwitchNode(final SwitchNode switchNode) {
        final boolean allInteger = switchNode.getTag().getSymbolType().isInteger();

        if (allInteger) {
            return switchNode;
        }

        final Expression     expression  = switchNode.getExpression();
        final List<CaseNode> cases       = switchNode.getCases();
        final List<CaseNode> newCases    = new ArrayList<>();

        for (final CaseNode caseNode : cases) {
            final Expression test = caseNode.getTest();
            newCases.add(test != null ? caseNode.setTest(convert(test, Type.OBJECT)) : caseNode);
        }

        return switchNode.
            setExpression(lc, convert(expression, Type.OBJECT)).
            setCases(lc, newCases);
    }

    @Override
    public Node leaveTernaryNode(final TernaryNode ternaryNode) {
        return ternaryNode.setTest(convert(ternaryNode.getTest(), Type.BOOLEAN));
    }

    @Override
    public Node leaveThrowNode(final ThrowNode throwNode) {
        return throwNode.setExpression(convert(throwNode.getExpression(), Type.OBJECT));
    }

    @Override
    public Node leaveVarNode(final VarNode varNode) {
        final Expression init = varNode.getInit();
        if (init != null) {
            final SpecializedNode specialized = specialize(varNode);
            final VarNode specVarNode = (VarNode)specialized.node;
            Type destType = specialized.type;
            if (destType == null) {
                destType = specVarNode.getName().getType();
            }
            assert specVarNode.getName().hasType() : specVarNode + " doesn't have a type";
            final Expression convertedInit = convert(init, destType);
            temporarySymbols.reuse();
            return specVarNode.setInit(convertedInit);
        }
        temporarySymbols.reuse();
        return varNode;
    }

    @Override
    public Node leaveWhileNode(final WhileNode whileNode) {
        final Expression test = whileNode.getTest();
        if (test != null) {
            return whileNode.setTest(lc, convert(test, Type.BOOLEAN));
        }
        return whileNode;
    }

    @Override
    public Node leaveWithNode(final WithNode withNode) {
        return withNode.setExpression(lc, convert(withNode.getExpression(), Type.OBJECT));
    }

    private static void updateSymbolsLog(final FunctionNode functionNode, final Symbol symbol, final boolean loseSlot) {
        if (LOG.isEnabled()) {
            if (!symbol.isScope()) {
                LOG.finest("updateSymbols: ", symbol, " => scope, because all vars in ", functionNode.getName(), " are in scope");
            }
            if (loseSlot && symbol.hasSlot()) {
                LOG.finest("updateSymbols: ", symbol, " => no slot, because all vars in ", functionNode.getName(), " are in scope");
            }
        }
    }

    /**
     * Called after a block or function node (subclass of block) is finished. Guarantees
     * that scope and slot information is correct for every symbol
     * @param block block for which to to finalize type info.
     */
    private void updateSymbols(final Block block) {
        if (!block.needsScope()) {
            return; // nothing to do
        }

        final FunctionNode   functionNode   = lc.getFunction(block);
        final boolean        allVarsInScope = functionNode.allVarsInScope();
        final boolean        isVarArg       = functionNode.isVarArg();

        for (final Symbol symbol : block.getSymbols()) {
            if (symbol.isInternal() || symbol.isThis() || symbol.isTemp()) {
                continue;
            }

            if (symbol.isVar()) {
                if (allVarsInScope || symbol.isScope()) {
                    updateSymbolsLog(functionNode, symbol, true);
                    Symbol.setSymbolIsScope(lc, symbol);
                    symbol.setNeedsSlot(false);
                } else {
                    assert symbol.hasSlot() : symbol + " should have a slot only, no scope";
                }
            } else if (symbol.isParam() && (allVarsInScope || isVarArg || symbol.isScope())) {
                updateSymbolsLog(functionNode, symbol, isVarArg);
                Symbol.setSymbolIsScope(lc, symbol);
                symbol.setNeedsSlot(!isVarArg);
            }
        }
    }

    /**
     * Exit a comparison node and do the appropriate replacements. We need to introduce runtime
     * nodes late for comparisons as types aren't known until the last minute
     *
     * Both compares and adds may turn into runtimes node at this level as when we first bump
     * into the op in Attr, we may type it according to what we know there, which may be wrong later
     *
     * e.g. i (int) < 5 -> normal compare
     *     i = object
     *  then the post pass that would add the conversion to the 5 needs to
     *
     * @param binaryNode binary node to leave
     * @param request    runtime request
     * @return lowered cmp node
     */
    @SuppressWarnings("fallthrough")
    private Node leaveCmp(final BinaryNode binaryNode, final RuntimeNode.Request request) {
        final Expression lhs    = binaryNode.lhs();
        final Expression rhs    = binaryNode.rhs();

        Type widest = Type.widest(lhs.getType(), rhs.getType());

        boolean newRuntimeNode = false, finalized = false;
        switch (request) {
        case EQ_STRICT:
        case NE_STRICT:
            if (lhs.getType().isBoolean() != rhs.getType().isBoolean()) {
                newRuntimeNode = true;
                widest = Type.OBJECT;
                finalized = true;
            }
            //fallthru
        default:
            if (newRuntimeNode || widest.isObject()) {
                return new RuntimeNode(binaryNode, request).setIsFinal(finalized);
            }
            break;
        }

        return binaryNode.setLHS(convert(lhs, widest)).setRHS(convert(rhs, widest));
    }

    /**
     * Compute the binary arithmetic type given the lhs and an rhs of a binary expression
     * @param lhsType  the lhs type
     * @param rhsType  the rhs type
     * @return the correct binary type
     */
    private static Type binaryArithType(final Type lhsType, final Type rhsType) {
        if (!Compiler.shouldUseIntegerArithmetic()) {
            return Type.NUMBER;
        }
        return Type.widest(lhsType, rhsType, Type.NUMBER);
    }

    private Node leaveBinaryArith(final BinaryNode binaryNode) {
        final Type type = binaryArithType(binaryNode.lhs().getType(), binaryNode.rhs().getType());
        return leaveBinary(binaryNode, type, type);
    }

    private Node leaveBinary(final BinaryNode binaryNode, final Type lhsType, final Type rhsType) {
        Node b =  binaryNode.setLHS(convert(binaryNode.lhs(), lhsType)).setRHS(convert(binaryNode.rhs(), rhsType));
        return b;
    }

    /**
     * A symbol (and {@link jdk.nashorn.internal.runtime.Property}) can be tagged as "may be primitive".
     * This is used a hint for dual fields that it is even worth it to try representing this
     * field as something other than java.lang.Object.
     *
     * @param node node in which to tag symbols as primitive
     * @param to   which primitive type to use for tagging
     */
    private static void setCanBePrimitive(final Node node, final Type to) {
        final HashSet<Node> exclude = new HashSet<>();

        node.accept(new NodeVisitor<LexicalContext>(new LexicalContext()) {
            private void setCanBePrimitive(final Symbol symbol) {
                LOG.info("*** can be primitive symbol ", symbol, " ", Debug.id(symbol));
                symbol.setCanBePrimitive(to);
            }

            @Override
            public boolean enterIdentNode(final IdentNode identNode) {
                if (!exclude.contains(identNode)) {
                    setCanBePrimitive(identNode.getSymbol());
                }
                return false;
            }

            @Override
            public boolean enterAccessNode(final AccessNode accessNode) {
                setCanBePrimitive(accessNode.getProperty().getSymbol());
                return false;
            }

            @Override
            public boolean enterIndexNode(final IndexNode indexNode) {
                exclude.add(indexNode.getBase()); //prevent array base node to be flagged as primitive, but k in a[k++] is fine
                return true;
            }
        });
    }

    private static class SpecializedNode {
        final Node node;
        final Type type;

        SpecializedNode(Node node, Type type) {
            this.node = node;
            this.type = type;
        }
    }

    <T extends Expression> SpecializedNode specialize(final Assignment<T> assignment) {
        final Node node = ((Node)assignment);
        final T lhs = assignment.getAssignmentDest();
        final Expression rhs = assignment.getAssignmentSource();

        if (!canHaveCallSiteType(lhs)) {
            return new SpecializedNode(node, null);
        }

        final Type to;
        if (node.isSelfModifying()) {
            to = node.getWidestOperationType();
        } else {
            to = rhs.getType();
        }

        if (!isSupportedCallSiteType(to)) {
            //meaningless to specialize to boolean or object
            return new SpecializedNode(node, null);
        }

        final Node newNode = assignment.setAssignmentDest(setTypeOverride(lhs, to));
        final Node typePropagatedNode;
        if(newNode instanceof Expression) {
            typePropagatedNode = propagateType((Expression)newNode, to);
        } else if(newNode instanceof VarNode) {
            // VarNode, being a statement, doesn't have its own symbol; it uses the symbol of its name instead.
            final VarNode varNode = (VarNode)newNode;
            typePropagatedNode = varNode.setName((IdentNode)propagateType(varNode.getName(), to));
        } else {
            throw new AssertionError();
        }
        return new SpecializedNode(typePropagatedNode, to);
    }


    /**
     * Is this a node that can have its type overridden. This is true for
     * AccessNodes, IndexNodes and IdentNodes
     *
     * @param node the node to check
     * @return true if node can have a callsite type
     */
    private static boolean canHaveCallSiteType(final Node node) {
        return node instanceof TypeOverride && ((TypeOverride<?>)node).canHaveCallSiteType();
    }

    /**
     * Is the specialization type supported. Currently we treat booleans as objects
     * and have no special boolean type accessor, thus booleans are ignored.
     * TODO - support booleans? NASHORN-590
     *
     * @param castTo the type to check
     * @return true if call site type is supported
     */
    private static boolean isSupportedCallSiteType(final Type castTo) {
        return castTo.isNumeric(); // don't specializable for boolean
    }

    /**
     * Override the type of a node for e.g. access specialization of scope
     * objects. Normally a variable can only get a wider type and narrower type
     * sets are ignored. Not that a variable can still be on object type as
     * per the type analysis, but a specific access may be narrower, e.g. if it
     * is used in an arithmetic op. This overrides a type, regardless of
     * type environment and is used primarily by the access specializer
     *
     * @param node    node for which to change type
     * @param to      new type
     */
    @SuppressWarnings("unchecked")
    <T extends Expression> T setTypeOverride(final T node, final Type to) {
        final Type from = node.getType();
        if (!node.getType().equals(to)) {
            LOG.info("Changing call override type for '", node, "' from ", node.getType(), " to ", to);
            if (!to.isObject() && from.isObject()) {
                setCanBePrimitive(node, to);
            }
        }
        LOG.info("Type override for lhs in '", node, "' => ", to);
        return ((TypeOverride<T>)node).setType(temporarySymbols, lc, to);
    }

    /**
     * Add an explicit conversion. This is needed when attribution has created types
     * that do not mesh into an op type, e.g. a = b, where b is object and a is double
     * at the end of Attr, needs explicit conversion logic.
     *
     * An explicit conversion can be one of the following:
     *   + Convert a literal - just replace it with another literal
     *   + Convert a scope object - just replace the type of the access, e.g. get()D->get()I
     *   + Explicit convert placement, e.g. a = (double)b - all other cases
     *
     * No other part of the world after {@link Attr} may introduce new symbols. This
     * is the only place.
     *
     * @param node node to convert
     * @param to   destination type
     * @return     conversion node
     */
    private Expression convert(final Expression node, final Type to) {
        assert !to.isUnknown() : "unknown type for " + node + " class=" + node.getClass();
        assert node != null : "node is null";
        assert node.getSymbol() != null : "node " + node + " " + node.getClass() + " has no symbol! " + lc.getCurrentFunction();
        assert node.tokenType() != TokenType.CONVERT : "assert convert in convert " + node + " in " + lc.getCurrentFunction();

        final Type from = node.getType();

        if (Type.areEquivalent(from, to)) {
            return node;
        }

        if (from.isObject() && to.isObject()) {
            return node;
        }

        Expression resultNode = node;

        if (node instanceof LiteralNode && !(node instanceof ArrayLiteralNode) && !to.isObject()) {
            final LiteralNode<?> newNode = new LiteralNodeConstantEvaluator((LiteralNode<?>)node, to).eval();
            if (newNode != null) {
                resultNode = newNode;
            }
        } else {
            if (canHaveCallSiteType(node) && isSupportedCallSiteType(to)) {
                assert node instanceof TypeOverride;
                return setTypeOverride(node, to);
            }
            resultNode = new UnaryNode(Token.recast(node.getToken(), TokenType.CONVERT), node);
        }

        LOG.info("CONVERT('", node, "', ", to, ") => '", resultNode, "'");

        assert !node.isTerminal();

        //This is the only place in this file that can create new temporaries
        //FinalizeTypes may not introduce ANY node that is not a conversion.
        return temporarySymbols.ensureSymbol(lc, to, resultNode);
    }

    private static Expression discard(final Expression node) {
        if (node.getSymbol() != null) {
            final UnaryNode discard = new UnaryNode(Token.recast(node.getToken(), TokenType.DISCARD), node);
            //discard never has a symbol in the discard node - then it would be a nop
            assert !node.isTerminal();
            return discard;
        }

        // node has no result (symbol) so we can keep it the way it is
        return node;
    }

    /**
     * Whenever an expression like an addition or an assignment changes type, it
     * may be that case that {@link Attr} created a symbol for an intermediate
     * result of the expression, say for an addition. This also has to be updated
     * if the expression type changes.
     *
     * Assignments use their lhs as node symbol, and in this case we can't modify
     * it. Then {@link CodeGenerator.Store} needs to do an explicit conversion.
     * This is happens very rarely.
     *
     * @param node
     * @param to
     */
    private Expression propagateType(final Expression node, final Type to) {
        Symbol symbol = node.getSymbol();
        if (symbol.isTemp() && symbol.getSymbolType() != to) {
            symbol = symbol.setTypeOverrideShared(to, temporarySymbols);
            LOG.info("Type override for temporary in '", node, "' => ", to);
        }
        return node.setSymbol(lc, symbol);
    }

    /**
     * Determine if the outcome of + operator is a string.
     *
     * @param node  Node to test.
     * @return true if a string result.
     */
    private boolean isAddString(final Node node) {
        if (node instanceof BinaryNode && node.isTokenType(TokenType.ADD)) {
            final BinaryNode binaryNode = (BinaryNode)node;
            final Node lhs = binaryNode.lhs();
            final Node rhs = binaryNode.rhs();

            return isAddString(lhs) || isAddString(rhs);
        }

        return node instanceof LiteralNode<?> && ((LiteralNode<?>)node).isString();
    }

    /**
     * Whenever an explicit conversion is needed and the convertee is a literal, we can
     * just change the literal
     */
    class LiteralNodeConstantEvaluator extends FoldConstants.ConstantEvaluator<LiteralNode<?>> {
        private final Type type;

        LiteralNodeConstantEvaluator(final LiteralNode<?> parent, final Type type) {
            super(parent);
            this.type = type;
        }

        @Override
        protected LiteralNode<?> eval() {
            final Object value = ((LiteralNode<?>)parent).getValue();

            LiteralNode<?> literalNode = null;

            if (type.isString()) {
                literalNode = LiteralNode.newInstance(token, finish, JSType.toString(value));
            } else if (type.isBoolean()) {
                literalNode = LiteralNode.newInstance(token, finish, JSType.toBoolean(value));
            } else if (type.isInteger()) {
                literalNode = LiteralNode.newInstance(token, finish, JSType.toInt32(value));
            } else if (type.isLong()) {
                literalNode = LiteralNode.newInstance(token, finish, JSType.toLong(value));
            } else if (type.isNumber() || parent.getType().isNumeric() && !parent.getType().isNumber()) {
                literalNode = LiteralNode.newInstance(token, finish, JSType.toNumber(value));
            }

            if (literalNode != null) {
                //inherit literal symbol for attr.
                literalNode = (LiteralNode<?>)literalNode.setSymbol(lc, parent.getSymbol());
            }

            return literalNode;
        }
    }
}