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

org.teavm.jso.impl.JSNativeInjector Maven / Gradle / Ivy

The newest version!
/*
 *  Copyright 2023 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.jso.impl;

import static org.teavm.backend.javascript.rendering.RenderingUtil.escapeString;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import org.teavm.ast.ArrayFromDataExpr;
import org.teavm.ast.ConstantExpr;
import org.teavm.ast.Expr;
import org.teavm.ast.InvocationExpr;
import org.teavm.ast.InvocationType;
import org.teavm.ast.NewArrayExpr;
import org.teavm.backend.javascript.codegen.SourceWriter;
import org.teavm.backend.javascript.rendering.Precedence;
import org.teavm.backend.javascript.spi.Injector;
import org.teavm.backend.javascript.spi.InjectorContext;
import org.teavm.dependency.DependencyAgent;
import org.teavm.dependency.DependencyNode;
import org.teavm.dependency.DependencyPlugin;
import org.teavm.dependency.MethodDependency;
import org.teavm.model.ClassReader;
import org.teavm.model.ElementModifier;
import org.teavm.model.MethodReader;
import org.teavm.model.MethodReference;
import org.teavm.model.ValueType;

public class JSNativeInjector implements Injector, DependencyPlugin {
    private Set reachedFunctorMethods = new HashSet<>();
    private Set functorParamNodes = new HashSet<>();
    private static final ValueType STRING_ARRAY = ValueType.arrayOf(ValueType.object("java.lang.String"));

    @Override
    public void generate(InjectorContext context, MethodReference methodRef) {
        SourceWriter writer = context.getWriter();
        switch (methodRef.getName()) {
            case "arrayData":
                context.writeExpr(context.getArgument(0));
                writer.append(".data");
                break;
            case "concatArray":
                writer.appendFunction("$rt_concatArrays").append("(");
                context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT);
                writer.append(",").ws();
                context.writeExpr(context.getArgument(1), Precedence.ASSIGNMENT);
                writer.append(")");
                break;
            case "get":
            case "getPure":
                if (isNull(context.getArgument(0))) {
                    writer.append(extractPropertyName(context.getArgument(1)));
                } else {
                    context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS);
                    renderProperty(context.getArgument(1), context);
                }
                break;
            case "set":
            case "setPure":
                if (isNull(context.getArgument(0))) {
                    writer.append(extractPropertyName(context.getArgument(1)));
                } else {
                    context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS.next());
                    renderProperty(context.getArgument(1), context);
                }
                writer.ws().append('=').ws();
                context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT.next());
                break;
            case "invoke":
                if (isNull(context.getArgument(0))) {
                    writer.append(extractPropertyName(context.getArgument(1)));
                } else {
                    context.writeExpr(context.getArgument(0), Precedence.GROUPING);
                    renderProperty(context.getArgument(1), context);
                }
                writer.append('(');
                for (int i = 2; i < context.argumentCount(); ++i) {
                    if (i > 2) {
                        writer.append(',').ws();
                    }
                    context.writeExpr(context.getArgument(i), Precedence.min());
                }
                writer.append(')');
                break;
            case "apply":
                applyFunction(context);
                break;
            case "arrayOf":
                writer.append('[');
                for (int i = 0; i < context.argumentCount(); ++i) {
                    if (i > 0) {
                        writer.append(',').ws();
                    }
                    context.writeExpr(context.getArgument(i), Precedence.min());
                }
                writer.append(']');
                break;
            case "construct":
                if (context.getPrecedence().ordinal() >= Precedence.FUNCTION_CALL.ordinal()) {
                    writer.append("(");
                }
                writer.append("new ");
                context.writeExpr(context.getArgument(0), Precedence.GROUPING);
                writer.append('(');
                for (int i = 1; i < context.argumentCount(); ++i) {
                    if (i > 1) {
                        writer.append(',').ws();
                    }
                    context.writeExpr(context.getArgument(i), Precedence.min());
                }
                writer.append(')');
                if (context.getPrecedence().ordinal() >= Precedence.FUNCTION_CALL.ordinal()) {
                    writer.append(")");
                }
                break;
            case "wrap":
                if (methodRef.getDescriptor().parameterType(0).isObject("java.lang.String")) {
                    if (context.getArgument(0) instanceof ConstantExpr) {
                        ConstantExpr constant = (ConstantExpr) context.getArgument(0);
                        if (constant.getValue() instanceof String) {
                            writer.append('"').append(escapeString((String) constant.getValue())).append('"');
                            break;
                        }
                    }
                    writer.appendFunction("$rt_ustr").append("(");
                    context.writeExpr(context.getArgument(0), Precedence.min());
                    writer.append(")");
                } else if (methodRef.getDescriptor().parameterType(0) == ValueType.BOOLEAN) {
                    if (context.getPrecedence().ordinal() >= Precedence.UNARY.ordinal()) {
                        writer.append("(");
                    }
                    writer.append("!!");
                    context.writeExpr(context.getArgument(0), Precedence.UNARY);
                    if (context.getPrecedence().ordinal() >= Precedence.UNARY.ordinal()) {
                        writer.append(")");
                    }
                } else {
                    context.writeExpr(context.getArgument(0), context.getPrecedence());
                }
                break;
            case "unwrapString":
                writer.appendFunction("$rt_str").append("(");
                context.writeExpr(context.getArgument(0), Precedence.min());
                writer.append(")");
                break;
            case "unwrapBoolean":
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append("(");
                }
                context.writeExpr(context.getArgument(0), Precedence.CONDITIONAL.next());
                writer.ws().append("?").ws().append("1").ws().append(":").ws().append("0");
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append(")");
                }
                break;

            case "dataToByteArray":
                dataToArray(context, "$rt_bytecls");
                break;
            case "dataToShortArray":
                dataToArray(context, "$rt_shortcls");
                break;
            case "dataToCharArray":
                dataToArray(context, "$rt_charcls");
                break;
            case "dataToIntArray":
                dataToArray(context, "$rt_intcls");
                break;
            case "dataToFloatArray":
                dataToArray(context, "$rt_floatcls");
                break;
            case "dataToDoubleArray":
                dataToArray(context, "$rt_doublecls");
                break;
            case "dataToArray":
                dataToArray(context, "$rt_objcls");
                break;
            case "global": {
                var cst = (ConstantExpr) context.getArgument(0);
                var name = (String) cst.getValue();
                writer.appendGlobal(name);
                break;
            }
            case "importModule": {
                var cst = (ConstantExpr) context.getArgument(0);
                var name = (String) cst.getValue();
                writer.appendFunction(context.importModule(name));
                break;
            }
            case "instanceOf": {
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append("(");
                }
                context.writeExpr(context.getArgument(0), Precedence.COMPARISON.next());
                writer.append(" instanceof ");
                context.writeExpr(context.getArgument(1), Precedence.COMPARISON.next());
                writer.ws().append("?").ws().append("1").ws().append(":").ws().append("0");
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append(")");
                }
                break;
            }
            case "instanceOfOrNull": {
                writer.appendFunction("$rt_instanceOfOrNull").append("(");
                context.writeExpr(context.getArgument(0), Precedence.min());
                writer.append(",").ws();
                context.writeExpr(context.getArgument(1), Precedence.min());
                writer.append(")");
                break;
            }
            case "isPrimitive": {
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append("(");
                }
                writer.append("typeof ");
                context.writeExpr(context.getArgument(0), Precedence.UNARY.next());
                writer.ws().append("===").ws();
                context.writeExpr(context.getArgument(1), Precedence.COMPARISON.next());
                writer.ws().append("?").ws().append("1").ws().append(":").ws().append("0");
                if (context.getPrecedence().ordinal() >= Precedence.CONDITIONAL.ordinal()) {
                    writer.append(")");
                }
                break;
            }
            case "throwCCEIfFalse": {
                writer.appendFunction("$rt_throwCCEIfFalse").append("(");
                context.writeExpr(context.getArgument(0), Precedence.min());
                writer.append(",").ws();
                context.writeExpr(context.getArgument(1), Precedence.min());
                writer.append(")");
                break;
            }
            case "argumentsBeginningAt": {
                writer.appendFunction("$rt_skip").append("(arguments,").ws();
                context.writeExpr(context.getArgument(0), Precedence.min());
                writer.append(")");
                break;
            }

            default:
                if (methodRef.getName().startsWith("unwrap")) {
                    context.writeExpr(context.getArgument(0), context.getPrecedence());
                }
                break;
        }
    }

    private static boolean isNull(Expr expr) {
        if (expr instanceof ConstantExpr) {
            var constantExpr = (ConstantExpr) expr;
            if (constantExpr.getValue() == null) {
                return true;
            }
        }
        return false;
    }

    private void applyFunction(InjectorContext context) {
        if (tryApplyFunctionOptimized(context)) {
            return;
        }
        var writer = context.getWriter();
        if (isNull(context.getArgument(0))) {
            writer.appendFunction("$rt_apply_topLevel").append("(");
            writer.append(extractPropertyName(context.getArgument(1)));
        } else {
            writer.appendFunction("$rt_apply").append("(");
            context.writeExpr(context.getArgument(0), Precedence.ASSIGNMENT);
            writer.append(",").ws();
            context.writeExpr(context.getArgument(1), Precedence.ASSIGNMENT);
        }
        writer.append(",").ws();
        context.writeExpr(context.getArgument(2), Precedence.ASSIGNMENT);
        writer.append(")");
    }

    private boolean tryApplyFunctionOptimized(InjectorContext context) {
        var paramList = new ArrayList();
        if (!extractConstantArgList(context.getArgument(2), paramList) || paramList.size() >= 13) {
            return false;
        }

        applyFunctionOptimized(context, paramList);
        return true;
    }

    private boolean extractConstantArgList(Expr expr, List target) {
        if (!(expr instanceof InvocationExpr)) {
            return false;
        }
        var invocation = (InvocationExpr) expr;
        if (!invocation.getMethod().getClassName().equals(JS.class.getName())) {
            return false;
        }

        switch (invocation.getMethod().getName()) {
            case "arrayOf":
                target.addAll(invocation.getArguments());
                return true;
            case "concatArray":
                return extractConstantArgList(invocation.getArguments().get(0), target)
                        && extractConstantArgList(invocation.getArguments().get(1), target);
            case "arrayData": {
                var arg = invocation.getArguments().get(0);
                if (arg instanceof ArrayFromDataExpr) {
                    target.addAll(((ArrayFromDataExpr) arg).getData());
                    return true;
                }
                if (arg instanceof NewArrayExpr && isEmptyArrayConstructor((NewArrayExpr) arg)) {
                    return true;
                }
                break;
            }
            case "wrap": {
                if (invocation.getMethod().parameterType(0).equals(STRING_ARRAY)) {
                    var arg = invocation.getArguments().get(0);
                    if (arg instanceof ArrayFromDataExpr) {
                        extractConstantStringArgList(((ArrayFromDataExpr) arg).getData(), target);
                        return true;
                    }
                    if (arg instanceof NewArrayExpr && isEmptyArrayConstructor((NewArrayExpr) arg)) {
                        return true;
                    }
                }
                break;
            }
        }
        return false;
    }

    private boolean isEmptyArrayConstructor(NewArrayExpr expr) {
        var length = expr.getLength();
        if (!(length instanceof ConstantExpr)) {
            return false;
        }
        return Objects.equals(((ConstantExpr) length).getValue(), 0);
    }

    private void extractConstantStringArgList(List source, List target) {
        for (var element : source) {
            var invocation = new InvocationExpr();
            invocation.setType(InvocationType.STATIC);
            invocation.setMethod(JSMethods.WRAP_STRING);
            invocation.setLocation(element.getLocation());
            invocation.getArguments().add(element);
            target.add(invocation);
        }
    }

    private void applyFunctionOptimized(InjectorContext context, List paramList) {
        var writer = context.getWriter();
        if (isNull(context.getArgument(0))) {
            writer.append(extractPropertyName(context.getArgument(1)));
        } else {
            context.writeExpr(context.getArgument(0), Precedence.GROUPING);
            renderProperty(context.getArgument(1), context);
        }
        writer.append('(');
        for (int i = 0; i < paramList.size(); ++i) {
            if (i > 0) {
                writer.append(',').ws();
            }
            context.writeExpr(paramList.get(i), Precedence.min());
        }
        writer.append(')');
    }

    private void dataToArray(InjectorContext context, String className) {
        var writer = context.getWriter();
        writer.appendFunction("$rt_wrapArray").append("(").appendFunction(className).append(",").ws();
        context.writeExpr(context.getArgument(0), Precedence.min());
        writer.append(")");
    }

    @Override
    public void methodReached(DependencyAgent agent, MethodDependency method) {
        switch (method.getReference().getName()) {
            case "invoke":
            case "construct":
            case "function":
                if (reachedFunctorMethods.add(method.getReference()) && !method.isMissing()) {
                    for (int i = 0; i < method.getReference().parameterCount(); ++i) {
                        DependencyNode node = method.getVariable(i);
                        if (functorParamNodes.add(node)) {
                            node.addConsumer(type -> {
                                if (agent.getClassHierarchy().isSuperType(method.getMethod().getOwnerName(),
                                        type.getName(), false)) {
                                    reachFunctorMethods(agent, type.getName());
                                }
                            });
                        }
                    }
                }
                break;
            case "unwrapString":
                method.getResult().propagate(agent.getType("java.lang.String"));
                break;

            case "dataToByteArray":
                method.getResult().propagate(agent.getType("[B"));
                break;
            case "dataToShortArray":
                method.getResult().propagate(agent.getType("[S"));
                break;
            case "dataToCharArray":
                method.getResult().propagate(agent.getType("[C"));
                break;
            case "dataToIntArray":
                method.getResult().propagate(agent.getType("[I"));
                break;
            case "dataToFloatArray":
                method.getResult().propagate(agent.getType("[F"));
                break;
            case "dataToDoubleArray":
                method.getResult().propagate(agent.getType("[D"));
                break;
            case "dataToArray":
                method.getResult().propagate(agent.getType("[Ljava/lang/Object;"));
                break;
        }
    }

    private void reachFunctorMethods(DependencyAgent agent, String type) {
        ClassReader cls = agent.getClassSource().get(type);
        if (cls != null) {
            for (MethodReader method : cls.getMethods()) {
                if (!method.hasModifier(ElementModifier.STATIC)) {
                    agent.linkMethod(method.getReference()).use();
                }
            }
        }
    }


    private void renderProperty(Expr property, InjectorContext context) {
        SourceWriter writer = context.getWriter();
        String name = extractPropertyName(property);
        if (name == null) {
            writer.append('[');
            context.writeExpr(property, Precedence.min());
            writer.append(']');
        } else if (!isIdentifier(name)) {
            writer.append("[\"");
            context.writeEscaped(name);
            writer.append("\"]");
        } else {
            writer.append(".").append(name);
        }
    }

    private String extractPropertyName(Expr propertyName) {
        if (!(propertyName instanceof InvocationExpr)) {
            return null;
        }
        InvocationExpr invoke = (InvocationExpr) propertyName;
        if (!invoke.getMethod().getClassName().equals(JS.class.getName())) {
            return null;
        }
        if (!invoke.getMethod().getName().equals("wrap")
                || !invoke.getMethod().getDescriptor().parameterType(0).isObject("java.lang.String")) {
            return null;
        }
        Expr arg = invoke.getArguments().get(0);
        if (!(arg instanceof ConstantExpr)) {
            return null;
        }
        ConstantExpr constant = (ConstantExpr) arg;
        return constant.getValue() instanceof String ? (String) constant.getValue() : null;
    }

    private boolean isIdentifier(String name) {
        if (name.isEmpty() || !Character.isJavaIdentifierStart(name.charAt(0))) {
            return false;
        }
        for (int i = 1; i < name.length(); ++i) {
            if (!Character.isJavaIdentifierPart(name.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy