From a6fb67817c5bd57c19df2ed1e18fd9831a9d30d6 Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Thu, 4 Apr 2024 21:18:54 +0200 Subject: [PATCH] jso: improve support of instanceof and cast against JS wrapper types Fix #808 --- .../org/teavm/backend/javascript/exception.js | 7 + .../java/org/teavm/jso/core/JSBoolean.java | 2 + .../main/java/org/teavm/jso/core/JSError.java | 22 ++- .../java/org/teavm/jso/core/JSFunction.java | 2 + .../java/org/teavm/jso/core/JSNumber.java | 2 + .../java/org/teavm/jso/core/JSObjects.java | 13 +- .../java/org/teavm/jso/core/JSString.java | 2 + .../java/org/teavm/jso/core/JSSymbol.java | 7 +- .../java/org/teavm/jso/core/JSUndefined.java | 33 ++++ .../src/main/java/org/teavm/jso/JSClass.java | 2 + .../java/org/teavm/jso/JSPrimitiveType.java | 27 ++++ .../src/main/java/org/teavm/jso/impl/JS.java | 12 ++ .../org/teavm/jso/impl/JSClassProcessor.java | 144 +++++++++++++++--- .../java/org/teavm/jso/impl/JSMethods.java | 7 + .../org/teavm/jso/impl/JSNativeInjector.java | 35 +++++ .../org/teavm/jso/impl/JSValueMarshaller.java | 31 +--- .../java/org/teavm/jso/impl/JSWrapper.java | 46 ++++-- .../org/teavm/jso/test/InstanceOfTest.java | 118 ++++++++++++++ .../org/teavm/jso/test/JSWrapperTest.java | 7 +- 19 files changed, 445 insertions(+), 74 deletions(-) create mode 100644 jso/apis/src/main/java/org/teavm/jso/core/JSUndefined.java create mode 100644 jso/core/src/main/java/org/teavm/jso/JSPrimitiveType.java create mode 100644 tests/src/test/java/org/teavm/jso/test/InstanceOfTest.java diff --git a/core/src/main/resources/org/teavm/backend/javascript/exception.js b/core/src/main/resources/org/teavm/backend/javascript/exception.js index 7d2aa1609..c442be9c7 100644 --- a/core/src/main/resources/org/teavm/backend/javascript/exception.js +++ b/core/src/main/resources/org/teavm/backend/javascript/exception.js @@ -125,6 +125,13 @@ let $rt_throwCCE = () => teavm_javaConstructorExists("java.lang.ClassCastExcepti ? $rt_throw(teavm_javaConstructor("java.lang.ClassCastException", "()V")()) : $rt_throw($rt_createException($rt_str(""))); +let $rt_throwCCEIfFalse = (value, o) => { + if (!value) { + $rt_throwCCE(); + } + return o; +} + let $rt_createStackElement = (className, methodName, fileName, lineNumber) => { if (teavm_javaConstructorExists("java.lang.StackTraceElement", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V")) { diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSBoolean.java b/jso/apis/src/main/java/org/teavm/jso/core/JSBoolean.java index d9c445eb4..00d0eb35f 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSBoolean.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSBoolean.java @@ -18,7 +18,9 @@ package org.teavm.jso.core; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; +@JSPrimitiveType("boolean") public abstract class JSBoolean implements JSObject { private JSBoolean() { } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSError.java b/jso/apis/src/main/java/org/teavm/jso/core/JSError.java index 77e86ec44..72136dab7 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSError.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSError.java @@ -16,11 +16,19 @@ package org.teavm.jso.core; import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; import org.teavm.jso.JSFunctor; import org.teavm.jso.JSObject; import org.teavm.jso.JSProperty; -public abstract class JSError implements JSObject { +@JSClass(name = "Error") +public class JSError implements JSObject { + public JSError() { + } + + public JSError(String message) { + } + @JSBody(params = { "tryClause", "catchClause" }, script = "" + "try {" + "return tryClause();" @@ -29,17 +37,19 @@ public abstract class JSError implements JSObject { + "}") public static native T catchNative(TryClause tryClause, CatchClause catchClause); - @JSBody(params = "object", script = "return object instanceof Error;") - public static native boolean isError(JSObject object); + @Deprecated + public static boolean isError(JSObject object) { + return object instanceof JSError; + } @JSProperty - public abstract String getStack(); + public native String getStack(); @JSProperty - public abstract String getMessage(); + public native String getMessage(); @JSProperty - public abstract String getName(); + public native String getName(); @JSFunctor public interface TryClause extends JSObject { diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSFunction.java b/jso/apis/src/main/java/org/teavm/jso/core/JSFunction.java index 33eb3d1e3..2cf1e94b4 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSFunction.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSFunction.java @@ -17,8 +17,10 @@ package org.teavm.jso.core; import org.teavm.jso.JSByRef; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; import org.teavm.jso.JSProperty; +@JSPrimitiveType("function") public abstract class JSFunction implements JSObject { @JSProperty public abstract int getLength(); diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSNumber.java b/jso/apis/src/main/java/org/teavm/jso/core/JSNumber.java index 5e8a49fbf..9fd79e60b 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSNumber.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSNumber.java @@ -18,7 +18,9 @@ package org.teavm.jso.core; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; +@JSPrimitiveType("number") public abstract class JSNumber implements JSObject { private JSNumber() { } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java b/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java index e33d08aa3..d1fe84407 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSObjects.java @@ -43,13 +43,14 @@ public final class JSObjects { @NoSideEffects public static native T createWithoutProto(); - @JSBody(params = "object", script = "return typeof object === 'undefined';") - @NoSideEffects - public static native boolean isUndefined(Object object); + public static boolean isUndefined(Object object) { + return object instanceof JSUndefined; + } - @JSBody(script = "return void 0;") - @NoSideEffects - public static native JSObject undefined(); + @Deprecated + public static JSObject undefined() { + return JSUndefined.instance(); + } @JSBody(params = "object", script = "return typeof object;") @NoSideEffects diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSString.java b/jso/apis/src/main/java/org/teavm/jso/core/JSString.java index 301746ef8..451320eb1 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSString.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSString.java @@ -18,8 +18,10 @@ package org.teavm.jso.core; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; import org.teavm.jso.JSProperty; +@JSPrimitiveType("string") public abstract class JSString implements JSObject { private JSString() { } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java index ce34f8fb5..7a44c6ada 100644 --- a/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java @@ -17,8 +17,11 @@ package org.teavm.jso.core; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; +import org.teavm.jso.JSIndexer; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; +@JSPrimitiveType("symbol") public class JSSymbol implements JSObject { private JSSymbol() { } @@ -26,10 +29,10 @@ public class JSSymbol implements JSObject { @JSBody(params = "name", script = "return Symbol(name);") public static native JSSymbol create(String name); - @JSBody(params = "obj", script = "return obj[this];") + @JSIndexer public native T get(Object obj); - @JSBody(params = { "obj", "value" }, script = "obj[this] = value;") + @JSIndexer @NoSideEffects public native void set(Object obj, T value); } diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSUndefined.java b/jso/apis/src/main/java/org/teavm/jso/core/JSUndefined.java new file mode 100644 index 000000000..b057eec53 --- /dev/null +++ b/jso/apis/src/main/java/org/teavm/jso/core/JSUndefined.java @@ -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. + */ +package org.teavm.jso.core; + +import org.teavm.interop.NoSideEffects; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; + +@JSClass +@JSPrimitiveType("undefined") +public class JSUndefined implements JSObject { + private JSUndefined() { + } + + @JSBody(script = "return void 0;") + @NoSideEffects + public static native JSUndefined instance(); +} diff --git a/jso/core/src/main/java/org/teavm/jso/JSClass.java b/jso/core/src/main/java/org/teavm/jso/JSClass.java index bffcab2ec..0630b871a 100644 --- a/jso/core/src/main/java/org/teavm/jso/JSClass.java +++ b/jso/core/src/main/java/org/teavm/jso/JSClass.java @@ -24,4 +24,6 @@ import java.lang.annotation.Target; @Target(ElementType.TYPE) public @interface JSClass { String name() default ""; + + boolean transparent() default false; } diff --git a/jso/core/src/main/java/org/teavm/jso/JSPrimitiveType.java b/jso/core/src/main/java/org/teavm/jso/JSPrimitiveType.java new file mode 100644 index 000000000..d4e45bdfb --- /dev/null +++ b/jso/core/src/main/java/org/teavm/jso/JSPrimitiveType.java @@ -0,0 +1,27 @@ +/* + * 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface JSPrimitiveType { + String value(); +} 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 11eabb122..16ffc22c3 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 @@ -704,4 +704,16 @@ final class JS { @InjectedBy(JSNativeInjector.class) @NoSideEffects public static native JSObject importModule(String name); + + @InjectedBy(JSNativeInjector.class) + @NoSideEffects + public static native boolean instanceOf(JSObject obj, JSObject cls); + + @InjectedBy(JSNativeInjector.class) + @NoSideEffects + public static native boolean isPrimitive(JSObject obj, JSObject primitive); + + @InjectedBy(JSNativeInjector.class) + @NoSideEffects + public static native JSObject throwCCEIfFalse(boolean value, JSObject o); } 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 d791979db..90ec61bf5 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 @@ -35,8 +35,10 @@ import org.teavm.diagnostics.Diagnostics; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; import org.teavm.jso.JSByRef; +import org.teavm.jso.JSClass; import org.teavm.jso.JSFunctor; import org.teavm.jso.JSObject; +import org.teavm.jso.JSPrimitiveType; import org.teavm.model.AnnotationContainerReader; import org.teavm.model.AnnotationHolder; import org.teavm.model.AnnotationReader; @@ -86,6 +88,10 @@ class JSClassProcessor { Object.class, JSObject.class); private static final MethodReference IS_JS = new MethodReference(JSWrapper.class, "isJs", Object.class, boolean.class); + private static final MethodReference IS_PRIMITIVE = new MethodReference(JSWrapper.class, "isPrimitive", + Object.class, JSObject.class, boolean.class); + private static final MethodReference INSTANCE_OF = new MethodReference(JSWrapper.class, "instanceOf", + Object.class, JSObject.class, boolean.class); private final ClassReaderSource classSource; private final JSBodyRepository repository; private final JavaInvocationProcessor javaInvocationProcessor; @@ -470,11 +476,23 @@ class JSClassProcessor { ClassReader targetClass = classSource.get(targetClassName); if (targetClass.getAnnotations().get(JSFunctor.class.getName()) == null) { - AssignInstruction assign = new AssignInstruction(); - assign.setLocation(location.getSourceLocation()); - assign.setAssignee(cast.getValue()); - assign.setReceiver(cast.getReceiver()); - replacement.add(assign); + if (isTransparent(targetClassName)) { + var assign = new AssignInstruction(); + assign.setLocation(location.getSourceLocation()); + assign.setAssignee(cast.getValue()); + assign.setReceiver(cast.getReceiver()); + replacement.add(assign); + } else { + var instanceOfResult = program.createVariable(); + processIsInstanceUnwrapped(cast.getLocation(), cast.getValue(), targetClassName, instanceOfResult); + + var invoke = new InvokeInstruction(); + invoke.setType(InvocationType.SPECIAL); + invoke.setMethod(JSMethods.THROW_CCE_IF_FALSE); + invoke.setArguments(instanceOfResult, cast.getValue()); + invoke.setReceiver(cast.getReceiver()); + replacement.add(invoke); + } return true; } @@ -499,23 +517,107 @@ class JSClassProcessor { return; } - var type = types.typeOf(isInstance.getValue()); - if (type == JSType.JS) { - var replacement = new IntegerConstantInstruction(); - replacement.setConstant(1); - replacement.setReceiver(isInstance.getReceiver()); - replacement.setLocation(isInstance.getLocation()); - isInstance.replace(replacement); - return; - } + replacement.clear(); + processIsInstance(isInstance.getLocation(), types.typeOf(isInstance.getValue()), isInstance.getValue(), + targetClassName, isInstance.getReceiver()); + isInstance.insertPreviousAll(replacement); + isInstance.delete(); + replacement.clear(); + } - var replacement = new InvokeInstruction(); - replacement.setType(InvocationType.SPECIAL); - replacement.setMethod(IS_JS); - replacement.setArguments(isInstance.getValue()); - replacement.setReceiver(isInstance.getReceiver()); - replacement.setLocation(isInstance.getLocation()); - isInstance.replace(replacement); + private void processIsInstance(TextLocation location, JSType type, Variable value, String targetClassName, + Variable receiver) { + if (type == JSType.JS) { + if (isTransparent(targetClassName)) { + var cst = new IntegerConstantInstruction(); + cst.setConstant(1); + cst.setReceiver(receiver); + cst.setLocation(location); + replacement.add(cst); + } else { + var primitiveType = getPrimitiveType(targetClassName); + var invoke = new InvokeInstruction(); + invoke.setType(InvocationType.SPECIAL); + invoke.setMethod(primitiveType != null ? JSMethods.IS_PRIMITIVE : JSMethods.INSTANCE_OF); + var secondArg = primitiveType != null + ? marshaller.addJsString(primitiveType, location) + : marshaller.classRef(targetClassName, location); + invoke.setArguments(value, secondArg); + invoke.setReceiver(receiver); + invoke.setLocation(location); + replacement.add(invoke); + } + } else { + if (isTransparent(targetClassName)) { + var invoke = new InvokeInstruction(); + invoke.setType(InvocationType.SPECIAL); + invoke.setMethod(IS_JS); + invoke.setArguments(value); + invoke.setReceiver(receiver); + invoke.setLocation(location); + replacement.add(invoke); + } else { + var primitiveType = getPrimitiveType(targetClassName); + var invoke = new InvokeInstruction(); + invoke.setType(InvocationType.SPECIAL); + invoke.setMethod(primitiveType != null ? IS_PRIMITIVE : INSTANCE_OF); + var secondArg = primitiveType != null + ? marshaller.addJsString(primitiveType, location) + : marshaller.classRef(targetClassName, location); + invoke.setArguments(value, secondArg); + invoke.setReceiver(receiver); + invoke.setLocation(location); + replacement.add(invoke); + } + } + } + + private void processIsInstanceUnwrapped(TextLocation location, Variable value, String targetClassName, + Variable receiver) { + var primitiveType = getPrimitiveType(targetClassName); + var invoke = new InvokeInstruction(); + invoke.setType(InvocationType.SPECIAL); + invoke.setMethod(primitiveType != null ? JSMethods.IS_PRIMITIVE : JSMethods.INSTANCE_OF); + var secondArg = primitiveType != null + ? marshaller.addJsString(primitiveType, location) + : marshaller.classRef(targetClassName, location); + invoke.setArguments(value, secondArg); + invoke.setReceiver(receiver); + invoke.setLocation(location); + replacement.add(invoke); + } + + private boolean isTransparent(String className) { + var cls = classSource.get(className); + if (cls == null) { + return true; + } + if (cls.hasModifier(ElementModifier.INTERFACE)) { + return true; + } + var clsAnnot = cls.getAnnotations().get(JSClass.class.getName()); + if (clsAnnot != null) { + var transparent = clsAnnot.getValue("transparent"); + if (transparent != null) { + return transparent.getBoolean(); + } + } + return false; + } + + private String getPrimitiveType(String className) { + var cls = classSource.get(className); + if (cls == null) { + return null; + } + var clsAnnot = cls.getAnnotations().get(JSPrimitiveType.class.getName()); + if (clsAnnot != null) { + var value = clsAnnot.getValue("value"); + if (value != null) { + return value.getString(); + } + } + return null; } private Variable wrapJsAsJava(Instruction instruction, Variable var, ValueType type) { 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 4633cb7f9..1fd64968a 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 @@ -125,6 +125,13 @@ final class JSMethods { public static final MethodReference IMPORT_MODULE = new MethodReference(JS.class, "importModule", String.class, JSObject.class); + public static final MethodReference INSTANCE_OF = new MethodReference(JS.class, "instanceOf", JSObject.class, + JSObject.class, boolean.class); + public static final MethodReference IS_PRIMITIVE = new MethodReference(JS.class, "isPrimitive", JSObject.class, + JSObject.class, boolean.class); + public static final MethodReference THROW_CCE_IF_FALSE = new MethodReference(JS.class, "throwCCEIfFalse", + boolean.class, JSObject.class, JSObject.class); + public static final ValueType JS_OBJECT = ValueType.object(JSObject.class.getName()); public static final ValueType OBJECT = ValueType.object("java.lang.Object"); public static final ValueType JS_ARRAY = ValueType.object(JSArray.class.getName()); 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 71323f586..9aa5faee9 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 @@ -190,6 +190,41 @@ public class JSNativeInjector implements Injector, DependencyPlugin { 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 "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; + } default: if (methodRef.getName().startsWith("unwrap")) { diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java index b8c4e0521..6842b66e0 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java @@ -550,7 +550,7 @@ class JSValueMarshaller { Variable addString(String str, TextLocation location) { Variable var = program.createVariable(); - StringConstantInstruction nameInsn = new StringConstantInstruction(); + var nameInsn = new StringConstantInstruction(); nameInsn.setReceiver(var); nameInsn.setConstant(str); nameInsn.setLocation(location); @@ -558,6 +558,10 @@ class JSValueMarshaller { return var; } + Variable addJsString(String str, TextLocation location) { + return addStringWrap(addString(str, location), location); + } + Variable classRef(String className, TextLocation location) { String name = null; String module = null; @@ -586,16 +590,10 @@ class JSValueMarshaller { } Variable globalRef(String name, TextLocation location) { - var nameInsn = new StringConstantInstruction(); - nameInsn.setReceiver(program.createVariable()); - nameInsn.setConstant(name); - nameInsn.setLocation(location); - replacement.add(nameInsn); - var invoke = new InvokeInstruction(); invoke.setType(InvocationType.SPECIAL); invoke.setMethod(JSMethods.GLOBAL); - invoke.setArguments(nameInsn.getReceiver()); + invoke.setArguments(addString(name, location)); invoke.setReceiver(program.createVariable()); invoke.setLocation(location); replacement.add(invoke); @@ -618,26 +616,11 @@ class JSValueMarshaller { invoke.setLocation(location); replacement.add(invoke); - var nameInsn = new StringConstantInstruction(); - nameInsn.setReceiver(program.createVariable()); - nameInsn.setConstant(name); - nameInsn.setLocation(location); - replacement.add(nameInsn); - - var wrapName = new InvokeInstruction(); - wrapName.setType(InvocationType.SPECIAL); - wrapName.setMethod(referenceCache.getCached(new MethodReference(JS.class, "wrap", - String.class, JSObject.class))); - wrapName.setReceiver(program.createVariable()); - wrapName.setArguments(nameInsn.getReceiver()); - wrapName.setLocation(location); - replacement.add(wrapName); - var get = new InvokeInstruction(); get.setType(InvocationType.SPECIAL); get.setMethod(JSMethods.GET_PURE); get.setReceiver(program.createVariable()); - get.setArguments(invoke.getReceiver(), wrapName.getReceiver()); + get.setArguments(invoke.getReceiver(), addJsString(name, location)); get.setLocation(location); replacement.add(get); diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java index 04d76feb8..46122b4ae 100644 --- a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java +++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java @@ -17,6 +17,7 @@ package org.teavm.jso.impl; import org.teavm.interop.NoSideEffects; import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; import org.teavm.jso.JSObject; import org.teavm.jso.core.JSBoolean; import org.teavm.jso.core.JSFinalizationRegistry; @@ -24,11 +25,12 @@ import org.teavm.jso.core.JSMap; import org.teavm.jso.core.JSNumber; import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSString; +import org.teavm.jso.core.JSUndefined; import org.teavm.jso.core.JSWeakMap; import org.teavm.jso.core.JSWeakRef; public final class JSWrapper { - private static final JSWeakMap hashCodes = new JSWeakMap<>(); + private static final JSWeakMap hashCodes = new JSWeakMap<>(); private static final JSWeakMap> wrappers = JSWeakRef.isSupported() ? new JSWeakMap<>() : null; private static final JSMap> stringWrappers = JSWeakRef.isSupported() @@ -68,8 +70,8 @@ public final class JSWrapper { if (wrappers != null) { if (isObject) { var existingRef = get(wrappers, js); - var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); - if (JSObjects.isUndefined(existing)) { + var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance(); + if (isUndefined(existing)) { var wrapper = new JSWrapper(js); set(wrappers, js, createWeakRef(wrapperToJs(wrapper))); return wrapper; @@ -79,8 +81,8 @@ public final class JSWrapper { } else if (type.equals("string")) { var jsString = (JSString) js; var existingRef = get(stringWrappers, jsString); - var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); - if (JSObjects.isUndefined(existing)) { + var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance(); + if (isUndefined(existing)) { var wrapper = new JSWrapper(js); var wrapperAsJs = wrapperToJs(wrapper); set(stringWrappers, jsString, createWeakRef(wrapperAsJs)); @@ -92,8 +94,8 @@ public final class JSWrapper { } else if (type.equals("number")) { var jsNumber = (JSNumber) js; var existingRef = get(numberWrappers, jsNumber); - var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); - if (JSObjects.isUndefined(existing)) { + var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance(); + if (isUndefined(existing)) { var wrapper = new JSWrapper(js); var wrapperAsJs = wrapperToJs(wrapper); set(numberWrappers, jsNumber, createWeakRef(wrapperAsJs)); @@ -104,8 +106,8 @@ public final class JSWrapper { } } else if (type.equals("undefined")) { var existingRef = undefinedWrapper; - var existing = existingRef != null ? deref(existingRef) : JSObjects.undefined(); - if (JSObjects.isUndefined(existing)) { + var existing = existingRef != null ? deref(existingRef) : JSUndefined.instance(); + if (isUndefined(existing)) { var wrapper = new JSWrapper(js); var wrapperAsJs = wrapperToJs(wrapper); undefinedWrapper = createWeakRef(wrapperAsJs); @@ -215,13 +217,21 @@ public final class JSWrapper { return !isJava(o) || o instanceof JSWrapper; } + public static boolean isPrimitive(Object o, JSObject primitive) { + return isJs(o) && JS.isPrimitive(maybeUnwrap(o), primitive); + } + + public static boolean instanceOf(Object o, JSObject type) { + return isJs(o) && JS.instanceOf(maybeUnwrap(o), type); + } + @Override public int hashCode() { var type = JSObjects.typeOf(js); if (type.equals("object") || type.equals("symbol") || type.equals("function")) { var code = hashCodes.get(js); - if (JSObjects.isUndefined(code)) { - code = JSNumber.valueOf(++hashCodeGen); + if (isUndefined(code)) { + code = JSTransparentInt.valueOf(++hashCodeGen); hashCodes.set(js, code); } return code.intValue(); @@ -263,6 +273,18 @@ public final class JSWrapper { @Override public String toString() { - return JSObjects.isUndefined(js) ? "undefined" : JSObjects.toString(js); + return isUndefined(js) ? "undefined" : JSObjects.toString(js); } + + @JSClass(transparent = true) + static abstract class JSTransparentInt implements JSObject { + @JSBody(script = "return this;") + native int intValue(); + + @JSBody(params = "value", script = "return value;") + static native JSTransparentInt valueOf(int value); + } + + @JSBody(params = "obj", script = "return typeof obj == 'undefined'") + private static native boolean isUndefined(JSObject obj); } diff --git a/tests/src/test/java/org/teavm/jso/test/InstanceOfTest.java b/tests/src/test/java/org/teavm/jso/test/InstanceOfTest.java new file mode 100644 index 000000000..82b7bc1e5 --- /dev/null +++ b/tests/src/test/java/org/teavm/jso/test/InstanceOfTest.java @@ -0,0 +1,118 @@ +/* + * 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 static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.jso.JSBody; +import org.teavm.jso.JSClass; +import org.teavm.jso.JSObject; +import org.teavm.jso.JSProperty; +import org.teavm.jso.core.JSNumber; +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; + +@RunWith(TeaVMTestRunner.class) +@SkipJVM +@OnlyPlatform(TestPlatform.JAVASCRIPT) +@EachTestCompiledSeparately +public class InstanceOfTest { + @Test + @AttachJavaScript("org/teavm/jso/test/classWithConstructor.js") + public void instanceOf() { + var a = createClassWithConstructor(); + assertTrue(a instanceof ClassWithConstructor); + assertFalse(a instanceof JSNumber); + assertTrue(a instanceof I); + assertTrue(a instanceof C); + + var b = callCreateClassWithConstructor(); + assertTrue(b instanceof ClassWithConstructor); + assertFalse(b instanceof JSNumber); + assertTrue(b instanceof I); + assertTrue(b instanceof C); + + var c = createNumber(); + assertFalse(c instanceof ClassWithConstructor); + assertTrue(c instanceof JSNumber); + assertTrue(c instanceof I); + assertTrue(c instanceof C); + + var d = callCreateNumber(); + assertFalse(d instanceof ClassWithConstructor); + assertTrue(d instanceof JSNumber); + assertTrue(d instanceof I); + assertTrue(d instanceof C); + } + + @Test + @AttachJavaScript("org/teavm/jso/test/classWithConstructor.js") + public void cast() { + var a = createClassWithConstructor(); + assertEquals(99, ((ClassWithConstructor) a).getFoo()); + + try { + assertEquals(99, ((JSNumber) a).intValue()); + fail("CCE not thrown"); + } catch (ClassCastException e) { + // expected + } + assertEquals(99, ((I) a).getFoo()); + assertEquals(99, ((C) a).getFoo()); + + var c = createNumber(); + assertEquals(23, ((JSNumber) c).intValue()); + try { + assertEquals(99, ((ClassWithConstructor) c).getFoo()); + fail("CCE not thrown"); + } catch (ClassCastException e) { + // expected + } + } + + private Object callCreateClassWithConstructor() { + return createClassWithConstructor(); + } + + @JSBody(script = "return new ClassWithConstructor();") + private static native JSObject createClassWithConstructor(); + + private Object callCreateNumber() { + return createNumber(); + } + + @JSBody(script = "return 23;") + private static native JSObject createNumber(); + + interface I extends JSObject { + @JSProperty + int getFoo(); + } + + @JSClass(transparent = true) + abstract class C implements JSObject { + @JSProperty + public abstract int getFoo(); + } +} diff --git a/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java index 611360f35..cf4578586 100644 --- a/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java +++ b/tests/src/test/java/org/teavm/jso/test/JSWrapperTest.java @@ -32,6 +32,7 @@ import org.teavm.jso.JSObject; import org.teavm.jso.core.JSNumber; import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSString; +import org.teavm.jso.core.JSUndefined; import org.teavm.junit.EachTestCompiledSeparately; import org.teavm.junit.OnlyPlatform; import org.teavm.junit.SkipJVM; @@ -313,10 +314,10 @@ public class JSWrapperTest { @Test public void wrapUndefined() { - field1 = JSObjects.undefined(); + field1 = JSUndefined.instance(); assertEquals("undefined", field1.toString()); - assertEquals(JSObjects.undefined(), field1); - assertSame(JSObjects.undefined(), field1); + assertEquals(JSUndefined.instance(), field1); + assertSame(JSUndefined.instance(), field1); assertTrue(field1 instanceof JSObject); assertTrue(JSObjects.isUndefined(field1)); }