From 582fcf904c11638e0cd3ae2ac5c18fd5ff853392 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Fri, 8 Mar 2024 21:06:08 +0100 Subject: [PATCH] jso: implement vararg support for method calls --- .../teavm/ast/decompilation/Decompiler.java | 4 +- .../org/teavm/ast/optimization/Optimizer.java | 11 +- .../ast/optimization/OptimizingVisitor.java | 96 +++++++++++-- .../model/instructions/InvokeInstruction.java | 2 +- .../transformation/BoundCheckInsertion.java | 6 + .../main/java/org/teavm/parsing/Parser.java | 2 +- .../org/teavm/backend/javascript/array.js | 16 +++ .../org/teavm/backend/javascript/runtime.js | 2 + .../java/org/teavm/interop/NoSideEffects.java | 2 +- .../main/java/org/teavm/jso/core/JSArray.java | 2 + .../src/main/java/org/teavm/jso/impl/JS.java | 79 +++++++++++ .../org/teavm/jso/impl/JSClassProcessor.java | 47 ++++++- .../java/org/teavm/jso/impl/JSMethods.java | 16 +++ .../org/teavm/jso/impl/JSNativeInjector.java | 128 ++++++++++++++++++ .../java/org/teavm/jso/test/CallTest.java | 91 +++++++++++++ .../resources/org/teavm/jso/test/vararg.js | 33 +++++ 16 files changed, 512 insertions(+), 25 deletions(-) create mode 100644 tests/src/test/java/org/teavm/jso/test/CallTest.java create mode 100644 tests/src/test/resources/org/teavm/jso/test/vararg.js diff --git a/core/src/main/java/org/teavm/ast/decompilation/Decompiler.java b/core/src/main/java/org/teavm/ast/decompilation/Decompiler.java index 3111a1862..a10f16205 100644 --- a/core/src/main/java/org/teavm/ast/decompilation/Decompiler.java +++ b/core/src/main/java/org/teavm/ast/decompilation/Decompiler.java @@ -125,7 +125,7 @@ public class Decompiler { methodNode.getVariables().add(variable); } - Optimizer optimizer = new Optimizer(); + Optimizer optimizer = new Optimizer(classSource); optimizer.optimize(methodNode, method.getProgram(), friendlyToDebugger); methodNode.getModifiers().addAll(method.getModifiers()); @@ -158,7 +158,7 @@ public class Decompiler { node.getVariables().add(variable); } - Optimizer optimizer = new Optimizer(); + Optimizer optimizer = new Optimizer(classSource); optimizer.optimize(node, splitter, friendlyToDebugger); node.getModifiers().addAll(method.getModifiers()); diff --git a/core/src/main/java/org/teavm/ast/optimization/Optimizer.java b/core/src/main/java/org/teavm/ast/optimization/Optimizer.java index 2a04ae84c..e7bf0ef50 100644 --- a/core/src/main/java/org/teavm/ast/optimization/Optimizer.java +++ b/core/src/main/java/org/teavm/ast/optimization/Optimizer.java @@ -21,6 +21,7 @@ import org.teavm.ast.AsyncMethodPart; import org.teavm.ast.RegularMethodNode; import org.teavm.common.Graph; import org.teavm.model.BasicBlock; +import org.teavm.model.ClassReaderSource; import org.teavm.model.Instruction; import org.teavm.model.MethodReference; import org.teavm.model.Program; @@ -32,6 +33,12 @@ import org.teavm.model.util.ProgramUtils; import org.teavm.model.util.UsageExtractor; public class Optimizer { + private ClassReaderSource classes; + + public Optimizer(ClassReaderSource classes) { + this.classes = classes; + } + public void optimize(RegularMethodNode method, Program program, boolean friendlyToDebugger) { ReadWriteStatsBuilder stats = new ReadWriteStatsBuilder(method.getVariables().size()); stats.analyze(program); @@ -48,7 +55,7 @@ public class Optimizer { } } OptimizingVisitor optimizer = new OptimizingVisitor(preservedVars, stats.writes, stats.reads, - stats.constants, friendlyToDebugger); + stats.constants, friendlyToDebugger, classes); method.getBody().acceptVisitor(optimizer); method.setBody(optimizer.resultStmt); int paramCount = method.getReference().parameterCount(); @@ -85,7 +92,7 @@ public class Optimizer { BreakEliminator breakEliminator = new BreakEliminator(); breakEliminator.eliminate(part.getStatement()); OptimizingVisitor optimizer = new OptimizingVisitor(preservedVars, stats.writes, stats.reads, - stats.constants, friendlyToDebugger); + stats.constants, friendlyToDebugger, classes); part.getStatement().acceptVisitor(optimizer); part.setStatement(optimizer.resultStmt); } diff --git a/core/src/main/java/org/teavm/ast/optimization/OptimizingVisitor.java b/core/src/main/java/org/teavm/ast/optimization/OptimizingVisitor.java index 6171b349c..3d004d81e 100644 --- a/core/src/main/java/org/teavm/ast/optimization/OptimizingVisitor.java +++ b/core/src/main/java/org/teavm/ast/optimization/OptimizingVisitor.java @@ -43,6 +43,7 @@ import org.teavm.ast.IdentifiedStatement; import org.teavm.ast.InitClassStatement; import org.teavm.ast.InstanceOfExpr; import org.teavm.ast.InvocationExpr; +import org.teavm.ast.InvocationType; import org.teavm.ast.MonitorEnterStatement; import org.teavm.ast.MonitorExitStatement; import org.teavm.ast.NewArrayExpr; @@ -64,6 +65,9 @@ import org.teavm.ast.UnaryOperation; import org.teavm.ast.UnwrapArrayExpr; import org.teavm.ast.VariableExpr; import org.teavm.ast.WhileStatement; +import org.teavm.interop.NoSideEffects; +import org.teavm.model.ClassReaderSource; +import org.teavm.model.MethodReference; import org.teavm.model.TextLocation; import org.teavm.model.ValueType; @@ -82,15 +86,17 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { private Deque locationStack = new LinkedList<>(); private Deque notNullLocationStack = new ArrayDeque<>(); private List pendingArrayOptimizations; + private ClassReaderSource classes; OptimizingVisitor(boolean[] preservedVars, int[] writeFrequencies, int[] readFrequencies, Object[] constants, - boolean friendlyToDebugger) { + boolean friendlyToDebugger, ClassReaderSource classes) { this.preservedVars = preservedVars; this.writeFrequencies = writeFrequencies; this.initialWriteFrequencies = writeFrequencies.clone(); this.readFrequencies = readFrequencies; this.constants = constants; this.friendlyToDebugger = friendlyToDebugger; + this.classes = classes; } private static boolean isZero(Expr expr) { @@ -385,6 +391,23 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { } } + private boolean isSideEffectFree(MethodReference method) { + var cls = classes.get(method.getClassName()); + if (cls == null) { + return false; + } + var methodReader = cls.getMethod(method.getDescriptor()); + if (methodReader == null) { + return false; + } + return methodReader.getAnnotations().get(NoSideEffects.class.getName()) != null; + } + + private boolean isSideEffectFreeCall(InvocationExpr expr) { + return (expr.getType() == InvocationType.SPECIAL || expr.getType() == InvocationType.STATIC) + && isSideEffectFree(expr.getMethod()); + } + @Override public void visit(InvocationExpr expr) { pushLocation(expr.getLocation()); @@ -392,9 +415,13 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { Expr[] args = expr.getArguments().toArray(new Expr[0]); int barrierPos; - for (barrierPos = 0; barrierPos < args.length; ++barrierPos) { - if (!isSideEffectFree(args[barrierPos])) { - break; + if (isSideEffectFreeCall(expr)) { + barrierPos = args.length; + } else { + for (barrierPos = 0; barrierPos < args.length; ++barrierPos) { + if (!isSideEffectFree(args[barrierPos])) { + break; + } } } @@ -639,8 +666,9 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { while (!pendingArrayOptimizations.isEmpty()) { statement = resultSequence.get(resultSequence.size() - 1); int i = pendingArrayOptimizations.size() - 1; - if (!tryArrayUnwrap(pendingArrayOptimizations.get(i), statement) - || tryArraySet(pendingArrayOptimizations.get(i), statement)) { + var opt = pendingArrayOptimizations.get(i); + var result = opt.isSet ? tryArraySet(opt, statement) : tryArrayUnwrap(opt, statement); + if (!result) { pendingArrayOptimizations.remove(i); } else { break; @@ -721,11 +749,9 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { return false; } - if (optimization.arraySize != readFrequencies[optimization.unwrappedArrayVariable]) { - return false; - } - optimization.arrayElementIndex = 0; + optimization.remainingUseCount = readFrequencies[optimization.unwrappedArrayVariable]; + optimization.isSet = true; return true; } @@ -756,10 +782,22 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { return false; } - if (!(subscript.getIndex() instanceof ConstantExpr)) { + var index = subscript.getIndex(); + if (index instanceof BoundCheckExpr) { + var boundCheck = (BoundCheckExpr) index; + if (!(boundCheck.getArray() instanceof VariableExpr)) { + return false; + } + if (((VariableExpr) boundCheck.getArray()).getIndex() != optimization.unwrappedArrayVariable) { + return false; + } + index = boundCheck.getIndex(); + optimization.remainingUseCount--; + } + if (!(index instanceof ConstantExpr)) { return false; } - Object constantValue = ((ConstantExpr) subscript.getIndex()).getValue(); + Object constantValue = ((ConstantExpr) index).getValue(); if (!Integer.valueOf(optimization.arrayElementIndex).equals(constantValue)) { return false; } @@ -772,11 +810,14 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { } optimization.elements.add(assign.getRightValue()); + --optimization.remainingUseCount; if (++optimization.arrayElementIndex == optimization.arraySize) { - applyArrayOptimization(optimization); - return true; + if (optimization.remainingUseCount == 0) { + applyArrayOptimization(optimization); + } + pendingArrayOptimizations.remove(pendingArrayOptimizations.size() - 1); } - return false; + return true; } private void applyArrayOptimization(ArrayOptimization optimization) { @@ -1303,6 +1344,29 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { return isSideEffectFree(((PrimitiveCastExpr) expr).getValue()); } + if (expr instanceof ArrayFromDataExpr) { + var list = ((ArrayFromDataExpr) expr).getData(); + for (var element : list) { + if (!isSideEffectFree(element)) { + return false; + } + } + return true; + } + + if (expr instanceof InvocationExpr) { + var invocation = (InvocationExpr) expr; + if (isSideEffectFreeCall(invocation)) { + for (var arg : invocation.getArguments()) { + if (!isSideEffectFree(arg)) { + return false; + } + } + return true; + } + return false; + } + if (expr instanceof NewExpr) { return true; } @@ -1326,6 +1390,7 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { } static class ArrayOptimization { + boolean isSet; int index; NewArrayExpr array; int arrayVariable; @@ -1333,6 +1398,7 @@ class OptimizingVisitor implements StatementVisitor, ExprVisitor { int unwrappedArrayVariable; int arrayElementIndex; int arraySize; + int remainingUseCount; List elements = new ArrayList<>(); } } diff --git a/core/src/main/java/org/teavm/model/instructions/InvokeInstruction.java b/core/src/main/java/org/teavm/model/instructions/InvokeInstruction.java index f38fc9050..2edbec8ce 100644 --- a/core/src/main/java/org/teavm/model/instructions/InvokeInstruction.java +++ b/core/src/main/java/org/teavm/model/instructions/InvokeInstruction.java @@ -85,7 +85,7 @@ public class InvokeInstruction extends Instruction { private List argumentList = new AbstractList() { @Override public Variable get(int index) { - if (arguments == null) { + if (arguments == null || index >= arguments.length) { throw new IndexOutOfBoundsException(); } return arguments[index]; diff --git a/core/src/main/java/org/teavm/model/transformation/BoundCheckInsertion.java b/core/src/main/java/org/teavm/model/transformation/BoundCheckInsertion.java index bfc089105..1c26ac637 100644 --- a/core/src/main/java/org/teavm/model/transformation/BoundCheckInsertion.java +++ b/core/src/main/java/org/teavm/model/transformation/BoundCheckInsertion.java @@ -37,6 +37,7 @@ import org.teavm.model.instructions.ConstructArrayInstruction; import org.teavm.model.instructions.GetElementInstruction; import org.teavm.model.instructions.IntegerConstantInstruction; import org.teavm.model.instructions.JumpInstruction; +import org.teavm.model.instructions.NullCheckInstruction; import org.teavm.model.instructions.NumericOperandType; import org.teavm.model.instructions.PutElementInstruction; import org.teavm.model.instructions.UnwrapArrayInstruction; @@ -344,6 +345,11 @@ public class BoundCheckInsertion { assign(insn.getArray(), insn.getReceiver()); } + @Override + public void visit(NullCheckInstruction insn) { + isConstantSizedArray[index(insn.getReceiver())] = isConstantSizedArray[index(insn.getValue())]; + } + private void assign(Variable from, Variable to) { map[to.getIndex()] = map[from.getIndex()]; } diff --git a/core/src/main/java/org/teavm/parsing/Parser.java b/core/src/main/java/org/teavm/parsing/Parser.java index 6290480fb..9e47e3398 100644 --- a/core/src/main/java/org/teavm/parsing/Parser.java +++ b/core/src/main/java/org/teavm/parsing/Parser.java @@ -496,7 +496,7 @@ public class Parser { } } if ((access & Opcodes.ACC_VARARGS) != 0) { - if (type == DECL_FIELD) { + if (type == DECL_METHOD) { member.getModifiers().add(ElementModifier.VARARGS); } } diff --git a/core/src/main/resources/org/teavm/backend/javascript/array.js b/core/src/main/resources/org/teavm/backend/javascript/array.js index 4e121ecc4..529e5e70c 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/array.js +++ b/core/src/main/resources/org/teavm/backend/javascript/array.js @@ -280,3 +280,19 @@ let $rt_createMultiArrayImpl = (cls, arrays, dimensions, start) => { } return arrays[0]; } + +let $rt_concatArrays = (a, b) => { + if (a.length === 0) { + return b; + } + if (b.length === 0) { + return a; + } + if (!teavm_globals.Array.isArray(a)) { + a = teavm_globals.Array.from(a); + } + if (!teavm_globals.Array.isArray(b)) { + b = teavm_globals.Array.from(b); + } + return a.concat(b); +} \ No newline at end of file diff --git a/core/src/main/resources/org/teavm/backend/javascript/runtime.js b/core/src/main/resources/org/teavm/backend/javascript/runtime.js index 58d19dcdc..d21bd7691 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/runtime.js +++ b/core/src/main/resources/org/teavm/backend/javascript/runtime.js @@ -107,3 +107,5 @@ let $rt_setThread = t => { return teavm_javaMethod("java.lang.Thread", "setCurrentThread(Ljava/lang/Thread;)V")(t); } } + +let $rt_apply = (instance, method, args) => instance[method].apply(instance, args); \ No newline at end of file diff --git a/interop/core/src/main/java/org/teavm/interop/NoSideEffects.java b/interop/core/src/main/java/org/teavm/interop/NoSideEffects.java index 8496b7b2b..efd83d6fa 100644 --- a/interop/core/src/main/java/org/teavm/interop/NoSideEffects.java +++ b/interop/core/src/main/java/org/teavm/interop/NoSideEffects.java @@ -21,6 +21,6 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) -@Target({ ElementType.METHOD, ElementType.TYPE }) +@Target({ ElementType.METHOD, ElementType.TYPE, ElementType.CONSTRUCTOR }) public @interface NoSideEffects { } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java b/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java index 7671383ee..199933d98 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java @@ -23,9 +23,11 @@ import org.teavm.jso.JSProperty; @JSClass(name = "Array") public class JSArray implements JSArrayReader { + @NoSideEffects public JSArray(int size) { } + @NoSideEffects public JSArray() { } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java index 3a53d789e..74eaa79bc 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java @@ -36,6 +36,10 @@ final class JS { @NoSideEffects public static native JSObject arrayData(Object array); + @InjectedBy(JSNativeInjector.class) + @NoSideEffects + public static native JSObject concatArray(JSObject a, JSObject b); + @InjectedBy(JSNativeInjector.class) @PluggableDependency(JSNativeInjector.class) @NoSideEffects @@ -249,6 +253,7 @@ final class JS { return JS::wrap; } + @NoSideEffects public static JSArray wrap(String[] array) { if (array == null) { return null; @@ -516,6 +521,80 @@ final class JS { JSObject d, JSObject e, JSObject f, JSObject g, JSObject h, JSObject i, JSObject j, JSObject k, JSObject l, JSObject m); + @InjectedBy(JSNativeInjector.class) + public static native JSObject apply(JSObject instance, JSObject method, JSArray v); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h, JSObject i); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h, JSObject i, JSObject j); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h, JSObject i, JSObject j, JSObject k); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h, JSObject i, JSObject j, JSObject k, JSObject l); + + @InjectedBy(JSNativeInjector.class) + @PluggableDependency(JSNativeInjector.class) + @NoSideEffects + public static native JSObject arrayOf(JSObject a, JSObject b, JSObject c, JSObject d, JSObject e, JSObject f, + JSObject g, JSObject h, JSObject i, JSObject j, JSObject k, JSObject l, JSObject m); + @InjectedBy(JSNativeInjector.class) @PluggableDependency(JSNativeInjector.class) public static native JSObject construct(JSObject cls); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java index 461d00dea..88eb8494b 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java @@ -769,23 +769,64 @@ class JSClassProcessor { return false; } + var vararg = method.hasModifier(ElementModifier.VARARGS); Variable result = invoke.getReceiver() != null ? program.createVariable() : null; InvokeInstruction newInvoke = new InvokeInstruction(); - newInvoke.setMethod(JSMethods.invoke(method.parameterCount())); + newInvoke.setMethod(vararg ? JSMethods.APPLY : JSMethods.invoke(method.parameterCount())); newInvoke.setType(InvocationType.SPECIAL); newInvoke.setReceiver(result); + List newArguments = new ArrayList<>(); newArguments.add(getCallTarget(invoke)); newArguments.add(marshaller.addStringWrap(marshaller.addString(name, invoke.getLocation()), invoke.getLocation())); newInvoke.setLocation(invoke.getLocation()); + + var callArguments = new ArrayList(); for (int i = 0; i < invoke.getArguments().size(); ++i) { var arg = invoke.getArguments().get(i); + var byRef = byRefParams[i]; + if (vararg && i == invoke.getArguments().size() - 1 + && typeHelper.isSupportedByRefType(method.parameterType(i))) { + byRef = true; + } arg = marshaller.wrapArgument(callLocation, arg, - method.parameterType(i), types.typeOf(arg), byRefParams[i]); - newArguments.add(arg); + method.parameterType(i), types.typeOf(arg), byRef); + callArguments.add(arg); + } + + if (vararg) { + Variable prefixArg = null; + if (callArguments.size() > 1) { + var arrayOfInvocation = new InvokeInstruction(); + arrayOfInvocation.setType(InvocationType.SPECIAL); + arrayOfInvocation.setArguments(callArguments.subList(0, callArguments.size() - 1) + .toArray(new Variable[0])); + arrayOfInvocation.setMethod(JSMethods.arrayOf(callArguments.size() - 1)); + arrayOfInvocation.setReceiver(program.createVariable()); + arrayOfInvocation.setLocation(invoke.getLocation()); + replacement.add(arrayOfInvocation); + prefixArg = arrayOfInvocation.getReceiver(); + } + + var arrayArg = callArguments.get(callArguments.size() - 1); + + if (prefixArg != null) { + var concat = new InvokeInstruction(); + concat.setType(InvocationType.SPECIAL); + concat.setArguments(prefixArg, arrayArg); + concat.setMethod(JSMethods.CONCAT_ARRAY); + concat.setReceiver(program.createVariable()); + concat.setLocation(invoke.getLocation()); + replacement.add(concat); + arrayArg = concat.getReceiver(); + } + newArguments.add(arrayArg); + } else { + newArguments.addAll(callArguments); } newInvoke.setArguments(newArguments.toArray(new Variable[0])); + replacement.add(newInvoke); if (result != null) { result = marshaller.unwrapReturnValue(callLocation, result, method.getResultType(), false, diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java index d0374c378..f17816277 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java @@ -31,10 +31,14 @@ final class JSMethods { JSObject.class, void.class); public static final MethodReference SET_PURE = new MethodReference(JS.class, "setPure", JSObject.class, JSObject.class, JSObject.class, void.class); + public static final MethodReference APPLY = new MethodReference(JS.class, "apply", JSObject.class, JSObject.class, + JSArray.class, JSObject.class); public static final MethodReference FUNCTION = new MethodReference(JS.class, "function", JSObject.class, JSObject.class, JSObject.class); public static final MethodReference ARRAY_DATA = new MethodReference(JS.class, "arrayData", Object.class, JSObject.class); + public static final MethodReference CONCAT_ARRAY = new MethodReference(JS.class, "concatArray", + JSObject.class, JSObject.class, JSObject.class); public static final MethodReference ARRAY_MAPPER = new MethodReference(JS.class, "arrayMapper", JS.WrapFunction.class, JS.WrapFunction.class); public static final MethodReference BOOLEAN_ARRAY_WRAPPER = new MethodReference(JS.class, "booleanArrayWrapper", @@ -111,6 +115,9 @@ final class JSMethods { public static final MethodReference DATA_TO_ARRAY = new MethodReference(JS.class, "dataToArray", JSObject.class, JSObject[].class); + public static final MethodReference WRAP_STRING = new MethodReference(JS.class, "wrap", + String.class, JSObject.class); + public static final MethodReference FUNCTION_AS_OBJECT = new MethodReference(JS.class, "functionAsObject", JSObject.class, JSObject.class, JSObject.class); @@ -122,6 +129,7 @@ final class JSMethods { public static final ValueType JS_ARRAY = ValueType.object(JSArray.class.getName()); private static final MethodReference[] INVOKE_METHODS = new MethodReference[13]; private static final MethodReference[] CONSTRUCT_METHODS = new MethodReference[13]; + private static final MethodReference[] ARRAY_OF_METHODS = new MethodReference[13]; static { for (int i = 0; i < INVOKE_METHODS.length; ++i) { @@ -132,6 +140,10 @@ final class JSMethods { var constructSignature = new ValueType[i + 2]; Arrays.fill(constructSignature, JS_OBJECT); CONSTRUCT_METHODS[i] = new MethodReference(JS.class.getName(), "construct", constructSignature); + + var arrayOfSignature = new ValueType[i + 1]; + Arrays.fill(arrayOfSignature, JS_OBJECT); + ARRAY_OF_METHODS[i] = new MethodReference(JS.class.getName(), "arrayOf", arrayOfSignature); } } @@ -145,4 +157,8 @@ final class JSMethods { public static MethodReference construct(int parameterCount) { return CONSTRUCT_METHODS[parameterCount]; } + + public static MethodReference arrayOf(int parameterCount) { + return ARRAY_OF_METHODS[parameterCount]; + } } diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java index fb621fa4f..71323f586 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java @@ -16,11 +16,17 @@ 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; @@ -38,6 +44,7 @@ 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) { @@ -47,6 +54,13 @@ public class JSNativeInjector implements Injector, DependencyPlugin { 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": context.writeExpr(context.getArgument(0), Precedence.MEMBER_ACCESS); @@ -71,6 +85,19 @@ public class JSNativeInjector implements Injector, DependencyPlugin { } 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("("); @@ -172,6 +199,107 @@ public class JSNativeInjector implements Injector, DependencyPlugin { } } + private void applyFunction(InjectorContext context) { + if (tryApplyFunctionOptimized(context)) { + return; + } + var writer = context.getWriter(); + 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(); + 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(); diff --git a/tests/src/test/java/org/teavm/jso/test/CallTest.java b/tests/src/test/java/org/teavm/jso/test/CallTest.java new file mode 100644 index 000000000..5c6d07eb0 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/CallTest.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024 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.test; + +import static org.junit.Assert.assertEquals; +import org.junit.runner.RunWith; +import org.teavm.jso.JSClass; +import org.teavm.jso.JSMethod; +import org.teavm.jso.JSObject; +import org.teavm.junit.AttachJavaScript; +import org.teavm.junit.EachTestCompiledSeparately; +import org.teavm.junit.OnlyPlatform; +import org.teavm.junit.SkipJVM; +import org.teavm.junit.TeaVMTestRunner; +import org.teavm.junit.TestPlatform; +import org.testng.annotations.Test; + +@RunWith(TeaVMTestRunner.class) +@SkipJVM +@OnlyPlatform(TestPlatform.JAVASCRIPT) +@EachTestCompiledSeparately +public class CallTest { + @Test + @AttachJavaScript("org/teavm/jso/test/vararg.js") + public void simpleVararg() { + assertEquals("va:q:w", TestClass.allVararg("q", "w")); + assertEquals("va:23:42", TestClass.allVarargInt(23, 42)); + + var array = new String[3]; + for (var i = 0; i < array.length; ++i) { + array[i] = String.valueOf((char) ('A' + i)); + } + assertEquals("va:A:B:C", TestClass.allVararg(array)); + + var intArray = new int[3]; + for (var i = 0; i < array.length; ++i) { + intArray[i] = 6 + i; + } + assertEquals("va:6:7:8", TestClass.allVarargInt(intArray)); + + assertEquals("va", TestClass.allVararg()); + assertEquals("va", TestClass.allVarargInt()); + } + + @Test + @AttachJavaScript("org/teavm/jso/test/vararg.js") + public void restVararg() { + assertEquals("a:q,b:23,va:w:e", TestClass.restVararg("q", 23, "w", "e")); + assertEquals("a:23,b:q,va:5:7", TestClass.restVararg(23, "q", 5, 7)); + + assertEquals("a:q,b:23,va", TestClass.restVararg("q", 23)); + assertEquals("a:23,b:q,va", TestClass.restVararg(23, "q")); + + var array = new String[3]; + for (var i = 0; i < array.length; ++i) { + array[i] = String.valueOf((char) ('A' + i)); + } + assertEquals("a:q,b:23,va:A:B:C", TestClass.restVararg("q", 23, array)); + + var intArray = new int[3]; + for (var i = 0; i < array.length; ++i) { + intArray[i] = 6 + i; + } + assertEquals("a:23,b:q,va:6:7:8", TestClass.restVararg(23, "q", intArray)); + } + + @JSClass + public static class TestClass implements JSObject { + public static native String allVararg(String... args); + + @JSMethod("allVararg") + public static native String allVarargInt(int... args); + + public static native String restVararg(String a, int b, String... args); + + public static native String restVararg(int a, String b, int... args); + } +} diff --git a/tests/src/test/resources/org/teavm/jso/test/vararg.js b/tests/src/test/resources/org/teavm/jso/test/vararg.js new file mode 100644 index 000000000..aa97209e8 --- /dev/null +++ b/tests/src/test/resources/org/teavm/jso/test/vararg.js @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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. + */ + +class TestClass { + static allVararg(...args) { + let result = "va"; + for (const arg of args) { + result += ":" + arg; + } + return result; + } + + static restVararg(a, b, ...args) { + let result = `a:${a},b:${b},va`; + for (const arg of args) { + result += ":" + arg; + } + return result; + } +} \ No newline at end of file