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

org.teavm.javascript.Renderer Maven / Gradle / Ivy

There is a newer version: 0.10.2
Show newest version
/*
 *  Copyright 2012 Alexey Andreev.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.teavm.javascript;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import org.teavm.codegen.NamingException;
import org.teavm.codegen.NamingOrderer;
import org.teavm.codegen.NamingStrategy;
import org.teavm.codegen.SourceWriter;
import org.teavm.common.ServiceRepository;
import org.teavm.debugging.information.DebugInformationEmitter;
import org.teavm.debugging.information.DeferredCallSite;
import org.teavm.debugging.information.DummyDebugInformationEmitter;
import org.teavm.diagnostics.Diagnostics;
import org.teavm.javascript.ast.*;
import org.teavm.javascript.spi.GeneratorContext;
import org.teavm.javascript.spi.InjectedBy;
import org.teavm.javascript.spi.Injector;
import org.teavm.javascript.spi.InjectorContext;
import org.teavm.model.*;

/**
 *
 * @author Alexey Andreev
 */
public class Renderer implements ExprVisitor, StatementVisitor, RenderingContext {
    private static final String variableNames = "abcdefghijkmnopqrstuvwxyz";
    private static final String variablePartNames = "abcdefghijkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private NamingStrategy naming;
    private SourceWriter writer;
    private ListableClassHolderSource classSource;
    private ClassLoader classLoader;
    private boolean minifying;
    private Map injectorMap = new HashMap<>();
    private Map stringPoolMap = new HashMap<>();
    private List stringPool = new ArrayList<>();
    private Properties properties = new Properties();
    private ServiceRepository services;
    private DebugInformationEmitter debugEmitter = new DummyDebugInformationEmitter();
    private Deque locationStack = new ArrayDeque<>();
    private DeferredCallSite lastCallSite;
    private DeferredCallSite prevCallSite;
    private Set asyncMethods;
    private Set asyncFamilyMethods;
    private Diagnostics diagnostics;
    private boolean async;
    private Priority priority;
    private Associativity associativity;
    private boolean wasGrouped;
    private Deque precedenceStack = new ArrayDeque<>();
    private Map blockIdMap = new HashMap<>();
    private List> debugNames = new ArrayList<>();
    private List cachedVariableNames = new ArrayList<>();
    private boolean end;
    private int currentPart;

    private static class OperatorPrecedence {
        Priority priority;
        Associativity associativity;
        boolean wasGrouped;
    }

    private static class InjectorHolder {
        public final Injector injector;

        public InjectorHolder(Injector injector) {
            this.injector = injector;
        }
    }

    private static class LocationStackEntry {
        NodeLocation location;

        public LocationStackEntry(NodeLocation location) {
            this.location = location;
        }
    }

    public void addInjector(MethodReference method, Injector injector) {
        injectorMap.put(method, new InjectorHolder(injector));
    }

    public Renderer(SourceWriter writer, ListableClassHolderSource classSource, ClassLoader classLoader,
            ServiceRepository services, Set asyncMethods, Set asyncFamilyMethods,
            Diagnostics diagnostics) {
        this.naming = writer.getNaming();
        this.writer = writer;
        this.classSource = classSource;
        this.classLoader = classLoader;
        this.services = services;
        this.asyncMethods = new HashSet<>(asyncMethods);
        this.asyncFamilyMethods = new HashSet<>(asyncFamilyMethods);
        this.diagnostics = diagnostics;
    }

    @Override
    public SourceWriter getWriter() {
        return writer;
    }

    @Override
    public NamingStrategy getNaming() {
        return naming;
    }

    @Override
    public boolean isMinifying() {
        return minifying;
    }

    public void setMinifying(boolean minifying) {
        this.minifying = minifying;
    }

    @Override
    public ListableClassHolderSource getClassSource() {
        return classSource;
    }

    @Override
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    @Override
    public Properties getProperties() {
        return new Properties(properties);
    }

    public DebugInformationEmitter getDebugEmitter() {
        return debugEmitter;
    }

    public void setDebugEmitter(DebugInformationEmitter debugEmitter) {
        this.debugEmitter = debugEmitter;
    }

    public void setProperties(Properties properties) {
        this.properties.clear();
        this.properties.putAll(properties);
    }

    public void renderStringPool() throws RenderingException {
        if (stringPool.isEmpty()) {
            return;
        }
        try {
            writer.append("$rt_stringPool([");
            for (int i = 0; i < stringPool.size(); ++i) {
                if (i > 0) {
                    writer.append(',').ws();
                }
                writer.append('"').append(escapeString(stringPool.get(i))).append('"');
            }
            writer.append("]);").newLine();
        } catch (IOException e) {
            throw new RenderingException("IO error", e);
        }
    }

    public void renderRuntime() throws RenderingException {
        try {
            renderRuntimeCls();
            renderRuntimeString();
            renderRuntimeUnwrapString();
            renderRuntimeObjcls();
            renderRuntimeNullCheck();
            renderRuntimeIntern();
            renderRuntimeThreads();
        } catch (NamingException e) {
            throw new RenderingException("Error rendering runtime methods. See a cause for details", e);
        } catch (IOException e) {
            throw new RenderingException("IO error", e);
        }
    }

