All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.oracle.truffle.js.parser.GraalJSTranslator Maven / Gradle / Ivy

There is a newer version: 4.15.102
Show newest version
/*
 * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * The Universal Permissive License (UPL), Version 1.0
 *
 * Subject to the condition set forth below, permission is hereby granted to any
 * person obtaining a copy of this software, associated documentation and/or
 * data (collectively the "Software"), free of charge and under any and all
 * copyright rights in the Software, and any and all patent rights owned or
 * freely licensable by each licensor hereunder covering either (i) the
 * unmodified Software as contributed to or provided by such licensor, or (ii)
 * the Larger Works (as defined below), to deal in both
 *
 * (a) the Software, and
 *
 * (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
 * one is included with the Software each a "Larger Work" to which the Software
 * is contributed by such licensors),
 *
 * without restriction, including without limitation the rights to copy, create
 * derivative works of, display, perform, and distribute the Software and make,
 * use, sell, offer for sale, import, export, have made, and have sold the
 * Software and the Larger Work(s), and to sublicense the foregoing rights on
 * either these or other terms.
 *
 * This license is subject to the following condition:
 *
 * The above copyright notice and either this complete permission notice or at a
 * minimum a reference to the UPL must be included in all copies or substantial
 * portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.oracle.truffle.js.parser;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

import com.oracle.js.parser.Lexer;
import com.oracle.js.parser.Token;
import com.oracle.js.parser.TokenType;
import com.oracle.js.parser.ir.AccessNode;
import com.oracle.js.parser.ir.BaseNode;
import com.oracle.js.parser.ir.BinaryNode;
import com.oracle.js.parser.ir.Block;
import com.oracle.js.parser.ir.BlockExpression;
import com.oracle.js.parser.ir.BlockStatement;
import com.oracle.js.parser.ir.CallNode;
import com.oracle.js.parser.ir.CaseNode;
import com.oracle.js.parser.ir.CatchNode;
import com.oracle.js.parser.ir.ClassNode;
import com.oracle.js.parser.ir.DebuggerNode;
import com.oracle.js.parser.ir.Expression;
import com.oracle.js.parser.ir.ExpressionStatement;
import com.oracle.js.parser.ir.ForNode;
import com.oracle.js.parser.ir.FunctionNode;
import com.oracle.js.parser.ir.IdentNode;
import com.oracle.js.parser.ir.IfNode;
import com.oracle.js.parser.ir.IndexNode;
import com.oracle.js.parser.ir.JoinPredecessorExpression;
import com.oracle.js.parser.ir.LexicalContext;
import com.oracle.js.parser.ir.LexicalContextNode;
import com.oracle.js.parser.ir.LexicalContextScope;
import com.oracle.js.parser.ir.LiteralNode;
import com.oracle.js.parser.ir.Module;
import com.oracle.js.parser.ir.Module.ImportEntry;
import com.oracle.js.parser.ir.ObjectNode;
import com.oracle.js.parser.ir.ParameterNode;
import com.oracle.js.parser.ir.PropertyNode;
import com.oracle.js.parser.ir.RuntimeNode;
import com.oracle.js.parser.ir.Scope;
import com.oracle.js.parser.ir.Statement;
import com.oracle.js.parser.ir.Symbol;
import com.oracle.js.parser.ir.TernaryNode;
import com.oracle.js.parser.ir.TryNode;
import com.oracle.js.parser.ir.UnaryNode;
import com.oracle.js.parser.ir.VarNode;
import com.oracle.js.parser.ir.WithNode;
import com.oracle.js.parser.ir.visitor.NodeVisitor;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.StandardTags;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.NodeUtil;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.js.lang.JavaScriptLanguage;
import com.oracle.truffle.js.nodes.JavaScriptNode;
import com.oracle.truffle.js.nodes.NodeFactory;
import com.oracle.truffle.js.nodes.NodeFactory.BinaryOperation;
import com.oracle.truffle.js.nodes.NodeFactory.UnaryOperation;
import com.oracle.truffle.js.nodes.ReadNode;
import com.oracle.truffle.js.nodes.RepeatableNode;
import com.oracle.truffle.js.nodes.ScriptNode;
import com.oracle.truffle.js.nodes.access.ArrayLiteralNode;
import com.oracle.truffle.js.nodes.access.CreateObjectNode;
import com.oracle.truffle.js.nodes.access.DeclareEvalVariableNode;
import com.oracle.truffle.js.nodes.access.DeclareGlobalNode;
import com.oracle.truffle.js.nodes.access.GlobalPropertyNode;
import com.oracle.truffle.js.nodes.access.GlobalScopeVarWrapperNode;
import com.oracle.truffle.js.nodes.access.JSConstantNode;
import com.oracle.truffle.js.nodes.access.JSReadFrameSlotNode;
import com.oracle.truffle.js.nodes.access.JSWriteFrameSlotNode;
import com.oracle.truffle.js.nodes.access.LazyReadFrameSlotNode;
import com.oracle.truffle.js.nodes.access.ObjectLiteralNode;
import com.oracle.truffle.js.nodes.access.ObjectLiteralNode.ObjectLiteralMemberNode;
import com.oracle.truffle.js.nodes.access.OptionalChainNode;
import com.oracle.truffle.js.nodes.access.ReadElementNode;
import com.oracle.truffle.js.nodes.access.ScopeFrameNode;
import com.oracle.truffle.js.nodes.access.WriteElementNode;
import com.oracle.truffle.js.nodes.access.WriteNode;
import com.oracle.truffle.js.nodes.access.WritePropertyNode;
import com.oracle.truffle.js.nodes.binary.DualNode;
import com.oracle.truffle.js.nodes.binary.JSBinaryNode;
import com.oracle.truffle.js.nodes.binary.JSTypeofIdenticalNode;
import com.oracle.truffle.js.nodes.control.AbstractBlockNode;
import com.oracle.truffle.js.nodes.control.BreakNode;
import com.oracle.truffle.js.nodes.control.BreakTarget;
import com.oracle.truffle.js.nodes.control.ContinueTarget;
import com.oracle.truffle.js.nodes.control.DiscardResultNode;
import com.oracle.truffle.js.nodes.control.EmptyNode;
import com.oracle.truffle.js.nodes.control.GeneratorWrapperNode;
import com.oracle.truffle.js.nodes.control.ResumableNode;
import com.oracle.truffle.js.nodes.control.ReturnNode;
import com.oracle.truffle.js.nodes.control.ReturnTargetNode;
import com.oracle.truffle.js.nodes.control.SequenceNode;
import com.oracle.truffle.js.nodes.control.StatementNode;
import com.oracle.truffle.js.nodes.control.SuspendNode;
import com.oracle.truffle.js.nodes.function.AbstractFunctionArgumentsNode;
import com.oracle.truffle.js.nodes.function.BlockScopeNode;
import com.oracle.truffle.js.nodes.function.EvalNode;
import com.oracle.truffle.js.nodes.function.FunctionBodyNode;
import com.oracle.truffle.js.nodes.function.FunctionRootNode;
import com.oracle.truffle.js.nodes.function.JSFunctionCallNode;
import com.oracle.truffle.js.nodes.function.JSFunctionExpressionNode;
import com.oracle.truffle.js.nodes.function.JSNewNode;
import com.oracle.truffle.js.nodes.function.SpreadArgumentNode;
import com.oracle.truffle.js.nodes.unary.JSUnaryNode;
import com.oracle.truffle.js.nodes.unary.TypeOfNode;
import com.oracle.truffle.js.nodes.unary.VoidNode;
import com.oracle.truffle.js.parser.env.BlockEnvironment;
import com.oracle.truffle.js.parser.env.DebugEnvironment;
import com.oracle.truffle.js.parser.env.Environment;
import com.oracle.truffle.js.parser.env.Environment.AbstractFrameVarRef;
import com.oracle.truffle.js.parser.env.Environment.FrameSlotVarRef;
import com.oracle.truffle.js.parser.env.Environment.VarRef;
import com.oracle.truffle.js.parser.env.EvalEnvironment;
import com.oracle.truffle.js.parser.env.FunctionEnvironment;
import com.oracle.truffle.js.parser.env.FunctionEnvironment.JumpTargetCloseable;
import com.oracle.truffle.js.parser.env.GlobalEnvironment;
import com.oracle.truffle.js.parser.env.WithEnvironment;
import com.oracle.truffle.js.parser.internal.ir.debug.PrintVisitor;
import com.oracle.truffle.js.runtime.BigInt;
import com.oracle.truffle.js.runtime.Errors;
import com.oracle.truffle.js.runtime.JSConfig;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.JSErrorType;
import com.oracle.truffle.js.runtime.JSFrameUtil;
import com.oracle.truffle.js.runtime.JSRuntime;
import com.oracle.truffle.js.runtime.builtins.JSFunctionData;
import com.oracle.truffle.js.runtime.objects.Dead;
import com.oracle.truffle.js.runtime.objects.Undefined;
import com.oracle.truffle.js.runtime.util.Pair;

abstract class GraalJSTranslator extends com.oracle.js.parser.ir.visitor.TranslatorNodeVisitor {
    public static final JavaScriptNode[] EMPTY_NODE_ARRAY = new JavaScriptNode[0];
    private static final JavaScriptNode ANY_JAVA_SCRIPT_NODE = new JavaScriptNode() {
        @Override
        public Object execute(VirtualFrame frame) {
            CompilerDirectives.transferToInterpreter();
            throw new UnsupportedOperationException();
        }
    };

    private static final SourceSection unavailableInternalSection = Source.newBuilder(JavaScriptLanguage.ID, "", "").mimeType(
                    JavaScriptLanguage.APPLICATION_MIME_TYPE).internal(true).build().createUnavailableSection();

    private Environment environment;
    protected final JSContext context;
    protected final NodeFactory factory;
    protected final Source source;
    protected final String[] argumentNames;
    protected final int sourceLength;
    protected final int prologLength;
    private final boolean isParentStrict;

    protected GraalJSTranslator(LexicalContext lc, NodeFactory factory, JSContext context, Source source, String[] argumentNames, int prologLength, Environment environment, boolean isParentStrict) {
        super(lc);
        this.context = context;
        this.environment = environment;
        this.factory = factory;
        this.source = source;
        this.argumentNames = argumentNames;
        this.isParentStrict = isParentStrict;
        this.sourceLength = source.getCharacters().length();
        this.prologLength = prologLength;
    }

    protected final JavaScriptNode transform(com.oracle.js.parser.ir.Node node) {
        if (node != null) {
            return node.accept(this);
        }
        return null;
    }

    private JavaScriptNode tagStatement(JavaScriptNode resultNode, com.oracle.js.parser.ir.Node parseNode) {
        if (!resultNode.hasSourceSection()) {
            assignSourceSection(resultNode, parseNode);
        }
        assert resultNode.getSourceSection() != null;
        if (resultNode instanceof GlobalScopeVarWrapperNode) {
            tagStatement(((GlobalScopeVarWrapperNode) resultNode).getDelegateNode(), parseNode);
        } else {
            resultNode.addStatementTag();
        }
        return resultNode;
    }

    private JavaScriptNode tagExpression(JavaScriptNode resultNode, com.oracle.js.parser.ir.Node parseNode) {
        if (!resultNode.hasSourceSection()) {
            assignSourceSection(resultNode, parseNode);
        }
        assert resultNode.getSourceSection() != null;
        if (resultNode instanceof GlobalScopeVarWrapperNode) {
            tagExpression(((GlobalScopeVarWrapperNode) resultNode).getDelegateNode(), parseNode);
        } else {
            resultNode.addExpressionTag();
        }
        return resultNode;
    }

    private static JavaScriptNode tagCall(JavaScriptNode resultNode) {
        resultNode.addCallTag();
        return resultNode;
    }

    private JavaScriptNode tagBody(JavaScriptNode resultNode, com.oracle.js.parser.ir.Node parseNode) {
        if (!resultNode.hasSourceSection()) {
            assignSourceSection(resultNode, parseNode);
        }
        assert resultNode.getSourceSection() != null;
        if (resultNode instanceof GlobalScopeVarWrapperNode) {
            tagBody(((GlobalScopeVarWrapperNode) resultNode).getDelegateNode(), parseNode);
        } else {
            resultNode.addRootBodyTag();
        }
        return resultNode;
    }

    private FunctionEnvironment currentFunction() {
        return environment.function();
    }

    private JavaScriptNode createBlock(JavaScriptNode... statements) {
        return createBlock(statements, false, false);
    }

    private JavaScriptNode createBlock(JavaScriptNode[] statements, boolean terminal, boolean expressionBlock) {
        if ((JSConfig.ReturnOptimizer && terminal) || expressionBlock || currentFunction().returnsLastStatementResult()) {
            return factory.createExprBlock(statements);
        } else {
            return factory.createVoidBlock(statements);
        }
    }

    protected final ScriptNode translateScript(FunctionNode functionNode) {
        if (!functionNode.isScript()) {
            throw new IllegalArgumentException("root function node is not a script");
        }
        JSFunctionExpressionNode functionExpression = (JSFunctionExpressionNode) transformFunction(functionNode);
        return ScriptNode.fromFunctionRoot(context, functionExpression.getFunctionNode());
    }

    protected final JavaScriptNode transformFunction(FunctionNode functionNode) {
        return transform(functionNode);
    }

    protected abstract GraalJSTranslator newTranslator(Environment env, LexicalContext savedLC);

    // ---

    @Override
    public JavaScriptNode enterFunctionNode(FunctionNode functionNode) {
        if (JSConfig.PrintParse) {
            printParse(functionNode);
        }

        boolean isStrict = functionNode.isStrict() || isParentStrict || (environment != null && environment.function() != null && environment.isStrictMode());
        boolean isArrowFunction = functionNode.isArrow();
        boolean isGeneratorFunction = functionNode.isGenerator();
        boolean isAsyncFunction = functionNode.isAsync();
        boolean isDerivedConstructor = functionNode.isDerivedConstructor();

        boolean isMethod = functionNode.isMethod();
        boolean needsNewTarget = functionNode.needsNewTarget();
        boolean isClassConstructor = functionNode.isClassConstructor();
        boolean isConstructor = !isArrowFunction && !isGeneratorFunction && !isAsyncFunction && ((!isMethod || context.getEcmaScriptVersion() == 5) || isClassConstructor);
        assert !isDerivedConstructor || isConstructor;
        boolean strictFunctionProperties = isStrict || isArrowFunction || isMethod || isGeneratorFunction;
        boolean isBuiltin = false;
        boolean hasSyntheticArguments = functionNode.isScript() && this.argumentNames != null;

        boolean isGlobal;
        boolean isEval = false;
        boolean isIndirectEval = false;
        boolean inDirectEval = false;
        if (environment instanceof EvalEnvironment) {
            isEval = true;
            boolean isDirectEval = ((EvalEnvironment) environment).isDirectEval();
            isIndirectEval = !isDirectEval;
            Environment evalParent = environment.getParent();
            isGlobal = evalParent == null || (isDirectEval && (!isStrict && evalParent.function().isGlobal()));
            inDirectEval = isDirectEval || (evalParent != null && evalParent.function().inDirectEval());
        } else if (environment instanceof DebugEnvironment) {
            isGlobal = environment.getParent() == null;
            isEval = true;
            inDirectEval = true;
        } else {
            isGlobal = environment == null && argumentNames == null;
            inDirectEval = environment != null && currentFunction().inDirectEval();
        }
        boolean functionMode = !isGlobal || (isStrict && isIndirectEval);

        boolean lazyTranslation = context.getContextOptions().isLazyTranslation() && functionMode && !functionNode.isProgram() && !inDirectEval;

        String functionName = getFunctionName(functionNode);
        JSFunctionData functionData;
        FunctionRootNode functionRoot;
        if (lazyTranslation) {
            assert functionMode && !functionNode.isProgram();

            // function needs parent frame analysis has already been done
            boolean needsParentFrame = functionNode.usesAncestorScope();

            functionData = factory.createFunctionData(context, functionNode.getLength(), functionName, isConstructor, isDerivedConstructor, isStrict, isBuiltin,
                            needsParentFrame, isGeneratorFunction, isAsyncFunction, isClassConstructor, strictFunctionProperties, needsNewTarget);

            LexicalContext savedLC = lc.copy();
            Environment parentEnv = environment;
            functionData.setLazyInit(fd -> {
                GraalJSTranslator translator = newTranslator(parentEnv, savedLC);
                translator.translateFunctionOnDemand(functionNode, fd, isStrict, isArrowFunction, isGeneratorFunction, isAsyncFunction, isDerivedConstructor, isGlobal,
                                needsNewTarget, needsParentFrame, functionName, hasSyntheticArguments);
            });
            functionRoot = null;
        } else {
            try (EnvironmentCloseable functionEnv = enterFunctionEnvironment(isStrict, isArrowFunction, isGeneratorFunction, isDerivedConstructor, isAsyncFunction, isGlobal, hasSyntheticArguments)) {
                FunctionEnvironment currentFunction = currentFunction();
                currentFunction.setFunctionName(functionName);
                currentFunction.setInternalFunctionName(functionNode.getInternalName());
                currentFunction.setNamedFunctionExpression(functionNode.isNamedFunctionExpression());

                declareParameters(functionNode);
                if (functionNode.getNumOfParams() > context.getFunctionArgumentsLimit()) {
                    throw Errors.createSyntaxError("function has too many arguments");
                }

                List declarations;
                if (functionMode) {
                    declarations = functionEnvInit(functionNode);
                } else if (functionNode.isModule()) {
                    assert currentFunction.isGlobal();
                    declarations = Collections.emptyList();
                } else {
                    assert currentFunction.isGlobal();
                    declarations = collectGlobalVars(functionNode, isEval);
                }

                if (functionNode.isProgram()) {
                    functionNeedsParentFramePass(functionNode, context);
                }

                boolean needsParentFrame = functionNode.usesAncestorScope();
                currentFunction.setNeedsParentFrame(needsParentFrame);

                JavaScriptNode body = translateFunctionBody(functionNode, isGeneratorFunction, isAsyncFunction, isDerivedConstructor, needsNewTarget, currentFunction, declarations);

                needsParentFrame = currentFunction.needsParentFrame();
                currentFunction.freeze();

                functionData = factory.createFunctionData(context, functionNode.getLength(), functionName, isConstructor, isDerivedConstructor, isStrict, isBuiltin,
                                needsParentFrame, isGeneratorFunction, isAsyncFunction, isClassConstructor, strictFunctionProperties, needsNewTarget);

                functionRoot = createFunctionRoot(functionNode, functionData, currentFunction, body);

                if (isEval) {
                    // force eager call target init for Function() code to avoid deopt at call site
                    functionData.getCallTarget();
                }
            }
        }

        JavaScriptNode functionExpression;
        if (isArrowFunction && functionNode.needsThis() && !currentFunction().getNonArrowParentFunction().isDerivedConstructor()) {
            JavaScriptNode thisNode = createThisNode();
            functionExpression = factory.createFunctionExpressionLexicalThis(functionData, functionRoot, thisNode);
        } else {
            functionExpression = factory.createFunctionExpression(functionData, functionRoot);
        }

        if (functionNode.isDeclared()) {
            ensureHasSourceSection(functionExpression, functionNode);
        } else {
            functionExpression = tagExpression(functionExpression, functionNode);
        }
        return functionExpression;
    }

    JavaScriptNode translateFunctionBody(FunctionNode functionNode, boolean isGeneratorFunction, boolean isAsyncFunction, boolean isDerivedConstructor, boolean needsNewTarget,
                    FunctionEnvironment currentFunction, List declarations) {
        JavaScriptNode body = transform(functionNode.getBody());

        if (!isGeneratorFunction) {
            // finishGeneratorBody has already taken care of this for (async) generator functions
            body = handleFunctionReturn(functionNode, body);

            if (isAsyncFunction) {
                ensureHasSourceSection(body, functionNode);
                body = handleAsyncFunctionBody(body);
            }
        }

        if (!declarations.isEmpty()) {
            body = prepareDeclarations(declarations, body);
        }
        if (currentFunction.hasArgumentsSlot() && !currentFunction.isDirectArgumentsAccess() && !currentFunction.isDirectEval()) {
            body = prepareArguments(body);
        }
        if (currentFunction.getParameterCount() > 0) {
            body = prepareParameters(body);
        }
        if (currentFunction.getThisSlot() != null && !isDerivedConstructor) {
            body = prepareThis(body, functionNode);
        }
        if (currentFunction.getSuperSlot() != null) {
            body = prepareSuper(body);
        }
        if (needsNewTarget) {
            body = prepareNewTarget(body);
        }

        if (isDerivedConstructor) {
            JavaScriptNode getThisBinding = checkThisBindingInitialized(
                            (functionNode.hasDirectSuper() || functionNode.hasEval() || functionNode.hasArrowEval()) ? environment.findThisVar().createReadNode() : factory.createConstantUndefined());
            body = factory.createDerivedConstructorResult(body, getThisBinding);
        }

        return body;
    }

    private FunctionRootNode translateFunctionOnDemand(FunctionNode functionNode, JSFunctionData functionData, boolean isStrict, boolean isArrowFunction, boolean isGeneratorFunction,
                    boolean isAsyncFunction, boolean isDerivedConstructor, boolean isGlobal, boolean needsNewTarget, boolean needsParentFrame, String functionName, boolean hasSyntheticArguments) {
        try (EnvironmentCloseable functionEnv = enterFunctionEnvironment(isStrict, isArrowFunction, isGeneratorFunction, isDerivedConstructor, isAsyncFunction, isGlobal, hasSyntheticArguments)) {
            FunctionEnvironment currentFunction = currentFunction();
            currentFunction.setFunctionName(functionName);
            currentFunction.setInternalFunctionName(functionNode.getInternalName());
            currentFunction.setNamedFunctionExpression(functionNode.isNamedFunctionExpression());

            currentFunction.setNeedsParentFrame(needsParentFrame);

            declareParameters(functionNode);
            if (functionNode.getNumOfParams() > context.getFunctionArgumentsLimit()) {
                throw Errors.createSyntaxError("function has too many arguments");
            }
            functionEnvInit(functionNode);

            currentFunction.freeze();
            assert currentFunction.isDeepFrozen();

            JavaScriptNode body = translateFunctionBody(functionNode, isGeneratorFunction, isAsyncFunction, isDerivedConstructor, needsNewTarget, currentFunction, Collections.emptyList());
            return createFunctionRoot(functionNode, functionData, currentFunction, body);
        }
    }

    private FunctionRootNode createFunctionRoot(FunctionNode functionNode, JSFunctionData functionData, FunctionEnvironment currentFunction, JavaScriptNode body) {
        SourceSection functionSourceSection = createSourceSection(functionNode);
        FunctionBodyNode functionBody = factory.createFunctionBody(body);
        FunctionRootNode functionRoot = factory.createFunctionRootNode(functionBody, environment.getFunctionFrameDescriptor(), functionData, functionSourceSection,
                        currentFunction.getInternalFunctionName());

        if (JSConfig.PrintAst) {
            printAST(functionRoot);
        }
        return functionRoot;
    }

    private static void printAST(FunctionRootNode functionRoot) {
        NodeUtil.printCompactTree(System.out, functionRoot);
    }

    private static void printParse(FunctionNode functionNode) {
        System.out.printf(new PrintVisitor(functionNode).toString());
    }

    /**
     * Async function parse-time AST modifications.
     *
     * @return instrumented function body
     */
    private JavaScriptNode handleAsyncFunctionBody(JavaScriptNode body) {
        assert currentFunction().isAsyncFunction() && !currentFunction().isGeneratorFunction();
        VarRef asyncContextVar = environment.findAsyncContextVar();
        VarRef asyncResultVar = environment.findAsyncResultVar();
        JSWriteFrameSlotNode writeResultNode = (JSWriteFrameSlotNode) asyncResultVar.createWriteNode(null);
        JSWriteFrameSlotNode writeContextNode = (JSWriteFrameSlotNode) asyncContextVar.createWriteNode(null);
        JSReadFrameSlotNode readContextNode = (JSReadFrameSlotNode) asyncContextVar.createReadNode();
        JavaScriptNode instrumentedBody = instrumentSuspendNodes(body);
        return factory.createAsyncFunctionBody(context, instrumentedBody, writeContextNode, readContextNode, writeResultNode);
    }

    /**
     * Generator function parse-time AST modifications.
     *
     * @return instrumented function body
     */
    private JavaScriptNode finishGeneratorBody(JavaScriptNode bodyBlock) {
        JavaScriptNode body = handleFunctionReturn(lc.getCurrentFunction(), bodyBlock);
        // Note: parameter initialization must precede (i.e. wrap) the (async) generator body
        if (currentFunction().isAsyncGeneratorFunction()) {
            return handleAsyncGeneratorBody(body);
        } else {
            return handleGeneratorBody(body);
        }
    }

    private JavaScriptNode handleGeneratorBody(JavaScriptNode body) {
        assert currentFunction().isGeneratorFunction() && !currentFunction().isAsyncGeneratorFunction();
        JavaScriptNode instrumentedBody = instrumentSuspendNodes(body);
        if (lc.getCurrentFunction().isModule()) {
            return factory.createModuleBody(instrumentedBody);
        }
        VarRef yieldVar = environment.findYieldValueVar();
        JSWriteFrameSlotNode writeYieldValueNode = (JSWriteFrameSlotNode) yieldVar.createWriteNode(null);
        JSReadFrameSlotNode readYieldResultNode = JSConfig.YieldResultInFrame ? (JSReadFrameSlotNode) environment.findTempVar(currentFunction().getYieldResultSlot()).createReadNode() : null;
        return factory.createGeneratorBody(context, instrumentedBody, writeYieldValueNode, readYieldResultNode);
    }

    private JavaScriptNode handleAsyncGeneratorBody(JavaScriptNode body) {
        assert currentFunction().isAsyncGeneratorFunction();
        VarRef asyncContextVar = environment.findAsyncContextVar();
        JavaScriptNode instrumentedBody = instrumentSuspendNodes(body);
        VarRef yieldVar = environment.findAsyncResultVar();
        JSWriteFrameSlotNode writeAsyncContextNode = (JSWriteFrameSlotNode) asyncContextVar.createWriteNode(null);
        JSReadFrameSlotNode readAsyncContextNode = (JSReadFrameSlotNode) asyncContextVar.createReadNode();
        JSWriteFrameSlotNode writeYieldValueNode = (JSWriteFrameSlotNode) yieldVar.createWriteNode(null);
        if (lc.getCurrentFunction().isModule()) {
            return factory.createTopLevelAsyncModuleBody(context, instrumentedBody, writeYieldValueNode, writeAsyncContextNode);
        }
        JSReadFrameSlotNode readYieldResultNode = JSConfig.YieldResultInFrame ? (JSReadFrameSlotNode) environment.findTempVar(currentFunction().getYieldResultSlot()).createReadNode() : null;
        return factory.createAsyncGeneratorBody(context, instrumentedBody, writeYieldValueNode, readYieldResultNode, writeAsyncContextNode, readAsyncContextNode);
    }

    /**
     * Instrument code paths leading to yield and await expressions.
     */
    private JavaScriptNode instrumentSuspendNodes(JavaScriptNode body) {
        if (!currentFunction().hasYield() && !currentFunction().hasAwait()) {
            return body;
        }
        JavaScriptNode newBody = (JavaScriptNode) instrumentSuspendHelper(body, null);
        Objects.requireNonNull(newBody);
        return newBody;
    }

    private Node instrumentSuspendHelper(Node parent, Node grandparent) {
        boolean hasSuspendChild = false;
        BitSet suspendableIndices = null;
        if (parent instanceof AbstractBlockNode) {
            Node[] statements = ((AbstractBlockNode) parent).getStatements();
            for (int i = 0; i < statements.length; i++) {
                Node newChild = instrumentSuspendHelper(statements[i], parent);
                if (newChild != null) {
                    hasSuspendChild = true;
                    statements[i] = newChild;
                    if (suspendableIndices == null) {
                        suspendableIndices = new BitSet();
                    }
                    suspendableIndices.set(i);
                }
            }
        } else {
            for (Node child : getChildrenInExecutionOrder(parent)) {
                Node newChild = instrumentSuspendHelper(child, parent);
                if (newChild != null) {
                    hasSuspendChild = true;
                    NodeUtil.replaceChild(parent, child, newChild);
                    assert !(child instanceof ResumableNode) || newChild instanceof GeneratorWrapperNode : "resumable node not wrapped: " + child;
                }
            }
        }
        if (parent instanceof SuspendNode) {
            return wrapResumableNode(parent);
        } else if (!hasSuspendChild) {
            return null;
        }

        if (parent instanceof AbstractBlockNode) {
            assert suspendableIndices != null && !suspendableIndices.isEmpty();
            return toGeneratorBlockNode((AbstractBlockNode) parent, suspendableIndices);
        } else if (parent instanceof ResumableNode) {
            return wrapResumableNode(parent);
        } else if (parent instanceof ReturnNode || parent instanceof ReturnTargetNode || isSideEffectFreeUnaryOpNode(parent)) {
            // these are side-effect-free, skip
            return parent;
        } else if (isSupportedDispersibleExpression(parent)) {
            // need to rescue side-effecting/non-repeatable expressions into temporaries
            // note that the expressions have to be extracted in evaluation order
            List extracted = new ArrayList<>();
            // we can only replace child fields assignable from JavaScriptNode
            if (grandparent == null || NodeUtil.isReplacementSafe(grandparent, parent, ANY_JAVA_SCRIPT_NODE)) {
                // extraction is a destructive step; only attempt it if replace can succeed
                extractChildrenTo(parent, extracted);
            } else {
                // not assignable to field type (e.g. JSTargetableNode), ignore for now
            }
            if (!extracted.isEmpty()) { // only if there's actually something to rescue
                extracted.add((JavaScriptNode) parent);
                // insert block node wrapper
                JavaScriptNode exprBlock = wrapResumableNode(factory.createExprBlock(extracted.toArray(EMPTY_NODE_ARRAY)));
                tagHiddenExpression(exprBlock);
                return exprBlock;
            } else {
                // nothing to do
                return parent;
            }
        } else {
            // if (parent instanceof JavaScriptNode):
            // unknown expression node type, either safe or unexpected (not handled)
            // else:
            // unsupported node type, skip over
            return parent;
        }
    }

    private JavaScriptNode wrapResumableNode(Node resumableNode) {
        if (resumableNode instanceof AbstractBlockNode) {
            BitSet all = new BitSet();
            all.set(0, ((AbstractBlockNode) resumableNode).getStatements().length);
            return toGeneratorBlockNode((AbstractBlockNode) resumableNode, all);
        }
        String identifier = ":generatorstate:" + environment.getFunctionFrameDescriptor().getSize();
        environment.getFunctionFrameDescriptor().addFrameSlot(identifier);
        LazyReadFrameSlotNode readState = factory.createLazyReadFrameSlot(identifier);
        WriteNode writeState = factory.createLazyWriteFrameSlot(identifier, null);
        return factory.createGeneratorWrapper((JavaScriptNode) resumableNode, readState, writeState);
    }

    private JavaScriptNode toGeneratorBlockNode(AbstractBlockNode blockNode, BitSet suspendableIndices) {
        String identifier = ":generatorstate:" + environment.getFunctionFrameDescriptor().getSize();
        environment.getFunctionFrameDescriptor().addFrameSlot(identifier);
        LazyReadFrameSlotNode readState = factory.createLazyReadFrameSlot(identifier);
        WriteNode writeState = factory.createLazyWriteFrameSlot(identifier, null);

        JavaScriptNode[] statements = blockNode.getStatements();
        boolean returnsResult = !blockNode.isResultAlwaysOfType(Undefined.class);
        JavaScriptNode genBlock;
        // we can resume at index 0 (start state) and every statement that contains a yield
        int resumePoints = suspendableIndices.cardinality() + (suspendableIndices.get(0) ? 0 : 1);
        if (resumePoints == statements.length) {
            // all statements are resume points
            genBlock = returnsResult ? factory.createGeneratorExprBlock(statements, readState, writeState) : factory.createGeneratorVoidBlock(statements, readState, writeState);
        } else {
            // split block into resumable chunks of at least 1 statement.
            JavaScriptNode[] chunks = new JavaScriptNode[resumePoints];
            int fromIndex = 0;
            int toIndex;
            for (int chunkI = 0; chunkI < resumePoints; chunkI++) {
                toIndex = suspendableIndices.nextSetBit(fromIndex + 1);
                if (toIndex < 0) {
                    assert chunkI == resumePoints - 1;
                    toIndex = statements.length;
                }
                returnsResult = chunkI == resumePoints - 1 && !blockNode.isResultAlwaysOfType(Undefined.class);
                JavaScriptNode chunk;
                if (fromIndex + 1 == toIndex) {
                    chunk = statements[fromIndex];
                } else {
                    JavaScriptNode[] chunkStatements = Arrays.copyOfRange(statements, fromIndex, toIndex);
                    chunk = (returnsResult && chunkI == resumePoints - 1) ? factory.createExprBlock(chunkStatements) : factory.createVoidBlock(chunkStatements);
                }
                chunks[chunkI] = chunk;
                fromIndex = toIndex;
            }
            genBlock = returnsResult ? factory.createGeneratorExprBlock(chunks, readState, writeState) : factory.createGeneratorVoidBlock(chunks, readState, writeState);
        }
        JavaScriptNode.transferSourceSectionAndTags(blockNode, genBlock);
        return genBlock;
    }

    private static boolean isSideEffectFreeUnaryOpNode(Node node) {
        // (conservative) non-exhaustive list
        return node instanceof DiscardResultNode || node instanceof VoidNode || node instanceof TypeOfNode || node instanceof JSTypeofIdenticalNode;
    }

    private static boolean isSupportedDispersibleExpression(Node node) {
        return node instanceof JSBinaryNode || node instanceof JSUnaryNode ||
                        node instanceof ArrayLiteralNode || node instanceof ObjectLiteralNode ||
                        node instanceof com.oracle.truffle.js.nodes.access.PropertyNode || node instanceof GlobalPropertyNode || node instanceof ReadElementNode ||
                        node instanceof WritePropertyNode || node instanceof WriteElementNode ||
                        node instanceof JSFunctionCallNode || node instanceof JSNewNode;
    }

    private static boolean isStatelessExpression(Node child) {
        return child instanceof JSConstantNode || child instanceof CreateObjectNode || (child instanceof RepeatableNode && !(child instanceof ReadNode));
    }

    private static boolean skipOverToChildren(Node node) {
        return node instanceof ObjectLiteralMemberNode || node instanceof AbstractFunctionArgumentsNode || node instanceof ArrayLiteralNode.SpreadArrayNode || node instanceof SpreadArgumentNode;
    }

    private void extractChildTo(Node child, Node parent, List extracted) {
        if (isStatelessExpression(child)) {
            return;
        }
        if (skipOverToChildren(child)) {
            extractChildrenTo(child, extracted);
        } else if (child instanceof JavaScriptNode) {
            JavaScriptNode jschild = (JavaScriptNode) child;
            String identifier = ":generatorexpr:" + environment.getFunctionFrameDescriptor().getSize();
            LazyReadFrameSlotNode readState = factory.createLazyReadFrameSlot(identifier);
            if (jschild.hasTag(StandardTags.ExpressionTag.class) ||
                            (jschild instanceof GeneratorWrapperNode && ((GeneratorWrapperNode) jschild).getResumableNode().hasTag(StandardTags.ExpressionTag.class))) {
                tagHiddenExpression(readState);
            }
            JavaScriptNode writeState = factory.createLazyWriteFrameSlot(identifier, jschild);
            if (NodeUtil.isReplacementSafe(parent, child, readState)) {
                environment.getFunctionFrameDescriptor().addFrameSlot(identifier);
                extracted.add(writeState);
                // replace child with saved expression result
                boolean ok = NodeUtil.replaceChild(parent, child, readState);
                assert ok;
            } else {
                // not assignable to field type (e.g. JSTargetableNode), cannot extract
                // but try to extract grandchildren instead, e.g.:
                // (yield)[yield](yield) => a = yield, b = yield, c = yield, a[b](c)
                extractChildrenTo(child, extracted);
            }
        }
    }

    private static Iterable getChildrenInExecutionOrder(Node parent) {
        // Note: Child and Children fields must be declared in execution order.
        return parent.getChildren();
    }

    private void extractChildrenTo(Node parent, List extracted) {
        for (Node child : getChildrenInExecutionOrder(parent)) {
            extractChildTo(child, parent, extracted);
        }
    }

    private JavaScriptNode handleFunctionReturn(FunctionNode functionNode, JavaScriptNode body) {
        assert (currentFunction().isGlobal() || currentFunction().isEval() || currentFunction().hasSyntheticArguments()) == (functionNode.isScript() || functionNode.isModule());
        if (currentFunction().returnsLastStatementResult()) {
            assert !currentFunction().hasReturn();
            return wrapGetCompletionValue(body);
        }
        if (currentFunction().hasReturn()) {
            if (JSConfig.ReturnValueInFrame) {
                return factory.createFrameReturnTarget(body, factory.createLocal(currentFunction().getReturnSlot(), 0, 0, ScopeFrameNode.EMPTY_FRAME_SLOT_ARRAY));
            } else {
                return factory.createReturnTarget(body);
            }
        }
        return body;
    }

    private EnvironmentCloseable enterFunctionEnvironment(boolean isStrict, boolean isArrowFunction, boolean isGeneratorFunction, boolean isDerivedConstructor, boolean isAsyncFunction,
                    boolean isGlobal, boolean hasSyntheticArguments) {
        Environment functionEnv;
        if (environment instanceof EvalEnvironment) {
            assert !isArrowFunction && !isGeneratorFunction && !isDerivedConstructor && !isAsyncFunction;
            functionEnv = new FunctionEnvironment(environment.getParent(), factory, context, isStrict, true, ((EvalEnvironment) environment).isDirectEval(), false, false, false, false, isGlobal,
                            hasSyntheticArguments);
        } else if (environment instanceof DebugEnvironment) {
            assert !isArrowFunction && !isGeneratorFunction && !isDerivedConstructor && !isAsyncFunction;
            functionEnv = new FunctionEnvironment(environment, factory, context, isStrict, true, true, false, false, false, false, isGlobal, hasSyntheticArguments);
        } else {
            functionEnv = new FunctionEnvironment(environment, factory, context, isStrict, false, false, isArrowFunction, isGeneratorFunction, isDerivedConstructor, isAsyncFunction, isGlobal,
                            hasSyntheticArguments);
        }
        return new EnvironmentCloseable(functionEnv);
    }

    private void declareParameters(FunctionNode functionNode) {
        FunctionEnvironment currentFunction = currentFunction();
        currentFunction.setSimpleParameterList(functionNode.hasSimpleParameterList());
        List parameters = functionNode.getParameters();
        for (int i = 0; i < parameters.size(); i++) {
            IdentNode parameter = parameters.get(i);
            // must be simple or rest parameter
            currentFunction.declareParameter(parameter.getName());
            if (parameter.isRestParameter()) {
                assert i == parameters.size() - 1;
                currentFunction.setRestParameter(true);
            }
        }
    }

    private JavaScriptNode prepareDeclarations(List declarations, JavaScriptNode body) {
        declarations.add(body);
        return factory.createExprBlock(declarations.toArray(EMPTY_NODE_ARRAY));
    }

    // footprint: avoid creating identical 0-sized arrays
    private static JavaScriptNode[] javaScriptNodeArray(int size) {
        return size == 0 ? EMPTY_NODE_ARRAY : new JavaScriptNode[size];
    }

    private String getFunctionName(FunctionNode functionNode) {
        if (context.getEcmaScriptVersion() < 6 && (functionNode.isGetter() || functionNode.isSetter())) {
            // strip getter/setter name prefix in ES5 mode
            assert !functionNode.isAnonymous();
            String name = functionNode.getName();
            if ((functionNode.isGetter() && name.startsWith("get ")) || (functionNode.isSetter() && name.startsWith("set "))) {
                name = name.substring(4);
            }
            return name;
        }
        return functionNode.getName();
    }

    private JavaScriptNode prepareParameters(JavaScriptNode body) {
        FrameSlot[] frameSlots = currentFunction().getParameters().toArray(new FrameSlot[currentFunction().getParameterCount()]);
        return createEnterFrameBlock(frameSlots, body);
    }

    private JavaScriptNode createEnterFrameBlock(FrameSlot[] parameterSlots, JavaScriptNode body) {
        if (parameterSlots.length != 0) {
            JavaScriptNode[] parameterAssignment = javaScriptNodeArray(parameterSlots.length + 1);
            int i = 0;
            FunctionEnvironment currentFunction = currentFunction();
            boolean hasRestParameter = currentFunction.hasRestParameter();
            for (int argIndex = currentFunction.getLeadingArgumentCount(); i < parameterSlots.length; i++, argIndex++) {
                final JavaScriptNode valueNode;
                if (hasRestParameter && i == parameterSlots.length - 1) {
                    valueNode = tagHiddenExpression(factory.createAccessRestArgument(context, argIndex, currentFunction.getTrailingArgumentCount()));
                } else {
                    valueNode = tagHiddenExpression(factory.createAccessArgument(argIndex));
                }
                parameterAssignment[i] = tagHiddenExpression(factory.createWriteCurrentFrameSlot(parameterSlots[i], currentFunction.getFunctionFrameDescriptor(), valueNode));
            }
            parameterAssignment[i] = body;
            return factory.createExprBlock(parameterAssignment);
        } else {
            return body;
        }
    }

    private static JavaScriptNode tagHiddenExpression(JavaScriptNode node) {
        node.setSourceSection(unavailableInternalSection);
        if (node instanceof GlobalScopeVarWrapperNode) {
            tagHiddenExpression(((GlobalScopeVarWrapperNode) node).getDelegateNode());
        } else {
            node.addExpressionTag();
        }
        return node;
    }

    static int getBlockScopedSymbolFlags(VarNode varNode) {
        if (varNode.isConst()) {
            return Symbol.IS_CONST;
        } else {
            assert varNode.isLet();
            return Symbol.IS_LET | (varNode.getName().isCatchParameter() ? Symbol.IS_CATCH_PARAMETER | Symbol.HAS_BEEN_DECLARED : 0);
        }
    }

    private List functionEnvInit(FunctionNode functionNode) {
        FunctionEnvironment currentFunction = currentFunction();
        assert !currentFunction.isGlobal() || currentFunction.isIndirectEval();

        if (JSConfig.ReturnOptimizer) {
            markTerminalReturnNodes(functionNode.getBody());
        }

        if (!functionNode.isArrow() && functionNode.needsArguments()) {
            currentFunction.reserveArgumentsSlot();

            if (JSConfig.OptimizeApplyArguments && functionNode.getNumOfParams() == 0 && !functionNode.hasEval() && functionNode.hasApplyArgumentsCall() &&
                            checkDirectArgumentsAccess(functionNode, currentFunction)) {
                currentFunction.setDirectArgumentsAccess(true);
            } else {
                currentFunction.declareVar(Environment.ARGUMENTS_NAME);
            }
        }

        // reserve this slot if function uses this or has super(). arrow functions and direct eval
        // in a derived class constructor must use the constructor's this slot.
        if (functionNode.needsThis() && !((functionNode.isArrow() || currentFunction.isDirectEval()) && currentFunction.getNonArrowParentFunction().isDerivedConstructor())) {
            currentFunction.reserveThisSlot();
        }
        if (functionNode.needsSuper()) {
            // arrow functions need to access [[HomeObject]] from outer non-arrow scope
            // note: an arrow function using  also needs  access
            assert !functionNode.isArrow();

            currentFunction.reserveThisSlot();
            currentFunction.reserveSuperSlot();
        }
        if (functionNode.needsNewTarget()) {
            currentFunction.reserveNewTargetSlot();
        }

        if (functionNode.isClassConstructor() && (lc.getCurrentClass().hasInstanceFields() || lc.getCurrentClass().hasPrivateInstanceMethods())) {
            // Allocate the this slot to ensure InitializeInstanceElements is performed
            // regardless of whether the class constructor itself uses `this`.
            // Note: the this slot could be elided in this case.
            currentFunction.reserveThisSlot();
        }

        if (functionNode.needsDynamicScope() && !currentFunction.isDirectEval()) {
            currentFunction.setIsDynamicallyScoped(true);
            currentFunction.reserveDynamicScopeSlot();
        }

        return Collections.emptyList();
    }

    private static void functionNeedsParentFramePass(FunctionNode rootFunctionNode, JSContext context) {
        if (!context.getContextOptions().isLazyTranslation()) {
            return; // nothing to do
        }

        com.oracle.js.parser.ir.visitor.NodeVisitor visitor = new com.oracle.js.parser.ir.visitor.NodeVisitor(new LexicalContext()) {
            @Override
            public boolean enterIdentNode(IdentNode identNode) {
                if (!identNode.isPropertyName()) {
                    String varName = identNode.getName();
                    findSymbol(varName);
                }
                return true;
            }

            @Override
            public boolean enterAccessNode(AccessNode accessNode) {
                if (accessNode.isPrivate()) {
                    findSymbol(accessNode.getPrivateName());
                }
                return true;
            }

            private void findSymbol(String varName) {
                boolean local = true;
                FunctionNode lastFunction = null;
                for (Iterator iterator = lc.getAllNodes(); iterator.hasNext();) {
                    LexicalContextNode node = iterator.next();
                    if (node instanceof LexicalContextScope) {
                        Symbol foundSymbol = ((LexicalContextScope) node).getScope().getExistingSymbol(varName);
                        if (foundSymbol != null && !foundSymbol.isGlobal()) {
                            if (!local) {
                                markUsesAncestorScopeUntil(lastFunction, true);
                            }
                            break;
                        }
                    } else if (node instanceof FunctionNode) {
                        FunctionNode function = (FunctionNode) node;
                        if (function.isNamedFunctionExpression() && varName.equals(function.getIdent().getName())) {
                            if (!local) {
                                markUsesAncestorScopeUntil(lastFunction, true);
                            }
                            break;
                        } else if (function.isArrow() && isVarLexicallyScopedInArrowFunction(varName)) {
                            FunctionNode nonArrowFunction = lc.getCurrentNonArrowFunction();
                            // `this` is read from the arrow function object,
                            // unless `this` is supplied by a subclass constructor
                            if (!varName.equals(Environment.THIS_NAME) || nonArrowFunction.isDerivedConstructor()) {
                                if (!nonArrowFunction.isProgram()) {
                                    markUsesAncestorScopeUntil(nonArrowFunction, false);
                                }
                            }
                            break;
                        } else if (!function.isProgram() && varName.equals(Environment.ARGUMENTS_NAME)) {
                            assert !function.isArrow();
                            assert local;
                            break;
                        } else if (function.hasEval() && !function.isProgram()) {
                            if (!local) {
                                markUsesAncestorScopeUntil(lastFunction, true);
                            }
                        } else if (function.isModule() && isImport(varName)) {
                            // needed for GetActiveScriptOrModule()
                            if (!local) {
                                markUsesAncestorScopeUntil(lastFunction, true);
                            }
                        }
                        lastFunction = function;
                        local = false;
                    } else if (node instanceof WithNode) {
                        if (!local) {
                            markUsesAncestorScopeUntil(lastFunction, true);
                        }
                    }
                }
            }

            private boolean isVarLexicallyScopedInArrowFunction(String varName) {
                switch (varName) {
                    case Environment.ARGUMENTS_NAME:
                    case Environment.NEW_TARGET_NAME:
                    case Environment.SUPER_NAME:
                    case Environment.THIS_NAME:
                        return true;
                    default:
                        return false;
                }
            }

            private boolean isImport(String varName) {
                switch (varName) {
                    case "import":
                    case "import.meta":
                        return true;
                    default:
                        return false;
                }
            }

            private void markUsesAncestorScopeUntil(FunctionNode untilFunction, boolean inclusive) {
                for (final Iterator functions = lc.getFunctions(); functions.hasNext();) {
                    FunctionNode function = functions.next();
                    if (!inclusive && function == untilFunction) {
                        break;
                    }
                    function.setUsesAncestorScope(true);
                    if (inclusive && function == untilFunction) {
                        break;
                    }
                }
            }

            @Override
            public boolean enterFunctionNode(FunctionNode functionNode) {
                if (functionNode.hasEval()) {
                    markUsesAncestorScopeUntil(null, false);
                }
                // TODO if function does not have nested functions we can skip it
                return true;
            }
        };

        rootFunctionNode.accept(visitor);
    }

    private static boolean checkDirectArgumentsAccess(FunctionNode functionNode, FunctionEnvironment currentFunction) {
        class DirectArgumentsAccessVisitor extends com.oracle.js.parser.ir.visitor.NodeVisitor {
            boolean directArgumentsAccess = true;

            DirectArgumentsAccessVisitor(LexicalContext lc) {
                super(lc);
            }

            @Override
            public boolean enterIdentNode(IdentNode identNode) {
                if (JSConfig.OptimizeApplyArguments) {
                    if (identNode.isArguments() && !identNode.isPropertyName() && functionNode.needsArguments() && !currentFunction.isDirectEval() && !identNode.isApplyArguments()) {
                        // function.apply(_, arguments);
                        directArgumentsAccess = false;
                    } else {
                        checkParameterUse(identNode);
                    }
                }
                return false;
            }

            private void checkParameterUse(IdentNode identNode) {
                if (directArgumentsAccess && !currentFunction.isStrictMode() && currentFunction.isParameter(identNode.getName())) {
                    directArgumentsAccess = false;
                }
            }

            @Override
            public boolean enterFunctionNode(FunctionNode nestedFunctionNode) {
                if (nestedFunctionNode == functionNode) {
                    return true;
                }
                if (JSConfig.OptimizeApplyArguments && (nestedFunctionNode.isArrow() || !currentFunction.isStrictMode())) {
                    // 1. arrow functions have lexical `arguments` binding;
                    // direct arguments access to outer frames currently not supported
                    // 2. if not in strict mode, nested functions might access mapped parameters;
                    // since we don't look inside them, bail out
                    directArgumentsAccess = false;
                }
                return false;
            }
        }

        DirectArgumentsAccessVisitor visitor = new DirectArgumentsAccessVisitor(new LexicalContext());
        functionNode.accept(visitor);
        return visitor.directArgumentsAccess;
    }

    private static void markTerminalReturnNodes(com.oracle.js.parser.ir.Node node) {
        if (node instanceof Block && ((Block) node).isTerminal()) {
            Statement lastStatement = ((Block) node).getLastStatement();
            if (lastStatement != null) {
                markTerminalReturnNodes(lastStatement);
            }
        } else if (node instanceof BlockStatement && ((BlockStatement) node).isTerminal()) {
            markTerminalReturnNodes(((BlockStatement) node).getBlock());
        } else if (node instanceof IfNode && ((IfNode) node).isTerminal()) {
            markTerminalReturnNodes(((IfNode) node).getPass());
            markTerminalReturnNodes(((IfNode) node).getFail());
        } else if (node instanceof com.oracle.js.parser.ir.ReturnNode) {
            ((com.oracle.js.parser.ir.ReturnNode) node).setInTerminalPosition(true);
        }
    }

    private List collectGlobalVars(FunctionNode functionNode, boolean configurable) {
        int symbolCount = functionNode.getBody().getSymbolCount();
        if (symbolCount == 0) {
            return Collections.emptyList();
        }
        final List declarations = new ArrayList<>(symbolCount);
        for (Symbol symbol : functionNode.getBody().getSymbols()) {
            if (symbol.isGlobal() && symbol.isVar()) {
                if (symbol.isHoistableDeclaration()) {
                    declarations.add(factory.createDeclareGlobalFunction(symbol.getName(), configurable, null));
                } else {
                    declarations.add(factory.createDeclareGlobalVariable(symbol.getName(), configurable));
                }
            } else if (!configurable) {
                assert symbol.isBlockScoped();
                declarations.add(factory.createDeclareGlobalLexicalVariable(symbol.getName(), symbol.isConst()));
            }
        }
        final List nodes = new ArrayList<>(2);
        nodes.add(factory.createGlobalDeclarationInstantiation(context, declarations));
        return nodes;
    }

    private JavaScriptNode prepareArguments(JavaScriptNode body) {
        VarRef argumentsVar = environment.findLocalVar(Environment.ARGUMENTS_NAME);
        boolean unmappedArgumentsObject = currentFunction().isStrictMode() || !currentFunction().hasSimpleParameterList();
        JavaScriptNode argumentsObject = factory.createArgumentsObjectNode(context, unmappedArgumentsObject, currentFunction().getLeadingArgumentCount(), currentFunction().getTrailingArgumentCount());
        if (!unmappedArgumentsObject) {
            argumentsObject = environment.findArgumentsVar().createWriteNode(argumentsObject);
        }
        JavaScriptNode setArgumentsNode = argumentsVar.createWriteNode(argumentsObject);
        return factory.createExprBlock(setArgumentsNode, body);
    }

    private JavaScriptNode prepareThis(JavaScriptNode body, FunctionNode functionNode) {
        // In a derived class constructor, we cannot get the this value from the arguments.
        assert !currentFunction().getNonArrowParentFunction().isDerivedConstructor();
        VarRef thisVar = environment.findThisVar();
        boolean isLexicalThis = functionNode.isArrow();
        JavaScriptNode getThisNode = isLexicalThis ? factory.createAccessLexicalThis() : factory.createAccessThis();
        if (!environment.isStrictMode() && !isLexicalThis) {
            getThisNode = factory.createPrepareThisBinding(context, getThisNode);
        }
        if (functionNode.isClassConstructor()) {
            getThisNode = initializeInstanceElements(getThisNode);
        }
        JavaScriptNode setThisNode = thisVar.createWriteNode(getThisNode);
        return factory.createExprBlock(setThisNode, body);
    }

    private JavaScriptNode prepareSuper(JavaScriptNode body) {
        JavaScriptNode getHomeObject = factory.createAccessHomeObject(context);
        JavaScriptNode setSuperNode = environment.findSuperVar().createWriteNode(getHomeObject);
        return factory.createExprBlock(setSuperNode, body);
    }

    private JavaScriptNode prepareNewTarget(JavaScriptNode body) {
        JavaScriptNode getNewTarget = factory.createAccessNewTarget();
        JavaScriptNode setNewTarget = environment.findNewTargetVar().createWriteNode(getNewTarget);
        return factory.createExprBlock(setNewTarget, body);
    }

    @Override
    public JavaScriptNode enterReturnNode(com.oracle.js.parser.ir.ReturnNode returnNode) {
        JavaScriptNode expression;
        if (returnNode.getExpression() != null) {
            expression = transform(returnNode.getExpression());
            if (currentFunction().isAsyncGeneratorFunction()) {
                expression = createAwaitNode(expression);
            }
        } else {
            expression = factory.createConstantUndefined();
        }

        ReturnNode returnStatement = returnNode.isInTerminalPosition() ? factory.createTerminalPositionReturn(expression) : createReturnNode(expression);
        return tagStatement(returnStatement, returnNode);
    }

    private ReturnNode createReturnNode(JavaScriptNode expression) {
        FunctionEnvironment currentFunction = currentFunction();
        currentFunction.addReturn();
        if (JSConfig.ReturnValueInFrame) {
            JavaScriptNode writeReturnSlotNode = environment.findTempVar(currentFunction.getReturnSlot()).createWriteNode(expression);
            return factory.createFrameReturn(writeReturnSlotNode);
        } else {
            return factory.createReturn(expression);
        }
    }

    @Override
    public JavaScriptNode enterBlock(Block block) {
        JavaScriptNode result;
        try (EnvironmentCloseable blockEnv = enterBlockEnvironment(block)) {
            List blockStatements = block.getStatements();
            List scopeInit = createTemporalDeadZoneInit(block);
            JavaScriptNode blockNode = transformStatements(blockStatements, block.isTerminal(), scopeInit, block.isExpressionBlock() || block.isParameterBlock());
            if (block.isFunctionBody() && currentFunction().isCallerContextEval()) {
                blockNode = prependDynamicScopeBindingInit(block, blockNode);
            }
            result = blockEnv.wrapBlockScope(blockNode);
        }
        // Parameter initialization must precede (i.e. wrap) the (async) generator function body
        if (block.isFunctionBody()) {
            if (currentFunction().isGeneratorFunction()) {
                result = finishGeneratorBody(result);
            }
            tagBody(result, block);
        }
        ensureHasSourceSection(result, block);
        return result;
    }

    /**
     * Initialize block-scoped symbols with a dead marker value.
     */
    private List createTemporalDeadZoneInit(Block block) {
        if ((!block.getScope().hasBlockScopedOrRedeclaredSymbols() && !block.isModuleBody()) || environment instanceof GlobalEnvironment) {
            return Collections.emptyList();
        }

        ArrayList blockWithInit = new ArrayList<>(block.getSymbolCount() + 1);
        for (Symbol symbol : block.getSymbols()) {
            if (symbol.isImportBinding()) {
                continue;
            }
            if (symbol.isBlockScoped()) {
                if (!symbol.hasBeenDeclared()) {
                    blockWithInit.add(findScopeVar(symbol.getName(), true).createWriteNode(factory.createConstant(Dead.instance())));
                }
            }
            if (symbol.isVarRedeclaredHere()) {
                // redeclaration of parameter binding; initial value is copied from outer scope.
                assert block.isFunctionBody();
                assert environment.getScopeLevel() == 1;
                JavaScriptNode outerVar = factory.createLocal(environment.getParent().findLocalVar(symbol.getName()).getFrameSlot(), 0, 1, environment.getParentSlots());
                blockWithInit.add(findScopeVar(symbol.getName(), true).createWriteNode(outerVar));
            }
        }
        if (block.isModuleBody()) {
            createResolveImports(lc.getCurrentFunction(), blockWithInit);
        }
        return blockWithInit;
    }

    private void createResolveImports(FunctionNode functionNode, List declarations) {
        assert functionNode.isModule();

        // Assert: all named exports from module are resolvable.
        for (ImportEntry importEntry : functionNode.getModule().getImportEntries()) {
            String moduleRequest = importEntry.getModuleRequest();
            String localName = importEntry.getLocalName();
            JSWriteFrameSlotNode writeLocalNode = (JSWriteFrameSlotNode) environment.findLocalVar(localName).createWriteNode(null);
            JavaScriptNode thisModule = getActiveModule();
            if (importEntry.getImportName().equals(Module.STAR_NAME)) {
                assert functionNode.getBody().getScope().hasSymbol(localName) && functionNode.getBody().getScope().getExistingSymbol(localName).hasBeenDeclared();
                declarations.add(factory.createResolveStarImport(context, thisModule, moduleRequest, writeLocalNode));
            } else {
                assert functionNode.getBody().getScope().hasSymbol(localName) && functionNode.getBody().getScope().getExistingSymbol(localName).isImportBinding();
                declarations.add(factory.createResolveNamedImport(context, thisModule, moduleRequest, importEntry.getImportName(), writeLocalNode));
            }
        }
    }

    /**
     * Create var-declared dynamic scope bindings in the variable environment of the caller.
     */
    private JavaScriptNode prependDynamicScopeBindingInit(Block block, JavaScriptNode blockNode) {
        assert currentFunction().isCallerContextEval();
        ArrayList blockWithInit = new ArrayList<>();
        for (Symbol symbol : block.getSymbols()) {
            if (symbol.isVar() && !environment.getVariableEnvironment().hasLocalVar(symbol.getName())) {
                blockWithInit.add(createDynamicScopeBinding(symbol.getName(), true));
            }
        }
        if (blockWithInit.isEmpty()) {
            return blockNode;
        }
        blockWithInit.add(blockNode);
        return factory.createExprBlock(blockWithInit.toArray(EMPTY_NODE_ARRAY));
    }

    private JavaScriptNode createDynamicScopeBinding(String varName, boolean deleteable) {
        assert deleteable;
        VarRef dynamicScopeVar = environment.findDynamicScopeVar();
        return new DeclareEvalVariableNode(context, varName, dynamicScopeVar.createReadNode(), (WriteNode) dynamicScopeVar.createWriteNode(null));
    }

    private JavaScriptNode transformStatements(List blockStatements, boolean terminal) {
        return transformStatements(blockStatements, terminal, Collections.emptyList(), false);
    }

    private JavaScriptNode transformStatements(List blockStatements, boolean terminal, List prolog, boolean expressionBlock) {
        final int size = prolog.size() + blockStatements.size();
        JavaScriptNode[] statements = javaScriptNodeArray(size);
        int pos = 0;
        if (!prolog.isEmpty()) {
            for (; pos < prolog.size(); pos++) {
                statements[pos] = prolog.get(pos);
            }
        }
        int lastNonEmptyIndex = -1;
        for (int i = 0; i < blockStatements.size(); i++) {
            Statement statement = blockStatements.get(i);
            JavaScriptNode statementNode = transformStatementInBlock(statement);
            if (currentFunction().returnsLastStatementResult()) {
                if (!statement.isCompletionValueNeverEmpty()) {
                    if (lastNonEmptyIndex >= 0) {
                        statements[lastNonEmptyIndex] = wrapSetCompletionValue(statements[lastNonEmptyIndex]);
                        lastNonEmptyIndex = -1;
                    }
                } else {
                    lastNonEmptyIndex = pos;
                }
            }
            statements[pos++] = statementNode;
        }
        if (currentFunction().returnsLastStatementResult() && lastNonEmptyIndex >= 0) {
            statements[lastNonEmptyIndex] = wrapSetCompletionValue(statements[lastNonEmptyIndex]);
        }

        assert pos == size;
        return createBlock(statements, terminal, expressionBlock);
    }

    private EnvironmentCloseable enterBlockEnvironment(Block block) {
        // Global lexical environment is shared by scripts (but not eval).
        // Note: indirect eval creates a new environment for lexically-scoped declarations.
        // Note 2: When argument names are present in Source, use standard (local) block environment
        // for function-like semantics, not the global scope.
        if (block.isFunctionBody() && lc.getCurrentFunction().isScript() && this.argumentNames == null) {
            FunctionEnvironment currentFunction = currentFunction();
            if (!currentFunction.isEval()) {
                GlobalEnvironment globalEnv = new GlobalEnvironment(environment, factory, context);
                setupGlobalEnvironment(globalEnv, block);
                return new EnvironmentCloseable(globalEnv);
            } else if (currentFunction.isIndirectEval()) {
                GlobalEnvironment globalEnv = new GlobalEnvironment(environment, factory, context);
                BlockEnvironment blockEnv = new BlockEnvironment(globalEnv, factory, context);
                blockEnv.addFrameSlotsFromSymbols(block.getScope().getSymbols());
                return new EnvironmentCloseable(blockEnv);
            } else {
                assert currentFunction.isDirectEval();
                // We already have a global environment.
            }
        }

        return enterBlockEnvironment(block.getScope());
    }

    private EnvironmentCloseable enterBlockEnvironment(Scope scope) {
        if (scope != null && (scope.hasDeclarations() || JSConfig.ManyBlockScopes)) {
            /*
             * The function environment is filled with top-level vars from the function body, unless
             * the function has parameter expressions, then the function body gets a separate scope
             * and we populate the env with parameter vars (cf. FunctionDeclarationInstantiation).
             */
            if (scope.isFunctionTopScope() || scope.isEvalScope()) {
                assert environment instanceof FunctionEnvironment;
                boolean onlyBlockScoped = currentFunction().isCallerContextEval();
                environment.addFrameSlotsFromSymbols(scope.getSymbols(), onlyBlockScoped);
                return new EnvironmentCloseable(environment);
            } else {
                BlockEnvironment blockEnv = new BlockEnvironment(environment, factory, context);
                blockEnv.addFrameSlotsFromSymbols(scope.getSymbols());
                return new EnvironmentCloseable(blockEnv);
            }
        } else {
            return new EnvironmentCloseable(environment);
        }
    }

    /**
     * Set up slots for lexical declarations in the global environment.
     *
     * @see #collectGlobalVars(FunctionNode, boolean)
     */
    private static void setupGlobalEnvironment(GlobalEnvironment globalEnv, Block block) {
        for (com.oracle.js.parser.ir.Symbol symbol : block.getSymbols()) {
            if (symbol.isImportBinding()) {
                continue; // no frame slot required
            }
            if (symbol.isBlockScoped()) {
                globalEnv.addLexicalDeclaration(symbol.getName(), symbol.isConst());
            } else if (symbol.isGlobal() && symbol.isVar()) {
                globalEnv.addVarDeclaration(symbol.getName());
            }
        }
    }

    private JavaScriptNode transformStatementInBlock(Statement statement) {
        return transform(statement);
    }

    @Override
    public JavaScriptNode enterBlockStatement(BlockStatement blockStatement) {
        return transform(blockStatement.getBlock());
    }

    @Override
    public JavaScriptNode enterLiteralNode(LiteralNode literalNode) {
        if (literalNode instanceof LiteralNode.ArrayLiteralNode) {
            return tagExpression(enterLiteralArrayNode((LiteralNode.ArrayLiteralNode) literalNode), literalNode);
        } else {
            return tagExpression(enterLiteralDefaultNode(literalNode), literalNode);
        }
    }

    private JavaScriptNode enterLiteralDefaultNode(LiteralNode literalNode) {
        Object value = literalNode.getValue();
        if (value == null) {
            return factory.createConstantNull();
        } else if (value instanceof Long) { // we don't support long type
            long longValue = (long) value;
            if (JSRuntime.isSafeInteger(longValue)) {
                return factory.createConstantSafeInteger(longValue);
            }
            return factory.createConstantDouble(longValue);
        } else if (value instanceof Lexer.RegexToken) {
            return factory.createRegExpLiteral(context, ((Lexer.RegexToken) value).getExpression(), ((Lexer.RegexToken) value).getOptions());
        } else if (value instanceof BigInteger) {
            value = BigInt.fromBigInteger((BigInteger) value);
        }
        return factory.createConstant(value);
    }

    private JavaScriptNode enterLiteralArrayNode(LiteralNode.ArrayLiteralNode arrayLiteralNode) {
        List elementExpressions = arrayLiteralNode.getElementExpressions();
        JavaScriptNode[] elements = javaScriptNodeArray(elementExpressions.size());
        boolean hasSpread = false;
        for (int i = 0; i < elementExpressions.size(); i++) {
            Expression elementExpression = elementExpressions.get(i);
            hasSpread = hasSpread || elementExpression != null && elementExpression.isTokenType(TokenType.SPREAD_ARRAY);
            elements[i] = elementExpression != null ? transform(elementExpression) : factory.createEmpty();
        }
        return hasSpread ? factory.createArrayLiteralWithSpread(context, elements) : factory.createArrayLiteral(context, elements);
    }

    @Override
    public JavaScriptNode enterIdentNode(IdentNode identNode) {
        assert !identNode.isPropertyName();
        final JavaScriptNode result;
        if (identNode.isThis()) {
            result = createThisNode();
        } else if (identNode.isSuper()) {
            result = enterIdentNodeSuper(identNode);
        } else if (identNode.isNewTarget()) {
            result = enterNewTarget();
        } else if (identNode.isImportMeta()) {
            result = enterImportMeta();
        } else {
            String varName = identNode.getName();
            VarRef varRef = findScopeVarCheckTDZ(varName, false);
            result = varRef.createReadNode();
        }
        return tagExpression(result, identNode);
    }

    private JavaScriptNode enterNewTarget() {
        return environment.findNewTargetVar().createReadNode();
    }

    private JavaScriptNode enterIdentNodeSuper(IdentNode identNode) {
        if (!identNode.isDirectSuper()) {
            // ES6 12.3.5.3 Runtime Semantics: MakeSuperPropertyReference(propertyKey, strict)
            // ES6 8.1.1.3.5 GetSuperBase()
            JavaScriptNode getSuperBase = factory.createGetPrototype(environment.findSuperVar().createReadNode());
            JavaScriptNode receiver = checkThisBindingInitialized(environment.findThisVar().createReadNode());
            return factory.createSuperPropertyReference(getSuperBase, receiver);
        } else {
            // ES6 12.3.5.2 Runtime Semantics: GetSuperConstructor()
            assert identNode.isDirectSuper(); // super accesses should not reach here
            JavaScriptNode activeFunction = factory.createAccessCallee(currentFunction().getThisFunctionLevel());
            JavaScriptNode superConstructor = factory.createGetPrototype(activeFunction);
            JavaScriptNode receiver = environment.findThisVar().createReadNode();
            return factory.createTargetableWrapper(superConstructor, receiver);
        }
    }

    private JavaScriptNode createThisNode() {
        return currentFunction().isGlobal() ? factory.createAccessThis() : checkThisBindingInitialized(environment.findThisVar().createReadNode());
    }

    private JavaScriptNode createThisNodeUnchecked() {
        return currentFunction().isGlobal() ? factory.createAccessThis() : environment.findThisVar().createReadNode();
    }

    private JavaScriptNode checkThisBindingInitialized(JavaScriptNode accessThisNode) {
        // TODO in most cases we should be able to prove that `this` is already initialized
        if (currentFunction().getNonArrowParentFunction().isDerivedConstructor()) {
            return factory.createDerivedConstructorThis(accessThisNode);
        }
        return accessThisNode;
    }

    private JavaScriptNode enterImportMeta() {
        return factory.createImportMeta(getActiveModule());
    }

    private JavaScriptNode getActiveModule() {
        assert lc.inModule();
        return factory.createAccessFrameArgument(currentFunction().getOutermostFunctionLevel(), 0);
    }

    private JavaScriptNode getActiveScriptOrModule() {
        if (lc.inModule()) {
            return getActiveModule();
        }
        return null;
    }

    private VarRef findScopeVar(String name, boolean skipWith) {
        return environment.findVar(name, skipWith);
    }

    private VarRef findScopeVarCheckTDZ(String name, boolean initializationAssignment) {
        VarRef varRef = findScopeVar(name, false);
        if (varRef.isFunctionLocal()) {
            Symbol symbol = lc.getCurrentScope().findBlockScopedSymbolInFunction(varRef.getName());
            if (symbol == null) {
                // variable is not block-scoped
                return varRef;
            } else if (symbol.hasBeenDeclared()) {
                // variable has been unconditionally declared already
                return varRef;
            } else if (symbol.isDeclaredInSwitchBlock()) {
                // we cannot statically determine whether a block-scoped variable is in TDZ
                // in an unprotected switch case context, so we always need a dynamic check
                return varRef.withTDZCheck();
            } else {
                assert !symbol.hasBeenDeclared();
                if (initializationAssignment) {
                    symbol.setHasBeenDeclared();
                    return varRef;
                }

                // variable reference is unconditionally in the temporal dead zone, i.e.,
                // var ref is in declaring function and in scope but before the actual declaration
                return new VarRef(name) {
                    @Override
                    public boolean isGlobal() {
                        return varRef.isGlobal();
                    }

                    @Override
                    public boolean isFunctionLocal() {
                        return varRef.isFunctionLocal();
                    }

                    @Override
                    public FrameSlot getFrameSlot() {
                        return null;
                    }

                    @Override
                    public JavaScriptNode createReadNode() {
                        return factory.createThrowError(JSErrorType.ReferenceError, String.format("\"%s\" is not defined", varRef.getName()));
                    }

                    @Override
                    public JavaScriptNode createWriteNode(JavaScriptNode rhs) {
                        JavaScriptNode throwErrorNode = createReadNode();
                        return isPotentiallySideEffecting(rhs) ? DualNode.create(rhs, throwErrorNode) : throwErrorNode;
                    }

                    @Override
                    public JavaScriptNode createDeleteNode() {
                        return createReadNode();
                    }
                };
            }
        }
        return varRef.withTDZCheck();
    }

    @Override
    public JavaScriptNode enterVarNode(VarNode varNode) {
        String varName = varNode.getName().getName();
        assert currentFunction().isGlobal() && (!varNode.isBlockScoped() || lc.getCurrentBlock().isFunctionBody()) || !findScopeVar(varName, true).isGlobal() ||
                        currentFunction().isCallerContextEval() : varNode;

        Symbol symbol = null;
        if (varNode.isBlockScoped()) {
            // below, `symbol!=null` implies `isBlockScoped`
            symbol = lc.getCurrentScope().getExistingSymbol(varName);
            assert symbol != null : varName;
        }

        JavaScriptNode assignment;
        if (varNode.isAssignment()) {
            assignment = createVarAssignNode(varNode, varName);
        } else if (symbol != null && (!varNode.isDestructuring() || symbol.isDeclaredInSwitchBlock()) && !symbol.hasBeenDeclared()) {
            assert varNode.isBlockScoped();
            assignment = findScopeVar(varName, false).createWriteNode(factory.createConstantUndefined());
        } else {
            assignment = factory.createEmpty();
        }
        // mark block-scoped symbols as declared, except:
        // (a) symbols declared in a switch case always need the dynamic TDZ check
        // (b) destructuring: the symbol does not come alive until the destructuring assignment
        if (symbol != null && (!symbol.isDeclaredInSwitchBlock() && !varNode.isDestructuring())) {
            assert varNode.isBlockScoped();
            symbol.setHasBeenDeclared();
        }
        return assignment;
    }

    private JavaScriptNode createVarAssignNode(VarNode varNode, String varName) {
        JavaScriptNode rhs = transform(varNode.getAssignmentSource());
        JavaScriptNode assignment = findScopeVar(varName, false).createWriteNode(rhs);
        if (varNode.isBlockScoped() && varNode.isFunctionDeclaration() && context.isOptionAnnexB()) {
            // B.3.3 Block-Level Function Declarations Web Legacy Compatibility Semantics
            FunctionNode fn = lc.getCurrentFunction();
            if (!fn.isStrict() && !varName.equals(Environment.ARGUMENTS_NAME)) {
                Symbol symbol = lc.getCurrentScope().getExistingSymbol(varName);
                if (symbol.isHoistedBlockFunctionDeclaration()) {
                    assert hasVarSymbol(fn.getVarDeclarationBlock().getScope(), varName) : varName;
                    assignment = environment.findVar(varName, true, false, true, false).withRequired(false).createWriteNode(assignment);
                    tagExpression(assignment, varNode);
                }
            }
        }

        // class declarations are not statements nor expressions
        if (varNode.isClassDeclaration()) {
            return discardResult(assignment);
        }
        // do not halt on function declarations
        if (!varNode.isHoistableDeclaration()) {
            tagStatement(assignment, varNode);
        }
        ensureHasSourceSection(assignment, varNode);
        return discardResult(assignment);
    }

    private static boolean hasVarSymbol(Scope scope, String varName) {
        Symbol varSymbol = scope.getExistingSymbol(varName);
        return varSymbol != null && (varSymbol.isVar() && !varSymbol.isParam());
    }

    @Override
    public JavaScriptNode enterWhileNode(com.oracle.js.parser.ir.WhileNode whileNode) {
        JavaScriptNode test = transform(whileNode.getTest());
        tagStatement(test, whileNode.getTest());
        try (JumpTargetCloseable target = currentFunction().pushContinueTarget(null)) {
            JavaScriptNode body = transform(whileNode.getBody());
            JavaScriptNode wrappedBody = wrapClearCompletionValue(target.wrapContinueTargetNode(body));
            JavaScriptNode result;
            if (whileNode.isDoWhile()) {
                result = createDoWhile(test, wrappedBody);
            } else {
                result = createWhileDo(test, wrappedBody);
            }
            return wrapClearAndGetCompletionValue(target.wrapBreakTargetNode(ensureHasSourceSection(result, whileNode)));
        }
    }

    private JavaScriptNode createDoWhile(JavaScriptNode condition, JavaScriptNode body) {
        return factory.createDoWhile(condition, body);
    }

    private JavaScriptNode createWhileDo(JavaScriptNode condition, JavaScriptNode body) {
        return factory.createWhileDo(condition, body);
    }

    private JavaScriptNode wrapGetCompletionValue(JavaScriptNode target) {
        if (currentFunction().returnsLastStatementResult()) {
            VarRef returnVar = environment.findTempVar(currentFunction().getReturnSlot());
            return factory.createExprBlock(target, returnVar.createReadNode());
        }
        return target;
    }

    /**
     * Sets the completion value to the return value of the statement, which must never be empty.
     */
    private JavaScriptNode wrapSetCompletionValue(JavaScriptNode statement) {
        if (currentFunction().returnsLastStatementResult()) {
            VarRef returnVar = environment.findTempVar(currentFunction().getReturnSlot());
            return returnVar.createWriteNode(statement);
        }
        return statement;
    }

    private JavaScriptNode wrapClearCompletionValue(JavaScriptNode statement) {
        if (currentFunction().returnsLastStatementResult()) {
            VarRef returnVar = environment.findTempVar(currentFunction().getReturnSlot());
            return factory.createExprBlock(returnVar.createWriteNode(factory.createConstantUndefined()), statement);
        }
        return statement;
    }

    /**
     * Wraps a statement, completion value of which is never the value empty. Sets the completion
     * value to undefined, executes the statement, and reads and returns the completion value.
     */
    private JavaScriptNode wrapClearAndGetCompletionValue(JavaScriptNode statement) {
        if (currentFunction().returnsLastStatementResult()) {
            VarRef returnVar = environment.findTempVar(currentFunction().getReturnSlot());
            return factory.createExprBlock(returnVar.createWriteNode(factory.createConstantUndefined()), statement, returnVar.createReadNode());
        }
        return statement;
    }

    private JavaScriptNode wrapSaveAndRestoreCompletionValue(JavaScriptNode statement) {
        if (currentFunction().returnsLastStatementResult()) {
            VarRef returnVar = environment.findTempVar(currentFunction().getReturnSlot());
            VarRef tempVar = environment.createTempVar();
            return factory.createExprBlock(tempVar.createWriteNode(returnVar.createReadNode()), statement, returnVar.createWriteNode(tempVar.createReadNode()));
        }
        return statement;
    }

    @Override
    public JavaScriptNode enterForNode(ForNode forNode) {
        // if init is destructuring, wait with transformation
        JavaScriptNode init = forNode.getInit() != null && !forNode.isForInOrOf() ? tagStatement(transform(forNode.getInit()), forNode.getInit()) : factory.createEmpty();
        JavaScriptNode test = forNode.getTest() != null && forNode.getTest().getExpression() != null ? tagStatement(transform(forNode.getTest()), forNode.getTest())
                        : factory.createConstantBoolean(true);
        JavaScriptNode modify = forNode.getModify() != null ? tagStatement(transform(forNode.getModify()), forNode.getModify()) : factory.createEmpty();
        try (JumpTargetCloseable target = currentFunction().pushContinueTarget(null)) {
            JavaScriptNode result;
            if (forNode.isForOf()) {
                result = desugarForOf(forNode, modify, target);
            } else if (forNode.isForIn()) {
                result = desugarForIn(forNode, modify, target);
            } else if (forNode.isForAwaitOf()) {
                result = desugarForAwaitOf(forNode, modify, target);
            } else {
                JavaScriptNode body = transform(forNode.getBody());
                JavaScriptNode wrappedBody = wrapClearCompletionValue(target.wrapContinueTargetNode(body));
                result = target.wrapBreakTargetNode(desugarFor(forNode, init, test, modify, wrappedBody));
            }

            return wrapClearAndGetCompletionValue(result);
        }
    }

    private JavaScriptNode desugarFor(ForNode forNode, JavaScriptNode init, JavaScriptNode test, JavaScriptNode modify, JavaScriptNode wrappedBody) {
        if (needsPerIterationScope(forNode)) {
            VarRef firstTempVar = environment.createTempVar();
            FrameDescriptor iterationBlockFrameDescriptor = environment.getBlockFrameDescriptor();
            StatementNode newFor = factory.createFor(test, wrappedBody, modify, iterationBlockFrameDescriptor, firstTempVar.createReadNode(),
                            firstTempVar.createWriteNode(factory.createConstantBoolean(false)));
            ensureHasSourceSection(newFor, forNode);
            return createBlock(init, firstTempVar.createWriteNode(factory.createConstantBoolean(true)), newFor);
        }
        JavaScriptNode whileDo = factory.createDesugaredFor(test, createBlock(wrappedBody, modify));
        if (forNode.getTest() == null) {
            tagStatement(test, forNode);
        } else {
            ensureHasSourceSection(whileDo, forNode);
        }
        return createBlock(init, whileDo);
    }

    private JavaScriptNode desugarForIn(ForNode forNode, JavaScriptNode modify, JumpTargetCloseable jumpTarget) {
        JavaScriptNode createIteratorNode;
        if (forNode.isForEach()) {
            createIteratorNode = factory.createEnumerate(context, modify, true);
        } else {
            assert forNode.isForIn() && !forNode.isForEach() && !forNode.isForOf();
            createIteratorNode = factory.createEnumerate(context, modify, false);
        }
        return desugarForInOrOfBody(forNode, factory.createGetIterator(context, createIteratorNode), jumpTarget);
    }

    private JavaScriptNode desugarForOf(ForNode forNode, JavaScriptNode modify, JumpTargetCloseable jumpTarget) {
        assert forNode.isForOf();
        JavaScriptNode getIterator = factory.createGetIterator(context, modify);
        return desugarForInOrOfBody(forNode, getIterator, jumpTarget);
    }

    private JavaScriptNode desugarForInOrOfBody(ForNode forNode, JavaScriptNode iterator, JumpTargetCloseable jumpTarget) {
        assert forNode.isForInOrOf();
        VarRef iteratorVar = environment.createTempVar();
        JavaScriptNode iteratorInit = iteratorVar.createWriteNode(iterator);
        VarRef nextResultVar = environment.createTempVar();
        JavaScriptNode iteratorNext = factory.createIteratorNext(iteratorVar.createReadNode());
        // nextResult = IteratorNext(iterator)
        // while(!(done = IteratorComplete(nextResult)))
        JavaScriptNode condition = factory.createDual(context,
                        factory.createIteratorSetDone(iteratorVar.createReadNode(), factory.createConstantBoolean(true)),
                        factory.createUnary(UnaryOperation.NOT, factory.createIteratorComplete(context, nextResultVar.createWriteNode(iteratorNext))));
        JavaScriptNode wrappedBody;
        try (EnvironmentCloseable blockEnv = needsPerIterationScope(forNode) ? enterBlockEnvironment(lc.getCurrentBlock()) : new EnvironmentCloseable(environment)) {
            // var nextValue = IteratorValue(nextResult);
            VarRef nextResultVar2 = environment.findTempVar(nextResultVar.getFrameSlot());
            VarRef nextValueVar = environment.createTempVar();
            VarRef iteratorVar2 = environment.findTempVar(iteratorVar.getFrameSlot());
            JavaScriptNode nextResult = nextResultVar2.createReadNode();
            JavaScriptNode nextValue = factory.createIteratorValue(context, nextResult);
            JavaScriptNode writeNextValue = nextValueVar.createWriteNode(nextValue);
            JavaScriptNode writeNext = tagStatement(desugarForHeadAssignment(forNode, nextValueVar.createReadNode()), forNode);
            JavaScriptNode body = transform(forNode.getBody());
            wrappedBody = blockEnv.wrapBlockScope(createBlock(
                            writeNextValue,
                            factory.createIteratorSetDone(iteratorVar2.createReadNode(), factory.createConstantBoolean(false)),
                            writeNext,
                            body));
        }
        wrappedBody = jumpTarget.wrapContinueTargetNode(wrappedBody);
        JavaScriptNode whileNode = forNode.isForOf() ? factory.createDesugaredForOf(condition, wrappedBody) : factory.createDesugaredForIn(condition, wrappedBody);
        JavaScriptNode wrappedWhile = factory.createIteratorCloseIfNotDone(context, jumpTarget.wrapBreakTargetNode(whileNode), iteratorVar.createReadNode());
        JavaScriptNode resetIterator = iteratorVar.createWriteNode(factory.createConstant(JSFrameUtil.DEFAULT_VALUE));
        wrappedWhile = factory.createTryFinally(wrappedWhile, resetIterator);
        ensureHasSourceSection(whileNode, forNode);
        return createBlock(iteratorInit, wrappedWhile);
    }

    private JavaScriptNode desugarForHeadAssignment(ForNode forNode, JavaScriptNode next) {
        boolean lexicalBindingInit = forNode.hasPerIterationScope();
        if (forNode.getInit() instanceof IdentNode && lexicalBindingInit) {
            return tagExpression(findScopeVarCheckTDZ(((IdentNode) forNode.getInit()).getName(), lexicalBindingInit).createWriteNode(next), forNode);
        } else {
            // transform destructuring assignment
            return tagExpression(transformAssignment(forNode.getInit(), forNode.getInit(), next, lexicalBindingInit), forNode);
        }
    }

    private JavaScriptNode desugarForAwaitOf(ForNode forNode, JavaScriptNode modify, JumpTargetCloseable jumpTarget) {
        assert forNode.isForAwaitOf();
        JavaScriptNode getIterator = factory.createGetAsyncIterator(context, modify);
        VarRef iteratorVar = environment.createTempVar();
        JavaScriptNode iteratorInit = iteratorVar.createWriteNode(getIterator);
        VarRef nextResultVar = environment.createTempVar();

        currentFunction().addAwait();
        JSReadFrameSlotNode asyncResultNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction().getAsyncResultSlot()).createReadNode();
        JSReadFrameSlotNode asyncContextNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction().getAsyncContextSlot()).createReadNode();
        JavaScriptNode iteratorNext = factory.createAsyncIteratorNext(context, iteratorVar.createReadNode(), asyncContextNode, asyncResultNode);
        // nextResult = Await(IteratorNext(iterator))
        // while(!(done = IteratorComplete(nextResult)))
        JavaScriptNode condition = factory.createDual(context,
                        factory.createIteratorSetDone(iteratorVar.createReadNode(), factory.createConstantBoolean(true)),
                        factory.createUnary(UnaryOperation.NOT, factory.createIteratorComplete(context, nextResultVar.createWriteNode(iteratorNext))));
        JavaScriptNode wrappedBody;
        try (EnvironmentCloseable blockEnv = needsPerIterationScope(forNode) ? enterBlockEnvironment(lc.getCurrentBlock()) : new EnvironmentCloseable(environment)) {
            // var nextValue = IteratorValue(nextResult);
            VarRef nextResultVar2 = environment.findTempVar(nextResultVar.getFrameSlot());
            VarRef nextValueVar = environment.createTempVar();
            VarRef iteratorVar2 = environment.findTempVar(iteratorVar.getFrameSlot());
            JavaScriptNode nextResult = nextResultVar2.createReadNode();
            JavaScriptNode nextValue = factory.createIteratorValue(context, nextResult);
            JavaScriptNode writeNextValue = nextValueVar.createWriteNode(nextValue);
            JavaScriptNode writeNext = tagStatement(desugarForHeadAssignment(forNode, nextValueVar.createReadNode()), forNode);
            JavaScriptNode body = transform(forNode.getBody());
            wrappedBody = blockEnv.wrapBlockScope(createBlock(
                            writeNextValue,
                            factory.createIteratorSetDone(iteratorVar2.createReadNode(), factory.createConstantBoolean(false)),
                            writeNext,
                            body));
        }
        wrappedBody = jumpTarget.wrapContinueTargetNode(wrappedBody);
        JavaScriptNode whileNode = factory.createDesugaredForAwaitOf(condition, wrappedBody);
        currentFunction().addAwait();
        JavaScriptNode wrappedWhile = factory.createAsyncIteratorCloseWrapper(context, jumpTarget.wrapBreakTargetNode(whileNode), iteratorVar.createReadNode(), asyncContextNode, asyncResultNode);
        JavaScriptNode resetIterator = iteratorVar.createWriteNode(factory.createConstant(JSFrameUtil.DEFAULT_VALUE));
        wrappedWhile = factory.createTryFinally(wrappedWhile, resetIterator);
        ensureHasSourceSection(whileNode, forNode);
        return createBlock(iteratorInit, wrappedWhile);
    }

    private boolean needsPerIterationScope(ForNode forNode) {
        // for loop init block may contain closures, too; that's why we check the surrounding block.
        return forNode.hasPerIterationScope() && hasClosures(lc.getCurrentBlock());
    }

    private static boolean hasClosures(com.oracle.js.parser.ir.Node node) {
        class HasClosuresVisitor extends NodeVisitor {
            boolean hasClosures;

            HasClosuresVisitor(LexicalContext lc) {
                super(lc);
            }

            @Override
            public boolean enterFunctionNode(FunctionNode functionNode) {
                hasClosures = true;
                return false; // do not descend into functions
            }
        }
        HasClosuresVisitor visitor = new HasClosuresVisitor(new LexicalContext());
        node.accept(visitor);
        return visitor.hasClosures;
    }

    @Override
    public JavaScriptNode enterLabelNode(com.oracle.js.parser.ir.LabelNode labelNode) {
        try (JumpTargetCloseable breakTarget = currentFunction().pushBreakTarget(labelNode.getLabelName())) {
            JavaScriptNode body = transform(labelNode.getBody());
            return breakTarget.wrapLabelBreakTargetNode(body);
        }
    }

    @Override
    public JavaScriptNode enterBreakNode(com.oracle.js.parser.ir.BreakNode breakNode) {
        return tagStatement(factory.createBreak(currentFunction().findBreakTarget(breakNode.getLabelName())), breakNode);
    }

    @Override
    public JavaScriptNode enterContinueNode(com.oracle.js.parser.ir.ContinueNode continueNode) {
        return tagStatement(factory.createContinue(currentFunction().findContinueTarget(continueNode.getLabelName())), continueNode);
    }

    @Override
    public JavaScriptNode enterIfNode(com.oracle.js.parser.ir.IfNode ifNode) {
        JavaScriptNode test = transform(ifNode.getTest());
        JavaScriptNode pass = transform(ifNode.getPass());
        JavaScriptNode fail = transform(ifNode.getFail());
        return tagStatement(factory.createIf(test, pass, fail), ifNode);
    }

    @Override
    public JavaScriptNode enterTernaryNode(TernaryNode ternaryNode) {
        JavaScriptNode test = transform(ternaryNode.getTest());
        JavaScriptNode pass = transform(ternaryNode.getTrueExpression());
        JavaScriptNode fail = transform(ternaryNode.getFalseExpression());
        return tagExpression(factory.createIf(test, pass, fail), ternaryNode);
    }

    @Override
    public JavaScriptNode enterUnaryNode(UnaryNode unaryNode) {
        switch (unaryNode.tokenType()) {
            case ADD:
            case BIT_NOT:
            case NOT:
            case SUB:
            case VOID:
                return enterUnaryDefaultNode(unaryNode);
            case TYPEOF:
                return enterTypeofNode(unaryNode);
            case INCPREFIX:
            case INCPOSTFIX:
            case DECPREFIX:
            case DECPOSTFIX:
                return enterUnaryIncDecNode(unaryNode);
            case NEW:
                return enterNewNode(unaryNode);
            case DELETE:
                return enterDelete(unaryNode);
            case SPREAD_ARGUMENT:
                return tagExpression(factory.createSpreadArgument(context, transform(unaryNode.getExpression())), unaryNode);
            case SPREAD_ARRAY:
                return tagExpression(factory.createSpreadArray(context, transform(unaryNode.getExpression())), unaryNode);
            case YIELD:
            case YIELD_STAR:
                return tagExpression(createYieldNode(unaryNode), unaryNode);
            case AWAIT:
                return tagExpression(translateAwaitNode(unaryNode), unaryNode);
            default:
                throw new UnsupportedOperationException(unaryNode.tokenType().toString());
        }
    }

    private JavaScriptNode translateAwaitNode(UnaryNode unaryNode) {
        JavaScriptNode expression = transform(unaryNode.getExpression());
        return createAwaitNode(expression);
    }

    private JavaScriptNode createAwaitNode(JavaScriptNode expression) {
        FunctionEnvironment currentFunction = currentFunction();
        currentFunction.addAwait();
        JSReadFrameSlotNode asyncContextNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction.getAsyncContextSlot()).createReadNode();
        JSReadFrameSlotNode asyncResultNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction.getAsyncResultSlot()).createReadNode();
        return factory.createAwait(context, expression, asyncContextNode, asyncResultNode);
    }

    private JavaScriptNode createYieldNode(UnaryNode unaryNode) {
        FunctionEnvironment currentFunction = currentFunction();
        assert currentFunction.isGeneratorFunction();
        if (lc.getCurrentFunction().isModule()) {
            currentFunction.addYield();
            return factory.createModuleYield();
        }

        boolean asyncGeneratorYield = currentFunction.isAsyncFunction();
        boolean yieldStar = unaryNode.tokenType() == TokenType.YIELD_STAR;

        JavaScriptNode expression = transform(unaryNode.getExpression());
        ReturnNode returnNode = createReturnNode(null);
        if (asyncGeneratorYield) {
            currentFunction.addAwait();
            JSReadFrameSlotNode asyncContextNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction.getAsyncContextSlot()).createReadNode();
            JSReadFrameSlotNode asyncResultNode = (JSReadFrameSlotNode) environment.findTempVar(currentFunction.getAsyncResultSlot()).createReadNode();
            if (yieldStar) {
                VarRef tempVar = environment.createTempVar();
                return factory.createAsyncGeneratorYieldStar(context, expression, asyncContextNode, asyncResultNode, returnNode, tempVar.createReadNode(), (WriteNode) tempVar.createWriteNode(null));
            } else {
                return factory.createAsyncGeneratorYield(context, expression, asyncContextNode, asyncResultNode, returnNode);
            }
        } else {
            currentFunction.addYield();
            JSWriteFrameSlotNode writeYieldResultNode = JSConfig.YieldResultInFrame ? (JSWriteFrameSlotNode) environment.findTempVar(currentFunction.getYieldResultSlot()).createWriteNode(null)
                            : null;
            return factory.createYield(context, expression, environment.findYieldValueVar().createReadNode(), yieldStar, returnNode, writeYieldResultNode);
        }
    }

    private JavaScriptNode enterUnaryDefaultNode(UnaryNode unaryNode) {
        assert unaryNode.tokenType() != TokenType.TYPEOF;
        JavaScriptNode operand = transform(unaryNode.getExpression());
        return tagExpression(factory.createUnary(tokenTypeToUnaryOperation(unaryNode.tokenType()), operand), unaryNode);
    }

    private JavaScriptNode enterTypeofNode(UnaryNode unaryNode) {
        assert unaryNode.tokenType() == TokenType.TYPEOF;
        JavaScriptNode operand = null;
        if (unaryNode.getExpression() instanceof IdentNode) {
            IdentNode identNode = (IdentNode) unaryNode.getExpression();
            String identNodeName = identNode.getName();
            if (context.isOptionNashornCompatibilityMode() && (identNodeName.equals("__LINE__") || identNodeName.equals("__FILE__") || identNodeName.equals("__DIR__"))) {
                operand = GlobalPropertyNode.createPropertyNode(context, identNodeName);
            } else if (!identNode.isThis() && !identNode.isMetaProperty()) {
                // typeof globalVar must not throw ReferenceError if globalVar does not exist
                operand = findScopeVarCheckTDZ(identNodeName, false).withRequired(false).createReadNode();
            }
        }
        if (operand == null) {
            operand = transform(unaryNode.getExpression());
        } else {
            tagExpression(operand, unaryNode.getExpression());
        }
        return tagExpression(factory.createUnary(tokenTypeToUnaryOperation(unaryNode.tokenType()), operand), unaryNode);
    }

    private JavaScriptNode enterUnaryIncDecNode(UnaryNode unaryNode) {
        if (JSConfig.LocalVarIncDecNode && unaryNode.getExpression() instanceof IdentNode) {
            IdentNode identNode = (IdentNode) unaryNode.getExpression();
            assert !identNode.isPropertyName() && !identNode.isThis() && !identNode.isMetaProperty() && !identNode.isSuper();
            VarRef varRef = findScopeVarCheckTDZ(identNode.getName(), false);
            if (varRef instanceof FrameSlotVarRef) {
                FrameSlotVarRef frameVarRef = (FrameSlotVarRef) varRef;
                FrameSlot frameSlot = frameVarRef.getFrameSlot();
                if (JSFrameUtil.isConst(frameSlot)) {
                    // we know this is going to throw. do the read and throw TypeError.
                    return tagExpression(checkMutableBinding(frameVarRef.createReadNode(), frameSlot.getIdentifier()), unaryNode);
                }
                return tagExpression(factory.createLocalVarInc(tokenTypeToUnaryOperation(unaryNode.tokenType()), frameSlot, frameVarRef.hasTDZCheck(),
                                frameVarRef.createScopeFrameNode(), frameVarRef.getFrameDescriptor()), unaryNode);
            }
        }

        BinaryOperation operation = unaryNode.tokenType() == TokenType.INCPREFIX || unaryNode.tokenType() == TokenType.INCPOSTFIX ? BinaryOperation.ADD : BinaryOperation.SUBTRACT;
        boolean isPostfix = unaryNode.tokenType() == TokenType.INCPOSTFIX || unaryNode.tokenType() == TokenType.DECPOSTFIX;
        return tagExpression(transformCompoundAssignment(unaryNode, unaryNode.getExpression(), factory.createConstantNumericUnit(), operation, isPostfix, true), unaryNode);
    }

    private static UnaryOperation tokenTypeToUnaryOperation(TokenType tokenType) {
        switch (tokenType) {
            case ADD:
                return UnaryOperation.PLUS;
            case BIT_NOT:
                return UnaryOperation.BITWISE_COMPLEMENT;
            case NOT:
                return UnaryOperation.NOT;
            case SUB:
                return UnaryOperation.MINUS;
            case TYPEOF:
                return UnaryOperation.TYPE_OF;
            case VOID:
                return UnaryOperation.VOID;
            case DECPREFIX:
                return UnaryOperation.PREFIX_LOCAL_DECREMENT;
            case DECPOSTFIX:
                return UnaryOperation.POSTFIX_LOCAL_DECREMENT;
            case INCPREFIX:
                return UnaryOperation.PREFIX_LOCAL_INCREMENT;
            case INCPOSTFIX:
                return UnaryOperation.POSTFIX_LOCAL_INCREMENT;
            case NEW:
            case DELETE:
            default:
                throw new UnsupportedOperationException(tokenType.toString());
        }
    }

    private JavaScriptNode enterDelete(UnaryNode unaryNode) {
        Expression rhs = unaryNode.getExpression();
        if (rhs instanceof AccessNode || rhs instanceof IndexNode) {
            return enterDeleteProperty(unaryNode);
        } else {
            return enterDeleteIdent(unaryNode);
        }
    }

    private JavaScriptNode enterDeleteIdent(UnaryNode unaryNode) {
        Expression rhs = unaryNode.getExpression();
        JavaScriptNode result;
        if (rhs instanceof IdentNode) {
            // attempt to delete a binding
            String varName = ((IdentNode) rhs).getName();
            VarRef varRef = findScopeVar(varName, varName.equals(Environment.THIS_NAME));
            result = varRef.createDeleteNode();
        } else {
            // deleting a non-reference, always returns true
            result = factory.createDual(context, transform(rhs), factory.createConstantBoolean(true));
        }
        return tagExpression(result, unaryNode);
    }

    private JavaScriptNode enterDeleteProperty(UnaryNode deleteNode) {
        BaseNode baseNode = (BaseNode) deleteNode.getExpression();
        if (baseNode.isSuper()) {
            return tagExpression(factory.createThrowError(JSErrorType.ReferenceError, "Unsupported reference to 'super'"), deleteNode);
        }

        JavaScriptNode target = transform(baseNode.getBase());
        JavaScriptNode key;
        if (baseNode instanceof AccessNode) {
            AccessNode accessNode = (AccessNode) baseNode;
            assert !accessNode.isPrivate();
            key = factory.createConstantString(accessNode.getProperty());
        } else {
            assert baseNode instanceof IndexNode;
            IndexNode indexNode = (IndexNode) baseNode;
            key = transform(indexNode.getIndex());
        }

        if (baseNode.isOptionalChain()) {
            target = filterOptionalChainTarget(target, baseNode.isOptional());
        }
        JavaScriptNode delete = factory.createDeleteProperty(target, key, environment.isStrictMode(), context);
        tagExpression(delete, deleteNode);
        if (baseNode.isOptionalChain()) {
            delete = factory.createOptionalChain(delete);
        }
        return delete;
    }

    private JavaScriptNode filterOptionalChainTarget(JavaScriptNode target, boolean optional) {
        JavaScriptNode innerAccess;
        if (target instanceof OptionalChainNode) {
            innerAccess = ((OptionalChainNode) target).getAccessNode();
        } else if (target instanceof OptionalChainNode.OptionalTargetableNode) {
            innerAccess = ((OptionalChainNode.OptionalTargetableNode) target).getDelegateNode();
        } else {
            innerAccess = target;
        }
        if (optional) {
            innerAccess = factory.createOptionalChainShortCircuit(innerAccess);
        }
        return innerAccess;
    }

    private JavaScriptNode[] transformArgs(List argList) {
        int len = argList.size();
        if (len > context.getFunctionArgumentsLimit()) {
            throw Errors.createSyntaxError("function has too many parameters");
        }
        JavaScriptNode[] args = javaScriptNodeArray(len);
        for (int i = 0; i < len; i++) {
            args[i] = transform(argList.get(i));
        }
        return args;
    }

    private JavaScriptNode enterNewNode(UnaryNode unaryNode) {
        CallNode callNode = (CallNode) unaryNode.getExpression();
        JavaScriptNode function = transform(callNode.getFunction());
        JavaScriptNode[] args = transformArgs(callNode.getArgs());
        JavaScriptNode call = factory.createNew(context, function, args);
        return tagExpression(tagCall(call), unaryNode);
    }

    @Override
    public JavaScriptNode enterCallNode(CallNode callNode) {
        JavaScriptNode function = transform(callNode.getFunction());
        JavaScriptNode[] args = transformArgs(callNode.getArgs());
        if (callNode.isOptionalChain()) {
            function = filterOptionalChainTarget(function, callNode.isOptional());
        }
        JavaScriptNode call;
        if (callNode.isEval() && args.length >= 1) {
            call = createCallEvalNode(function, args);
        } else if (callNode.isApplyArguments() && currentFunction().isDirectArgumentsAccess()) {
            call = createCallApplyArgumentsNode(function, args);
        } else if (callNode.getFunction() instanceof IdentNode && ((IdentNode) callNode.getFunction()).isDirectSuper()) {
            call = createCallDirectSuper(function, args);
        } else if (callNode.isImport()) {
            call = createImportCallNode(args);
        } else {
            call = factory.createFunctionCall(context, function, args);
        }
        tagExpression(tagCall(call), callNode);
        if (callNode.isOptionalChain()) {
            call = factory.createOptionalChain(call);
        }
        return call;
    }

    private JavaScriptNode[] insertNewTargetArg(JavaScriptNode[] args) {
        JavaScriptNode[] result = new JavaScriptNode[args.length + 1];
        result[0] = environment.findNewTargetVar().createReadNode();
        System.arraycopy(args, 0, result, 1, args.length);
        return result;
    }

    /**
     * Initialize derived constructor this value.
     */
    private JavaScriptNode initializeThis(JavaScriptNode thisValueNode) {
        VarRef thisVar = environment.findThisVar();
        // (GR-2061) we don't have to do this check if super() can be called only once, provably
        // (incl. possible super calls in nested arrow functions)
        // => return factory.createWriteNode(thisVarNode, thisValueNode, context);
        VarRef tempVar = environment.createTempVar();
        JavaScriptNode uninitialized = factory.createBinary(context, BinaryOperation.IDENTICAL, thisVar.createReadNode(), factory.createConstantUndefined());
        return factory.createIf(factory.createDual(context, tempVar.createWriteNode(thisValueNode), uninitialized),
                        initializeInstanceElements(thisVar.createWriteNode(tempVar.createReadNode())),
                        factory.createThrowError(JSErrorType.ReferenceError, "super() called twice"));
    }

    private JavaScriptNode initializeInstanceElements(JavaScriptNode thisValueNode) {
        ClassNode classNode = lc.getCurrentClass();
        if (!classNode.hasInstanceFields() && !classNode.hasPrivateInstanceMethods()) {
            return thisValueNode;
        }

        JavaScriptNode constructor = factory.createAccessCallee(currentFunction().getThisFunctionLevel());
        return factory.createInitializeInstanceElements(context, thisValueNode, constructor);
    }

    private JavaScriptNode createCallEvalNode(JavaScriptNode function, JavaScriptNode[] args) {
        assert (currentFunction().isGlobal() || currentFunction().isStrictMode() || currentFunction().isDirectEval()) || currentFunction().isDynamicallyScoped();
        for (FunctionEnvironment func = currentFunction(); func.getParentFunction() != null; func = func.getParentFunction()) {
            func.setNeedsParentFrame(true);
        }
        return EvalNode.create(context, function, args, createThisNodeUnchecked(), new DirectEvalContext(lc.getCurrentScope(), environment, lc.getCurrentClass()));
    }

    private JavaScriptNode createCallApplyArgumentsNode(JavaScriptNode function, JavaScriptNode[] args) {
        return factory.createCallApplyArguments(context, (JSFunctionCallNode) factory.createFunctionCall(context, function, args));
    }

    private JavaScriptNode createCallDirectSuper(JavaScriptNode function, JavaScriptNode[] args) {
        return initializeThis(factory.createFunctionCallWithNewTarget(context, function, insertNewTargetArg(args)));
    }

    private JavaScriptNode createImportCallNode(JavaScriptNode[] args) {
        assert args.length == 1;
        return factory.createImportCall(context, args[0], getActiveScriptOrModule());
    }

    @Override
    public JavaScriptNode enterBinaryNode(BinaryNode binaryNode) {
        switch (binaryNode.tokenType()) {
            case ASSIGN:
            case ASSIGN_INIT:
                return enterBinaryAssignNode(binaryNode);
            case ASSIGN_ADD:
            case ASSIGN_BIT_AND:
            case ASSIGN_BIT_OR:
            case ASSIGN_BIT_XOR:
            case ASSIGN_DIV:
            case ASSIGN_MOD:
            case ASSIGN_MUL:
            case ASSIGN_EXP:
            case ASSIGN_SAR:
            case ASSIGN_SHL:
            case ASSIGN_SHR:
            case ASSIGN_SUB:
            case ASSIGN_AND:
            case ASSIGN_OR:
            case ASSIGN_NULLCOAL:
                return enterBinaryTransformNode(binaryNode);
            case ADD:
            case SUB:
            case MUL:
            case EXP:
            case DIV:
            case MOD:
            case EQ:
            case EQ_STRICT:
            case GE:
            case GT:
            case LE:
            case LT:
            case NE:
            case NE_STRICT:
            case BIT_AND:
            case BIT_OR:
            case BIT_XOR:
            case SAR:
            case SHL:
            case SHR:
            case AND:
            case OR:
            case NULLISHCOALESC:
            case INSTANCEOF:
            case IN:
            case COMMARIGHT:
                return enterBinaryExpressionNode(binaryNode);
            case COMMALEFT:
            case ARROW:
            default:
                throw new UnsupportedOperationException(binaryNode.tokenType().toString());
        }
    }

    private JavaScriptNode enterBinaryExpressionNode(BinaryNode binaryNode) {
        JavaScriptNode lhs = transform(binaryNode.getLhs());
        JavaScriptNode rhs = transform(binaryNode.getRhs());
        return tagExpression(factory.createBinary(context, tokenTypeToBinaryOperation(binaryNode.tokenType()), lhs, rhs), binaryNode);
    }

    private JavaScriptNode enterBinaryTransformNode(BinaryNode binaryNode) {
        JavaScriptNode assignedValue = transform(binaryNode.getAssignmentSource());
        return tagExpression(transformCompoundAssignment(binaryNode, binaryNode.getAssignmentDest(), assignedValue, tokenTypeToBinaryOperation(binaryNode.tokenType()), false, false), binaryNode);
    }

    private JavaScriptNode enterBinaryAssignNode(BinaryNode binaryNode) {
        Expression assignmentDest = binaryNode.getAssignmentDest();
        JavaScriptNode assignedValue = transform(binaryNode.getAssignmentSource());
        JavaScriptNode assignment = transformAssignment(binaryNode, assignmentDest, assignedValue, binaryNode.isTokenType(TokenType.ASSIGN_INIT));
        assert assignedValue != null && (assignedValue.hasTag(StandardTags.ExpressionTag.class) || !assignedValue.isInstrumentable()) : "ExpressionTag expected but not found for: " + assignedValue;
        return tagExpression(assignment, binaryNode);
    }

    private static BinaryOperation tokenTypeToBinaryOperation(TokenType tokenType) {
        switch (tokenType) {
            case ASSIGN_ADD:
            case ADD:
                return BinaryOperation.ADD;
            case ASSIGN_SUB:
            case SUB:
                return BinaryOperation.SUBTRACT;
            case ASSIGN_MUL:
            case MUL:
                return BinaryOperation.MULTIPLY;
            case ASSIGN_EXP:
            case EXP:
                return BinaryOperation.EXPONENTIATE;
            case ASSIGN_DIV:
            case DIV:
                return BinaryOperation.DIVIDE;
            case ASSIGN_MOD:
            case MOD:
                return BinaryOperation.MODULO;
            case ASSIGN_BIT_AND:
            case BIT_AND:
                return BinaryOperation.BITWISE_AND;
            case ASSIGN_BIT_OR:
            case BIT_OR:
                return BinaryOperation.BITWISE_OR;
            case ASSIGN_BIT_XOR:
            case BIT_XOR:
                return BinaryOperation.BITWISE_XOR;
            case ASSIGN_SHL:
            case SHL:
                return BinaryOperation.BITWISE_LEFT_SHIFT;
            case ASSIGN_SAR:
            case SAR:
                return BinaryOperation.BITWISE_RIGHT_SHIFT;
            case ASSIGN_SHR:
            case SHR:
                return BinaryOperation.BITWISE_UNSIGNED_RIGHT_SHIFT;
            case EQ:
                return BinaryOperation.EQUAL;
            case EQ_STRICT:
                return BinaryOperation.IDENTICAL;
            case GE:
                return BinaryOperation.GREATER_OR_EQUAL;
            case GT:
                return BinaryOperation.GREATER;
            case LE:
                return BinaryOperation.LESS_OR_EQUAL;
            case LT:
                return BinaryOperation.LESS;
            case NE:
                return BinaryOperation.NOT_EQUAL;
            case NE_STRICT:
                return BinaryOperation.NOT_IDENTICAL;
            case ASSIGN_AND:
            case AND:
                return BinaryOperation.LOGICAL_AND;
            case ASSIGN_OR:
            case OR:
                return BinaryOperation.LOGICAL_OR;
            case ASSIGN_NULLCOAL:
            case NULLISHCOALESC:
                return BinaryOperation.NULLISH_COALESCING;
            case INSTANCEOF:
                return BinaryOperation.INSTANCEOF;
            case IN:
                return BinaryOperation.IN;
            case COMMARIGHT:
                return BinaryOperation.DUAL;
            default:
                throw new UnsupportedOperationException(tokenType.toString());
        }
    }

    private JavaScriptNode transformAssignment(Expression assignmentExpression, Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) {
        return transformAssignmentImpl(assignmentExpression, lhsExpression, assignedValue, initializationAssignment, null, false, false);
    }

    private JavaScriptNode transformCompoundAssignment(Expression assignmentExpression, Expression lhsExpression, JavaScriptNode assignedValue,
                    BinaryOperation binaryOp, boolean returnOldValue, boolean convertLHSToNumeric) {
        return transformAssignmentImpl(assignmentExpression, lhsExpression, assignedValue, false, binaryOp, returnOldValue, convertLHSToNumeric);
    }

    private JavaScriptNode transformAssignmentImpl(Expression assignmentExpression, Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment,
                    BinaryOperation binaryOp, boolean returnOldValue, boolean convertLHSToNumeric) {
        JavaScriptNode assignedNode;
        switch (lhsExpression.tokenType()) {
            // Checkstyle: stop
            default: // ident with other token type
                // Checkstyle: resume
                if (!(lhsExpression instanceof IdentNode)) {
                    throw Errors.unsupported("unsupported assignment to token type: " + lhsExpression.tokenType().toString() + " " + lhsExpression.toString());
                }
                // fall through
            case IDENT:
                assignedNode = transformAssignmentIdent((IdentNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric, initializationAssignment);
                break;
            case LBRACKET:
                // target[element]
                assignedNode = transformIndexAssignment((IndexNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric);
                break;
            case PERIOD:
                // target.property
                assignedNode = transformPropertyAssignment((AccessNode) lhsExpression, assignedValue, binaryOp, returnOldValue, convertLHSToNumeric);
                break;
            case ARRAY:
                assert binaryOp == null;
                assignedNode = transformDestructuringArrayAssignment(lhsExpression, assignedValue, initializationAssignment);
                break;
            case LBRACE:
                assert binaryOp == null;
                assignedNode = transformDestructuringObjectAssignment(lhsExpression, assignedValue, initializationAssignment);
                break;
        }
        if (returnOldValue && assignedNode instanceof DualNode) {
            ensureHasSourceSection(((DualNode) assignedNode).getLeft(), assignmentExpression);
        }
        return tagExpression(assignedNode, assignmentExpression);
    }

    private static boolean isLogicalOp(BinaryOperation op) {
        return op == BinaryOperation.LOGICAL_AND || op == BinaryOperation.LOGICAL_OR || op == BinaryOperation.NULLISH_COALESCING;
    }

    private JavaScriptNode transformAssignmentIdent(IdentNode identNode, JavaScriptNode assignedValue, BinaryOperation binaryOp, boolean returnOldValue, boolean convertLHSToNumeric,
                    boolean initializationAssignment) {
        JavaScriptNode rhs = assignedValue;
        String ident = identNode.getName();
        VarRef scopeVar = findScopeVarCheckTDZ(ident, initializationAssignment);

        // if scopeVar is const, the assignment will never succeed and is only there to perform
        // the temporal dead zone check and throw a ReferenceError instead of a TypeError
        if (!initializationAssignment && scopeVar.isConst()) {
            if (context.getContextOptions().isV8LegacyConst() && !environment.isStrictMode()) {
                // Note that there is no TDZ check for const in this mode either.
                return rhs;
            }
            rhs = checkMutableBinding(rhs, scopeVar.getName());
        }

        if (binaryOp == null) {
            return scopeVar.createWriteNode(rhs);
        } else {
            if (isLogicalOp(binaryOp)) {
                assert !convertLHSToNumeric && !returnOldValue;
                JavaScriptNode readNode = tagExpression(scopeVar.createReadNode(), identNode);
                JavaScriptNode writeNode = scopeVar.createWriteNode(assignedValue);
                return factory.createBinary(context, binaryOp, readNode, writeNode);
            } else {
                // e.g.: lhs *= rhs => lhs = lhs * rhs
                // If lhs is a side-effecting getter that deletes lhs, we must not throw a
                // ReferenceError at the lhs assignment since the lhs reference is already resolved.
                // We also need to ensure that HasBinding is idempotent or evaluated at most once.
                Pair, UnaryOperator> pair = scopeVar.createCompoundAssignNode();
                JavaScriptNode readNode = tagExpression(pair.getFirst().get(), identNode);
                if (convertLHSToNumeric) {
                    readNode = factory.createToNumeric(readNode);
                }
                VarRef prevValueTemp = null;
                if (returnOldValue) {
                    prevValueTemp = environment.createTempVar();
                    readNode = prevValueTemp.createWriteNode(readNode);
                }
                JavaScriptNode binOpNode = tagExpression(factory.createBinary(context, binaryOp, readNode, rhs), identNode);
                JavaScriptNode writeNode = pair.getSecond().apply(binOpNode);
                if (returnOldValue) {
                    return factory.createDual(context, writeNode, prevValueTemp.createReadNode());
                } else {
                    return writeNode;
                }
            }
        }
    }

    /**
     * If this is an attempt to change the value of an immutable binding, throw a runtime TypeError.
     */
    private JavaScriptNode checkMutableBinding(JavaScriptNode rhsNode, Object identifier) {
        if (context.getContextOptions().isV8LegacyConst() && !environment.isStrictMode()) {
            return rhsNode;
        }
        // evaluate rhs and throw TypeError
        String message = context.isOptionV8CompatibilityMode() ? "Assignment to constant variable." : "Assignment to constant \"" + identifier + "\"";
        JavaScriptNode throwTypeError = factory.createThrowError(JSErrorType.TypeError, message);
        return isPotentiallySideEffecting(rhsNode) ? createBlock(rhsNode, throwTypeError) : throwTypeError;
    }

    private JavaScriptNode transformPropertyAssignment(AccessNode accessNode, JavaScriptNode assignedValue, BinaryOperation binaryOp, boolean returnOldValue, boolean convertToNumeric) {
        JavaScriptNode assignedNode;
        JavaScriptNode target = transform(accessNode.getBase());

        if (binaryOp == null) {
            assignedNode = createWriteProperty(accessNode, target, assignedValue);
        } else {
            JavaScriptNode target1;
            JavaScriptNode target2;
            if (target instanceof RepeatableNode) {
                target1 = target;
                target2 = factory.copy(target);
            } else {
                VarRef targetTemp = environment.createTempVar();
                target1 = targetTemp.createWriteNode(target);
                target2 = targetTemp.createReadNode();
            }
            if (isLogicalOp(binaryOp)) {
                assert !convertToNumeric && !returnOldValue;
                JavaScriptNode readNode = tagExpression(createReadProperty(accessNode, target1), accessNode);
                JavaScriptNode writeNode = createWriteProperty(accessNode, target2, assignedValue);
                assignedNode = factory.createBinary(context, binaryOp, readNode, writeNode);
            } else {
                VarRef prevValueTemp = null;
                JavaScriptNode readNode = tagExpression(createReadProperty(accessNode, target2), accessNode);
                if (convertToNumeric) {
                    readNode = factory.createToNumeric(readNode);
                }
                if (returnOldValue) {
                    prevValueTemp = environment.createTempVar();
                    readNode = prevValueTemp.createWriteNode(readNode);
                }
                JavaScriptNode binOpNode = tagExpression(factory.createBinary(context, binaryOp, readNode, assignedValue), accessNode);
                JavaScriptNode writeNode = createWriteProperty(accessNode, target1, binOpNode);
                if (returnOldValue) {
                    assignedNode = factory.createDual(context, writeNode, prevValueTemp.createReadNode());
                } else {
                    assignedNode = writeNode;
                }
            }
        }
        return assignedNode;
    }

    private JavaScriptNode transformIndexAssignment(IndexNode indexNode, JavaScriptNode assignedValue, BinaryOperation binaryOp, boolean returnOldValue, boolean convertToNumeric) {
        JavaScriptNode assignedNode;
        JavaScriptNode target = transform(indexNode.getBase());
        JavaScriptNode elem = transform(indexNode.getIndex());

        if (binaryOp == null) {
            assignedNode = factory.createWriteElementNode(target, elem, assignedValue, context, environment.isStrictMode());
        } else {
            // Evaluation order:
            // 1. target = GetValue(baseReference)
            // 2. key = GetValue(propertyNameReference)
            // 3. RequireObjectCoercible(target); safely repeatable
            // 4. key = ToPropertyKey(key); only once
            // 5. lhs = target[key];
            // 6. result = lhs op rhs;
            // 7. target[key] = result

            // Index must be ToPropertyKey-converted only once, save it in temp var
            VarRef keyTemp = environment.createTempVar();
            JavaScriptNode readIndex = keyTemp.createReadNode();
            JSWriteFrameSlotNode writeIndex = (JSWriteFrameSlotNode) keyTemp.createWriteNode(null);

            JavaScriptNode target1;
            JavaScriptNode target2;
            if (target instanceof RepeatableNode) {
                target1 = target;
                target2 = factory.copy(target);
            } else {
                VarRef targetTemp = environment.createTempVar();
                target1 = targetTemp.createWriteNode(target);
                target2 = targetTemp.createReadNode();
            }

            if (isLogicalOp(binaryOp)) {
                assert !convertToNumeric && !returnOldValue;
                JavaScriptNode readNode = tagExpression(factory.createReadElementNode(context, target1, keyTemp.createWriteNode(elem)), indexNode);
                JavaScriptNode writeNode = factory.createCompoundWriteElementNode(target2, readIndex, assignedValue, null, context, environment.isStrictMode());
                assignedNode = factory.createBinary(context, binaryOp, readNode, writeNode);
            } else {
                JavaScriptNode readNode = tagExpression(factory.createReadElementNode(context, target2, readIndex), indexNode);
                if (convertToNumeric) {
                    readNode = factory.createToNumeric(readNode);
                }
                VarRef prevValueTemp = null;
                if (returnOldValue) {
                    prevValueTemp = environment.createTempVar();
                    readNode = prevValueTemp.createWriteNode(readNode);
                }
                JavaScriptNode binOpNode = tagExpression(factory.createBinary(context, binaryOp, readNode, assignedValue), indexNode);
                JavaScriptNode writeNode = factory.createCompoundWriteElementNode(target1, elem, binOpNode, writeIndex, context, environment.isStrictMode());
                if (returnOldValue) {
                    assignedNode = factory.createDual(context, writeNode, prevValueTemp.createReadNode());
                } else {
                    assignedNode = writeNode;
                }
            }
        }
        return assignedNode;
    }

    private JavaScriptNode transformDestructuringArrayAssignment(Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) {
        LiteralNode.ArrayLiteralNode arrayLiteralNode = (LiteralNode.ArrayLiteralNode) lhsExpression;
        List elementExpressions = arrayLiteralNode.getElementExpressions();
        JavaScriptNode[] initElements = javaScriptNodeArray(elementExpressions.size());
        VarRef iteratorTempVar = environment.createTempVar();
        VarRef valueTempVar = environment.createTempVar();
        JavaScriptNode initValue = valueTempVar.createWriteNode(assignedValue);
        // By default, we use the hint to track the type of iterator.
        JavaScriptNode getIterator = factory.createGetIterator(context, initValue);
        JavaScriptNode initIteratorTempVar = iteratorTempVar.createWriteNode(getIterator);

        for (int i = 0; i < elementExpressions.size(); i++) {
            Expression element = elementExpressions.get(i);
            Expression lhsExpr;
            Expression init = null;
            if (element instanceof IdentNode) {
                lhsExpr = element;
            } else if (element instanceof BinaryNode) {
                assert element.isTokenType(TokenType.ASSIGN) || element.isTokenType(TokenType.ASSIGN_INIT);
                lhsExpr = ((BinaryNode) element).getLhs();
                init = ((BinaryNode) element).getRhs();
            } else {
                lhsExpr = element;
            }
            JavaScriptNode rhsNode = factory.createIteratorGetNextValue(context, iteratorTempVar.createReadNode(), factory.createConstantUndefined(), true);
            if (init != null) {
                rhsNode = factory.createNotUndefinedOr(rhsNode, transform(init));
            }
            if (lhsExpr != null && lhsExpr.isTokenType(TokenType.SPREAD_ARRAY)) {
                rhsNode = factory.createIteratorToArray(context, iteratorTempVar.createReadNode());
                lhsExpr = ((UnaryNode) lhsExpr).getExpression();
            }
            if (lhsExpr != null) {
                initElements[i] = transformAssignment(lhsExpr, lhsExpr, rhsNode, initializationAssignment);
            } else {
                initElements[i] = rhsNode;
            }
        }
        JavaScriptNode closeIfNotDone = factory.createIteratorCloseIfNotDone(context, createBlock(initElements), iteratorTempVar.createReadNode());
        return factory.createExprBlock(initIteratorTempVar, closeIfNotDone, valueTempVar.createReadNode());
    }

    private JavaScriptNode transformDestructuringObjectAssignment(Expression lhsExpression, JavaScriptNode assignedValue, boolean initializationAssignment) {
        ObjectNode objectLiteralNode = (ObjectNode) lhsExpression;
        List propertyExpressions = objectLiteralNode.getElements();
        if (propertyExpressions.isEmpty()) {
            return factory.createRequireObjectCoercible(assignedValue);
        }

        int numberOfProperties = propertyExpressions.size();
        boolean hasRest = propertyExpressions.get(numberOfProperties - 1).isRest();
        boolean requireObjectCoercible = hasRest && numberOfProperties == 1;
        JavaScriptNode[] initElements = javaScriptNodeArray(numberOfProperties);
        JavaScriptNode[] excludedKeys = hasRest ? javaScriptNodeArray(numberOfProperties - 1) : null;

        VarRef valueTempVar = environment.createTempVar();
        JavaScriptNode initValueTempVar = valueTempVar.createWriteNode(requireObjectCoercible ? factory.createRequireObjectCoercible(assignedValue) : assignedValue);

        for (int i = 0; i < numberOfProperties; i++) {
            PropertyNode property = propertyExpressions.get(i);
            Expression lhsExpr;
            Expression init = null;
            if (property.getValue() instanceof BinaryNode) {
                assert property.getValue().isTokenType(TokenType.ASSIGN) || property.getValue().isTokenType(TokenType.ASSIGN_INIT);
                lhsExpr = ((BinaryNode) property.getValue()).getLhs();
                init = ((BinaryNode) property.getValue()).getRhs();
            } else if (property.isRest()) {
                assert hasRest;
                lhsExpr = ((UnaryNode) property.getKey()).getExpression();
            } else {
                lhsExpr = property.getValue();
            }
            JavaScriptNode rhsNode;
            JavaScriptNode toPropertyKey = null;
            if (property.isRest()) {
                JavaScriptNode excludedItemsArray = excludedKeys.length == 0 ? null : factory.createArrayLiteral(context, excludedKeys);
                rhsNode = factory.createRestObject(context, valueTempVar.createReadNode(), excludedItemsArray);
            } else if (property.getKey() instanceof IdentNode && !property.isComputed()) {
                String keyName = property.getKeyName();
                if (hasRest) {
                    excludedKeys[i] = factory.createConstantString(keyName);
                }
                rhsNode = factory.createReadProperty(context, valueTempVar.createReadNode(), keyName);
            } else {
                JavaScriptNode key = transform(property.getKey());
                VarRef keyTempVar = environment.createTempVar();
                if (hasRest) {
                    excludedKeys[i] = keyTempVar.createReadNode();
                }
                toPropertyKey = keyTempVar.createWriteNode(factory.createToPropertyKey(key));
                rhsNode = factory.createReadElementNode(context, valueTempVar.createReadNode(), keyTempVar.createReadNode());
            }
            if (init != null) {
                rhsNode = factory.createNotUndefinedOr(rhsNode, transform(init));
            }
            JavaScriptNode initElement = transformAssignment(lhsExpr, lhsExpr, rhsNode, initializationAssignment);
            initElements[i] = (toPropertyKey == null) ? initElement : factory.createDual(context, toPropertyKey, initElement);
        }
        return factory.createExprBlock(initValueTempVar, createBlock(initElements), valueTempVar.createReadNode());
    }

    @Override
    public JavaScriptNode enterAccessNode(AccessNode accessNode) {
        JavaScriptNode base = transform(accessNode.getBase());
        if (accessNode.isOptionalChain()) {
            return createOptionalAccessNode(accessNode, base);
        }
        JavaScriptNode read = createReadProperty(accessNode, base);
        tagExpression(read, accessNode);
        return read;
    }

    private JavaScriptNode createOptionalAccessNode(AccessNode accessNode, JavaScriptNode base) {
        JavaScriptNode innerAccess = filterOptionalChainTarget(base, accessNode.isOptional());
        JavaScriptNode read = createReadProperty(accessNode, innerAccess);
        tagExpression(read, accessNode);
        return factory.createOptionalChain(read);
    }

    private JavaScriptNode createReadProperty(AccessNode accessNode, JavaScriptNode base) {
        if (accessNode.isPrivate()) {
            return createPrivateFieldGet(accessNode, base);
        } else {
            return factory.createReadProperty(context, base, accessNode.getProperty(), accessNode.isFunction());
        }
    }

    private JavaScriptNode createWriteProperty(AccessNode accessNode, JavaScriptNode base, JavaScriptNode rhs) {
        if (accessNode.isPrivate()) {
            return createPrivateFieldSet(accessNode, base, rhs);
        } else {
            return factory.createWriteProperty(base, accessNode.getProperty(), rhs, context, environment.isStrictMode());
        }
    }

    private JavaScriptNode createPrivateFieldGet(AccessNode accessNode, JavaScriptNode base) {
        VarRef privateNameVar = environment.findLocalVar(accessNode.getPrivateName());
        JavaScriptNode privateName = privateNameVar.createReadNode();
        return factory.createPrivateFieldGet(context, insertPrivateBrandCheck(base, privateNameVar), privateName);
    }

    private JavaScriptNode createPrivateFieldSet(AccessNode accessNode, JavaScriptNode base, JavaScriptNode rhs) {
        VarRef privateNameVar = environment.findLocalVar(accessNode.getPrivateName());
        JavaScriptNode privateName = privateNameVar.createReadNode();
        return factory.createPrivateFieldSet(context, insertPrivateBrandCheck(base, privateNameVar), privateName, rhs);
    }

    private JavaScriptNode insertPrivateBrandCheck(JavaScriptNode base, VarRef privateNameVar) {
        FrameSlot frameSlot = privateNameVar.getFrameSlot();
        if (JSFrameUtil.needsPrivateBrandCheck(frameSlot)) {
            int frameLevel = ((AbstractFrameVarRef) privateNameVar).getFrameLevel();
            int scopeLevel = ((AbstractFrameVarRef) privateNameVar).getScopeLevel();
            Environment memberEnv = environment.getParentAt(frameLevel, scopeLevel);
            FrameSlot constructorSlot = memberEnv.getBlockFrameDescriptor().findFrameSlot(ClassNode.PRIVATE_CONSTRUCTOR_BINDING_NAME);
            JavaScriptNode constructor = environment.createLocal(constructorSlot, frameLevel, scopeLevel);
            JavaScriptNode brand;
            if (JSFrameUtil.isPrivateNameStatic(frameSlot)) {
                brand = constructor;
            } else {
                brand = factory.createGetPrivateBrand(context, constructor);
            }
            return factory.createPrivateBrandCheck(base, brand);
        } else {
            return base;
        }
    }

    @Override
    public JavaScriptNode enterIndexNode(IndexNode indexNode) {
        JavaScriptNode base = transform(indexNode.getBase());
        JavaScriptNode index = transform(indexNode.getIndex());
        if (indexNode.isOptionalChain()) {
            return createOptionalIndexNode(indexNode, base, index);
        }
        return tagExpression(factory.createReadElementNode(context, base, index), indexNode);
    }

    private JavaScriptNode createOptionalIndexNode(IndexNode indexNode, JavaScriptNode base, JavaScriptNode index) {
        JavaScriptNode read = factory.createReadElementNode(context, filterOptionalChainTarget(base, indexNode.isOptional()), index);
        tagExpression(read, indexNode);
        return factory.createOptionalChain(read);
    }

    @Override
    public JavaScriptNode enterObjectNode(ObjectNode objectNode) {
        ArrayList members = transformPropertyDefinitionList(objectNode.getElements(), false, null);
        return tagExpression(factory.createObjectLiteral(context, members), objectNode);
    }

    private ArrayList transformPropertyDefinitionList(List properties, boolean isClass, Symbol classNameSymbol) {
        ArrayList members = new ArrayList<>(properties.size());
        for (int i = 0; i < properties.size(); i++) {
            PropertyNode property = properties.get(i);

            final ObjectLiteralMemberNode member;
            if (property.getValue() != null || property.isClassField()) {
                member = enterObjectPropertyNode(property, isClass, classNameSymbol);
            } else if (property.isRest()) {
                assert !isClass;
                JavaScriptNode from = transform(((UnaryNode) property.getKey()).getExpression());
                member = factory.createSpreadObjectMember(property.isStatic(), from);
            } else {
                member = enterObjectAccessorNode(property, isClass);
            }
            members.add(member);
        }
        return members;
    }

    private ObjectLiteralMemberNode enterObjectAccessorNode(PropertyNode property, boolean isClass) {
        assert property.getGetter() != null || property.getSetter() != null;
        JavaScriptNode getter = getAccessor(property.getGetter());
        JavaScriptNode setter = getAccessor(property.getSetter());
        boolean enumerable = !isClass;
        if (property.isComputed()) {
            return factory.createComputedAccessorMember(transform(property.getKey()), property.isStatic(), enumerable, getter, setter);
        } else if (property.isPrivate()) {
            VarRef privateVar = environment.findLocalVar(property.getPrivateName());
            JSWriteFrameSlotNode writePrivateNode = (JSWriteFrameSlotNode) privateVar.createWriteNode(null);
            return factory.createPrivateAccessorMember(property.isStatic(), getter, setter, writePrivateNode);
        } else {
            return factory.createAccessorMember(property.getKeyName(), property.isStatic(), enumerable, getter, setter);
        }
    }

    private JavaScriptNode getAccessor(FunctionNode accessorFunction) {
        if (accessorFunction == null) {
            return null;
        }
        JavaScriptNode function = transform(accessorFunction);
        if (accessorFunction.needsSuper()) {
            assert accessorFunction.isMethod();
            function = factory.createMakeMethod(context, function);
        }
        return function;
    }

    private JavaScriptNode transformPropertyValue(Expression propertyValue, Symbol classNameSymbol) {
        if (propertyValue == null) {
            // class field without an initializer
            return factory.createConstantUndefined();
        }

        // TDZ: class name symbol cannot be used as a key but may be used as a value.
        if (classNameSymbol != null) {
            classNameSymbol.setHasBeenDeclared(true);
        }
        JavaScriptNode value = transform(propertyValue);
        if (classNameSymbol != null) {
            classNameSymbol.setHasBeenDeclared(false);
        }

        if (propertyValue instanceof FunctionNode && ((FunctionNode) propertyValue).needsSuper()) {
            assert ((FunctionNode) propertyValue).isMethod();
            value = factory.createMakeMethod(context, value);
        }
        return value;
    }

    private ObjectLiteralMemberNode enterObjectPropertyNode(PropertyNode property, boolean isClass, Symbol classNameSymbol) {
        JavaScriptNode value = transformPropertyValue(property.getValue(), classNameSymbol);

        boolean enumerable = !isClass || property.isClassField();
        if (property.isComputed()) {
            JavaScriptNode computedKey = transform(property.getKey());
            return factory.createComputedDataMember(computedKey, property.isStatic(), enumerable, value, property.isClassField(), property.isAnonymousFunctionDefinition());
        } else if (!isClass && property.isProto()) {
            return factory.createProtoMember(property.getKeyName(), property.isStatic(), value);
        } else if (property.isPrivate()) {
            VarRef privateVar = environment.findLocalVar(property.getPrivateName());
            if (property.isClassField()) {
                JSWriteFrameSlotNode writePrivateNode = (JSWriteFrameSlotNode) privateVar.createWriteNode(factory.createNewPrivateName(property.getPrivateName()));
                return factory.createPrivateFieldMember(privateVar.createReadNode(), property.isStatic(), value, writePrivateNode);
            } else {
                JSWriteFrameSlotNode writePrivateNode = (JSWriteFrameSlotNode) privateVar.createWriteNode(null);
                return factory.createPrivateMethodMember(property.isStatic(), value, writePrivateNode);
            }
        } else {
            return factory.createDataMember(property.getKeyName(), property.isStatic(), enumerable, value, property.isClassField());
        }
    }

    @Override
    public JavaScriptNode enterTryNode(TryNode tryNode) {
        JavaScriptNode tryBlock = transform(tryNode.getBody());
        JavaScriptNode result = tryBlock;
        if (!tryNode.getCatchBlocks().isEmpty()) {
            for (Block catchParamBlock : tryNode.getCatchBlocks()) {
                CatchNode catchClause = (CatchNode) catchParamBlock.getLastStatement();
                Expression catchParameter = catchClause.getException();
                Block catchBody = catchClause.getBody();
                Expression pattern = catchClause.getDestructuringPattern();

                // manually enter the catch block; this hack is only necessary to be able to
                // evaluate the condition in the same block
                try (EnvironmentCloseable catchParamEnv = enterBlockEnvironment(catchParamBlock)) {
                    lc.push(catchParamBlock);
                    try {
                        // mark variables as declared
                        for (Statement statement : catchParamBlock.getStatements().subList(0, catchParamBlock.getStatementCount() - 1)) {
                            assert statement instanceof VarNode;
                            JavaScriptNode empty = transform(statement);
                            assert empty instanceof EmptyNode;
                        }

                        JavaScriptNode writeErrorVar = null;
                        JavaScriptNode destructuring = null;
                        if (catchParameter != null) {
                            String errorVarName = ((IdentNode) catchParameter).getName();
                            VarRef errorVar = environment.findLocalVar(errorVarName);
                            writeErrorVar = errorVar.createWriteNode(null);
                            if (pattern != null) {
                                // exception is being destructured
                                destructuring = transformAssignment(pattern, pattern, errorVar.createReadNode(), true);
                            }
                        }

                        JavaScriptNode catchBlock = transform(catchBody);

                        JavaScriptNode conditionExpression;
                        if (catchClause.getExceptionCondition() != null) {
                            conditionExpression = transform(catchClause.getExceptionCondition());
                        } else {
                            conditionExpression = null; // equivalent to constant true
                        }
                        BlockScopeNode blockScope = (BlockScopeNode) catchParamEnv.wrapBlockScope(null);
                        result = factory.createTryCatch(context, result, catchBlock, writeErrorVar, blockScope, destructuring, conditionExpression);
                        ensureHasSourceSection(result, tryNode);
                    } finally {
                        lc.pop(catchParamBlock);
                    }
                }
            }
        }
        if (tryNode.getFinallyBody() != null) {
            JavaScriptNode finallyBlock = transform(tryNode.getFinallyBody());
            result = factory.createTryFinally(result, wrapSaveAndRestoreCompletionValue(wrapClearCompletionValue(finallyBlock)));
        }
        result = wrapClearAndGetCompletionValue(result);
        return result;
    }

    @Override
    public JavaScriptNode enterThrowNode(com.oracle.js.parser.ir.ThrowNode throwNode) {
        return tagStatement(factory.createThrow(context, transform(throwNode.getExpression())), throwNode);
    }

    @Override
    public JavaScriptNode enterSwitchNode(com.oracle.js.parser.ir.SwitchNode switchNode) {
        Block switchBlock = lc.getCurrentBlock();
        assert switchBlock.isSwitchBlock();

        String switchVarName = makeUniqueTempVarNameForStatement(switchNode);
        environment.declareLocalVar(switchVarName);

        JavaScriptNode switchExpression = transform(switchNode.getExpression());
        boolean isSwitchTypeofString = isSwitchTypeofStringConstant(switchNode, switchExpression);
        if (isSwitchTypeofString) {
            switchExpression = ((TypeOfNode) switchExpression).getOperand();
        }

        VarRef switchVar = environment.findLocalVar(switchVarName);
        JavaScriptNode writeSwitchNode = switchVar.createWriteNode(switchExpression);

        JavaScriptNode switchBody;
        try (JumpTargetCloseable target = currentFunction().pushBreakTarget(null)) {
            // when this switch does not use fall-through, rewrite it to an if-else-cascade
            if (JSConfig.OptimizeNoFallthroughSwitch && isNoFallthroughSwitch(switchNode)) {
                switchBody = ifElseFromSwitch(switchNode, switchVar, isSwitchTypeofString);
            } else {
                switchBody = defaultSwitchNode(switchNode, switchVar, isSwitchTypeofString);
            }
            tagStatement(switchBody, switchNode);
            switchBody = wrapClearAndGetCompletionValue(target.wrapBreakTargetNode(switchBody));
        }
        return createBlock(writeSwitchNode, switchBody);
    }

    private JavaScriptNode defaultSwitchNode(com.oracle.js.parser.ir.SwitchNode switchNode, VarRef switchVar, boolean isSwitchTypeofString) {
        List cases = switchNode.getCases();
        int size = cases.size() + (switchNode.hasDefaultCase() ? 0 : 1);
        int[] jumptable = new int[size];
        int defaultpos = -1;
        List statementList = new ArrayList<>();
        List caseExprList = new ArrayList<>();
        int lastNonEmptyIndex = -1;
        for (CaseNode switchCase : cases) {
            if (switchCase.getTest() != null) {
                jumptable[caseExprList.size()] = statementList.size();
                JavaScriptNode readSwitchVarNode = switchVar.createReadNode();
                caseExprList.add(createSwitchCaseExpr(isSwitchTypeofString, switchCase, readSwitchVarNode));
            } else {
                defaultpos = statementList.size();
            }
            if (!switchCase.getStatements().isEmpty()) {
                List statements = switchCase.getStatements();
                for (int i = 0; i < statements.size(); i++) {
                    Statement statement = statements.get(i);
                    JavaScriptNode statementNode = transform(statement);
                    if (currentFunction().returnsLastStatementResult()) {
                        if (!statement.isCompletionValueNeverEmpty()) {
                            if (lastNonEmptyIndex >= 0) {
                                statementList.set(lastNonEmptyIndex, wrapSetCompletionValue(statementList.get(lastNonEmptyIndex)));
                                lastNonEmptyIndex = -1;
                            }
                        } else {
                            lastNonEmptyIndex = statementList.size();
                        }
                    }
                    statementList.add(statementNode);
                }
            }
        }
        if (currentFunction().returnsLastStatementResult() && lastNonEmptyIndex >= 0) {
            statementList.set(lastNonEmptyIndex, wrapSetCompletionValue(statementList.get(lastNonEmptyIndex)));
        }
        // set default case position to the end
        jumptable[jumptable.length - 1] = defaultpos != -1 ? defaultpos : statementList.size();
        return factory.createSwitch(caseExprList.toArray(EMPTY_NODE_ARRAY), jumptable, statementList.toArray(EMPTY_NODE_ARRAY));
    }

    private JavaScriptNode createSwitchCaseExpr(boolean isSwitchTypeofString, CaseNode switchCase, JavaScriptNode readSwitchVarNode) {
        tagHiddenExpression(readSwitchVarNode);
        if (isSwitchTypeofString) {
            String typeString = (String) ((LiteralNode) switchCase.getTest()).getValue();
            return tagExpression(factory.createTypeofIdentical(readSwitchVarNode, typeString), switchCase);
        } else {
            return tagExpression(factory.createBinary(context, BinaryOperation.IDENTICAL, readSwitchVarNode, transform(switchCase.getTest())), switchCase);
        }
    }

    /**
     * When a SwitchNode does not have any fall-through behavior, it can be transferred into an
     * if-else-cascade.
     */
    private JavaScriptNode ifElseFromSwitch(com.oracle.js.parser.ir.SwitchNode switchNode, VarRef switchVar, boolean isSwitchTypeofString) {
        assert isNoFallthroughSwitch(switchNode);

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

        JavaScriptNode curNode = null;
        if (defaultCase != null) {
            curNode = dropTerminalDirectBreakStatement(transformStatements(defaultCase.getStatements(), false));
            ensureHasSourceSection(curNode, defaultCase);
        }

        boolean defaultCascade = false;
        boolean lastCase = true;
        for (int i = cases.size() - 1; i >= 0; i--) {
            CaseNode caseNode = cases.get(i);
            if (caseNode.getTest() == null) {
                // start of the cascade with the default case
                // (the default case is the last case in the cascade)
                defaultCascade = true;
            } else {
                JavaScriptNode readSwitchVarNode = switchVar.createReadNode();
                JavaScriptNode test = createSwitchCaseExpr(isSwitchTypeofString, caseNode, readSwitchVarNode);
                if (caseNode.getStatements().isEmpty() && !lastCase) {
                    // fall through to the previous case
                    if (defaultCascade) {
                        // fall through to default case, execute test only for potential side effect
                        if (isPotentiallySideEffecting(test)) {
                            test = factory.createIf(test, null, null);
                            ensureHasSourceSection(test, caseNode);
                            curNode = curNode == null ? discardResult(test) : createBlock(test, curNode);
                        }
                    } else {
                        assert curNode instanceof com.oracle.truffle.js.nodes.control.IfNode;
                        // if (condition) => if (test || condition)
                        com.oracle.truffle.js.nodes.control.IfNode prevIfNode = (com.oracle.truffle.js.nodes.control.IfNode) curNode;
                        curNode = factory.copyIfWithCondition(prevIfNode, factory.createLogicalOr(test, prevIfNode.getCondition()));
                    }
                } else {
                    // start of a cascade (without the default case)
                    JavaScriptNode pass = dropTerminalDirectBreakStatement(transformStatements(caseNode.getStatements(), false));
                    ensureHasSourceSection(pass, caseNode);
                    curNode = factory.createIf(test, pass, curNode);
                    defaultCascade = false;
                }
                ensureHasSourceSection(curNode, caseNode.getTest());
            }
            lastCase = false;
        }
        return curNode == null ? factory.createEmpty() : curNode;
    }

    static boolean isPotentiallySideEffecting(JavaScriptNode test) {
        if (test instanceof JSReadFrameSlotNode) {
            return ((JSReadFrameSlotNode) test).hasTemporalDeadZone();
        }
        return !(test instanceof RepeatableNode);
    }

    private JavaScriptNode dropTerminalDirectBreakStatement(JavaScriptNode pass) {
        if (pass instanceof SequenceNode) {
            JavaScriptNode[] statements = ((SequenceNode) pass).getStatements();
            if (statements.length > 0 && isDirectBreakStatement(statements[statements.length - 1])) {
                return createBlock(Arrays.copyOfRange(statements, 0, statements.length - 1));
            }
        }
        return pass;
    }

    private static boolean isDirectBreakStatement(JavaScriptNode statement) {
        return statement instanceof BreakNode && ((BreakNode) statement).isDirectBreak();
    }

    private static boolean isNoFallthroughSwitch(com.oracle.js.parser.ir.SwitchNode switchNode) {
        List cases = switchNode.getCases();
        for (int i = 0; i < cases.size() - 1; i++) { // all but the last need to be checked
            CaseNode caseNode = cases.get(i);
            List statements = caseNode.getStatements();
            if (statements.isEmpty()) {
                // fall-through supported if case body is empty
                if (caseNode.getTest() == null) {
                    // default case fallthrough to other cases is not supported currently;
                    // i.e., default case must either appear last or end in abrupt completion
                    return false;
                }
                continue;
            }
            // Case clause must end in a break, continue, or terminal statement (return, throw).
            Statement lastStatement = statements.get(statements.size() - 1);
            if (!lastStatement.hasTerminalFlags()) {
                return false;
            }
        }
        return true;
    }

    /**
     * Identifies whether a SwitchNode matches the pattern where the expression is a typeof() and
     * the cases are all string constants.
     */
    private static boolean isSwitchTypeofStringConstant(com.oracle.js.parser.ir.SwitchNode switchNode, JavaScriptNode switchExpression) {
        if (!(switchExpression instanceof TypeOfNode)) {
            return false;
        }
        for (CaseNode switchCase : switchNode.getCases()) {
            com.oracle.js.parser.ir.Node test = switchCase.getTest();
            if (!(test == null || (test instanceof LiteralNode && ((LiteralNode) test).getValue() instanceof String))) {
                return false;
            }
        }
        return true;
    }

    private JavaScriptNode discardResult(JavaScriptNode test) {
        if (currentFunction().returnsLastStatementResult()) {
            return factory.createVoidBlock(test);
        }
        return test;
    }

    @Override
    public JavaScriptNode enterEmptyNode(com.oracle.js.parser.ir.EmptyNode emptyNode) {
        return factory.createEmpty();
    }

    @Override
    public JavaScriptNode enterWithNode(com.oracle.js.parser.ir.WithNode withNode) {
        if (context.isOptionDisableWith()) {
            throw Errors.createSyntaxError("with statement is disabled.");
        }
        JavaScriptNode withExpression = transform(withNode.getExpression());
        JavaScriptNode toObject = factory.createToObjectFromWith(context, withExpression, true);
        String withVarName = makeUniqueTempVarNameForStatement(withNode);
        environment.declareLocalVar(withVarName);
        JavaScriptNode writeWith = environment.findLocalVar(withVarName).createWriteNode(toObject);
        try (EnvironmentCloseable withEnv = enterWithEnvironment(withVarName)) {
            JavaScriptNode withBody = transform(withNode.getBody());
            return tagStatement(factory.createWith(writeWith, wrapClearAndGetCompletionValue(withBody)), withNode);
        }
    }

    private EnvironmentCloseable enterWithEnvironment(String withVarName) {
        return new EnvironmentCloseable(new WithEnvironment(environment, factory, context, withVarName));
    }

    @Override
    public JavaScriptNode enterRuntimeNode(RuntimeNode runtimeNode) {
        if (runtimeNode.getRequest() == RuntimeNode.Request.REFERENCE_ERROR) {
            String msg = com.oracle.js.parser.ECMAErrors.getMessage("parser.error.invalid.lvalue");
            return factory.createThrowError(JSErrorType.ReferenceError, error(msg, runtimeNode.getToken(), lc));
        } else if (runtimeNode.getRequest() == RuntimeNode.Request.GET_TEMPLATE_OBJECT) {
            JavaScriptNode rawStrings = transform(runtimeNode.getArgs().get(0));
            JavaScriptNode cookedStrings = transform(runtimeNode.getArgs().get(1));
            return tagExpression(factory.createTemplateObject(context, rawStrings, cookedStrings), runtimeNode);
        } else if (runtimeNode.getRequest() == RuntimeNode.Request.TO_STRING) {
            JavaScriptNode value = transform(runtimeNode.getArgs().get(0));
            return tagExpression(factory.createToString(value), runtimeNode);
        }
        throw new UnsupportedOperationException(runtimeNode.toString());
    }

    @Override
    public JavaScriptNode enterDebuggerNode(DebuggerNode debuggerNode) {
        return tagStatement(factory.createDebugger(), debuggerNode);
    }

    protected static String error(final String message, final long errorToken, final LexicalContext lc) {
        final int position = Token.descPosition(errorToken);
        com.oracle.js.parser.Source internalSource = lc.getCurrentFunction().getSource();
        final int lineNum = internalSource.getLine(position);
        final int columnNum = internalSource.getColumn(position);
        final String formatted = com.oracle.js.parser.ErrorManager.format(message, internalSource, lineNum, columnNum, errorToken);
        return formatted.replace("\r\n", "\n");
    }

    @Override
    public JavaScriptNode enterExpressionStatement(ExpressionStatement expressionStatement) {
        JavaScriptNode expression = transform(expressionStatement.getExpression());
        return tagStatement(expression, expressionStatement);
    }

    @Override
    public JavaScriptNode enterJoinPredecessorExpression(JoinPredecessorExpression expr) {
        return tagExpression(transform(expr.getExpression()), expr);
    }

    @Override
    public JavaScriptNode enterClassNode(ClassNode classNode) {
        Scope classScope = classNode.getScope();
        try (EnvironmentCloseable blockEnv = enterBlockEnvironment(classScope)) {
            String className = null;
            Symbol classNameSymbol = null;
            if (classNode.getIdent() != null) {
                className = classNode.getIdent().getName();
                classNameSymbol = classScope.getExistingSymbol(className);
            }

            JavaScriptNode classHeritage = transform(classNode.getClassHeritage());
            JavaScriptNode classFunction = transform(classNode.getConstructor().getValue());

            ArrayList members = transformPropertyDefinitionList(classNode.getClassElements(), true, classNameSymbol);

            JSWriteFrameSlotNode writeClassBinding = className == null ? null : (JSWriteFrameSlotNode) findScopeVar(className, true).createWriteNode(null);

            JavaScriptNode classDefinition = factory.createClassDefinition(context, (JSFunctionExpressionNode) classFunction, classHeritage,
                            members.toArray(ObjectLiteralMemberNode.EMPTY), writeClassBinding, className,
                            classNode.getInstanceFieldCount(), classNode.getStaticFieldCount(), classNode.hasPrivateInstanceMethods());

            if (classNode.hasPrivateMethods()) {
                // internal constructor binding used for private brand checks.
                classDefinition = environment.findLocalVar(ClassNode.PRIVATE_CONSTRUCTOR_BINDING_NAME).createWriteNode(classDefinition);
            }

            return tagExpression(blockEnv.wrapBlockScope(classDefinition), classNode);
        }
    }

    @Override
    public JavaScriptNode enterBlockExpression(BlockExpression blockExpression) {
        return tagExpression(transform(blockExpression.getBlock()), blockExpression);
    }

    @Override
    public JavaScriptNode enterParameterNode(ParameterNode paramNode) {
        final FunctionEnvironment currentFunction = currentFunction();
        final JavaScriptNode valueNode;
        if (paramNode.isRestParameter()) {
            valueNode = factory.createAccessRestArgument(context, currentFunction.getLeadingArgumentCount() + paramNode.getIndex(), currentFunction.getTrailingArgumentCount());
        } else {
            valueNode = factory.createAccessArgument(currentFunction.getLeadingArgumentCount() + paramNode.getIndex());
        }
        return tagExpression(tagHiddenExpression(valueNode), paramNode);
    }

    // ---

    @Override
    protected JavaScriptNode enterDefault(com.oracle.js.parser.ir.Node node) {
        throw shouldNotReachHere(node);
    }

    private static AssertionError shouldNotReachHere(com.oracle.js.parser.ir.Node node) {
        throw new AssertionError(String.format("should not reach here. %s(%s)", node.getClass().getSimpleName(), node));
    }

    // ---

    private SourceSection createSourceSection(FunctionNode functionNode) {
        int start = functionNode.getStartWithoutParens() - prologLength;
        int finish = functionNode.getFinishWithoutParens() - prologLength;
        int length = sourceLength;
        if (finish <= 0 || length <= start) {
            return source.createUnavailableSection();
        } else {
            start = Math.max(0, start);
            finish = Math.min(length, finish);
            return source.createSection(start, finish - start);
        }
    }

    private JavaScriptNode ensureHasSourceSection(JavaScriptNode resultNode, com.oracle.js.parser.ir.Node parseNode) {
        if (!resultNode.hasSourceSection()) {
            assignSourceSection(resultNode, parseNode);
            if (resultNode instanceof GlobalScopeVarWrapperNode) {
                ensureHasSourceSection(((GlobalScopeVarWrapperNode) resultNode).getDelegateNode(), parseNode);
            }
        }
        return resultNode;
    }

    private void assignSourceSection(JavaScriptNode resultNode, com.oracle.js.parser.ir.Node parseNode) {
        int start = parseNode.getStart() - prologLength;
        int finish = parseNode.getFinish() - prologLength;
        int length = sourceLength;
        if (finish <= 0 || length <= start) {
            resultNode.setSourceSection(source.createUnavailableSection());
        } else {
            start = Math.max(0, start);
            finish = Math.min(length, finish);
            resultNode.setSourceSection(source, start, finish - start);
        }
    }

    private String makeUniqueTempVarNameForStatement(Statement statement) {
        String name = ':' + statement.getClass().getSimpleName() + ':' + statement.getLineNumber() + ':' + statement.getStart();
        assert !environment.hasLocalVar(name);
        return name;
    }

    private final class EnvironmentCloseable implements AutoCloseable {
        private final Environment prevEnv = environment;
        private final Environment newEnv;
        private int wrappedInBlockScopeNode;

        EnvironmentCloseable(Environment newEnv) {
            this.newEnv = newEnv;
            environment = newEnv;
        }

        public JavaScriptNode wrapBlockScope(JavaScriptNode block) {
            if (prevEnv != newEnv) {
                wrappedInBlockScopeNode++;
                if (newEnv instanceof BlockEnvironment) {
                    BlockEnvironment blockEnv = (BlockEnvironment) newEnv;
                    return factory.createBlockScope(blockEnv.getBlockFrameDescriptor(), blockEnv.getParentSlot(), block);
                }
            }
            return block;
        }

        @Override
        public void close() {
            assert environment == newEnv;
            assert newEnv == prevEnv || !(newEnv instanceof BlockEnvironment) || wrappedInBlockScopeNode == 1;
            environment = prevEnv;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy