jso: improve support of instanceof and cast against JS wrapper types

Fix #808
This commit is contained in:
Alexey Andreev 2024-04-04 21:18:54 +02:00
parent e069bc3a44
commit a6fb67817c
19 changed files with 445 additions and 74 deletions

View File

@ -125,6 +125,13 @@ let $rt_throwCCE = () => teavm_javaConstructorExists("java.lang.ClassCastExcepti
? $rt_throw(teavm_javaConstructor("java.lang.ClassCastException", "()V")()) ? $rt_throw(teavm_javaConstructor("java.lang.ClassCastException", "()V")())
: $rt_throw($rt_createException($rt_str(""))); : $rt_throw($rt_createException($rt_str("")));
let $rt_throwCCEIfFalse = (value, o) => {
if (!value) {
$rt_throwCCE();
}
return o;
}
let $rt_createStackElement = (className, methodName, fileName, lineNumber) => { let $rt_createStackElement = (className, methodName, fileName, lineNumber) => {
if (teavm_javaConstructorExists("java.lang.StackTraceElement", if (teavm_javaConstructorExists("java.lang.StackTraceElement",
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V")) { "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V")) {

View File

@ -18,7 +18,9 @@ package org.teavm.jso.core;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
@JSPrimitiveType("boolean")
public abstract class JSBoolean implements JSObject { public abstract class JSBoolean implements JSObject {
private JSBoolean() { private JSBoolean() {
} }

View File

@ -16,11 +16,19 @@
package org.teavm.jso.core; package org.teavm.jso.core;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty; 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 = "" @JSBody(params = { "tryClause", "catchClause" }, script = ""
+ "try {" + "try {"
+ "return tryClause();" + "return tryClause();"
@ -29,17 +37,19 @@ public abstract class JSError implements JSObject {
+ "}") + "}")
public static native <T> T catchNative(TryClause<T> tryClause, CatchClause<T> catchClause); public static native <T> T catchNative(TryClause<T> tryClause, CatchClause<T> catchClause);
@JSBody(params = "object", script = "return object instanceof Error;") @Deprecated
public static native boolean isError(JSObject object); public static boolean isError(JSObject object) {
return object instanceof JSError;
}
@JSProperty @JSProperty
public abstract String getStack(); public native String getStack();
@JSProperty @JSProperty
public abstract String getMessage(); public native String getMessage();
@JSProperty @JSProperty
public abstract String getName(); public native String getName();
@JSFunctor @JSFunctor
public interface TryClause<T> extends JSObject { public interface TryClause<T> extends JSObject {

View File

@ -17,8 +17,10 @@ package org.teavm.jso.core;
import org.teavm.jso.JSByRef; import org.teavm.jso.JSByRef;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;
@JSPrimitiveType("function")
public abstract class JSFunction implements JSObject { public abstract class JSFunction implements JSObject {
@JSProperty @JSProperty
public abstract int getLength(); public abstract int getLength();

View File

@ -18,7 +18,9 @@ package org.teavm.jso.core;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
@JSPrimitiveType("number")
public abstract class JSNumber implements JSObject { public abstract class JSNumber implements JSObject {
private JSNumber() { private JSNumber() {
} }

View File

@ -43,13 +43,14 @@ public final class JSObjects {
@NoSideEffects @NoSideEffects
public static native <T extends JSObject> T createWithoutProto(); public static native <T extends JSObject> T createWithoutProto();
@JSBody(params = "object", script = "return typeof object === 'undefined';") public static boolean isUndefined(Object object) {
@NoSideEffects return object instanceof JSUndefined;
public static native boolean isUndefined(Object object); }
@JSBody(script = "return void 0;") @Deprecated
@NoSideEffects public static JSObject undefined() {
public static native JSObject undefined(); return JSUndefined.instance();
}
@JSBody(params = "object", script = "return typeof object;") @JSBody(params = "object", script = "return typeof object;")
@NoSideEffects @NoSideEffects

View File

@ -18,8 +18,10 @@ package org.teavm.jso.core;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
import org.teavm.jso.JSProperty; import org.teavm.jso.JSProperty;
@JSPrimitiveType("string")
public abstract class JSString implements JSObject { public abstract class JSString implements JSObject {
private JSString() { private JSString() {
} }

View File

@ -17,8 +17,11 @@ package org.teavm.jso.core;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSIndexer;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
@JSPrimitiveType("symbol")
public class JSSymbol<T> implements JSObject { public class JSSymbol<T> implements JSObject {
private JSSymbol() { private JSSymbol() {
} }
@ -26,10 +29,10 @@ public class JSSymbol<T> implements JSObject {
@JSBody(params = "name", script = "return Symbol(name);") @JSBody(params = "name", script = "return Symbol(name);")
public static native <T> JSSymbol<T> create(String name); public static native <T> JSSymbol<T> create(String name);
@JSBody(params = "obj", script = "return obj[this];") @JSIndexer
public native T get(Object obj); public native T get(Object obj);
@JSBody(params = { "obj", "value" }, script = "obj[this] = value;") @JSIndexer
@NoSideEffects @NoSideEffects
public native void set(Object obj, T value); public native void set(Object obj, T value);
} }

View File

@ -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();
}

View File

@ -24,4 +24,6 @@ import java.lang.annotation.Target;
@Target(ElementType.TYPE) @Target(ElementType.TYPE)
public @interface JSClass { public @interface JSClass {
String name() default ""; String name() default "";
boolean transparent() default false;
} }

View File

@ -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();
}

View File

@ -704,4 +704,16 @@ final class JS {
@InjectedBy(JSNativeInjector.class) @InjectedBy(JSNativeInjector.class)
@NoSideEffects @NoSideEffects
public static native JSObject importModule(String name); 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);
} }

View File

@ -35,8 +35,10 @@ import org.teavm.diagnostics.Diagnostics;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSByRef; import org.teavm.jso.JSByRef;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSFunctor; import org.teavm.jso.JSFunctor;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.JSPrimitiveType;
import org.teavm.model.AnnotationContainerReader; import org.teavm.model.AnnotationContainerReader;
import org.teavm.model.AnnotationHolder; import org.teavm.model.AnnotationHolder;
import org.teavm.model.AnnotationReader; import org.teavm.model.AnnotationReader;
@ -86,6 +88,10 @@ class JSClassProcessor {
Object.class, JSObject.class); Object.class, JSObject.class);
private static final MethodReference IS_JS = new MethodReference(JSWrapper.class, "isJs", private static final MethodReference IS_JS = new MethodReference(JSWrapper.class, "isJs",
Object.class, boolean.class); 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 ClassReaderSource classSource;
private final JSBodyRepository repository; private final JSBodyRepository repository;
private final JavaInvocationProcessor javaInvocationProcessor; private final JavaInvocationProcessor javaInvocationProcessor;
@ -470,11 +476,23 @@ class JSClassProcessor {
ClassReader targetClass = classSource.get(targetClassName); ClassReader targetClass = classSource.get(targetClassName);
if (targetClass.getAnnotations().get(JSFunctor.class.getName()) == null) { if (targetClass.getAnnotations().get(JSFunctor.class.getName()) == null) {
AssignInstruction assign = new AssignInstruction(); if (isTransparent(targetClassName)) {
assign.setLocation(location.getSourceLocation()); var assign = new AssignInstruction();
assign.setAssignee(cast.getValue()); assign.setLocation(location.getSourceLocation());
assign.setReceiver(cast.getReceiver()); assign.setAssignee(cast.getValue());
replacement.add(assign); 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; return true;
} }
@ -499,23 +517,107 @@ class JSClassProcessor {
return; return;
} }
var type = types.typeOf(isInstance.getValue()); replacement.clear();
if (type == JSType.JS) { processIsInstance(isInstance.getLocation(), types.typeOf(isInstance.getValue()), isInstance.getValue(),
var replacement = new IntegerConstantInstruction(); targetClassName, isInstance.getReceiver());
replacement.setConstant(1); isInstance.insertPreviousAll(replacement);
replacement.setReceiver(isInstance.getReceiver()); isInstance.delete();
replacement.setLocation(isInstance.getLocation()); replacement.clear();
isInstance.replace(replacement); }
return;
}
var replacement = new InvokeInstruction(); private void processIsInstance(TextLocation location, JSType type, Variable value, String targetClassName,
replacement.setType(InvocationType.SPECIAL); Variable receiver) {
replacement.setMethod(IS_JS); if (type == JSType.JS) {
replacement.setArguments(isInstance.getValue()); if (isTransparent(targetClassName)) {
replacement.setReceiver(isInstance.getReceiver()); var cst = new IntegerConstantInstruction();
replacement.setLocation(isInstance.getLocation()); cst.setConstant(1);
isInstance.replace(replacement); 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) { private Variable wrapJsAsJava(Instruction instruction, Variable var, ValueType type) {

View File

@ -125,6 +125,13 @@ final class JSMethods {
public static final MethodReference IMPORT_MODULE = new MethodReference(JS.class, "importModule", public static final MethodReference IMPORT_MODULE = new MethodReference(JS.class, "importModule",
String.class, JSObject.class); 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 JS_OBJECT = ValueType.object(JSObject.class.getName());
public static final ValueType OBJECT = ValueType.object("java.lang.Object"); public static final ValueType OBJECT = ValueType.object("java.lang.Object");
public static final ValueType JS_ARRAY = ValueType.object(JSArray.class.getName()); public static final ValueType JS_ARRAY = ValueType.object(JSArray.class.getName());

View File

@ -190,6 +190,41 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
writer.appendFunction(context.importModule(name)); writer.appendFunction(context.importModule(name));
break; 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: default:
if (methodRef.getName().startsWith("unwrap")) { if (methodRef.getName().startsWith("unwrap")) {

View File

@ -550,7 +550,7 @@ class JSValueMarshaller {
Variable addString(String str, TextLocation location) { Variable addString(String str, TextLocation location) {
Variable var = program.createVariable(); Variable var = program.createVariable();
StringConstantInstruction nameInsn = new StringConstantInstruction(); var nameInsn = new StringConstantInstruction();
nameInsn.setReceiver(var); nameInsn.setReceiver(var);
nameInsn.setConstant(str); nameInsn.setConstant(str);
nameInsn.setLocation(location); nameInsn.setLocation(location);
@ -558,6 +558,10 @@ class JSValueMarshaller {
return var; return var;
} }
Variable addJsString(String str, TextLocation location) {
return addStringWrap(addString(str, location), location);
}
Variable classRef(String className, TextLocation location) { Variable classRef(String className, TextLocation location) {
String name = null; String name = null;
String module = null; String module = null;
@ -586,16 +590,10 @@ class JSValueMarshaller {
} }
Variable globalRef(String name, TextLocation location) { 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(); var invoke = new InvokeInstruction();
invoke.setType(InvocationType.SPECIAL); invoke.setType(InvocationType.SPECIAL);
invoke.setMethod(JSMethods.GLOBAL); invoke.setMethod(JSMethods.GLOBAL);
invoke.setArguments(nameInsn.getReceiver()); invoke.setArguments(addString(name, location));
invoke.setReceiver(program.createVariable()); invoke.setReceiver(program.createVariable());
invoke.setLocation(location); invoke.setLocation(location);
replacement.add(invoke); replacement.add(invoke);
@ -618,26 +616,11 @@ class JSValueMarshaller {
invoke.setLocation(location); invoke.setLocation(location);
replacement.add(invoke); 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(); var get = new InvokeInstruction();
get.setType(InvocationType.SPECIAL); get.setType(InvocationType.SPECIAL);
get.setMethod(JSMethods.GET_PURE); get.setMethod(JSMethods.GET_PURE);
get.setReceiver(program.createVariable()); get.setReceiver(program.createVariable());
get.setArguments(invoke.getReceiver(), wrapName.getReceiver()); get.setArguments(invoke.getReceiver(), addJsString(name, location));
get.setLocation(location); get.setLocation(location);
replacement.add(get); replacement.add(get);

View File

@ -17,6 +17,7 @@ package org.teavm.jso.impl;
import org.teavm.interop.NoSideEffects; import org.teavm.interop.NoSideEffects;
import org.teavm.jso.JSBody; import org.teavm.jso.JSBody;
import org.teavm.jso.JSClass;
import org.teavm.jso.JSObject; import org.teavm.jso.JSObject;
import org.teavm.jso.core.JSBoolean; import org.teavm.jso.core.JSBoolean;
import org.teavm.jso.core.JSFinalizationRegistry; 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.JSNumber;
import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSObjects;
import org.teavm.jso.core.JSString; import org.teavm.jso.core.JSString;
import org.teavm.jso.core.JSUndefined;
import org.teavm.jso.core.JSWeakMap; import org.teavm.jso.core.JSWeakMap;
import org.teavm.jso.core.JSWeakRef; import org.teavm.jso.core.JSWeakRef;
public final class JSWrapper { public final class JSWrapper {
private static final JSWeakMap<JSObject, JSNumber> hashCodes = new JSWeakMap<>(); private static final JSWeakMap<JSObject, JSTransparentInt> hashCodes = new JSWeakMap<>();
private static final JSWeakMap<JSObject, JSWeakRef<JSObject>> wrappers = JSWeakRef.isSupported() private static final JSWeakMap<JSObject, JSWeakRef<JSObject>> wrappers = JSWeakRef.isSupported()
? new JSWeakMap<>() : null; ? new JSWeakMap<>() : null;
private static final JSMap<JSString, JSWeakRef<JSObject>> stringWrappers = JSWeakRef.isSupported() private static final JSMap<JSString, JSWeakRef<JSObject>> stringWrappers = JSWeakRef.isSupported()
@ -68,8 +70,8 @@ public final class JSWrapper {
if (wrappers != null) { if (wrappers != null) {
if (isObject) { if (isObject) {
var existingRef = get(wrappers, js); var existingRef = get(wrappers, js);
var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance();
if (JSObjects.isUndefined(existing)) { if (isUndefined(existing)) {
var wrapper = new JSWrapper(js); var wrapper = new JSWrapper(js);
set(wrappers, js, createWeakRef(wrapperToJs(wrapper))); set(wrappers, js, createWeakRef(wrapperToJs(wrapper)));
return wrapper; return wrapper;
@ -79,8 +81,8 @@ public final class JSWrapper {
} else if (type.equals("string")) { } else if (type.equals("string")) {
var jsString = (JSString) js; var jsString = (JSString) js;
var existingRef = get(stringWrappers, jsString); var existingRef = get(stringWrappers, jsString);
var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance();
if (JSObjects.isUndefined(existing)) { if (isUndefined(existing)) {
var wrapper = new JSWrapper(js); var wrapper = new JSWrapper(js);
var wrapperAsJs = wrapperToJs(wrapper); var wrapperAsJs = wrapperToJs(wrapper);
set(stringWrappers, jsString, createWeakRef(wrapperAsJs)); set(stringWrappers, jsString, createWeakRef(wrapperAsJs));
@ -92,8 +94,8 @@ public final class JSWrapper {
} else if (type.equals("number")) { } else if (type.equals("number")) {
var jsNumber = (JSNumber) js; var jsNumber = (JSNumber) js;
var existingRef = get(numberWrappers, jsNumber); var existingRef = get(numberWrappers, jsNumber);
var existing = !JSObjects.isUndefined(existingRef) ? deref(existingRef) : JSObjects.undefined(); var existing = !isUndefined(existingRef) ? deref(existingRef) : JSUndefined.instance();
if (JSObjects.isUndefined(existing)) { if (isUndefined(existing)) {
var wrapper = new JSWrapper(js); var wrapper = new JSWrapper(js);
var wrapperAsJs = wrapperToJs(wrapper); var wrapperAsJs = wrapperToJs(wrapper);
set(numberWrappers, jsNumber, createWeakRef(wrapperAsJs)); set(numberWrappers, jsNumber, createWeakRef(wrapperAsJs));
@ -104,8 +106,8 @@ public final class JSWrapper {
} }
} else if (type.equals("undefined")) { } else if (type.equals("undefined")) {
var existingRef = undefinedWrapper; var existingRef = undefinedWrapper;
var existing = existingRef != null ? deref(existingRef) : JSObjects.undefined(); var existing = existingRef != null ? deref(existingRef) : JSUndefined.instance();
if (JSObjects.isUndefined(existing)) { if (isUndefined(existing)) {
var wrapper = new JSWrapper(js); var wrapper = new JSWrapper(js);
var wrapperAsJs = wrapperToJs(wrapper); var wrapperAsJs = wrapperToJs(wrapper);
undefinedWrapper = createWeakRef(wrapperAsJs); undefinedWrapper = createWeakRef(wrapperAsJs);
@ -215,13 +217,21 @@ public final class JSWrapper {
return !isJava(o) || o instanceof 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 @Override
public int hashCode() { public int hashCode() {
var type = JSObjects.typeOf(js); var type = JSObjects.typeOf(js);
if (type.equals("object") || type.equals("symbol") || type.equals("function")) { if (type.equals("object") || type.equals("symbol") || type.equals("function")) {
var code = hashCodes.get(js); var code = hashCodes.get(js);
if (JSObjects.isUndefined(code)) { if (isUndefined(code)) {
code = JSNumber.valueOf(++hashCodeGen); code = JSTransparentInt.valueOf(++hashCodeGen);
hashCodes.set(js, code); hashCodes.set(js, code);
} }
return code.intValue(); return code.intValue();
@ -263,6 +273,18 @@ public final class JSWrapper {
@Override @Override
public String toString() { 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);
} }

View File

@ -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();
}
}

View File

@ -32,6 +32,7 @@ import org.teavm.jso.JSObject;
import org.teavm.jso.core.JSNumber; import org.teavm.jso.core.JSNumber;
import org.teavm.jso.core.JSObjects; import org.teavm.jso.core.JSObjects;
import org.teavm.jso.core.JSString; import org.teavm.jso.core.JSString;
import org.teavm.jso.core.JSUndefined;
import org.teavm.junit.EachTestCompiledSeparately; import org.teavm.junit.EachTestCompiledSeparately;
import org.teavm.junit.OnlyPlatform; import org.teavm.junit.OnlyPlatform;
import org.teavm.junit.SkipJVM; import org.teavm.junit.SkipJVM;
@ -313,10 +314,10 @@ public class JSWrapperTest {
@Test @Test
public void wrapUndefined() { public void wrapUndefined() {
field1 = JSObjects.undefined(); field1 = JSUndefined.instance();
assertEquals("undefined", field1.toString()); assertEquals("undefined", field1.toString());
assertEquals(JSObjects.undefined(), field1); assertEquals(JSUndefined.instance(), field1);
assertSame(JSObjects.undefined(), field1); assertSame(JSUndefined.instance(), field1);
assertTrue(field1 instanceof JSObject); assertTrue(field1 instanceof JSObject);
assertTrue(JSObjects.isUndefined(field1)); assertTrue(JSObjects.isUndefined(field1));
} }