    private void renderRuntimeCls() throws IOException {
        writer.append("function $rt_cls(cls)").ws().append("{").softNewLine().indent();
        writer.append("return ").appendMethodBody("java.lang.Class", "getClass",
                ValueType.object("org.teavm.platform.PlatformClass"),
                ValueType.object("java.lang.Class")).append("(cls);")
                .softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeString() throws IOException {
        MethodReference stringCons = new MethodReference(String.class, "", char[].class, void.class);
        writer.append("function $rt_str(str) {").indent().softNewLine();
        writer.append("if (str===null){").indent().softNewLine();
        writer.append("return null;").softNewLine();
        writer.outdent().append("}").softNewLine();
        writer.append("var characters = $rt_createCharArray(str.length);").softNewLine();
        writer.append("var charsBuffer = characters.data;").softNewLine();
        writer.append("for (var i = 0; i < str.length; i = (i + 1) | 0) {").indent().softNewLine();
        writer.append("charsBuffer[i] = str.charCodeAt(i) & 0xFFFF;").softNewLine();
        writer.outdent().append("}").softNewLine();
        writer.append("return ").append(naming.getNameForInit(stringCons)).append("(characters);").softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeUnwrapString() throws IOException {
        MethodReference stringLen = new MethodReference(String.class, "length", int.class);
        MethodReference getChars = new MethodReference(String.class, "getChars", int.class, int.class,
                char[].class, int.class, void.class);
        writer.append("function $rt_ustr(str) {").indent().softNewLine();
        writer.append("var result = \"\";").softNewLine();
        writer.append("var sz = ").appendMethodBody(stringLen).append("(str);").softNewLine();
        writer.append("var array = $rt_createCharArray(sz);").softNewLine();
        writer.appendMethodBody(getChars).append("(str, 0, sz, array, 0);").softNewLine();
        writer.append("for (var i = 0; i < sz; i = (i + 1) | 0) {").indent().softNewLine();
        writer.append("result += String.fromCharCode(array.data[i]);").softNewLine();
        writer.outdent().append("}").softNewLine();
        writer.append("return result;").softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeNullCheck() throws IOException {
        writer.append("function $rt_nullCheck(val) {").indent().softNewLine();
        writer.append("if (val === null) {").indent().softNewLine();
        writer.append("$rt_throw(").append(naming.getNameForInit(new MethodReference(NullPointerException.class,
                "", void.class))).append("());").softNewLine();
        writer.outdent().append("}").softNewLine();
        writer.append("return val;").softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeIntern() throws IOException {
        writer.append("function $rt_intern(str) {").indent().softNewLine();
        writer.append("return ").appendMethodBody(new MethodReference(String.class, "intern", String.class))
            .append("(str);").softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeObjcls() throws IOException {
        writer.append("function $rt_objcls() { return ").appendClass("java.lang.Object").append("; }").newLine();
    }

    private void renderRuntimeThreads() throws IOException {
        writer.append("function $rt_getThread()").ws().append("{").indent().softNewLine();
        writer.append("return ").appendMethodBody(Thread.class, "currentThread", Thread.class).append("();")
                .softNewLine();
        writer.outdent().append("}").newLine();

        writer.append("function $rt_setThread(t)").ws().append("{").indent().softNewLine();
        writer.append("return ").appendMethodBody(Thread.class, "setCurrentThread", Thread.class, void.class)
                .append("(t);").softNewLine();
        writer.outdent().append("}").newLine();
    }

    private void renderRuntimeAliases() throws IOException {
        String[] names = { "$rt_throw", "$rt_compare", "$rt_nullCheck", "$rt_cls", "$rt_createArray",
                "$rt_isInstance", "$rt_nativeThread", "$rt_suspending", "$rt_resuming", "$rt_invalidPointer" };
        boolean first = true;
        for (String name : names) {
            if (!first) {
                writer.softNewLine();
            }
            first = false;
            writer.append("var ").appendFunction(name).ws().append('=').ws().append(name).append(";").softNewLine();
        }
        writer.newLine();
    }

    public void render(List classes) throws RenderingException {
        if (minifying) {
            NamingOrderer orderer = new NamingOrderer();
            NameFrequencyEstimator estimator = new NameFrequencyEstimator(orderer, classSource, asyncMethods,
                    asyncFamilyMethods);
            for (ClassNode cls : classes) {
                estimator.estimate(cls);
            }
            orderer.apply(naming);
        }

        if (minifying) {
            try {
                renderRuntimeAliases();
            } catch (IOException e) {
                throw new RenderingException(e);
            }
        }
        for (ClassNode cls : classes) {
            renderDeclaration(cls);
        }
        for (ClassNode cls : classes) {
            renderMethodBodies(cls);
        }
        renderClassMetadata(classes);
    }

    private void renderDeclaration(ClassNode cls) throws RenderingException {
        debugEmitter.addClass(cls.getName(), cls.getParentName());
        try {
            writer.append("function ").appendClass(cls.getName()).append("()").ws().append("{")
                    .indent().softNewLine();
            boolean thisAliased = false;
            List nonStaticFields = new ArrayList<>();
            List staticFields = new ArrayList<>();
            for (FieldNode field : cls.getFields()) {
                if (field.getModifiers().contains(NodeModifier.STATIC)) {
                    staticFields.add(field);
                } else {
                    nonStaticFields.add(field);
                }
            }
            if (nonStaticFields.size() > 1) {
                thisAliased = true;
                writer.append("var a").ws().append("=").ws().append("this;").ws();
            }
            if (cls.getParentName() != null) {
                writer.appendClass(cls.getParentName()).append(".call(").append(thisAliased ? "a" : "this")
                        .append(");").softNewLine();
            }
            for (FieldNode field : nonStaticFields) {
                Object value = field.getInitialValue();
                if (value == null) {
                    value = getDefaultValue(field.getType());
                }
                FieldReference fieldRef = new FieldReference(cls.getName(), field.getName());
                writer.append(thisAliased ? "a" : "this").append(".").appendField(fieldRef).ws()
                        .append("=").ws().append(constantToString(value)).append(";").softNewLine();
                debugEmitter.addField(field.getName(), naming.getNameFor(fieldRef));
            }
            writer.outdent().append("}").newLine();

            for (FieldNode field : staticFields) {
                Object value = field.getInitialValue();
                if (value == null) {
                    value = getDefaultValue(field.getType());
                }
                FieldReference fieldRef = new FieldReference(cls.getName(), field.getName());
                writer.appendClass(cls.getName()).append('.').appendField(fieldRef).ws().append("=").ws()
                        .append(constantToString(value)).append(";").softNewLine();
            }
        } catch (NamingException e) {
            throw new RenderingException("Error rendering class " + cls.getName() + ". See cause for details", e);
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    private void renderMethodBodies(ClassNode cls) throws RenderingException {
        debugEmitter.emitClass(cls.getName());
        try {
            List nonInitMethods = new ArrayList<>();
            List virtualMethods = new ArrayList<>();
            MethodHolder clinit = classSource.get(cls.getName()).getMethod(
                    new MethodDescriptor("", ValueType.VOID));
            List clinitMethods = new ArrayList<>();
            for (MethodNode method : cls.getMethods()) {
                if (clinit == null || (!method.getModifiers().contains(NodeModifier.STATIC) &&
                        !method.getReference().getName().equals(""))) {
                    nonInitMethods.add(method);
                } else {
                    clinitMethods.add(method);
                }
            }
            boolean needsClinit = clinit != null;

            if (needsClinit) {
                writer.append("function ").appendClass(cls.getName()).append("_$clinit()").ws()
                        .append("{").softNewLine().indent();
                writer.appendClass(cls.getName()).append("_$clinit").ws().append("=").ws()
                        .append("function(){};").newLine();
                for (MethodNode method : clinitMethods) {
                    renderBody(method, true);
                }
                if (clinit != null) {
                    writer.appendMethodBody(new MethodReference(cls.getName(), clinit.getDescriptor()))
                            .append("();").softNewLine();
                }
                writer.outdent().append("}").newLine();
            }
            if (!cls.getModifiers().contains(NodeModifier.INTERFACE)) {
                for (MethodNode method : cls.getMethods()) {
                    cls.getMethods();
                    if (!method.getModifiers().contains(NodeModifier.STATIC)) {
                        if (method.getReference().getName().equals("")) {
                            renderInitializer(method);
                        } else {
                            virtualMethods.add(method);
                        }
                    }
                }
            }

            for (MethodNode method : nonInitMethods) {
                renderBody(method, false);
            }
        } catch (NamingException e) {
            throw new RenderingException("Error rendering class " + cls.getName() + ". See a cause for details", e);
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
        debugEmitter.emitClass(null);
    }

    private void renderClassMetadata(List classes) {
        try {
            writer.append("$rt_metadata([");
            boolean first = true;
            for (ClassNode cls : classes) {
                if (!first) {
                    writer.append(',').softNewLine();
                }
                first = false;
                writer.appendClass(cls.getName()).append(",").ws();
                writer.append("\"").append(escapeString(cls.getName())).append("\",").ws();
                if (cls.getParentName() != null) {
                    writer.appendClass(cls.getParentName());
                } else {
                    writer.append("0");
                }
                writer.append(',').ws();
                writer.append("[");
                for (int i = 0; i < cls.getInterfaces().size(); ++i) {
                    String iface = cls.getInterfaces().get(i);
                    if (i > 0) {
                        writer.append(",").ws();
                    }
                    writer.appendClass(iface);
                }
                writer.append("],").ws();
                int flags = 0;
                if (cls.getModifiers().contains(NodeModifier.ENUM)) {
                    flags |= 1;
                }
                writer.append(flags).append(',').ws();
                MethodHolder clinit = classSource.get(cls.getName()).getMethod(
                        new MethodDescriptor("", ValueType.VOID));
                if (clinit != null) {
                    writer.appendClass(cls.getName()).append("_$clinit");
                } else {
                    writer.append('0');
                }
                writer.append(',').ws();

                List stubNames = new ArrayList<>();
                List virtualMethods = new ArrayList<>();
                for (MethodNode method : cls.getMethods()) {
                    if (clinit != null && (method.getModifiers().contains(NodeModifier.STATIC) ||
                            method.getReference().getName().equals(""))) {
                        stubNames.add(naming.getFullNameFor(method.getReference()));
                    }
                    if (!method.getModifiers().contains(NodeModifier.STATIC)) {
                        virtualMethods.add(method);
                    }
                }
                if (stubNames.size() == 1) {
                    writer.append("'").append(stubNames.get(0)).append("'");
                } else {
                    writer.append('[');
                    for (int j = 0; j < stubNames.size(); ++j) {
                        if (j > 0) {
                            writer.append(",").ws();
                        }
                        writer.append("'").append(stubNames.get(j)).append("'");
                    }
                    writer.append(']');
                }
                writer.append(',').ws();

                renderVirtualDeclarations(virtualMethods);
            }
            writer.append("]);").newLine();
        } catch (NamingException e) {
            throw new RenderingException("Error rendering class metadata. See a cause for details", e);
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    private static Object getDefaultValue(ValueType type) {
        if (type instanceof ValueType.Primitive) {
            ValueType.Primitive primitive = (ValueType.Primitive)type;
            switch (primitive.getKind()) {
                case BOOLEAN:
                    return false;
                case BYTE:
                    return (byte)0;
                case SHORT:
                    return (short)0;
                case INTEGER:
                    return 0;
                case CHARACTER:
                    return '\0';
                case LONG:
                    return 0L;
                case FLOAT:
                    return 0F;
                case DOUBLE:
                    return 0.0;
            }
        }
        return null;
    }

    private void renderInitializer(MethodNode method) throws IOException {
        MethodReference ref = method.getReference();
        debugEmitter.emitMethod(ref.getDescriptor());
        writer.append("function ").append(naming.getNameForInit(ref)).append("(");
        for (int i = 1; i <= ref.parameterCount(); ++i) {
            if (i > 1) {
                writer.append(",").ws();
            }
            writer.append(variableName(i));
        }
        writer.append(")").ws().append("{").softNewLine().indent();
        writer.append("var $r").ws().append("=").ws().append("new ").appendClass(
                ref.getClassName()).append("();").softNewLine();
        writer.append(naming.getFullNameFor(ref)).append("($r");
        for (int i = 1; i <= ref.parameterCount(); ++i) {
            writer.append(",").ws();
            writer.append(variableName(i));
        }
        writer.append(");").softNewLine();
        writer.append("return $r;").softNewLine();
        writer.outdent().append("}").newLine();
        debugEmitter.emitMethod(null);
    }

    private void renderVirtualDeclarations(List methods) throws NamingException, IOException {
        writer.append("[");
        boolean first = true;
        for (MethodNode method : methods) {
            debugEmitter.emitMethod(method.getReference().getDescriptor());
            MethodReference ref = method.getReference();
            if (!first) {
                writer.append(",").ws();
            }
            first = false;
            emitVirtualDeclaration(ref);
            debugEmitter.emitMethod(null);
        }
        writer.append("]");
    }

    private void emitVirtualDeclaration(MethodReference ref) throws IOException {
        String methodName = naming.getNameFor(ref.getDescriptor());
        writer.append("\"").append(methodName).append("\"");
        writer.append(",").ws().append("function(");
        List args = new ArrayList<>();
        for (int i = 1; i <= ref.parameterCount(); ++i) {
            args.add(variableName(i));
        }
        for (int i = 0; i < args.size(); ++i) {
            if (i > 0) {
                writer.append(",").ws();
            }
            writer.append(args.get(i));
        }
        writer.append(")").ws().append("{").ws();
        if (ref.getDescriptor().getResultType() != ValueType.VOID) {
            writer.append("return ");
        }
        writer.appendMethodBody(ref).append("(");
        writer.append("this");
        for (int i = 0; i < args.size(); ++i) {
            writer.append(",").ws().append(args.get(i));
        }
        writer.append(");").ws().append("}");
    }

    public void renderBody(MethodNode method, boolean inner) throws IOException {
        debugNames.clear();
        cachedVariableNames.clear();
        debugNames.addAll(method.getParameterDebugNames());
        blockIdMap.clear();
        MethodReference ref = method.getReference();
        debugEmitter.emitMethod(ref.getDescriptor());
        String name = naming.getFullNameFor(ref);
        if (inner) {
            writer.append(name).ws().append("=").ws().append("function(");
        } else {
            writer.append("function ").append(name).append("(");
        }
        int startParam = 0;
        if (method.getModifiers().contains(NodeModifier.STATIC)) {
            startParam = 1;
        }
        for (int i = startParam; i <= ref.parameterCount(); ++i) {
            if (i > startParam) {
                writer.append(",").ws();
            }
            writer.append(variableName(i));
        }
        writer.append(")").ws().append("{").softNewLine().indent();
        method.acceptVisitor(new MethodBodyRenderer());
        writer.outdent().append("}");
        if (inner) {
            writer.append(';');
        }
        writer.newLine();
        debugEmitter.emitMethod(null);
    }

    private class MethodBodyRenderer implements MethodNodeVisitor, GeneratorContext {
        private boolean async;

        @Override
        public void visit(NativeMethodNode methodNode) {
            try {
                this.async = methodNode.isAsync();
                Renderer.this.async = methodNode.isAsync();
                methodNode.getGenerator().generate(this, writer, methodNode.getReference());
            } catch (IOException e) {
                throw new RenderingException("IO error occured", e);
            }
        }

        @Override
        public void visit(RegularMethodNode method) {
            try {
                Renderer.this.async = false;
                this.async = false;
                MethodReference ref = method.getReference();
                for (int i = 0; i < method.getParameterDebugNames().size(); ++i) {
                    debugEmitter.emitVariable(method.getParameterDebugNames().get(i).toArray(new String[0]),
                            variableName(i));
                }

                int variableCount = 0;
                for (int var : method.getVariables()) {
                    variableCount = Math.max(variableCount, var + 1);
                }
                TryCatchFinder tryCatchFinder = new TryCatchFinder();
                method.getBody().acceptVisitor(tryCatchFinder);
                boolean hasTryCatch = tryCatchFinder.tryCatchFound;
                List variableNames = new ArrayList<>();
                for (int i = ref.parameterCount() + 1; i < variableCount; ++i) {
                    variableNames.add(variableName(i));
                }
                if (hasTryCatch) {
                    variableNames.add("$je");
                }
                if (!variableNames.isEmpty()) {
                    writer.append("var ");
                    for (int i = 0; i < variableNames.size(); ++i) {
                        if (i > 0) {
                            writer.append(",").ws();
                        }
                        writer.append(variableNames.get(i));
                    }
                    writer.append(";").softNewLine();
                }
                end = true;
                currentPart = 0;
                method.getBody().acceptVisitor(Renderer.this);
            } catch (IOException e) {
                throw new RenderingException("IO error occured", e);
            }
        }

        @Override
        public void visit(AsyncMethodNode methodNode) {
            try {
                Renderer.this.async = true;
                this.async = true;
                MethodReference ref = methodNode.getReference();
                for (int i = 0; i < methodNode.getParameterDebugNames().size(); ++i) {
                    debugEmitter.emitVariable(methodNode.getParameterDebugNames().get(i).toArray(new String[0]),
                            variableName(i));
                }
                int variableCount = 0;
                for (int var : methodNode.getVariables()) {
                    variableCount = Math.max(variableCount, var + 1);
                }
                List variableNames = new ArrayList<>();
                for (int i = ref.parameterCount() + 1; i < variableCount; ++i) {
                    variableNames.add(variableName(i));
                }
                TryCatchFinder tryCatchFinder = new TryCatchFinder();
                for (AsyncMethodPart part :  methodNode.getBody()) {
                    if (!tryCatchFinder.tryCatchFound) {
                        part.getStatement().acceptVisitor(tryCatchFinder);
                    }
                }
                boolean hasTryCatch = tryCatchFinder.tryCatchFound;
                if (hasTryCatch) {
                    variableNames.add("$je");
                }
                variableNames.add(pointerName());
                variableNames.add(tempVarName());
                if (!variableNames.isEmpty()) {
                    writer.append("var ");
                    for (int i = 0; i < variableNames.size(); ++i) {
                        if (i > 0) {
                            writer.append(",").ws();
                        }
                        writer.append(variableNames.get(i));
                    }
                    writer.append(";").softNewLine();
                }

                int firstToSave = 0;
                if (methodNode.getModifiers().contains(NodeModifier.STATIC)) {
                    firstToSave = 1;
                }

                String popName = minifying ? "l" : "pop";
                String pushName = minifying ? "s" : "push";
                writer.append(pointerName()).ws().append('=').ws().append("0;").softNewLine();
                writer.append("if").ws().append("(").appendFunction("$rt_resuming").append("())").ws()
                        .append("{").indent().softNewLine();
                writer.append("var ").append(threadName()).ws().append('=').ws()
                        .appendFunction("$rt_nativeThread").append("();").softNewLine();
                writer.append(pointerName()).ws().append('=').ws().append(threadName()).append(".")
                        .append(popName).append("();");
                for (int i = variableCount - 1; i >= firstToSave; --i) {
                    writer.append(variableName(i)).ws().append('=').ws().append(threadName()).append(".")
                            .append(popName).append("();");
                }
                writer.softNewLine();
                writer.outdent().append("}").softNewLine();

                if (methodNode.getModifiers().contains(NodeModifier.SYNCHRONIZED)) {
                    writer.append("try").ws().append('{').indent().softNewLine();
                }
                writer.append(mainLoopName()).append(":").ws().append("while").ws().append("(true)")
                        .ws().append("{").ws();
                writer.append("switch").ws().append("(").append(pointerName()).append(")").ws()
                        .append('{').softNewLine();
                for (int i = 0; i < methodNode.getBody().size(); ++i) {
                    writer.append("case ").append(i).append(":").indent().softNewLine();
                    if (i == 0 && methodNode.getModifiers().contains(NodeModifier.SYNCHRONIZED)) {
                        writer.appendMethodBody(new MethodReference(Object.class, "monitorEnter",
                                Object.class, void.class));
                        writer.append("(");
                        appendMonitor(methodNode);
                        writer.append(");").softNewLine();
                        emitSuspendChecker();
                    }
                    AsyncMethodPart part = methodNode.getBody().get(i);
                    end = true;
                    currentPart = i;
                    part.getStatement().acceptVisitor(Renderer.this);
                    writer.outdent();
                }
                writer.append("default:").ws().appendFunction("$rt_invalidPointer").append("();").softNewLine();
                writer.append("}}").softNewLine();

                if (methodNode.getModifiers().contains(NodeModifier.SYNCHRONIZED)) {
                    writer.outdent().append("}").ws().append("finally").ws().append('{').indent().softNewLine();
                    writer.append("if").ws().append("(!").appendFunction("$rt_suspending").append("())")
                            .ws().append("{").indent().softNewLine();
                    writer.appendMethodBody(new MethodReference(Object.class, "monitorExit",
                            Object.class, void.class));
                    writer.append("(");
                    appendMonitor(methodNode);
                    writer.append(");").softNewLine();
                    writer.outdent().append('}').softNewLine();
                    writer.outdent().append('}').softNewLine();
                }

                writer.appendFunction("$rt_nativeThread").append("().").append(pushName).append("(");
                for (int i = firstToSave; i < variableCount; ++i) {
                    writer.append(variableName(i)).append(',').ws();
                }
                writer.append(pointerName()).append(");");
                writer.softNewLine();
            } catch (IOException e) {
                throw new RenderingException("IO error occured", e);
            }
        }

        @Override
        public String getParameterName(int index) {
            return variableName(index);
        }

        @Override
        public ListableClassReaderSource getClassSource() {
            return classSource;
        }

        @Override
        public ClassLoader getClassLoader() {
            return classLoader;
        }

        @Override
        public Properties getProperties() {
            return new Properties(properties);
        }

        @Override
        public  T getService(Class type) {
            return services.getService(type);
        }

        @Override
        public boolean isAsync() {
            return async;
        }

        @Override
        public boolean isAsync(MethodReference method) {
            return asyncMethods.contains(method);
        }

        @Override
        public boolean isAsyncFamily(MethodReference method) {
            return asyncFamilyMethods.contains(method);
        }

        @Override
        public Diagnostics getDiagnostics() {
            return diagnostics;
        }
    }

    private void appendMonitor(MethodNode methodNode) throws IOException {
        if (methodNode.getModifiers().contains(NodeModifier.STATIC)) {
            writer.appendFunction("$rt_cls").append("(")
                    .appendClass(methodNode.getReference().getClassName()).append(")");
        } else {
            writer.append(variableName(0));
        }
    }

    private void pushLocation(NodeLocation location) {
        LocationStackEntry prevEntry = locationStack.peek();
        if (location != null) {
            if (prevEntry == null || !location.equals(prevEntry.location)) {
                debugEmitter.emitLocation(location.getFileName(), location.getLine());
            }
        } else {
            if (prevEntry != null) {
                debugEmitter.emitLocation(null, -1);
            }
        }
        locationStack.push(new LocationStackEntry(location));
    }

    private void popLocation() {
        LocationStackEntry prevEntry = locationStack.pop();
        LocationStackEntry entry = locationStack.peek();
        if (entry != null) {
            if (!entry.location.equals(prevEntry.location)) {
                debugEmitter.emitLocation(entry.location.getFileName(), entry.location.getLine());
            }
        } else {
            debugEmitter.emitLocation(null, -1);
        }
    }

    @Override
    public void visit(AssignmentStatement statement) throws RenderingException {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            prevCallSite = debugEmitter.emitCallSite();
            if (statement.getLeftValue() != null) {
                if (statement.isAsync()) {
                    writer.append(tempVarName());
                } else {
                    priority = Priority.COMMA;
                    associativity = Associativity.NONE;
                    statement.getLeftValue().acceptVisitor(this);
                }
                writer.ws().append("=").ws();
            }
            priority = Priority.COMMA;
            associativity = Associativity.NONE;
            statement.getRightValue().acceptVisitor(this);
            debugEmitter.emitCallSite();
            writer.append(";").softNewLine();
            if (statement.isAsync()) {
                emitSuspendChecker();
                if (statement.getLeftValue() != null) {
                    priority = Priority.COMMA;
                    associativity = Associativity.NONE;
                    statement.getLeftValue().acceptVisitor(this);
                    writer.ws().append("=").ws().append(tempVarName()).append(";").softNewLine();
                }
            }
            if (statement.getLocation() != null) {
                popLocation();
            }
            if (statement.getLeftValue() instanceof VariableExpr) {
                VariableExpr receiver = (VariableExpr)statement.getLeftValue();
                debugEmitter.emitVariable(statement.getDebugNames().toArray(new String[0]),
                        variableName(receiver.getIndex()));
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(SequentialStatement statement) {
        visitStatements(statement.getSequence());
    }

    @Override
    public void visit(ConditionalStatement statement) {
        try {
            while (true) {
                debugEmitter.emitStatementStart();
                if (statement.getCondition().getLocation() != null) {
                    pushLocation(statement.getCondition().getLocation());
                }
                prevCallSite = debugEmitter.emitCallSite();
                writer.append("if").ws().append("(");
                priority = Priority.COMMA;
                associativity = Associativity.NONE;
                statement.getCondition().acceptVisitor(this);
                if (statement.getCondition().getLocation() != null) {
                    popLocation();
                }
                debugEmitter.emitCallSite();
                writer.append(")").ws().append("{").softNewLine().indent();
                visitStatements(statement.getConsequent());
                if (!statement.getAlternative().isEmpty()) {
                    writer.outdent().append("}").ws();
                    if (statement.getAlternative().size() == 1 &&
                            statement.getAlternative().get(0) instanceof ConditionalStatement) {
                        statement = (ConditionalStatement)statement.getAlternative().get(0);
                        writer.append("else ");
                        continue;
                    }
                    writer.append("else").ws().append("{").indent().softNewLine();
                    visitStatements(statement.getAlternative());
                }
                break;
            }
            writer.outdent().append("}").softNewLine();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(SwitchStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getValue().getLocation() != null) {
                pushLocation(statement.getValue().getLocation());
            }
            if (statement.getId() != null) {
                writer.append(mapBlockId(statement.getId())).append(":").ws();
            }
            prevCallSite = debugEmitter.emitCallSite();
            writer.append("switch").ws().append("(");
            priority = Priority.COMMA;
            associativity = Associativity.NONE;
            statement.getValue().acceptVisitor(this);
            if (statement.getValue().getLocation() != null) {
                popLocation();
            }
            debugEmitter.emitCallSite();
            writer.append(")").ws().append("{").softNewLine().indent();
            for (SwitchClause clause : statement.getClauses()) {
                for (int condition : clause.getConditions()) {
                    writer.append("case ").append(condition).append(":").softNewLine();
                }
                writer.indent();
                boolean oldEnd = end;
                for (Statement part : clause.getBody()) {
                    end = false;
                    part.acceptVisitor(this);
                }
                end = oldEnd;
                writer.outdent();
            }
            if (statement.getDefaultClause() != null) {
                writer.append("default:").softNewLine().indent();
                boolean oldEnd = end;
                for (Statement part : statement.getDefaultClause()) {
                    end = false;
                    part.acceptVisitor(this);
                }
                end = oldEnd;
                writer.outdent();
            }
            writer.outdent().append("}").softNewLine();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(WhileStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getCondition() != null && statement.getCondition().getLocation() != null) {
                pushLocation(statement.getCondition().getLocation());
            }
            if (statement.getId() != null) {
                writer.append(mapBlockId(statement.getId())).append(":").ws();
            }
            writer.append("while").ws().append("(");
            if (statement.getCondition() != null) {
                prevCallSite = debugEmitter.emitCallSite();
                priority = Priority.COMMA;
                associativity = Associativity.NONE;
                statement.getCondition().acceptVisitor(this);
                debugEmitter.emitCallSite();
                if (statement.getCondition().getLocation() != null) {
                    popLocation();
                }
            } else {
                writer.append("true");
            }
            writer.append(")").ws().append("{").softNewLine().indent();
            boolean oldEnd = end;
            for (Statement part : statement.getBody()) {
                end = false;
                part.acceptVisitor(this);
            }
            end = oldEnd;
            writer.outdent().append("}").softNewLine();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    private String mapBlockId(String id) {
        String name = blockIdMap.get(id);
        if (name == null) {
            int index = blockIdMap.size();
            name = "$b" + indexToId(index);
            blockIdMap.put(id, name);
        }
        return name;
    }

    private String indexToId(int index) {
        StringBuilder sb = new StringBuilder();
        do {
            sb.append(variablePartNames.charAt(index % variablePartNames.length()));
            index /= variablePartNames.length();
        } while (index > 0);
        return sb.toString();
    }

    @Override
    public void visit(BlockStatement statement) {
        try {
            writer.append(mapBlockId(statement.getId())).append(":").ws().append("{").softNewLine().indent();
            visitStatements(statement.getBody());
            writer.outdent().append("}").softNewLine();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(BreakStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            writer.append("break");
            if (statement.getTarget() != null) {
                writer.append(' ').append(mapBlockId(statement.getTarget().getId()));
            }
            writer.append(";").softNewLine();
            if (statement.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(ContinueStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            writer.append("continue");
            if (statement.getTarget() != null) {
                writer.append(' ').append(mapBlockId(statement.getTarget().getId()));
            }
            writer.append(";").softNewLine();
            if (statement.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(ReturnStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            writer.append("return");
            if (statement.getResult() != null) {
                writer.append(' ');
                prevCallSite = debugEmitter.emitCallSite();
                priority = Priority.COMMA;
                associativity = Associativity.NONE;
                statement.getResult().acceptVisitor(this);
                debugEmitter.emitCallSite();
            }
            writer.append(";").softNewLine();
            if (statement.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(ThrowStatement statement) {
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            writer.appendFunction("$rt_throw").append("(");
            prevCallSite = debugEmitter.emitCallSite();
            priority = Priority.COMMA;
            associativity = Associativity.NONE;
            statement.getException().acceptVisitor(this);
            writer.append(");").softNewLine();
            debugEmitter.emitCallSite();
            if (statement.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(InitClassStatement statement) {
        ClassReader cls = classSource.get(statement.getClassName());
        if (cls == null) {
            return;
        }
        MethodReader method = cls.getMethod(new MethodDescriptor("", void.class));
        if (method == null) {
            return;
        }
        try {
            debugEmitter.emitStatementStart();
            if (statement.getLocation() != null) {
                pushLocation(statement.getLocation());
            }
            writer.appendClass(statement.getClassName()).append("_$clinit();").softNewLine();
            if (statement.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    public String variableName(int index) {
        while (index >= cachedVariableNames.size()) {
            cachedVariableNames.add(null);
        }
        String name = cachedVariableNames.get(index);
        if (name == null) {
            name = generateVariableName(index);
            cachedVariableNames.set(index, name);
        }
        return name;
    }

    private String generateVariableName(int index) {
        if (index == 0) {
            return minifying ? "$t" : "$this";
        }

        Set names = index < debugNames.size() ? debugNames.get(index) : null;
        StringBuilder sb = new StringBuilder();
        --index;
        if (index < variableNames.length()) {
            sb.append(Character.toString(variableNames.charAt(index)));
        } else {
            sb.append(Character.toString(variableNames.charAt(index % variableNames.length())) +
                    index / variableNames.length());
        }
        if (!minifying && names != null && !names.isEmpty()) {
            List nameList = new ArrayList<>(names);
            Collections.sort(nameList);
            for (String name : nameList) {
                sb.append('_').append(name);
            }
        }
        return sb.toString();
    }

    private String pointerName() {
        return minifying ? "$p" : "$ptr";
    }

    private String mainLoopName() {
        return minifying ? "$m" : "$main";
    }

    private String tempVarName() {
        return minifying ? "$z" : "$tmp";
    }

    private String threadName() {
        return minifying ? "$T" : "$thread";
    }

    private void visitBinary(BinaryExpr expr, String op, Priority priority, Associativity associativity) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(priority, associativity == Associativity.LEFT ? associativity : Associativity.NONE, true);
            expr.getFirstOperand().acceptVisitor(this);
            writer.ws().append(op).ws();
            this.associativity = associativity == Associativity.RIGHT ? associativity : Associativity.NONE;
            expr.getSecondOperand().acceptVisitor(this);
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    private void enterPriority(Priority priority, Associativity associativity, boolean autoGroup) throws IOException {
        OperatorPrecedence precedence = new OperatorPrecedence();
        precedence.wasGrouped = this.wasGrouped;
        precedence.priority = this.priority;
        precedence.associativity = this.associativity;
        precedenceStack.push(precedence);
        wasGrouped = false;
        if (autoGroup && (priority.ordinal() < this.priority.ordinal() ||
                priority.ordinal() == this.priority.ordinal() &&
                (associativity != this.associativity || associativity == Associativity.NONE))) {
            wasGrouped = true;
            writer.append('(');
        }
        this.priority = priority;
        this.associativity = associativity;
    }

    private void exitPriority() throws IOException {
        if (wasGrouped) {
            writer.append(')');
        }
        OperatorPrecedence precedence = precedenceStack.pop();
        this.priority = precedence.priority;
        this.associativity = precedence.associativity;
        this.wasGrouped = precedence.wasGrouped;
    }

    private void visitBinaryFunction(BinaryExpr expr, String function) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(Priority.COMMA, Associativity.NONE, false);
            writer.append(function);
            writer.append('(');
            expr.getFirstOperand().acceptVisitor(this);
            writer.append(",").ws();
            expr.getSecondOperand().acceptVisitor(this);
            writer.append(')');
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(BinaryExpr expr) {
        switch (expr.getOperation()) {
            case ADD:
                visitBinary(expr, "+", Priority.ADDITION, Associativity.LEFT);
                break;
            case ADD_LONG:
                visitBinaryFunction(expr, "Long_add");
                break;
            case SUBTRACT:
                visitBinary(expr, "-", Priority.ADDITION, Associativity.LEFT);
                break;
            case SUBTRACT_LONG:
                visitBinaryFunction(expr, "Long_sub");
                break;
            case MULTIPLY:
                visitBinary(expr, "*", Priority.MULTIPLICATION, Associativity.LEFT);
                break;
            case MULTIPLY_LONG:
                visitBinaryFunction(expr, "Long_mul");
                break;
            case DIVIDE:
                visitBinary(expr, "/", Priority.MULTIPLICATION, Associativity.LEFT);
                break;
            case DIVIDE_LONG:
                visitBinaryFunction(expr, "Long_div");
                break;
            case MODULO:
                visitBinary(expr, "%", Priority.MULTIPLICATION, Associativity.LEFT);
                break;
            case MODULO_LONG:
                visitBinaryFunction(expr, "Long_rem");
                break;
            case EQUALS:
                visitBinary(expr, "==", Priority.EQUALITY, Associativity.LEFT);
                break;
            case NOT_EQUALS:
                visitBinary(expr, "!=", Priority.EQUALITY, Associativity.LEFT);
                break;
            case GREATER:
                visitBinary(expr, ">", Priority.COMPARISON, Associativity.LEFT);
                break;
            case GREATER_OR_EQUALS:
                visitBinary(expr, ">=", Priority.COMPARISON, Associativity.LEFT);
                break;
            case LESS:
                visitBinary(expr, "<", Priority.COMPARISON, Associativity.LEFT);
                break;
            case LESS_OR_EQUALS:
                visitBinary(expr, "<=", Priority.COMPARISON, Associativity.LEFT);
                break;
            case STRICT_EQUALS:
                visitBinary(expr, "===", Priority.COMPARISON, Associativity.LEFT);
                break;
            case STRICT_NOT_EQUALS:
                visitBinary(expr, "!==", Priority.COMPARISON, Associativity.LEFT);
                break;
            case COMPARE:
                visitBinaryFunction(expr, naming.getNameForFunction("$rt_compare"));
                break;
            case COMPARE_LONG:
                visitBinaryFunction(expr, "Long_compare");
                break;
            case OR:
                visitBinary(expr, "||", Priority.LOGICAL_OR, Associativity.LEFT);
                break;
            case AND:
                visitBinary(expr, "&&", Priority.LOGICAL_AND, Associativity.LEFT);
                break;
            case BITWISE_OR:
                visitBinary(expr, "|", Priority.BITWISE_OR, Associativity.LEFT);
                break;
            case BITWISE_OR_LONG:
                visitBinaryFunction(expr, "Long_or");
                break;
            case BITWISE_AND:
                visitBinary(expr, "&", Priority.BITWISE_AND, Associativity.LEFT);
                break;
            case BITWISE_AND_LONG:
                visitBinaryFunction(expr, "Long_and");
                break;
            case BITWISE_XOR:
                visitBinary(expr, "^", Priority.BITWISE_XOR, Associativity.LEFT);
                break;
            case BITWISE_XOR_LONG:
                visitBinaryFunction(expr, "Long_xor");
                break;
            case LEFT_SHIFT:
                visitBinary(expr, "<<", Priority.BITWISE_SHIFT, Associativity.LEFT);
                break;
            case LEFT_SHIFT_LONG:
                visitBinaryFunction(expr, "Long_shl");
                break;
            case RIGHT_SHIFT:
                visitBinary(expr, ">>", Priority.BITWISE_SHIFT, Associativity.LEFT);
                break;
            case RIGHT_SHIFT_LONG:
                visitBinaryFunction(expr, "Long_shr");
                break;
            case UNSIGNED_RIGHT_SHIFT:
                visitBinary(expr, ">>>", Priority.BITWISE_SHIFT, Associativity.LEFT);
                break;
            case UNSIGNED_RIGHT_SHIFT_LONG:
                visitBinaryFunction(expr, "Long_shru");
                break;
        }
    }

    @Override
    public void visit(UnaryExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            switch (expr.getOperation()) {
                case NOT:
                    enterPriority(Priority.UNARY, Associativity.RIGHT, true);
                    writer.append("!");
                    expr.getOperand().acceptVisitor(this);
                    exitPriority();
                    break;
                case NEGATE:
                    enterPriority(Priority.MULTIPLICATION, Associativity.RIGHT, true);
                    writer.append("-");
                    expr.getOperand().acceptVisitor(this);
                    exitPriority();
                    break;
                case LENGTH:
                    enterPriority(Priority.MEMBER_ACCESS, Associativity.LEFT, true);
                    expr.getOperand().acceptVisitor(this);
                    exitPriority();
                    writer.append(".length");
                    break;
                case INT_TO_LONG:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.append("Long_fromInt(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
                case NUM_TO_LONG:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.append("Long_fromNumber(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
                case LONG_TO_NUM:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.append("Long_toNumber(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
                case LONG_TO_INT:
                    enterPriority(Priority.MEMBER_ACCESS, Associativity.LEFT, false);
                    expr.getOperand().acceptVisitor(this);
                    exitPriority();
                    writer.append(".lo");
                    break;
                case NEGATE_LONG:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.append("Long_neg(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
                case NOT_LONG:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.append("Long_not(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
                case INT_TO_BYTE:
                    enterPriority(Priority.BITWISE_SHIFT, Associativity.LEFT, true);
                    writer.append("(");
                    expr.getOperand().acceptVisitor(this);
                    writer.ws().append("<<").ws().append("24)").ws().append(">>").ws().append("24");
                    exitPriority();
                    break;
                case INT_TO_SHORT:
                    enterPriority(Priority.BITWISE_SHIFT, Associativity.LEFT, true);
                    writer.append("(");
                    expr.getOperand().acceptVisitor(this);
                    writer.ws().append("<<").ws().append("16)").ws().append(">>").ws().append("16");
                    exitPriority();
                    break;
                case INT_TO_CHAR:
                    enterPriority(Priority.BITWISE_AND, Associativity.LEFT, true);
                    expr.getOperand().acceptVisitor(this);
                    writer.ws().append("&").ws().append("65535");
                    exitPriority();
                    break;
                case NULL_CHECK:
                    enterPriority(Priority.COMMA, Associativity.NONE, false);
                    writer.appendFunction("$rt_nullCheck").append("(");
                    expr.getOperand().acceptVisitor(this);
                    writer.append(')');
                    exitPriority();
                    break;
            }
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(ConditionalExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(priority, Associativity.RIGHT, async);
            expr.getCondition().acceptVisitor(this);
            writer.ws().append("?").ws();
            expr.getConsequent().acceptVisitor(this);
            writer.ws().append(":").ws();
            expr.getAlternative().acceptVisitor(this);
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(ConstantExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            String str = constantToString(expr.getValue());
            if (str.startsWith("-")) {
                enterPriority(Priority.MULTIPLICATION, Associativity.NONE, true);
            }
            writer.append(str);
            if (str.startsWith("-")) {
                exitPriority();
            }
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    public String constantToString(Object cst) {
        if (cst == null) {
            return "null";
        }
        if (cst instanceof ValueType) {
            ValueType type = (ValueType)cst;
            return naming.getNameForFunction("$rt_cls") + "(" + typeToClsString(naming, type) + ")";
        } else if (cst instanceof String) {
            String string = (String)cst;
            Integer index = stringPoolMap.get(string);
            if (index == null) {
                index = stringPool.size();
                stringPool.add(string);
                stringPoolMap.put(string, index);
            }
            return "$rt_s(" + index + ")";
        } else if (cst instanceof Long) {
            long value = (Long)cst;
            if (value == 0) {
                return "Long_ZERO";
            } else if ((int)value == value) {
                return "Long_fromInt(" + value + ")";
            } else {
                return "new Long(" + (value & 0xFFFFFFFFL) + ", " + (value >>> 32) + ")";
            }
        } else if (cst instanceof Character) {
            return Integer.toString((Character)cst);
        } else {
            return cst.toString();
        }
    }

    public static String typeToClsString(NamingStrategy naming, ValueType type) {
        int arrayCount = 0;
        while (type instanceof ValueType.Array) {
            arrayCount++;
            type = ((ValueType.Array)type).getItemType();
        }
        String value;
        if (type instanceof ValueType.Object) {
            ValueType.Object objType = (ValueType.Object)type;
            value = naming.getNameFor(objType.getClassName());
        } else if (type instanceof ValueType.Void) {
            value = "$rt_voidcls()";
        } else if (type instanceof ValueType.Primitive) {
            ValueType.Primitive primitiveType = (ValueType.Primitive)type;
            switch (primitiveType.getKind()) {
                case BOOLEAN:
                    value = "$rt_booleancls()";
                    break;
                case CHARACTER:
                    value = "$rt_charcls()";
                    break;
                case BYTE:
                    value = "$rt_bytecls()";
                    break;
                case SHORT:
                    value = "$rt_shortcls()";
                    break;
                case INTEGER:
                    value = "$rt_intcls()";
                    break;
                case LONG:
                    value = "$rt_longcls()";
                    break;
                case FLOAT:
                    value = "$rt_floatcls()";
                    break;
                case DOUBLE:
                    value = "$rt_doublecls()";
                    break;
                default:
                    throw new IllegalArgumentException("The type is not renderable");
            }
        } else {
            throw new IllegalArgumentException("The type is not renderable");
        }

        for (int i = 0; i < arrayCount; ++i) {
            value = "$rt_arraycls(" + value + ")";
        }
        return value;
    }

    public static String escapeString(String str) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < str.length(); ++i) {
            char c = str.charAt(i);
            switch (c) {
                case '\r':
                    sb.append("\\r");
                    break;
                case '\n':
                    sb.append("\\n");
                    break;
                case '\t':
                    sb.append("\\t");
                    break;
                case '\'':
                    sb.append("\\'");
                    break;
                case '\"':
                    sb.append("\\\"");
                    break;
                case '\\':
                    sb.append("\\\\");
                    break;
                default:
                    if (c < ' ') {
                        sb.append("\\u00").append(Character.forDigit(c / 16, 16))
                                .append(Character.forDigit(c % 16, 16));
                    } else if (Character.isLowSurrogate(c) || Character.isHighSurrogate(c)) {
                        sb.append("\\u")
                                .append(Character.forDigit(c / 0x1000, 0x10))
                                .append(Character.forDigit((c / 0x100) % 0x10, 0x10))
                                .append(Character.forDigit((c / 0x10) % 0x10, 0x10))
                                .append(Character.forDigit(c % 0x10, 0x10));
                    } else {
                        sb.append(c);
                    }
                    break;
            }
        }
        return sb.toString();
    }

    @Override
    public void visit(VariableExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            writer.append(variableName(expr.getIndex()));
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(SubscriptExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(Priority.MEMBER_ACCESS, Associativity.LEFT, true);
            expr.getArray().acceptVisitor(this);
            writer.append('[');
            enterPriority(Priority.COMMA, Associativity.NONE, false);
            expr.getIndex().acceptVisitor(this);
            exitPriority();
            writer.append(']');
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(UnwrapArrayExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(Priority.MEMBER_ACCESS, Associativity.LEFT, true);
            expr.getArray().acceptVisitor(this);
            writer.append(".data");
            if (expr.getLocation() != null) {
                popLocation();
            }
            exitPriority();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(InvocationExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            Injector injector = getInjector(expr.getMethod());
            if (injector != null) {
                injector.generate(new InjectorContextImpl(expr.getArguments()), expr.getMethod());
            } else {
                if (expr.getType() == InvocationType.DYNAMIC) {
                    expr.getArguments().get(0).acceptVisitor(this);
                }
                MethodReference method = expr.getMethod();
                String name = naming.getNameFor(method.getDescriptor());
                DeferredCallSite callSite = prevCallSite;
                boolean shouldEraseCallSite = lastCallSite == null;
                if (lastCallSite == null) {
                    lastCallSite = callSite;
                }
                boolean virtual = false;
                enterPriority(Priority.COMMA, Associativity.NONE, false);
                switch (expr.getType()) {
                    case STATIC:
                        writer.append(naming.getFullNameFor(method)).append("(");
                        prevCallSite = debugEmitter.emitCallSite();
                        for (int i = 0; i < expr.getArguments().size(); ++i) {
                            if (i > 0) {
                                writer.append(",").ws();
                            }
                            expr.getArguments().get(i).acceptVisitor(this);
                        }
                        break;
                    case SPECIAL:
                        writer.append(naming.getFullNameFor(method)).append("(");
                        prevCallSite = debugEmitter.emitCallSite();
                        expr.getArguments().get(0).acceptVisitor(this);
                        for (int i = 1; i < expr.getArguments().size(); ++i) {
                            writer.append(",").ws();
                            expr.getArguments().get(i).acceptVisitor(this);
                        }
                        break;
                    case DYNAMIC:
                        writer.append(".").append(name).append("(");
                        prevCallSite = debugEmitter.emitCallSite();
                        for (int i = 1; i < expr.getArguments().size(); ++i) {
                            if (i > 1) {
                                writer.append(",").ws();
                            }
                            expr.getArguments().get(i).acceptVisitor(this);
                        }
                        virtual = true;
                        break;
                    case CONSTRUCTOR:
                        writer.append(naming.getNameForInit(expr.getMethod())).append("(");
                        prevCallSite = debugEmitter.emitCallSite();
                        for (int i = 0; i < expr.getArguments().size(); ++i) {
                            if (i > 0) {
                                writer.append(",").ws();
                            }
                            expr.getArguments().get(i).acceptVisitor(this);
                        }
                        break;
                }
                writer.append(')');
                exitPriority();
                if (lastCallSite != null) {
                    if (virtual) {
                        lastCallSite.setVirtualMethod(expr.getMethod());
                    } else {
                        lastCallSite.setStaticMethod(expr.getMethod());
                    }
                    lastCallSite = callSite;
                }
                if (shouldEraseCallSite) {
                    lastCallSite = null;
                }
            }
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(QualificationExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(Priority.MEMBER_ACCESS, Associativity.LEFT, true);
            expr.getQualified().acceptVisitor(this);
            writer.append('.').appendField(expr.getField());
            if (expr.getLocation() != null) {
                popLocation();
            }
            exitPriority();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(NewExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            enterPriority(Priority.FUNCTION_CALL, Associativity.RIGHT, true);
            writer.append("new ").append(naming.getNameFor(expr.getConstructedClass()));
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(NewArrayExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            ValueType type = expr.getType();
            enterPriority(Priority.COMMA, Associativity.NONE, false);
            if (type instanceof ValueType.Primitive) {
                switch (((ValueType.Primitive)type).getKind()) {
                    case BOOLEAN:
                        writer.append("$rt_createBooleanArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case BYTE:
                        writer.append("$rt_createByteArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case SHORT:
                        writer.append("$rt_createShortArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case INTEGER:
                        writer.append("$rt_createIntArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case LONG:
                        writer.append("$rt_createLongArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case FLOAT:
                        writer.append("$rt_createFloatArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case DOUBLE:
                        writer.append("$rt_createDoubleArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                    case CHARACTER:
                        writer.append("$rt_createCharArray(");
                        expr.getLength().acceptVisitor(this);
                        writer.append(")");
                        break;
                }
            } else {
                writer.appendFunction("$rt_createArray").append("(").append(typeToClsString(naming, expr.getType()))
                        .append(",").ws();
                expr.getLength().acceptVisitor(this);
                writer.append(")");
            }
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(NewMultiArrayExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            ValueType type = expr.getType();
            for (int i = 0; i < expr.getDimensions().size(); ++i) {
                type = ((ValueType.Array)type).getItemType();
            }
            enterPriority(Priority.COMMA, Associativity.NONE, false);
            if (type instanceof ValueType.Primitive) {
                switch (((ValueType.Primitive)type).getKind()) {
                    case BOOLEAN:
                        writer.append("$rt_createBooleanMultiArray(");
                        break;
                    case BYTE:
                        writer.append("$rt_createByteMultiArray(");
                        break;
                    case SHORT:
                        writer.append("$rt_createShortMultiArray(");
                        break;
                    case INTEGER:
                        writer.append("$rt_createIntMultiArray(");
                        break;
                    case LONG:
                        writer.append("$rt_createLongMultiArray(");
                        break;
                    case FLOAT:
                        writer.append("$rt_createFloatMultiArray(");
                        break;
                    case DOUBLE:
                        writer.append("$rt_createDoubleMultiArray(");
                        break;
                    case CHARACTER:
                        writer.append("$rt_createCharMultiArray(");
                        break;
                }
            } else {
                writer.append("$rt_createMultiArray(").append(typeToClsString(naming, expr.getType()))
                        .append(",").ws();
            }
            writer.append("[");
            boolean first = true;
            List dimensions = new ArrayList<>(expr.getDimensions());
            Collections.reverse(dimensions);
            for (Expr dimension : dimensions) {
                if (!first) {
                    writer.append(",").ws();
                }
                first = false;
                dimension.acceptVisitor(this);
            }
            writer.append("])");
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(InstanceOfExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            if (expr.getType() instanceof ValueType.Object) {
                String clsName = ((ValueType.Object)expr.getType()).getClassName();
                ClassHolder cls = classSource.get(clsName);
                if (cls != null && !cls.getModifiers().contains(ElementModifier.INTERFACE)) {
                    enterPriority(Priority.COMPARISON, Associativity.LEFT, true);
                    expr.getExpr().acceptVisitor(this);
                    writer.append(" instanceof ").appendClass(clsName);
                    exitPriority();
                    if (expr.getLocation() != null) {
                        popLocation();
                    }
                    return;
                }
            }
            enterPriority(Priority.COMMA, Associativity.NONE, false);
            writer.appendFunction("$rt_isInstance").append("(");
            expr.getExpr().acceptVisitor(this);
            writer.append(",").ws().append(typeToClsString(naming, expr.getType())).append(")");
            exitPriority();
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(StaticClassExpr expr) {
        try {
            if (expr.getLocation() != null) {
                pushLocation(expr.getLocation());
            }
            writer.append(typeToClsString(naming, expr.getType()));
            if (expr.getLocation() != null) {
                popLocation();
            }
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    private void visitStatements(List statements) {
        boolean oldEnd = end;
        for (int i = 0; i < statements.size() - 1; ++i) {
            end = false;
            statements.get(i).acceptVisitor(this);
        }
        end = oldEnd;
        statements.get(statements.size() - 1).acceptVisitor(this);
        end = oldEnd;
    }

    @Override
    public void visit(TryCatchStatement statement) {
        try {
            writer.append("try").ws().append("{").softNewLine().indent();
            List sequence = new ArrayList<>();
            sequence.add(statement);
            List protectedBody = statement.getProtectedBody();
            while (protectedBody.size() == 1 && protectedBody.get(0) instanceof TryCatchStatement) {
                TryCatchStatement nextStatement = (TryCatchStatement)protectedBody.get(0);
                sequence.add(nextStatement);
                protectedBody = nextStatement.getProtectedBody();
            }
            visitStatements(protectedBody);
            writer.outdent().append("}").ws().append("catch").ws().append("($e)")
                    .ws().append("{").indent().softNewLine();
            writer.append("$je").ws().append("=").ws().append("$e.$javaException;").softNewLine();
            for (TryCatchStatement catchClause : sequence) {
                writer.append("if").ws().append("($je");
                if (catchClause.getExceptionType() != null) {
                    writer.ws().append("&&").ws().append("$je instanceof ")
                            .appendClass(catchClause.getExceptionType());
                }
                writer.append(")").ws().append("{").indent().softNewLine();
                if (catchClause.getExceptionVariable() != null) {
                    writer.append(variableName(catchClause.getExceptionVariable())).ws().append("=").ws()
                            .append("$je;").softNewLine();
                }
                visitStatements(catchClause.getHandler());
                writer.outdent().append("}").ws().append("else ");
            }
            writer.append("{").indent().softNewLine();
            writer.append("throw $e;").softNewLine();
            writer.outdent().append("}").softNewLine();
            writer.outdent().append("}").softNewLine();
        } catch (IOException e) {
            throw new RenderingException("IO error occured", e);
        }
    }

    @Override
    public void visit(GotoPartStatement statement) {
        try {
            if (statement.getPart() != currentPart) {
                writer.append(pointerName()).ws().append("=").ws().append(statement.getPart()).append(";")
                        .softNewLine();
            }
            if (!end || statement.getPart() != currentPart + 1) {
                writer.append("continue ").append(mainLoopName()).append(";").softNewLine();
            }
        } catch (IOException ex){
            throw new RenderingException("IO error occured", ex);
        }
    }

    @Override
    public void visit(MonitorEnterStatement statement) {
        try {
            if (async) {
                MethodReference monitorEnterRef = new MethodReference(
                        Object.class, "monitorEnter", Object.class, void.class);
                writer.appendMethodBody(monitorEnterRef).append("(");
                statement.getObjectRef().acceptVisitor(this);
                writer.append(");").softNewLine();
                emitSuspendChecker();
            } else {
                MethodReference monitorEnterRef = new MethodReference(
                        Object.class, "monitorEnterSync", Object.class, void.class);
                writer.appendMethodBody(monitorEnterRef).append('(');
                statement.getObjectRef().acceptVisitor(this);
                writer.append(");").softNewLine();
            }
        } catch (IOException ex){
            throw new RenderingException("IO error occured", ex);
        }
    }

    private void emitSuspendChecker() throws IOException {
        writer.append("if").ws().append("(").appendFunction("$rt_suspending").append("())").ws()
                .append("{").indent().softNewLine();
        writer.append("break ").append(mainLoopName()).append(";").softNewLine();
        writer.outdent().append("}").softNewLine();
    }

    @Override
    public void visit(MonitorExitStatement statement) {
        try {
            if (async) {
                MethodReference monitorExitRef = new MethodReference(
                        Object.class, "monitorExit", Object.class, void.class);
                writer.appendMethodBody(monitorExitRef).append("(");
                statement.getObjectRef().acceptVisitor(this);
                writer.append(");").softNewLine();
            } else {
                MethodReference monitorEnterRef = new MethodReference(
                        Object.class, "monitorExitSync", Object.class, void.class);
                writer.appendMethodBody(monitorEnterRef).append('(');
                statement.getObjectRef().acceptVisitor(this);
                writer.append(");").softNewLine();
            }
        } catch (IOException ex){
            throw new RenderingException("IO error occured", ex);
        }
    }


    private Injector getInjector(MethodReference ref) {
        InjectorHolder holder = injectorMap.get(ref);
        if (holder == null) {
            holder = new InjectorHolder(null);
            ClassHolder cls = classSource.get(ref.getClassName());
            if (cls != null) {
                MethodHolder method = cls.getMethod(ref.getDescriptor());
                if (method != null) {
                    AnnotationHolder injectedByAnnot = method.getAnnotations().get(InjectedBy.class.getName());
                    if (injectedByAnnot != null) {
                        ValueType type = injectedByAnnot.getValues().get("value").getJavaClass();
                        holder = new InjectorHolder(instantiateInjector(((ValueType.Object)type).getClassName()));
                    }
                }
            }
            injectorMap.put(ref, holder);
        }
        return holder.injector;
    }

    private Injector instantiateInjector(String type) {
        try {
            Class cls = Class.forName(type, true, classLoader).asSubclass(Injector.class);
            Constructor cons = cls.getConstructor();
            return cons.newInstance();
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Illegal injector: " + type, e);
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Default constructor was not found in the " + type + " injector", e);
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) {
            throw new RuntimeException("Error instantiating injector " + type, e);
        }
    }

    private class InjectorContextImpl implements InjectorContext {
        private List arguments;

        public InjectorContextImpl(List arguments) {
            this.arguments = arguments;
        }

        @Override
        public Expr getArgument(int index) {
            return arguments.get(index);
        }

        @Override
        public boolean isMinifying() {
            return minifying;
        }

        @Override
        public SourceWriter getWriter() {
            return writer;
        }

        @Override
        public void writeEscaped(String str) throws IOException {
            writer.append(escapeString(str));
        }

        @Override
        public void writeType(ValueType type) throws IOException {
            writer.append(typeToClsString(naming, type));
        }

        @Override
        public void writeExpr(Expr expr) throws IOException {
            expr.acceptVisitor(Renderer.this);
        }

        @Override
        public int argumentCount() {
            return arguments.size();
        }

        @Override
        public  T getService(Class type) {
            return services.getService(type);
        }

        @Override
        public Properties getProperties() {
            return new Properties(properties);
        }
    }

    @Override
    public  T getService(Class type) {
        return services.getService(type);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy