diff --git a/src/main/java/dev/latvian/mods/rhino/Context.java b/src/main/java/dev/latvian/mods/rhino/Context.java
index 1854741a..37b2c6e7 100644
--- a/src/main/java/dev/latvian/mods/rhino/Context.java
+++ b/src/main/java/dev/latvian/mods/rhino/Context.java
@@ -15,81 +15,48 @@
import dev.latvian.mods.rhino.util.ClassVisibilityContext;
import dev.latvian.mods.rhino.util.CustomJavaToJsWrapper;
import dev.latvian.mods.rhino.util.JavaSetWrapper;
+import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory;
import java.io.IOException;
import java.io.Reader;
+import java.lang.reflect.Array;
import java.lang.reflect.Field;
+import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
+import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-/**
- * This class represents the runtime context of an executing script.
- *
- * Before executing a script, an instance of Context must be created
- * and associated with the thread that will be executing the script.
- * The Context will be used to store information about the executing
- * of the script such as the call stack. Contexts are associated with
- * the current thread using the {@link #enter()} method.
- *
- * Different forms of script execution are supported. Scripts may be
- * evaluated from the source directly, or first compiled and then later
- * executed. Interactive execution is also supported.
- *
- * Some aspects of script execution, such as type conversions and
- * object creation, may be accessed directly through methods of
- * Context.
- *
- * @author Norris Boyd
- * @author Brendan Eich
- * @see Scriptable
- */
-
@SuppressWarnings("ThrowableNotThrown")
public class Context {
- /**
- * Report a warning using the error reporter for the current thread.
- *
- * @param message the warning message to report
- * @param sourceName a string describing the source, such as a filename
- * @param lineno the starting line number
- * @param lineSource the text of the line (may be null)
- * @param lineOffset the offset into lineSource where problem was detected
- * @see ErrorReporter
- */
+ public static final int CONVERSION_TRIVIAL = 1;
+ public static final int CONVERSION_NONTRIVIAL = 0;
+ public static final int CONVERSION_NONE = 99;
+ public static final int JSTYPE_UNDEFINED = 0; // undefined type
+ public static final int JSTYPE_NULL = 1; // null
+ public static final int JSTYPE_BOOLEAN = 2; // boolean
+ public static final int JSTYPE_NUMBER = 3; // number
+ public static final int JSTYPE_STRING = 4; // string
+ public static final int JSTYPE_JAVA_CLASS = 5; // JavaClass
+ public static final int JSTYPE_JAVA_OBJECT = 6; // JavaObject
+ public static final int JSTYPE_JAVA_ARRAY = 7; // JavaArray
+ public static final int JSTYPE_OBJECT = 8; // Scriptable
+
public static void reportWarning(Context cx, String message, String sourceName, int lineno, String lineSource, int lineOffset) {
cx.getErrorReporter().warning(message, sourceName, lineno, lineSource, lineOffset);
}
- /**
- * Report a warning using the error reporter for the current thread.
- *
- * @param message the warning message to report
- * @param cx
- * @see ErrorReporter
- */
public static void reportWarning(String message, Context cx) {
int[] linep = {0};
String filename = getSourcePositionFromStack(cx, linep);
Context.reportWarning(cx, message, filename, linep[0], null, 0);
}
- /**
- * Report an error using the error reporter for the current thread.
- *
- * @param cx
- * @param message the error message to report
- * @param lineno the starting line number
- * @param lineSource the text of the line (may be null)
- * @param lineOffset the offset into lineSource where problem was detected
- * @param sourceName a string describing the source, such as a filename
- * @see ErrorReporter
- */
public static void reportError(Context cx, String message, int lineno, String lineSource, int lineOffset, String sourceName) {
if (cx != null) {
cx.getErrorReporter().error(cx, message, sourceName, lineno, lineSource, lineOffset);
@@ -98,12 +65,6 @@ public static void reportError(Context cx, String message, int lineno, String li
}
}
- /**
- * Report an error using the error reporter for the current thread.
- *
- * @param message the error message to report
- * @see ErrorReporter
- */
public static void reportError(Context cx, String message) {
int[] linep = {0};
String filename = getSourcePositionFromStack(cx, linep);
@@ -174,69 +135,6 @@ public static Object getUndefinedValue() {
return Undefined.INSTANCE;
}
- /**
- * Convenient method to convert java value to its closest representation
- * in JavaScript.
- *
- * If value is an instance of String, Number, Boolean, Function or
- * Scriptable, it is returned as it and will be treated as the corresponding
- * JavaScript type of string, number, boolean, function and object.
- *
- * Note that for Number instances during any arithmetic operation in
- * JavaScript the engine will always use the result of
- * Number.doubleValue()
resulting in a precision loss if
- * the number can not fit into double.
- *
- * If value is an instance of Character, it will be converted to string of
- * length 1 and its JavaScript type will be string.
- *
- * The rest of values will be wrapped as LiveConnect objects
- * by calling {@link Scriptable#getPrototype(Context)} as in:
- *
- * Context cx = Context.getCurrentContext();
- * return cx.getWrapFactory().wrap(cx, scope, value, null);
- *
- *
- * @param value any Java object
- * @param scope top scope object
- * @return value suitable to pass to any API that takes JavaScript values.
- */
- public Object javaToJS(Object value, Scriptable scope) {
- if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Scriptable) {
- return value;
- } else if (value instanceof Character) {
- return String.valueOf(((Character) value).charValue());
- } else {
- return wrap(scope, value, null, null);
- }
- }
-
- /**
- * Convert a JavaScript value into the desired type.
- * Uses the semantics defined with LiveConnect3 and throws an
- * Illegal argument exception if the conversion cannot be performed.
- *
- * @param value the JavaScript value to convert
- * @param desiredType the Java type to convert to. Primitive Java
- * types are represented using the TYPE fields in the corresponding
- * wrapper class in java.lang.
- * @return the converted value
- * @throws EvaluatorException if the conversion cannot be performed
- */
- public Object jsToJava(Object value, Class> desiredType, Type desiredGenericType) throws EvaluatorException {
- if (desiredType == null) {
- return value;
- } else if (desiredType == Object.class) {
- return Wrapper.unwrapped(value);
- } else {
- return NativeJavaObject.coerceTypeImpl(factory.getTypeWrappers(), desiredType, value, this);
- }
- }
-
- public Object jsToJava(Object value, Class> desiredType) throws EvaluatorException {
- return jsToJava(value, desiredType, desiredType);
- }
-
/**
* Rethrow the exception wrapping it as the script runtime exception.
* Unless the exception is instance of {@link EcmaError} or
@@ -311,10 +209,6 @@ public static String getSourcePositionFromStack(Context cx, int[] linep) {
boolean isContinuationsTopCall;
NativeCall currentActivationCall;
BaseFunction typeErrorThrower;
- // for Objects, Arrays to tag themselves as being printed out,
- // so they don't print themselves out recursively.
- // Use ObjToIntMap instead of java.util.HashSet for JDK 1.1 compatibility
- ObjToIntMap iterating;
RegExp regExp;
// For the interpreter to store the last frame for error reports etc.
Object lastInterpreterFrame;
@@ -866,7 +760,6 @@ public void setGenerateObserverCount(boolean generateObserverCount) {
protected void observeInstructionCount(int instructionCount) {
}
- /********** end of API **********/
public final ClassLoader getApplicationClassLoader() {
if (applicationClassLoader == null) {
@@ -1271,12 +1164,12 @@ public Scriptable wrapNewObject(Scriptable scope, Object obj) {
return wrapAsJavaObject(scope, obj, null, null);
}
- public int getConversionWeight(Object fromObj, Class> to) {
- if (factory.getTypeWrappers().getWrapperFactory(to, fromObj) != null) {
- return NativeJavaObject.CONVERSION_NONTRIVIAL;
+ public int internalConversionWeight(Object fromObj, Class> target, Type genericTarget) {
+ if (factory.getTypeWrappers().hasWrapper(fromObj, target, genericTarget)) {
+ return CONVERSION_NONTRIVIAL;
}
- return NativeJavaObject.CONVERSION_NONE;
+ return CONVERSION_NONE;
}
/**
@@ -1305,4 +1198,585 @@ public String getMappedMethod(Class> from, Method method) {
public int getMaximumInterpreterStackDepth() {
return Integer.MAX_VALUE;
}
+
+ public Object createInterfaceAdapter(Class> type, Type genericType, ScriptableObject so) {
+ // XXX: Currently only instances of ScriptableObject are
+ // supported since the resulting interface proxies should
+ // be reused next time conversion is made and generic
+ // Callable has no storage for it. Weak references can
+ // address it but for now use this restriction.
+
+ Object key = Kit.makeHashKeyFromPair("Coerced Interface", type);
+ Object old = so.getAssociatedValue(key);
+ if (old != null) {
+ // Function was already wrapped
+ return old;
+ }
+ Object glue = InterfaceAdapter.create(this, type, so);
+ // Store for later retrieval
+ glue = so.associateValue(key, glue);
+ return glue;
+ }
+
+ public Object javaToJS(Object value, Scriptable scope) {
+ if (value instanceof String || value instanceof Number || value instanceof Boolean || value instanceof Scriptable) {
+ return value;
+ } else if (value instanceof Character) {
+ return String.valueOf(((Character) value).charValue());
+ } else {
+ return wrap(scope, value, null, null);
+ }
+ }
+
+ public final Object jsToJava(Object value, Class> desiredType, Type desiredGenericType) throws EvaluatorException {
+ if (desiredType == null) {
+ return value;
+ } else if (desiredType == Object.class) {
+ return Wrapper.unwrapped(value);
+ } else if (desiredType.isArray() && value instanceof NativeArray array) {
+ if (desiredGenericType instanceof GenericArrayType type) {
+ var componentType = type.getGenericComponentType();
+
+ if (componentType instanceof Class> arrayType) {
+ // Make a new java array, and coerce the JS array components
+ // to the target (component) type.
+ long length = array.getLength();
+ var result = Array.newInstance(arrayType, (int) length);
+ for (int i = 0; i < length; ++i) {
+ try {
+ Array.set(result, i, jsToJava(array.get(this, i, array), arrayType, type.getGenericComponentType()));
+ } catch (EvaluatorException ee) {
+ return reportConversionError(value, arrayType);
+ }
+ }
+
+ return result;
+ } else if (componentType instanceof GenericArrayType subtype) {
+ System.out.println("uhhh");
+ }
+ }
+ }
+
+ return internalJsToJava(value, desiredType, desiredGenericType);
+ }
+
+ public final Object jsToJava(Object value, Class> desiredType) throws EvaluatorException {
+ return jsToJava(value, desiredType, desiredType);
+ }
+
+ private static int getJSTypeCode(Object value) {
+ if (value == null) {
+ return JSTYPE_NULL;
+ } else if (value == Undefined.INSTANCE) {
+ return JSTYPE_UNDEFINED;
+ } else if (value instanceof CharSequence) {
+ return JSTYPE_STRING;
+ } else if (value instanceof Number) {
+ return JSTYPE_NUMBER;
+ } else if (value instanceof Boolean) {
+ return JSTYPE_BOOLEAN;
+ } else if (value instanceof Scriptable) {
+ if (value instanceof NativeJavaClass) {
+ return JSTYPE_JAVA_CLASS;
+ } else if (value instanceof NativeJavaArray) {
+ return JSTYPE_JAVA_ARRAY;
+ } else if (value instanceof Wrapper) {
+ return JSTYPE_JAVA_OBJECT;
+ } else {
+ return JSTYPE_OBJECT;
+ }
+ } else if (value instanceof Class) {
+ return JSTYPE_JAVA_CLASS;
+ } else {
+ Class> valueClass = value.getClass();
+ if (valueClass.isArray()) {
+ return JSTYPE_JAVA_ARRAY;
+ }
+ return JSTYPE_JAVA_OBJECT;
+ }
+ }
+
+ protected Object internalJsToJava(Object value, Class> target, Type genericTarget) {
+ var typeWrappers = factory.getTypeWrappers();
+
+ if (value == null || value.getClass() == target) {
+ return value;
+ }
+
+ if (target.isArray()) {
+ // Make a new java array, and coerce the JS array components to the target (component) type.
+ Class> arrayType = target.getComponentType();
+ var arrayGenericType = genericTarget instanceof GenericArrayType arr ? arr.getGenericComponentType() : arrayType;
+
+ if (value instanceof NativeArray array) {
+ long length = array.getLength();
+
+ Object result = Array.newInstance(arrayType, (int) length);
+ for (int i = 0; i < length; ++i) {
+ try {
+ Array.set(result, i, jsToJava(array.get(this, i, array), arrayType, arrayGenericType));
+ } catch (EvaluatorException ee) {
+ return reportConversionError(value, target);
+ }
+ }
+
+ return result;
+ } else {
+ // Convert a single value to an array
+ Object result = Array.newInstance(arrayType, 1);
+ Array.set(result, 0, jsToJava(value, arrayType, arrayGenericType));
+ return result;
+ }
+ }
+ /*else if (o instanceof Iterable) {
+ int size;
+
+ if (o instanceof Collection) {
+ size = ((Collection) o).size();
+ } else {
+ size = 0;
+
+ for (Object o1 : (Iterable) o) {
+ size++;
+ }
+ }
+
+ if (size == 0) {
+ return emptyArray;
+ }
+
+ T[] array = (T[]) Array.newInstance(target, size);
+ int index = 0;
+
+ for (Object o1 : (Iterable) o) {
+ if (typeWrapper.validator.test(o1)) {
+ array[index] = typeWrapper.factory.wrap(cx, o1);
+ index++;
+ }
+ }
+
+ return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget);
+ }
+ */
+
+ Object unwrappedValue = Wrapper.unwrapped(value);
+
+
+ TypeWrapperFactory> typeWrapper = typeWrappers == null ? null : typeWrappers.getWrapperFactory(unwrappedValue, target, genericTarget);
+
+ if (typeWrapper != null) {
+ return typeWrapper.wrap(this, unwrappedValue, target, genericTarget);
+ }
+
+ switch (getJSTypeCode(value)) {
+ case JSTYPE_NULL -> {
+ // raise error if type.isPrimitive()
+ if (target.isPrimitive()) {
+ return reportConversionError(value, target);
+ }
+ return null;
+ }
+ case JSTYPE_UNDEFINED -> {
+ if (target == ScriptRuntime.StringClass || target == ScriptRuntime.ObjectClass) {
+ return "undefined";
+ }
+ return reportConversionError("undefined", target, value);
+ }
+ case JSTYPE_BOOLEAN -> {
+ // Under LC3, only JS Booleans can be coerced into a Boolean value
+ if (target == Boolean.TYPE || target == ScriptRuntime.BooleanClass || target == ScriptRuntime.ObjectClass) {
+ return value;
+ } else if (target == ScriptRuntime.StringClass) {
+ return value.toString();
+ } else {
+ return reportConversionError(value, target);
+ }
+ }
+ case JSTYPE_NUMBER -> {
+ if (target == ScriptRuntime.StringClass) {
+ return ScriptRuntime.toString(this, value);
+ } else if (target == ScriptRuntime.ObjectClass) {
+ /*
+ if (cx.hasFeature(Context.FEATURE_INTEGER_WITHOUT_DECIMAL_PLACE)) {
+ //to process numbers like 2.0 as 2 without decimal place
+ long roundedValue = Math.round(toDouble(value));
+ if (roundedValue == toDouble(value)) {
+ return coerceToNumber(Long.TYPE, value);
+ }
+ }
+ */
+ return coerceToNumber(Double.TYPE, value);
+ } else if ((target.isPrimitive() && target != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(target)) {
+ return coerceToNumber(target, value);
+ } else {
+ return reportConversionError(value, target);
+ }
+ }
+ case JSTYPE_STRING -> {
+ if (target == ScriptRuntime.StringClass || target.isInstance(value)) {
+ return value.toString();
+ } else if (target == Character.TYPE || target == ScriptRuntime.CharacterClass) {
+ // Special case for converting a single char string to a
+ // character
+ // Placed here because it applies *only* to JS strings,
+ // not other JS objects converted to strings
+ if (((CharSequence) value).length() == 1) {
+ return ((CharSequence) value).charAt(0);
+ }
+ return coerceToNumber(target, value);
+ } else if ((target.isPrimitive() && target != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(target)) {
+ return coerceToNumber(target, value);
+ } else {
+ return reportConversionError(value, target);
+ }
+ }
+ case JSTYPE_JAVA_CLASS -> {
+ if (target == ScriptRuntime.ClassClass || target == ScriptRuntime.ObjectClass) {
+ return unwrappedValue;
+ } else if (target == ScriptRuntime.StringClass) {
+ return unwrappedValue.toString();
+ } else {
+ return reportConversionError(unwrappedValue, target);
+ }
+ }
+ case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> {
+ if (target.isPrimitive()) {
+ if (target == Boolean.TYPE) {
+ return reportConversionError(unwrappedValue, target);
+ }
+ return coerceToNumber(target, unwrappedValue);
+ }
+ if (target == ScriptRuntime.StringClass) {
+ return unwrappedValue.toString();
+ }
+ if (target.isInstance(unwrappedValue)) {
+ return unwrappedValue;
+ }
+ return reportConversionError(unwrappedValue, target);
+ }
+ case JSTYPE_OBJECT -> {
+ if (target == ScriptRuntime.StringClass) {
+ return ScriptRuntime.toString(this, value);
+ } else if (target.isPrimitive()) {
+ if (target == Boolean.TYPE) {
+ return reportConversionError(value, target);
+ }
+ return coerceToNumber(target, value);
+ } else if (target.isInstance(value)) {
+ return value;
+ } else if (target == ScriptRuntime.DateClass && value instanceof NativeDate) {
+ double time = ((NativeDate) value).getJSTimeValue();
+ // XXX: This will replace NaN by 0
+ return new Date((long) time);
+ } else if (value instanceof Wrapper) {
+ if (target.isInstance(unwrappedValue)) {
+ return unwrappedValue;
+ }
+ return reportConversionError(unwrappedValue, target);
+ } else if (target.isInterface() && (value instanceof NativeObject || value instanceof NativeFunction || value instanceof ArrowFunction)) {
+ // Try to use function/object as implementation of Java interface.
+ return createInterfaceAdapter(target, genericTarget, (ScriptableObject) value);
+ } else {
+ return reportConversionError(value, target);
+ }
+ }
+ }
+
+ return value;
+ }
+
+ public final boolean canConvert(Object from, Class> target, Type genericTarget) {
+ return getConversionWeight(from, target, genericTarget) < CONVERSION_NONE;
+ }
+
+ public final int getConversionWeight(Object from, Class> target, Type genericTarget) {
+ int fcw = internalConversionWeight(from, target, genericTarget);
+
+ if (fcw != CONVERSION_NONE) {
+ return fcw;
+ }
+
+ int fromCode = getJSTypeCode(from);
+
+ switch (fromCode) {
+ case JSTYPE_UNDEFINED -> {
+ if (target == ScriptRuntime.StringClass || target == ScriptRuntime.ObjectClass) {
+ return 1;
+ }
+ }
+ case JSTYPE_NULL -> {
+ if (!target.isPrimitive()) {
+ return 1;
+ }
+ }
+ case JSTYPE_BOOLEAN -> {
+ // "boolean" is #1
+ if (target == Boolean.TYPE) {
+ return 1;
+ } else if (target == ScriptRuntime.BooleanClass) {
+ return 2;
+ } else if (target == ScriptRuntime.ObjectClass) {
+ return 3;
+ } else if (target == ScriptRuntime.StringClass) {
+ return 4;
+ }
+ }
+ case JSTYPE_NUMBER -> {
+ if (target.isPrimitive()) {
+ if (target == Double.TYPE) {
+ return 1;
+ } else if (target != Boolean.TYPE) {
+ return 1 + getSizeRank(target);
+ }
+ } else {
+ if (target == ScriptRuntime.StringClass) {
+ // native numbers are #1-8
+ return 9;
+ } else if (target == ScriptRuntime.ObjectClass) {
+ return 10;
+ } else if (ScriptRuntime.NumberClass.isAssignableFrom(target)) {
+ // "double" is #1
+ return 2;
+ }
+ }
+ }
+ case JSTYPE_STRING -> {
+ if (target == ScriptRuntime.StringClass) {
+ return 1;
+ } else if (target.isInstance(from)) {
+ return 2;
+ } else if (target.isPrimitive()) {
+ if (target == Character.TYPE) {
+ return 3;
+ } else if (target != Boolean.TYPE) {
+ return 4;
+ }
+ }
+ }
+ case JSTYPE_JAVA_CLASS -> {
+ if (target == ScriptRuntime.ClassClass) {
+ return 1;
+ } else if (target == ScriptRuntime.ObjectClass) {
+ return 3;
+ } else if (target == ScriptRuntime.StringClass) {
+ return 4;
+ }
+ }
+ case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> {
+ Object javaObj = from;
+ if (javaObj instanceof Wrapper) {
+ javaObj = ((Wrapper) javaObj).unwrap();
+ }
+ if (target.isInstance(javaObj)) {
+ return CONVERSION_NONTRIVIAL;
+ }
+ if (target == ScriptRuntime.StringClass) {
+ return 2;
+ } else if (target.isPrimitive() && target != Boolean.TYPE) {
+ return (fromCode == JSTYPE_JAVA_ARRAY) ? CONVERSION_NONE : 2 + getSizeRank(target);
+ }
+ }
+ case JSTYPE_OBJECT -> {
+ // Other objects takes #1-#3 spots
+ if (target != ScriptRuntime.ObjectClass && target.isInstance(from)) {
+ // No conversion required, but don't apply for java.lang.Object
+ return 1;
+ }
+ if (target.isArray()) {
+ if (from instanceof NativeArray) {
+ // This is a native array conversion to a java array
+ // Array conversions are all equal, and preferable to object
+ // and string conversion, per LC3.
+ return 2;
+ }
+ } else if (target == ScriptRuntime.ObjectClass) {
+ return 3;
+ } else if (target == ScriptRuntime.StringClass) {
+ return 4;
+ } else if (target == ScriptRuntime.DateClass) {
+ if (from instanceof NativeDate) {
+ // This is a native date to java date conversion
+ return 1;
+ }
+ } else if (target.isInterface()) {
+
+ if (from instanceof NativeFunction) {
+ // See comments in createInterfaceAdapter
+ return 1;
+ }
+ if (from instanceof NativeObject) {
+ return 2;
+ }
+ return 12;
+ } else if (target.isPrimitive() && target != Boolean.TYPE) {
+ return 4 + getSizeRank(target);
+ }
+ }
+ }
+
+ return CONVERSION_NONE;
+ }
+
+ public static int getSizeRank(Class> aType) {
+ if (aType == Double.TYPE) {
+ return 1;
+ } else if (aType == Float.TYPE) {
+ return 2;
+ } else if (aType == Long.TYPE) {
+ return 3;
+ } else if (aType == Integer.TYPE) {
+ return 4;
+ } else if (aType == Short.TYPE) {
+ return 5;
+ } else if (aType == Character.TYPE) {
+ return 6;
+ } else if (aType == Byte.TYPE) {
+ return 7;
+ } else if (aType == Boolean.TYPE) {
+ return CONVERSION_NONE;
+ } else {
+ return 8;
+ }
+ }
+
+ protected Object coerceToNumber(Class> type, Object value) {
+ Class> valueClass = value.getClass();
+
+ // Character
+ if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) {
+ if (valueClass == ScriptRuntime.CharacterClass) {
+ return value;
+ }
+ return (char) toInteger(value, ScriptRuntime.CharacterClass, Character.MIN_VALUE, Character.MAX_VALUE);
+ }
+
+ // Double, Float
+ if (type == ScriptRuntime.ObjectClass || type == ScriptRuntime.DoubleClass || type == Double.TYPE) {
+ return valueClass == ScriptRuntime.DoubleClass ? value : Double.valueOf(toDouble(value));
+ }
+
+ if (type == ScriptRuntime.FloatClass || type == Float.TYPE) {
+ if (valueClass == ScriptRuntime.FloatClass) {
+ return value;
+ }
+ double number = toDouble(value);
+ if (Double.isInfinite(number) || Double.isNaN(number) || number == 0.0) {
+ return (float) number;
+ }
+
+ double absNumber = Math.abs(number);
+ if (absNumber < Float.MIN_VALUE) {
+ return (number > 0.0) ? +0.0f : -0.0f;
+ } else if (absNumber > Float.MAX_VALUE) {
+ return (number > 0.0) ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY;
+ } else {
+ return (float) number;
+ }
+ }
+
+ // Integer, Long, Short, Byte
+ if (type == ScriptRuntime.IntegerClass || type == Integer.TYPE) {
+ if (valueClass == ScriptRuntime.IntegerClass) {
+ return value;
+ }
+ return (int) toInteger(value, ScriptRuntime.IntegerClass, Integer.MIN_VALUE, Integer.MAX_VALUE);
+ }
+
+ if (type == ScriptRuntime.LongClass || type == Long.TYPE) {
+ if (valueClass == ScriptRuntime.LongClass) {
+ return value;
+ }
+ /* Long values cannot be expressed exactly in doubles.
+ * We thus use the largest and smallest double value that
+ * has a value expressible as a long value. We build these
+ * numerical values from their hexidecimal representations
+ * to avoid any problems caused by attempting to parse a
+ * decimal representation.
+ */
+ final double max = Double.longBitsToDouble(0x43dfffffffffffffL);
+ final double min = Double.longBitsToDouble(0xc3e0000000000000L);
+ return toInteger(value, ScriptRuntime.LongClass, min, max);
+ }
+
+ if (type == ScriptRuntime.ShortClass || type == Short.TYPE) {
+ if (valueClass == ScriptRuntime.ShortClass) {
+ return value;
+ }
+ return (short) toInteger(value, ScriptRuntime.ShortClass, Short.MIN_VALUE, Short.MAX_VALUE);
+ }
+
+ if (type == ScriptRuntime.ByteClass || type == Byte.TYPE) {
+ if (valueClass == ScriptRuntime.ByteClass) {
+ return value;
+ }
+ return (byte) toInteger(value, ScriptRuntime.ByteClass, Byte.MIN_VALUE, Byte.MAX_VALUE);
+ }
+
+ return toDouble(value);
+ }
+
+ protected double toDouble(Object value) {
+ if (value instanceof Number) {
+ return ((Number) value).doubleValue();
+ } else if (value instanceof String) {
+ return ScriptRuntime.toNumber(this, (String) value);
+ } else if (value instanceof Scriptable) {
+ if (value instanceof Wrapper) {
+ // XXX: optimize tail-recursion?
+ return toDouble(((Wrapper) value).unwrap());
+ }
+ return ScriptRuntime.toNumber(this, value);
+ } else {
+ Method meth;
+ try {
+ meth = value.getClass().getMethod("doubleValue", (Class[]) null);
+ } catch (NoSuchMethodException | SecurityException e) {
+ meth = null;
+ }
+ if (meth != null) {
+ try {
+ return ((Number) meth.invoke(value, (Object[]) null)).doubleValue();
+ } catch (IllegalAccessException | InvocationTargetException e) {
+ // XXX: ignore, or error message?
+ reportConversionError(value, Double.TYPE);
+ }
+ }
+ return ScriptRuntime.toNumber(this, value.toString());
+ }
+ }
+
+ protected long toInteger(Object value, Class> type, double min, double max) {
+ double d = toDouble(value);
+
+ if (Double.isInfinite(d) || Double.isNaN(d)) {
+ // Convert to string first, for more readable message
+ reportConversionError(ScriptRuntime.toString(this, value), type);
+ }
+
+ if (d > 0.0) {
+ d = Math.floor(d);
+ } else {
+ d = Math.ceil(d);
+ }
+
+ if (d < min || d > max) {
+ // Convert to string first, for more readable message
+ reportConversionError(ScriptRuntime.toString(this, value), type);
+ }
+ return (long) d;
+ }
+
+ private Object reportConversionError(Object value, Class> type) {
+ return reportConversionError(value, type, value);
+ }
+
+ private Object reportConversionError(Object value, Class> type, Object stringValue) {
+ // It uses String.valueOf(value), not value.toString() since
+ // value can be null, bug 282447.
+ throw Context.reportRuntimeError2("msg.conversion.not.allowed", String.valueOf(stringValue), JavaMembers.javaSignature(type), this);
+ }
+
+ public String defaultObjectToSource(Scriptable scope, Scriptable thisObj, Object[] args) {
+ return "not_supported";
+ }
}
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeArray.java b/src/main/java/dev/latvian/mods/rhino/NativeArray.java
index f36df5a6..5c98a7f7 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeArray.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeArray.java
@@ -407,86 +407,58 @@ private static void setRawElem(Context cx, Scriptable target, long index, Object
private static String toStringHelper(Context cx, Scriptable scope, Scriptable thisObj, boolean toLocale) {
Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
- /* It's probably redundant to handle long lengths in this
- * function; StringBuilders are limited to 2^31 in java.
- */
- long length = getLengthProperty(cx, o, false);
+ int length = (int) getLengthProperty(cx, o, false);
- StringBuilder result = new StringBuilder(256);
-
- // whether to return '4,unquoted,5' or '[4, "quoted", 5]'
- String separator;
+ if (length == 0) {
+ return "[]";
+ }
- separator = ",";
+ StringBuilder result = new StringBuilder(256);
+ result.append('[');
- boolean haslast = false;
- long i = 0;
+ // make toSource print null and undefined values in recent versions
+ for (int i = 0; i < length; i++) {
+ if (i > 0) {
+ result.append(", ");
+ }
+ Object elem = getRawElem(o, i, cx);
+ if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) {
+ continue;
+ }
- boolean toplevel, iterating;
- if (cx.iterating == null) {
- toplevel = true;
- iterating = false;
- cx.iterating = new ObjToIntMap(31);
- } else {
- toplevel = false;
- iterating = cx.iterating.has(o);
+ result.append(ScriptRuntime.uneval(cx, scope, elem));
}
- // Make sure cx.iterating is set to null when done
- // so we don't leak memory
- try {
- if (!iterating) {
- // stop recursion
- cx.iterating.put(o, 0);
+ result.append(']');
- // make toSource print null and undefined values in recent versions
- for (i = 0; i < length; i++) {
- if (i > 0) {
- result.append(separator);
- }
- Object elem = getRawElem(o, i, cx);
- if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) {
- haslast = false;
- continue;
- }
- haslast = true;
+ return result.toString();
+ }
- if (false) {
- result.append(ScriptRuntime.uneval(cx, scope, elem));
+ private static String toSource(Context cx, Scriptable scope, Scriptable thisObj) {
+ Scriptable o = ScriptRuntime.toObject(cx, scope, thisObj);
- } else if (elem instanceof String) {
- result.append((String) elem);
+ int length = (int) getLengthProperty(cx, o, false);
- } else {
- if (toLocale) {
- Callable fun;
- Scriptable funThis;
- fun = ScriptRuntime.getPropFunctionAndThis(cx, scope, elem, "toLocaleString");
- funThis = cx.lastStoredScriptable();
- elem = fun.call(cx, scope, funThis, ScriptRuntime.EMPTY_OBJECTS);
- }
- result.append(ScriptRuntime.toString(cx, elem));
- }
- }
+ if (length == 0) {
+ return "[]";
+ }
+
+ StringBuilder result = new StringBuilder(256);
+ result.append('[');
- // processing of thisObj done, remove it from the recursion detector
- // to allow thisObj to be again in the array later on
- cx.iterating.remove(o);
+ for (int i = 0; i < length; i++) {
+ if (i > 0) {
+ result.append(", ");
}
- } finally {
- if (toplevel) {
- cx.iterating = null;
+ Object elem = getRawElem(o, i, cx);
+ if (elem == NOT_FOUND || elem == null || elem == Undefined.INSTANCE) {
+ continue;
}
- }
- if (false) {
- //for [,,].length behavior; we want toString to be symmetric.
- if (!haslast && i > 0) {
- result.append(", ]");
- } else {
- result.append(']');
- }
+ result.append(ScriptRuntime.uneval(cx, scope, elem));
}
+
+ result.append(']');
return result.toString();
}
@@ -1741,7 +1713,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
return toStringHelper(cx, scope, thisObj, true);
case Id_toSource:
- return "not_supported";
+ return toSource(cx, scope, thisObj);
case Id_join:
return js_join(cx, scope, thisObj, args);
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java b/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java
index 440b2a38..54bdc2bd 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeBoolean.java
@@ -92,18 +92,11 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
}
boolean value = ((NativeBoolean) thisObj).booleanValue;
- switch (id) {
-
- case Id_toString:
- return value ? "true" : "false";
-
- case Id_toSource:
- return "not_supported";
-
- case Id_valueOf:
- return value;
- }
- throw new IllegalArgumentException(String.valueOf(id));
+ return switch (id) {
+ case Id_toString, Id_toSource -> value ? "true" : "false";
+ case Id_valueOf -> value;
+ default -> throw new IllegalArgumentException(String.valueOf(id));
+ };
}
@Override
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeDate.java b/src/main/java/dev/latvian/mods/rhino/NativeDate.java
index 85bf6265..f068d2a1 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeDate.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeDate.java
@@ -1642,7 +1642,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
return js_NaN_date_str;
case Id_toSource:
- return "not_supported";
+ return "Date";
case Id_valueOf:
case Id_getTime:
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeError.java b/src/main/java/dev/latvian/mods/rhino/NativeError.java
index 8e68ffac..a1d64e5f 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeError.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeError.java
@@ -239,7 +239,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
return js_toString(cx, thisObj);
case Id_toSource:
- return "not_supported";
+ return "Error";
case ConstructorId_captureStackTrace:
js_captureStackTrace(cx, thisObj, args);
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java
index 376aaca1..ecf105c4 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaClass.java
@@ -103,9 +103,6 @@ private static Class> findNestedClass(Class> parentClass, String name) {
private Map staticFieldAndMethods;
- public NativeJavaClass() {
- }
-
public NativeJavaClass(Context cx, Scriptable scope, Class> cl) {
this(cx, scope, cl, false);
}
@@ -242,7 +239,7 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) {
// When running on Android create an InterfaceAdapter since our
// bytecode generation won't work on Dalvik VM.
if ("Dalvik".equals(System.getProperty("java.vm.name")) && classObject.isInterface()) {
- Object obj = createInterfaceAdapter(cx, classObject, ScriptableObject.ensureScriptableObject(args[0], cx));
+ Object obj = cx.createInterfaceAdapter(classObject, classObject, ScriptableObject.ensureScriptableObject(args[0], cx));
return cx.wrapAsJavaObject(scope, obj, null, null);
}
// use JavaAdapter to construct a new class on the fly that
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java
index 3917e816..eed3ec48 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaMethod.java
@@ -8,6 +8,7 @@
import java.lang.reflect.Array;
import java.lang.reflect.Method;
+import java.lang.reflect.Type;
import java.util.concurrent.CopyOnWriteArrayList;
/**
@@ -92,7 +93,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) {
}
}
for (int j = 0; j != alength; ++j) {
- if (!NativeJavaObject.canConvert(cx, args[j], member.argTypes[j])) {
+ if (!cx.canConvert(args[j], member.argTypes[j], member.genericArgTypes[j])) {
if (debug) {
printDebug("Rejecting (args can't convert) ", member, args);
}
@@ -124,7 +125,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) {
}
}
for (int j = 0; j < alength; j++) {
- if (!NativeJavaObject.canConvert(cx, args[j], member.argTypes[j])) {
+ if (!cx.canConvert(args[j], member.argTypes[j], member.genericArgTypes[j])) {
if (debug) {
printDebug("Rejecting (args can't convert) ", member, args);
}
@@ -153,7 +154,7 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) {
bestFitIndex = extraBestFits[j];
}
MemberBox bestFit = methodsOrCtors[bestFitIndex];
- int preference = preferSignature(cx, args, member.argTypes, member.vararg, bestFit.argTypes, bestFit.vararg);
+ int preference = preferSignature(cx, args, member.argTypes, member.genericArgTypes, member.vararg, bestFit.argTypes, bestFit.genericArgTypes, bestFit.vararg);
if (preference == PREFERENCE_AMBIGUOUS) {
break;
} else if (preference == PREFERENCE_FIRST_ARG) {
@@ -253,20 +254,25 @@ static int findFunction(Context cx, MemberBox[] methodsOrCtors, Object[] args) {
* Returns one of PREFERENCE_EQUAL, PREFERENCE_FIRST_ARG,
* PREFERENCE_SECOND_ARG, or PREFERENCE_AMBIGUOUS.
*/
- private static int preferSignature(Context cx, Object[] args, Class>[] sig1, boolean vararg1, Class>[] sig2, boolean vararg2) {
+ private static int preferSignature(Context cx, Object[] args, Class>[] sig1, Type[] gsig1, boolean vararg1, Class>[] sig2, Type[] gsig2, boolean vararg2) {
int totalPreference = 0;
for (int j = 0; j < args.length; j++) {
Class> type1 = vararg1 && j >= sig1.length ? sig1[sig1.length - 1] : sig1[j];
Class> type2 = vararg2 && j >= sig2.length ? sig2[sig2.length - 1] : sig2[j];
+
if (type1 == type2) {
continue;
}
+
+ Type gType1 = vararg1 && j >= gsig1.length ? gsig1[gsig1.length - 1] : gsig1[j];
+ Type gType2 = vararg2 && j >= gsig2.length ? gsig2[gsig2.length - 1] : gsig2[j];
+
Object arg = args[j];
// Determine which of type1, type2 is easier to convert from arg.
- int rank1 = NativeJavaObject.getConversionWeight(cx, arg, type1);
- int rank2 = NativeJavaObject.getConversionWeight(cx, arg, type2);
+ int rank1 = cx.getConversionWeight(arg, type1, gType1);
+ int rank2 = cx.getConversionWeight(arg, type2, gType2);
int preference;
if (rank1 < rank2) {
@@ -275,7 +281,7 @@ private static int preferSignature(Context cx, Object[] args, Class>[] sig1, b
preference = PREFERENCE_SECOND_ARG;
} else {
// Equal ranks
- if (rank1 == NativeJavaObject.CONVERSION_NONTRIVIAL) {
+ if (rank1 == Context.CONVERSION_NONTRIVIAL) {
if (type1.isAssignableFrom(type2)) {
preference = PREFERENCE_SECOND_ARG;
} else if (type2.isAssignableFrom(type1)) {
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java b/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java
index eaca1f20..09dd4f35 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeJavaObject.java
@@ -9,15 +9,8 @@
import dev.latvian.mods.rhino.util.DefaultValueTypeHint;
import dev.latvian.mods.rhino.util.Deletable;
import dev.latvian.mods.rhino.util.JavaIteratorWrapper;
-import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory;
-import dev.latvian.mods.rhino.util.wrap.TypeWrappers;
-import org.jetbrains.annotations.Nullable;
import org.openjdk.nashorn.internal.runtime.NativeJavaPackage;
-import java.lang.reflect.Array;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@@ -33,539 +26,6 @@
*/
public class NativeJavaObject implements Scriptable, SymbolScriptable, Wrapper {
- public static final int CONVERSION_TRIVIAL = 1;
- public static final int CONVERSION_NONTRIVIAL = 0;
- public static final int CONVERSION_NONE = 99;
- private static final Object COERCED_INTERFACE_KEY = "Coerced Interface";
- private static final int JSTYPE_UNDEFINED = 0; // undefined type
- private static final int JSTYPE_NULL = 1; // null
- private static final int JSTYPE_BOOLEAN = 2; // boolean
- private static final int JSTYPE_NUMBER = 3; // number
- private static final int JSTYPE_STRING = 4; // string
- private static final int JSTYPE_JAVA_CLASS = 5; // JavaClass
- private static final int JSTYPE_JAVA_OBJECT = 6; // JavaObject
- private static final int JSTYPE_JAVA_ARRAY = 7; // JavaArray
- private static final int JSTYPE_OBJECT = 8; // Scriptable
-
- /**
- * Determine whether we can/should convert between the given type and the
- * desired one. This should be superceded by a conversion-cost calculation
- * function, but for now I'll hide behind precedent.
- */
- public static boolean canConvert(Context cx, Object fromObj, Class> to) {
- return getConversionWeight(cx, fromObj, to) < CONVERSION_NONE;
- }
-
- /**
- * Derive a ranking based on how "natural" the conversion is.
- * The special value CONVERSION_NONE means no conversion is possible,
- * and CONVERSION_NONTRIVIAL signals that more type conformance testing
- * is required.
- * Based on
- *
- * "preferred method conversions" from Live Connect 3
- */
- static int getConversionWeight(Context cx, Object fromObj, Class> to) {
- int fcw = cx.getConversionWeight(fromObj, to);
-
- if (fcw != CONVERSION_NONE) {
- return fcw;
- }
-
- int fromCode = getJSTypeCode(fromObj);
-
- switch (fromCode) {
-
- case JSTYPE_UNDEFINED:
- if (to == ScriptRuntime.StringClass || to == ScriptRuntime.ObjectClass) {
- return 1;
- }
- break;
-
- case JSTYPE_NULL:
- if (!to.isPrimitive()) {
- return 1;
- }
- break;
-
- case JSTYPE_BOOLEAN:
- // "boolean" is #1
- if (to == Boolean.TYPE) {
- return 1;
- } else if (to == ScriptRuntime.BooleanClass) {
- return 2;
- } else if (to == ScriptRuntime.ObjectClass) {
- return 3;
- } else if (to == ScriptRuntime.StringClass) {
- return 4;
- }
- break;
-
- case JSTYPE_NUMBER:
- if (to.isPrimitive()) {
- if (to == Double.TYPE) {
- return 1;
- } else if (to != Boolean.TYPE) {
- return 1 + getSizeRank(to);
- }
- } else {
- if (to == ScriptRuntime.StringClass) {
- // native numbers are #1-8
- return 9;
- } else if (to == ScriptRuntime.ObjectClass) {
- return 10;
- } else if (ScriptRuntime.NumberClass.isAssignableFrom(to)) {
- // "double" is #1
- return 2;
- }
- }
- break;
-
- case JSTYPE_STRING:
- if (to == ScriptRuntime.StringClass) {
- return 1;
- } else if (to.isInstance(fromObj)) {
- return 2;
- } else if (to.isPrimitive()) {
- if (to == Character.TYPE) {
- return 3;
- } else if (to != Boolean.TYPE) {
- return 4;
- }
- }
- break;
-
- case JSTYPE_JAVA_CLASS:
- if (to == ScriptRuntime.ClassClass) {
- return 1;
- } else if (to == ScriptRuntime.ObjectClass) {
- return 3;
- } else if (to == ScriptRuntime.StringClass) {
- return 4;
- }
- break;
-
- case JSTYPE_JAVA_OBJECT:
- case JSTYPE_JAVA_ARRAY:
- Object javaObj = fromObj;
- if (javaObj instanceof Wrapper) {
- javaObj = ((Wrapper) javaObj).unwrap();
- }
- if (to.isInstance(javaObj)) {
- return CONVERSION_NONTRIVIAL;
- }
- if (to == ScriptRuntime.StringClass) {
- return 2;
- } else if (to.isPrimitive() && to != Boolean.TYPE) {
- return (fromCode == JSTYPE_JAVA_ARRAY) ? CONVERSION_NONE : 2 + getSizeRank(to);
- }
- break;
-
- case JSTYPE_OBJECT:
- // Other objects takes #1-#3 spots
- if (to != ScriptRuntime.ObjectClass && to.isInstance(fromObj)) {
- // No conversion required, but don't apply for java.lang.Object
- return 1;
- }
- if (to.isArray()) {
- if (fromObj instanceof NativeArray) {
- // This is a native array conversion to a java array
- // Array conversions are all equal, and preferable to object
- // and string conversion, per LC3.
- return 2;
- }
- } else if (to == ScriptRuntime.ObjectClass) {
- return 3;
- } else if (to == ScriptRuntime.StringClass) {
- return 4;
- } else if (to == ScriptRuntime.DateClass) {
- if (fromObj instanceof NativeDate) {
- // This is a native date to java date conversion
- return 1;
- }
- } else if (to.isInterface()) {
-
- if (fromObj instanceof NativeFunction) {
- // See comments in createInterfaceAdapter
- return 1;
- }
- if (fromObj instanceof NativeObject) {
- return 2;
- }
- return 12;
- } else if (to.isPrimitive() && to != Boolean.TYPE) {
- return 4 + getSizeRank(to);
- }
- break;
- }
-
- return CONVERSION_NONE;
- }
-
- static int getSizeRank(Class> aType) {
- if (aType == Double.TYPE) {
- return 1;
- } else if (aType == Float.TYPE) {
- return 2;
- } else if (aType == Long.TYPE) {
- return 3;
- } else if (aType == Integer.TYPE) {
- return 4;
- } else if (aType == Short.TYPE) {
- return 5;
- } else if (aType == Character.TYPE) {
- return 6;
- } else if (aType == Byte.TYPE) {
- return 7;
- } else if (aType == Boolean.TYPE) {
- return CONVERSION_NONE;
- } else {
- return 8;
- }
- }
-
- private static int getJSTypeCode(Object value) {
- if (value == null) {
- return JSTYPE_NULL;
- } else if (value == Undefined.INSTANCE) {
- return JSTYPE_UNDEFINED;
- } else if (value instanceof CharSequence) {
- return JSTYPE_STRING;
- } else if (value instanceof Number) {
- return JSTYPE_NUMBER;
- } else if (value instanceof Boolean) {
- return JSTYPE_BOOLEAN;
- } else if (value instanceof Scriptable) {
- if (value instanceof NativeJavaClass) {
- return JSTYPE_JAVA_CLASS;
- } else if (value instanceof NativeJavaArray) {
- return JSTYPE_JAVA_ARRAY;
- } else if (value instanceof Wrapper) {
- return JSTYPE_JAVA_OBJECT;
- } else {
- return JSTYPE_OBJECT;
- }
- } else if (value instanceof Class) {
- return JSTYPE_JAVA_CLASS;
- } else {
- Class> valueClass = value.getClass();
- if (valueClass.isArray()) {
- return JSTYPE_JAVA_ARRAY;
- }
- return JSTYPE_JAVA_OBJECT;
- }
- }
-
- /**
- * Type-munging for field setting and method invocation.
- * Conforms to LC3 specification
- */
- static Object coerceTypeImpl(@Nullable TypeWrappers typeWrappers, Class> type, Object value, Context cx) {
- if (value == null || value.getClass() == type) {
- return value;
- }
-
- Object unwrappedValue = Wrapper.unwrapped(value);
- TypeWrapperFactory> typeWrapper = typeWrappers == null ? null : typeWrappers.getWrapperFactory(type, unwrappedValue);
-
- if (typeWrapper != null) {
- return typeWrapper.wrap(cx, unwrappedValue);
- }
-
- switch (getJSTypeCode(value)) {
- case JSTYPE_NULL -> {
- // raise error if type.isPrimitive()
- if (type.isPrimitive()) {
- return reportConversionError(value, type, cx);
- }
- return null;
- }
- case JSTYPE_UNDEFINED -> {
- if (type == ScriptRuntime.StringClass || type == ScriptRuntime.ObjectClass) {
- return "undefined";
- }
- return reportConversionError("undefined", type, value, cx);
- }
- case JSTYPE_BOOLEAN -> {
- // Under LC3, only JS Booleans can be coerced into a Boolean value
- if (type == Boolean.TYPE || type == ScriptRuntime.BooleanClass || type == ScriptRuntime.ObjectClass) {
- return value;
- } else if (type == ScriptRuntime.StringClass) {
- return value.toString();
- } else {
- return reportConversionError(value, type, cx);
- }
- }
- case JSTYPE_NUMBER -> {
- if (type == ScriptRuntime.StringClass) {
- return ScriptRuntime.toString(cx, value);
- } else if (type == ScriptRuntime.ObjectClass) {
- /*
- if (cx.hasFeature(Context.FEATURE_INTEGER_WITHOUT_DECIMAL_PLACE)) {
- //to process numbers like 2.0 as 2 without decimal place
- long roundedValue = Math.round(toDouble(value));
- if (roundedValue == toDouble(value)) {
- return coerceToNumber(Long.TYPE, value);
- }
- }
- */
- return coerceToNumber(Double.TYPE, value, cx);
- } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) {
- return coerceToNumber(type, value, cx);
- } else {
- return reportConversionError(value, type, cx);
- }
- }
- case JSTYPE_STRING -> {
- if (type == ScriptRuntime.StringClass || type.isInstance(value)) {
- return value.toString();
- } else if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) {
- // Special case for converting a single char string to a
- // character
- // Placed here because it applies *only* to JS strings,
- // not other JS objects converted to strings
- if (((CharSequence) value).length() == 1) {
- return ((CharSequence) value).charAt(0);
- }
- return coerceToNumber(type, value, cx);
- } else if ((type.isPrimitive() && type != Boolean.TYPE) || ScriptRuntime.NumberClass.isAssignableFrom(type)) {
- return coerceToNumber(type, value, cx);
- } else {
- return reportConversionError(value, type, cx);
- }
- }
- case JSTYPE_JAVA_CLASS -> {
- if (type == ScriptRuntime.ClassClass || type == ScriptRuntime.ObjectClass) {
- return unwrappedValue;
- } else if (type == ScriptRuntime.StringClass) {
- return unwrappedValue.toString();
- } else {
- return reportConversionError(unwrappedValue, type, cx);
- }
- }
- case JSTYPE_JAVA_OBJECT, JSTYPE_JAVA_ARRAY -> {
- if (type.isPrimitive()) {
- if (type == Boolean.TYPE) {
- return reportConversionError(unwrappedValue, type, cx);
- }
- return coerceToNumber(type, unwrappedValue, cx);
- }
- if (type == ScriptRuntime.StringClass) {
- return unwrappedValue.toString();
- }
- if (type.isInstance(unwrappedValue)) {
- return unwrappedValue;
- }
- return reportConversionError(unwrappedValue, type, cx);
- }
- case JSTYPE_OBJECT -> {
- if (type == ScriptRuntime.StringClass) {
- return ScriptRuntime.toString(cx, value);
- } else if (type.isPrimitive()) {
- if (type == Boolean.TYPE) {
- return reportConversionError(value, type, cx);
- }
- return coerceToNumber(type, value, cx);
- } else if (type.isInstance(value)) {
- return value;
- } else if (type == ScriptRuntime.DateClass && value instanceof NativeDate) {
- double time = ((NativeDate) value).getJSTimeValue();
- // XXX: This will replace NaN by 0
- return new Date((long) time);
- } else if (type.isArray() && value instanceof NativeArray array) {
- // Make a new java array, and coerce the JS array components
- // to the target (component) type.
- long length = array.getLength();
- Class> arrayType = type.getComponentType();
- Object Result = Array.newInstance(arrayType, (int) length);
- for (int i = 0; i < length; ++i) {
- try {
- Array.set(Result, i, coerceTypeImpl(typeWrappers, arrayType, array.get(cx, i, array), cx));
- } catch (EvaluatorException ee) {
- return reportConversionError(value, type, cx);
- }
- }
-
- return Result;
- } else if (value instanceof Wrapper) {
- if (type.isInstance(unwrappedValue)) {
- return unwrappedValue;
- }
- return reportConversionError(unwrappedValue, type, cx);
- } else if (type.isInterface() && (value instanceof NativeObject || value instanceof NativeFunction || value instanceof ArrowFunction)) {
- // Try to use function/object as implementation of Java interface.
- return createInterfaceAdapter(cx, type, (ScriptableObject) value);
- } else {
- return reportConversionError(value, type, cx);
- }
- }
- }
-
-
- return value;
- }
-
- public static Object createInterfaceAdapter(Context cx, Class> type, ScriptableObject so) {
- // XXX: Currently only instances of ScriptableObject are
- // supported since the resulting interface proxies should
- // be reused next time conversion is made and generic
- // Callable has no storage for it. Weak references can
- // address it but for now use this restriction.
-
- Object key = Kit.makeHashKeyFromPair(COERCED_INTERFACE_KEY, type);
- Object old = so.getAssociatedValue(key);
- if (old != null) {
- // Function was already wrapped
- return old;
- }
- Object glue = InterfaceAdapter.create(cx, type, so);
- // Store for later retrieval
- glue = so.associateValue(key, glue);
- return glue;
- }
-
- private static Object coerceToNumber(Class> type, Object value, Context cx) {
- Class> valueClass = value.getClass();
-
- // Character
- if (type == Character.TYPE || type == ScriptRuntime.CharacterClass) {
- if (valueClass == ScriptRuntime.CharacterClass) {
- return value;
- }
- return (char) toInteger(value, ScriptRuntime.CharacterClass, Character.MIN_VALUE, Character.MAX_VALUE, cx);
- }
-
- // Double, Float
- if (type == ScriptRuntime.ObjectClass || type == ScriptRuntime.DoubleClass || type == Double.TYPE) {
- return valueClass == ScriptRuntime.DoubleClass ? value : Double.valueOf(toDouble(value, cx));
- }
-
- if (type == ScriptRuntime.FloatClass || type == Float.TYPE) {
- if (valueClass == ScriptRuntime.FloatClass) {
- return value;
- }
- double number = toDouble(value, cx);
- if (Double.isInfinite(number) || Double.isNaN(number) || number == 0.0) {
- return (float) number;
- }
-
- double absNumber = Math.abs(number);
- if (absNumber < Float.MIN_VALUE) {
- return (number > 0.0) ? +0.0f : -0.0f;
- } else if (absNumber > Float.MAX_VALUE) {
- return (number > 0.0) ? Float.POSITIVE_INFINITY : Float.NEGATIVE_INFINITY;
- } else {
- return (float) number;
- }
- }
-
- // Integer, Long, Short, Byte
- if (type == ScriptRuntime.IntegerClass || type == Integer.TYPE) {
- if (valueClass == ScriptRuntime.IntegerClass) {
- return value;
- }
- return (int) toInteger(value, ScriptRuntime.IntegerClass, Integer.MIN_VALUE, Integer.MAX_VALUE, cx);
- }
-
- if (type == ScriptRuntime.LongClass || type == Long.TYPE) {
- if (valueClass == ScriptRuntime.LongClass) {
- return value;
- }
- /* Long values cannot be expressed exactly in doubles.
- * We thus use the largest and smallest double value that
- * has a value expressible as a long value. We build these
- * numerical values from their hexidecimal representations
- * to avoid any problems caused by attempting to parse a
- * decimal representation.
- */
- final double max = Double.longBitsToDouble(0x43dfffffffffffffL);
- final double min = Double.longBitsToDouble(0xc3e0000000000000L);
- return toInteger(value, ScriptRuntime.LongClass, min, max, cx);
- }
-
- if (type == ScriptRuntime.ShortClass || type == Short.TYPE) {
- if (valueClass == ScriptRuntime.ShortClass) {
- return value;
- }
- return (short) toInteger(value, ScriptRuntime.ShortClass, Short.MIN_VALUE, Short.MAX_VALUE, cx);
- }
-
- if (type == ScriptRuntime.ByteClass || type == Byte.TYPE) {
- if (valueClass == ScriptRuntime.ByteClass) {
- return value;
- }
- return (byte) toInteger(value, ScriptRuntime.ByteClass, Byte.MIN_VALUE, Byte.MAX_VALUE, cx);
- }
-
- return toDouble(value, cx);
- }
-
- private static double toDouble(Object value, Context cx) {
- if (value instanceof Number) {
- return ((Number) value).doubleValue();
- } else if (value instanceof String) {
- return ScriptRuntime.toNumber(cx, (String) value);
- } else if (value instanceof Scriptable) {
- if (value instanceof Wrapper) {
- // XXX: optimize tail-recursion?
- return toDouble(((Wrapper) value).unwrap(), cx);
- }
- return ScriptRuntime.toNumber(cx, value);
- } else {
- Method meth;
- try {
- meth = value.getClass().getMethod("doubleValue", (Class[]) null);
- } catch (NoSuchMethodException e) {
- meth = null;
- } catch (SecurityException e) {
- meth = null;
- }
- if (meth != null) {
- try {
- return ((Number) meth.invoke(value, (Object[]) null)).doubleValue();
- } catch (IllegalAccessException e) {
- // XXX: ignore, or error message?
- reportConversionError(value, Double.TYPE, cx);
- } catch (InvocationTargetException e) {
- // XXX: ignore, or error message?
- reportConversionError(value, Double.TYPE, cx);
- }
- }
- return ScriptRuntime.toNumber(cx, value.toString());
- }
- }
-
- private static long toInteger(Object value, Class> type, double min, double max, Context cx) {
- double d = toDouble(value, cx);
-
- if (Double.isInfinite(d) || Double.isNaN(d)) {
- // Convert to string first, for more readable message
- reportConversionError(ScriptRuntime.toString(cx, value), type, cx);
- }
-
- if (d > 0.0) {
- d = Math.floor(d);
- } else {
- d = Math.ceil(d);
- }
-
- if (d < min || d > max) {
- // Convert to string first, for more readable message
- reportConversionError(ScriptRuntime.toString(cx, value), type, cx);
- }
- return (long) d;
- }
-
- static Object reportConversionError(Object value, Class> type, Context cx) {
- return reportConversionError(value, type, value, cx);
- }
-
- static Object reportConversionError(Object value, Class> type, Object stringValue, Context cx) {
- // It uses String.valueOf(value), not value.toString() since
- // value can be null, bug 282447.
- throw Context.reportRuntimeError2("msg.conversion.not.allowed", String.valueOf(stringValue), JavaMembers.javaSignature(type), cx);
- }
-
/**
* The prototype of this object.
*/
@@ -581,9 +41,6 @@ static Object reportConversionError(Object value, Class> type, Object stringVa
protected transient Map customMembers;
protected transient boolean isAdapter;
- public NativeJavaObject() {
- }
-
public NativeJavaObject(Scriptable scope, Object javaObject, Class> staticType, Context cx) {
this(scope, javaObject, staticType, false, cx);
}
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeNumber.java b/src/main/java/dev/latvian/mods/rhino/NativeNumber.java
index 83757dfc..c645b071 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeNumber.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeNumber.java
@@ -270,9 +270,8 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
int base = (args.length == 0 || args[0] == Undefined.INSTANCE) ? 10 : ScriptRuntime.toInt32(cx, args[0]);
return ScriptRuntime.numberToString(cx, value, base);
}
-
case Id_toSource:
- return "not_supported";
+ return ScriptRuntime.numberToString(cx, value, 10);
case Id_valueOf:
return ScriptRuntime.wrapNumber(value);
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeObject.java b/src/main/java/dev/latvian/mods/rhino/NativeObject.java
index 9db3d326..79f47d66 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeObject.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeObject.java
@@ -314,7 +314,7 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
}
case Id_toSource:
- return ScriptRuntime.defaultObjectToSource(cx, scope, thisObj, args);
+ return cx.defaultObjectToSource(scope, thisObj, args);
case Id___defineGetter__:
case Id___defineSetter__: {
if (args.length < 2 || !(args[1] instanceof Callable getterOrSetter)) {
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeString.java b/src/main/java/dev/latvian/mods/rhino/NativeString.java
index a3af050e..e75e8f21 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeString.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeString.java
@@ -836,11 +836,9 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
case Id_toString:
case Id_valueOf:
// ECMA 15.5.4.2: 'the toString function is not generic.
- CharSequence cs = realThis(thisObj, f, cx).string;
- return cs instanceof String ? cs : cs.toString();
-
+ return realThis(thisObj, f, cx).string.toString();
case Id_toSource: {
- return "not_supported";
+ return ScriptRuntime.escapeAndWrapString(realThis(thisObj, f, cx).string.toString());
}
case Id_charAt:
diff --git a/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java b/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java
index d252e806..bc0c6ce9 100644
--- a/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java
+++ b/src/main/java/dev/latvian/mods/rhino/NativeSymbol.java
@@ -203,31 +203,26 @@ public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, Scrip
return super.execIdCall(f, cx, scope, thisObj, args);
}
int id = f.methodId();
- switch (id) {
- case ConstructorId_for:
- return js_for(cx, scope, args);
- case ConstructorId_keyFor:
- return js_keyFor(cx, scope, args);
-
- case Id_constructor:
+ return switch (id) {
+ case ConstructorId_for -> js_for(cx, scope, args);
+ case ConstructorId_keyFor -> js_keyFor(cx, scope, args);
+ case Id_constructor -> {
if (thisObj == null) {
if (cx.getThreadLocal(CONSTRUCTOR_SLOT) == null) {
// We should never get to this via "new".
throw ScriptRuntime.typeError0(cx, "msg.no.symbol.new");
}
// Unless we are being called by our own internal "new"
- return js_constructor(cx, args);
+ yield js_constructor(cx, args);
}
- return construct(cx, scope, args);
-
- case Id_toString:
- return getSelf(cx, thisObj).toString();
- case Id_valueOf:
- case SymbolId_toPrimitive:
- return getSelf(cx, thisObj).js_valueOf();
- default:
- return super.execIdCall(f, cx, scope, thisObj, args);
- }
+ yield construct(cx, scope, args);
+ // We should never get to this via "new".
+ // Unless we are being called by our own internal "new"
+ }
+ case Id_toString -> getSelf(cx, thisObj).toString();
+ case Id_valueOf, SymbolId_toPrimitive -> getSelf(cx, thisObj).js_valueOf();
+ default -> super.execIdCall(f, cx, scope, thisObj, args);
+ };
}
private Object js_valueOf() {
diff --git a/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java b/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java
index 68c1f4ec..6de928bd 100644
--- a/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java
+++ b/src/main/java/dev/latvian/mods/rhino/ScriptRuntime.java
@@ -16,6 +16,7 @@
import dev.latvian.mods.rhino.v8dtoa.DoubleConversion;
import dev.latvian.mods.rhino.v8dtoa.FastDtoa;
+import java.lang.reflect.Array;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Date;
@@ -652,8 +653,10 @@ public static Object[] padArguments(Object[] args, int count) {
return result;
}
- public static String escapeString(String s) {
- return escapeString(s, '"');
+ public static String escapeAndWrapString(String s) {
+ var c = s.indexOf('\'') == -1 ? '\'' : '"';
+ var escaped = escapeString(s, c);
+ return c + escaped + c;
}
/**
@@ -779,6 +782,28 @@ public static String toString(Context cx, Object val) {
}
return toString(cx, val);
}
+ if (val.getClass().isArray()) {
+ var builder = new StringBuilder();
+ int length = Array.getLength(val);
+
+ if (length == 0) {
+ builder.append("[]");
+ } else {
+ builder.append('[');
+
+ for (int i = 0; i < length; i++) {
+ if (i > 0) {
+ builder.append(", ");
+ }
+
+ builder.append(toString(cx, Array.get(val, i)));
+ }
+
+ builder.append(']');
+ }
+
+ return builder.toString();
+ }
return ToStringJS.toStringJS(cx, val);
}
@@ -843,8 +868,7 @@ static String uneval(Context cx, Scriptable scope, Object value) {
return "undefined";
}
if (value instanceof CharSequence) {
- String escaped = escapeString(value.toString());
- return '\"' + escaped + '\"';
+ return escapeAndWrapString(value.toString());
}
if (value instanceof Number) {
double d = ((Number) value).doubleValue();
@@ -871,10 +895,6 @@ static String uneval(Context cx, Scriptable scope, Object value) {
return value.toString();
}
- static String defaultObjectToSource(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) {
- return "not_supported";
- }
-
/**
* Warning : This doesn't allow to resolve primitive
* prototype properly when many top scopes are involved
diff --git a/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java b/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java
new file mode 100644
index 00000000..660e82f9
--- /dev/null
+++ b/src/main/java/dev/latvian/mods/rhino/mod/core/mixin/common/StringRepresentableMixin.java
@@ -0,0 +1,13 @@
+package dev.latvian.mods.rhino.mod.core.mixin.common;
+
+import dev.latvian.mods.rhino.util.RemappedEnumConstant;
+import net.minecraft.util.StringRepresentable;
+import org.spongepowered.asm.mixin.Mixin;
+
+@Mixin(StringRepresentable.class)
+public interface StringRepresentableMixin extends RemappedEnumConstant {
+ @Override
+ default String getRemappedEnumConstantName() {
+ return ((StringRepresentable) this).getSerializedName();
+ }
+}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java b/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java
index 228fe89b..69719642 100644
--- a/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java
+++ b/src/main/java/dev/latvian/mods/rhino/util/EnumTypeWrapper.java
@@ -3,12 +3,14 @@
import dev.latvian.mods.rhino.Context;
import dev.latvian.mods.rhino.util.wrap.TypeWrapperFactory;
+import java.lang.reflect.Type;
import java.util.HashMap;
+import java.util.IdentityHashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class EnumTypeWrapper implements TypeWrapperFactory {
- private static final Map, EnumTypeWrapper>> WRAPPERS = new HashMap<>();
+ private static final Map, EnumTypeWrapper>> WRAPPERS = new IdentityHashMap<>();
@SuppressWarnings("unchecked")
public static EnumTypeWrapper get(Class enumType) {
@@ -16,7 +18,9 @@ public static EnumTypeWrapper get(Class enumType) {
throw new IllegalArgumentException("Class " + enumType.getName() + " is not an enum!");
}
- return (EnumTypeWrapper) WRAPPERS.computeIfAbsent(enumType, EnumTypeWrapper::new);
+ synchronized (WRAPPERS) {
+ return (EnumTypeWrapper) WRAPPERS.computeIfAbsent(enumType, EnumTypeWrapper::new);
+ }
}
public static String getName(Class> enumType, Enum> e, boolean cache) {
@@ -46,7 +50,7 @@ private EnumTypeWrapper(Class enumType) {
this.enumType = enumType;
this.indexValues = enumType.getEnumConstants();
this.nameValues = new HashMap<>();
- this.valueNames = new HashMap<>();
+ this.valueNames = new IdentityHashMap<>();
for (T t : indexValues) {
String name = getName(enumType, (Enum>) t, false).toLowerCase();
@@ -56,9 +60,9 @@ private EnumTypeWrapper(Class enumType) {
}
@Override
- public T wrap(Context cx, Object o) {
- if (o instanceof CharSequence) {
- String s = o.toString().toLowerCase();
+ public T wrap(Context cx, Object from, Class> toType, Type toGenericType) {
+ if (from instanceof CharSequence) {
+ String s = from.toString().toLowerCase();
if (s.isEmpty()) {
return null;
@@ -71,8 +75,8 @@ public T wrap(Context cx, Object o) {
}
return t;
- } else if (o instanceof Number) {
- int index = ((Number) o).intValue();
+ } else if (from instanceof Number) {
+ int index = ((Number) from).intValue();
if (index < 0 || index >= indexValues.length) {
throw new IllegalArgumentException(index + " is not a valid enum index! Valid values are: 0 - " + (indexValues.length - 1));
@@ -81,6 +85,6 @@ public T wrap(Context cx, Object o) {
return indexValues[index];
}
- return (T) o;
+ return (T) from;
}
}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java
new file mode 100644
index 00000000..d033a002
--- /dev/null
+++ b/src/main/java/dev/latvian/mods/rhino/util/TypeUtils.java
@@ -0,0 +1,43 @@
+package dev.latvian.mods.rhino.util;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+
+public class TypeUtils {
+ public static final Type[] NO_TYPES = new Type[0];
+
+ public static Type[] getGenericTypes(Type type) {
+ return switch (type) {
+ case ParameterizedType paramType -> paramType.getActualTypeArguments();
+ case GenericArrayType arrayType -> new Type[]{arrayType.getGenericComponentType()};
+ case WildcardType wildcard -> new Type[]{getRawType(wildcard.getUpperBounds()[0])};
+ case null, default -> NO_TYPES;
+ };
+ }
+
+ public static Class> getRawType(Type type) {
+ if (type instanceof Class> clz) {
+ return clz;
+ } else if (type instanceof ParameterizedType paramType) {
+ var rawType = paramType.getRawType();
+
+ if (rawType instanceof Class> clz) {
+ return clz;
+ }
+ } else if (type instanceof GenericArrayType arrType) {
+ var componentType = arrType.getGenericComponentType();
+ return Array.newInstance(getRawType(componentType), 0).getClass();
+ } else if (type instanceof TypeVariable) {
+ return Object.class;
+ } else if (type instanceof WildcardType wildcard) {
+ return getRawType(wildcard.getUpperBounds()[0]);
+ }
+
+ var className = type == null ? "null" : type.getClass().getName();
+ throw new IllegalArgumentException("Expected a Class, ParameterizedType, GenericArrayType, TypeVariable or WildcardType, but <" + type + "> is of type " + className);
+ }
+}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java
deleted file mode 100644
index 3fc0230e..00000000
--- a/src/main/java/dev/latvian/mods/rhino/util/wrap/ArrayTypeWrapperFactory.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package dev.latvian.mods.rhino.util.wrap;
-
-import dev.latvian.mods.rhino.Context;
-
-import java.lang.reflect.Array;
-import java.util.Arrays;
-import java.util.Collection;
-
-/**
- * @author LatvianModder
- */
-public class ArrayTypeWrapperFactory implements TypeWrapperFactory {
- public final TypeWrapper typeWrapper;
- public final Class target;
- public final Class arrayTarget;
- private final T[] emptyArray;
-
- @SuppressWarnings("unchecked")
- public ArrayTypeWrapperFactory(TypeWrapper tw, Class t, Class at) {
- typeWrapper = tw;
- target = t;
- arrayTarget = at;
- emptyArray = (T[]) Array.newInstance(target, 0);
- }
-
- @Override
- @SuppressWarnings("all")
- public T[] wrap(Context cx, Object o) {
- if (o == null) {
- return emptyArray;
- } else if (o instanceof Iterable) {
- int size;
-
- if (o instanceof Collection) {
- size = ((Collection) o).size();
- } else {
- size = 0;
-
- for (Object o1 : (Iterable) o) {
- size++;
- }
- }
-
- if (size == 0) {
- return emptyArray;
- }
-
- T[] array = (T[]) Array.newInstance(target, size);
- int index = 0;
-
- for (Object o1 : (Iterable) o) {
- if (typeWrapper.validator.test(o1)) {
- array[index] = typeWrapper.factory.wrap(cx, o1);
- index++;
- }
- }
-
- return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget);
- } else if (o.getClass().isArray()) {
- int size = Array.getLength(o);
-
- if (size == 0) {
- return emptyArray;
- }
-
- T[] array = (T[]) Array.newInstance(target, size);
- int index = 0;
-
- for (int i = 0; i < array.length; i++) {
- Object o1 = Array.get(o, i);
-
- if (typeWrapper.validator.test(o1)) {
- array[index] = typeWrapper.factory.wrap(cx, o1);
- index++;
- }
- }
-
- return index == 0 ? emptyArray : index == array.length ? array : Arrays.copyOf(array, index, arrayTarget);
- } else if (typeWrapper.validator.test(o)) {
- T[] array = (T[]) Array.newInstance(target, 1);
- array[0] = typeWrapper.factory.wrap(cx, o);
- return array;
- }
-
- return emptyArray;
- }
-}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java
index fdcda6a1..232b2447 100644
--- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java
+++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapper.java
@@ -1,20 +1,7 @@
package dev.latvian.mods.rhino.util.wrap;
-import java.util.function.Predicate;
-
/**
* @author LatvianModder
*/
-public class TypeWrapper {
- public static final Predicate ALWAYS_VALID = o -> true;
-
- public final Class target;
- public final Predicate validator;
- public final TypeWrapperFactory factory;
-
- TypeWrapper(Class t, Predicate v, TypeWrapperFactory f) {
- target = t;
- validator = v;
- factory = f;
- }
+public record TypeWrapper(Class target, TypeWrapperValidator validator, TypeWrapperFactory factory) {
}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java
index a0ce7ed6..9f87d909 100644
--- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java
+++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperFactory.java
@@ -2,19 +2,12 @@
import dev.latvian.mods.rhino.Context;
+import java.lang.reflect.Type;
+
/**
* @author LatvianModder
*/
@FunctionalInterface
public interface TypeWrapperFactory {
- interface Simple extends TypeWrapperFactory {
- T wrapSimple(Object o);
-
- @Override
- default T wrap(Context cx, Object o) {
- return wrapSimple(o);
- }
- }
-
- T wrap(Context cx, Object o);
+ T wrap(Context cx, Object from, Class> toType, Type toGenericType);
}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java
new file mode 100644
index 00000000..7e4a2fa2
--- /dev/null
+++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrapperValidator.java
@@ -0,0 +1,10 @@
+package dev.latvian.mods.rhino.util.wrap;
+
+import java.lang.reflect.Type;
+
+@FunctionalInterface
+public interface TypeWrapperValidator {
+ TypeWrapperValidator ALWAYS_VALID = (from, target, genericTarget) -> true;
+
+ boolean isValid(Object from, Class> target, Type genericTarget);
+}
diff --git a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java
index 137de2c6..db9f2e8b 100644
--- a/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java
+++ b/src/main/java/dev/latvian/mods/rhino/util/wrap/TypeWrappers.java
@@ -3,75 +3,55 @@
import dev.latvian.mods.rhino.util.EnumTypeWrapper;
import org.jetbrains.annotations.Nullable;
-import java.lang.reflect.Array;
+import java.lang.reflect.Type;
import java.util.LinkedHashMap;
import java.util.Map;
-import java.util.function.Predicate;
/**
* @author LatvianModder
*/
public class TypeWrappers {
- private final Map, TypeWrapper>> wrappers = new LinkedHashMap<>();
+ public final Map, TypeWrapper>> wrappers = new LinkedHashMap<>();
- @SuppressWarnings("unchecked")
- public void register(Class target, Predicate validator, TypeWrapperFactory factory) {
+ public void register(Class target, TypeWrapperValidator validator, TypeWrapperFactory factory) {
if (target == null || target == Object.class) {
throw new IllegalArgumentException("target can't be Object.class!");
} else if (target.isArray()) {
throw new IllegalArgumentException("target can't be an array!");
} else if (wrappers.containsKey(target)) {
throw new IllegalArgumentException("Wrapper for class " + target.getName() + " already exists!");
+ } else {
+ wrappers.put(target, new TypeWrapper<>(target, validator, factory));
}
-
- TypeWrapper typeWrapper0 = new TypeWrapper<>(target, validator, factory);
- wrappers.put(target, typeWrapper0);
-
- // I know this looks like cancer but it's actually pretty simple - grab T[].class, register ArrayTypeWrapperFactory
- // You may say that it would be better to just implement N-sized array checking directly in java parser, but this is way more efficient
-
- // 1D
- Class target1 = (Class) Array.newInstance(target, 0).getClass();
- TypeWrapper typeWrapper1 = new TypeWrapper<>(target1, validator, new ArrayTypeWrapperFactory<>(typeWrapper0, target, target1));
- wrappers.put(target1, typeWrapper1);
-
- // 2D
- Class target2 = (Class) Array.newInstance(target1, 0).getClass();
- TypeWrapper typeWrapper2 = new TypeWrapper<>(target2, validator, new ArrayTypeWrapperFactory<>(typeWrapper1, target1, target2));
- wrappers.put(target2, typeWrapper2);
-
- // 3D
- Class target3 = (Class) Array.newInstance(target2, 0).getClass();
- TypeWrapper typeWrapper3 = new TypeWrapper<>(target3, validator, new ArrayTypeWrapperFactory<>(typeWrapper2, target2, target3));
- wrappers.put(target3, typeWrapper3);
-
- // 4D.. yeah no. 3D already is an overkill
}
public void register(Class target, TypeWrapperFactory factory) {
- register(target, TypeWrapper.ALWAYS_VALID, factory);
+ register(target, TypeWrapperValidator.ALWAYS_VALID, factory);
}
- public void registerSimple(Class target, Predicate validator, TypeWrapperFactory.Simple factory) {
- register(target, validator, factory);
- }
+ public boolean hasWrapper(Object from, Class> target, Type genericTarget) {
+ if (target.isEnum() || target.isRecord()) {
+ return true;
+ }
- public void registerSimple(Class target, TypeWrapperFactory.Simple factory) {
- register(target, TypeWrapper.ALWAYS_VALID, factory);
+ var wrapper = wrappers.get(target);
+ return wrapper != null && wrapper.validator().isValid(from, target, genericTarget);
}
@Nullable
- public TypeWrapperFactory> getWrapperFactory(Class> target, @Nullable Object from) {
+ public TypeWrapperFactory> getWrapperFactory(@Nullable Object from, Class> target, Type genericTarget) {
if (target == Object.class) {
return null;
}
- TypeWrapper> wrapper = wrappers.get(target);
+ var wrapper = wrappers.get(target);
- if (wrapper != null && wrapper.validator.test(from)) {
- return wrapper.factory;
+ if (wrapper != null && wrapper.validator().isValid(from, target, genericTarget)) {
+ return wrapper.factory();
} else if (target.isEnum()) {
return EnumTypeWrapper.get(target);
+ } else if (target.isRecord()) {
+
}
//else if (from != null && target.isArray() && !from.getClass().isArray() && target.getComponentType() == from.getClass() && !target.isPrimitive())
diff --git a/src/main/resources/rhino.mixins.json b/src/main/resources/rhino.mixins.json
index 6ffab0a2..a3d1d0a8 100644
--- a/src/main/resources/rhino.mixins.json
+++ b/src/main/resources/rhino.mixins.json
@@ -9,6 +9,7 @@
"NumericTagMixin",
"ResourceKeyMixin",
"ResourceLocationMixin",
+ "StringRepresentableMixin",
"StringTagMixin",
"TextColorMixin"
],
diff --git a/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java b/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java
index f72446f7..82d06049 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/MiscTests.java
@@ -18,7 +18,7 @@ public void testFunctionAssignment() {
x.abc = 1;
console.info(x.abc);
""",
- "1.0"
+ "1"
);
}
@@ -112,9 +112,9 @@ public void keysValuesEntries() {
console.info(Object.values(shared.testObject))
console.info(Object.entries(shared.testObject))
""", """
- [a, b, c]
- [-39.0, 2.0, 3439438.0]
- [[a, -39.0], [b, 2.0], [c, 3439438.0]]
+ ['a', 'b', 'c']
+ [-39, 2, 3439438]
+ [['a', -39], ['b', 2], ['c', 3439438]]
""");
}
@@ -147,10 +147,25 @@ public void typeWrappers() {
}
@Test
- public void testJsonStringifyWithNestedArrays() {
+ public void jsonStringifyWithNestedArrays() {
TEST.test("jsonStringifyWithNestedArrays", """
const thing = {nested: [1, 2, 3]};
console.info(JSON.stringify(thing));
""", "{\"nested\":[1.0,2.0,3.0]}");
}
+
+ @Test
+ public void genericTypes() {
+ TEST.test("genericTypes", "console.genericType(['a', 'b']);", "Generic type:\n[WithContext[a], WithContext[b]]");
+ }
+
+ @Test
+ public void genericTypesUnwrapped() {
+ TEST.test("genericTypesUnwrapped", "console.genericType('a');", "Generic type:\n[WithContext[a]]");
+ }
+
+ @Test
+ public void genericTypesFromList() {
+ TEST.test("genericTypesFromList", "console.genericType('a');", "Generic type:\n[WithContext[a]]");
+ }
}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java b/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java
index 1cf126a8..20575179 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/NullishCoalescingTests.java
@@ -14,7 +14,7 @@ public void bothNonNull() {
let c = a ?? b
console.info(c)
""", """
- 10.0
+ 10
""");
}
@@ -26,7 +26,7 @@ public void firstNull() {
let c = a ?? b
console.info(c)
""", """
- 20.0
+ 20
""");
}
@@ -38,7 +38,7 @@ public void firstUndefined() {
let c = a ?? b
console.info(c)
""", """
- 20.0
+ 20
""");
}
@@ -62,7 +62,7 @@ public void firstZero() {
let c = a ?? b
console.info(c)
""", """
- 0.0
+ 0
""");
}
@@ -74,7 +74,7 @@ public void secondNull() {
let c = a ?? b
console.info(c)
""", """
- 10.0
+ 10
""");
}
}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/PowTests.java b/src/test/java/dev/latvian/mods/rhino/test/PowTests.java
index 52b208d6..686abd59 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/PowTests.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/PowTests.java
@@ -14,7 +14,7 @@ public void bothWhole() {
let c = a ** b
console.info(c)
""", """
- 1000.0
+ 1000
""");
}
@@ -50,7 +50,7 @@ public void zeroExponent() {
let c = a ** b
console.info(c)
""", """
- 1.0
+ 1
""");
}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java b/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java
index 769ea1c6..b84e8178 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/RhinoTest.java
@@ -19,7 +19,7 @@ public RhinoTest(String n) {
this.shared = new HashMap<>();
var typeWrappers = factory.getTypeWrappers();
- typeWrappers.registerSimple(TestMaterial.class, TestMaterial::get);
+ typeWrappers.register(TestMaterial.class, (cx, from, target, genericTarget) -> TestMaterial.get(from));
}
public void test(String name, String script, String match) {
@@ -28,6 +28,7 @@ public void test(String name, String script, String match) {
var rootScope = context.initStandardObjects();
context.addToScope(rootScope, "console", console);
context.addToScope(rootScope, "shared", shared);
+ context.addToScope(rootScope, "testName", name);
context.evaluateString(rootScope, script, testName + "/" + name, 1, null);
} catch (Exception ex) {
ex.printStackTrace();
diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java b/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java
index d6e44dee..25a29e64 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/TestConsole.java
@@ -2,6 +2,7 @@
import dev.latvian.mods.rhino.Context;
import dev.latvian.mods.rhino.ContextFactory;
+import dev.latvian.mods.rhino.ScriptRuntime;
import dev.latvian.mods.rhino.util.RemapPrefixForJS;
import dev.latvian.mods.unit.UnitContext;
@@ -21,7 +22,7 @@ public TestConsole(ContextFactory factory) {
}
public void info(Object o) {
- String s = String.valueOf(o);
+ String s = ScriptRuntime.toString(factory.enter(), o);
StringBuilder builder = new StringBuilder();
@@ -83,4 +84,9 @@ public void printUnit(String input) {
public void printMaterial(TestMaterial material) {
info("%s#%08x".formatted(material.name(), material.hashCode()));
}
+
+ public void genericType(WithContext[] test) {
+ info("Generic type:");
+ info(test);
+ }
}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestContext.java b/src/test/java/dev/latvian/mods/rhino/test/TestContext.java
new file mode 100644
index 00000000..9ffc2f6d
--- /dev/null
+++ b/src/test/java/dev/latvian/mods/rhino/test/TestContext.java
@@ -0,0 +1,40 @@
+package dev.latvian.mods.rhino.test;
+
+import dev.latvian.mods.rhino.Context;
+import dev.latvian.mods.rhino.EvaluatorException;
+import dev.latvian.mods.rhino.util.TypeUtils;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+public class TestContext extends Context {
+ public TestContext(TestContextFactory factory) {
+ super(factory);
+ }
+
+ @Override
+ public int internalConversionWeight(Object fromObj, Class> target, Type genericTarget) {
+ if (target == WithContext.class) {
+ return CONVERSION_NONTRIVIAL;
+ }
+
+ return super.internalConversionWeight(fromObj, target, genericTarget);
+ }
+
+ @Override
+ protected Object internalJsToJava(Object value, Class> target, Type genericTarget) throws EvaluatorException {
+ if (genericTarget instanceof ParameterizedType parameterizedType) {
+ if (target == WithContext.class) {
+ var types = parameterizedType.getActualTypeArguments();
+
+ if (types.length == 1) {
+ return new WithContext<>(this, jsToJava(value, TypeUtils.getRawType(types[0]), types[0]));
+ }
+
+ return new WithContext<>(this, value);
+ }
+ }
+
+ return super.internalJsToJava(value, target, genericTarget);
+ }
+}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java b/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java
index d4145742..23e1be39 100644
--- a/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java
+++ b/src/test/java/dev/latvian/mods/rhino/test/TestContextFactory.java
@@ -1,6 +1,11 @@
package dev.latvian.mods.rhino.test;
+import dev.latvian.mods.rhino.Context;
import dev.latvian.mods.rhino.ContextFactory;
public class TestContextFactory extends ContextFactory {
+ @Override
+ protected Context createContext() {
+ return new TestContext(this);
+ }
}
diff --git a/src/test/java/dev/latvian/mods/rhino/test/WithContext.java b/src/test/java/dev/latvian/mods/rhino/test/WithContext.java
new file mode 100644
index 00000000..e2e73d03
--- /dev/null
+++ b/src/test/java/dev/latvian/mods/rhino/test/WithContext.java
@@ -0,0 +1,22 @@
+package dev.latvian.mods.rhino.test;
+
+import dev.latvian.mods.rhino.Context;
+
+import java.util.Objects;
+
+public record WithContext(Context cx, T value) {
+ @Override
+ public int hashCode() {
+ return value == null ? 0 : value.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ return o instanceof WithContext> wc && Objects.equals(value, wc.value);
+ }
+
+ @Override
+ public String toString() {
+ return "WithContext[" + value + "]";
+ }
+}