diff --git a/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java new file mode 100644 index 0000000000..f1e81d25e4 --- /dev/null +++ b/benchmarks/org/mozilla/javascript/benchmarks/BuiltinBenchmark.java @@ -0,0 +1,383 @@ +package org.mozilla.javascript.benchmarks; + +import java.lang.reflect.InvocationTargetException; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.IdFunctionObject; +import org.mozilla.javascript.IdScriptableObject; +import org.mozilla.javascript.LambdaConstructor; +import org.mozilla.javascript.Script; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.Undefined; +import org.mozilla.javascript.annotations.*; +import org.openjdk.jmh.annotations.*; + +public class BuiltinBenchmark { + + @State(Scope.Thread) + public static class AbstractClassState { + + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + cx = Context.enter(); + cx.setOptimizationLevel(9); + cx.setLanguageVersion(Context.VERSION_ES6); + + scope = cx.initStandardObjects(); + ScriptableObject.defineClass(scope, AnnotatedClass.class); + IdClass.init(scope); + DumbLambdaClass.init(scope); + } + + void compileScript(String testClassName) { + testScript = + cx.compileString( + "o = new " + + testClassName + + "();\n" + + "o.setValue(99);\n" + + "for (var i = 0; i < 1000; i++) {\n" + + "if (o.getValue() !== 99) { throw 'Not working!'; }\n" + + "}\n" + + "o.getValue();", + "test.js", + 1, + null); + } + + @TearDown(Level.Trial) + public void close() { + Context.exit(); + } + + Context cx; + Scriptable scope; + Script testScript; + } + + @State(Scope.Thread) + public static class AnnotatedClassState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("AnnotatedClass"); + } + } + + @Benchmark + public Object annotatedClassMethods(AnnotatedClassState state) { + return state.testScript.exec(state.cx, state.scope); + } + + public static class AnnotatedClass extends ScriptableObject { + + @Override + public String getClassName() { + return "AnnotatedClass"; + } + + @JSFunction + public void one() {} + + @JSFunction + public void two() {} + + @JSFunction + public void three() {} + + @JSFunction + public void four() {} + + @JSFunction + public void five() {} + + @JSFunction + public void six() {} + + @JSFunction + public void seven() {} + + @JSFunction + public void eight() {} + + @JSFunction + public void nine() {} + + @JSFunction + public void setValue(int value) { + this.value = value; + } + + @JSFunction + public int getValue() { + return value; + } + + private int value; + } + + @State(Scope.Thread) + public static class IdClassState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("IdClass"); + } + } + + @Benchmark + public Object idClassMethods(IdClassState state) { + return state.testScript.exec(state.cx, state.scope); + } + + public static class IdClass extends IdScriptableObject { + + private static final String TAG = "IdClass"; + + public static void init(Scriptable scope) { + IdClass idc = new IdClass(); + idc.exportAsJSClass(MAX_ID, scope, false); + } + + @Override + public String getClassName() { + return "IdClass"; + } + + @Override + protected void initPrototypeId(int id) { + String s, fnName = null; + int arity; + switch (id) { + case Id_one: + arity = 0; + s = "one"; + break; + case Id_two: + arity = 0; + s = "two"; + break; + case Id_three: + arity = 0; + s = "three"; + break; + case Id_four: + arity = 0; + s = "four"; + break; + case Id_five: + arity = 0; + s = "five"; + break; + case Id_six: + arity = 0; + s = "six"; + break; + case Id_seven: + arity = 0; + s = "seven"; + break; + case Id_eight: + arity = 0; + s = "eight"; + break; + case Id_nine: + arity = 0; + s = "nine"; + break; + case Id_setValue: + arity = 1; + s = "setValue"; + break; + case Id_getValue: + arity = 0; + s = "getValue"; + break; + case Id_constructor: + arity = 0; + s = "constructor"; + break; + default: + throw new IllegalArgumentException(String.valueOf(id)); + } + + initPrototypeMethod(TAG, id, s, fnName, arity); + } + + @Override + public Object execIdCall( + IdFunctionObject f, + Context cx, + Scriptable scope, + Scriptable thisObj, + Object[] args) { + if (!f.hasTag(TAG)) { + return super.execIdCall(f, cx, scope, thisObj, args); + } + int id = f.methodId(); + IdClass self; + switch (id) { + case Id_constructor: + return new IdClass(); + case Id_setValue: + self = (IdClass) thisObj; + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "not enough args"); + } + self.value = ScriptRuntime.toInt32(args[0]); + break; + case Id_getValue: + self = (IdClass) thisObj; + return self.value; + default: + throw new IllegalArgumentException( + "Array.prototype has no method: " + f.getFunctionName()); + } + return Undefined.instance; + } + + // #string_id_map# + + @Override + protected int findPrototypeId(String s) { + int id; + // #generated# Last update: 2021-04-13 16:17:26 PDT + switch (s) { + case "one": + id = Id_one; + break; + case "two": + id = Id_two; + break; + case "three": + id = Id_three; + break; + case "four": + id = Id_four; + break; + case "five": + id = Id_five; + break; + case "six": + id = Id_six; + break; + case "seven": + id = Id_seven; + break; + case "eight": + id = Id_eight; + break; + case "nine": + id = Id_nine; + break; + case "getValue": + id = Id_getValue; + break; + case "setValue": + id = Id_setValue; + break; + case "constructor": + id = Id_constructor; + break; + default: + id = 0; + break; + } + // #/generated# + return id; + } + + private static final int Id_one = 1, + Id_two = 2, + Id_three = 3, + Id_four = 4, + Id_five = 5, + Id_six = 6, + Id_seven = 7, + Id_eight = 8, + Id_nine = 9, + Id_getValue = 10, + Id_setValue = 11, + Id_constructor = 12, + MAX_ID = Id_constructor; + + // #/string_id_map# + + private int value; + } + + @State(Scope.Thread) + public static class DumbLambdaState extends AbstractClassState { + + @Setup(Level.Trial) + public void init() + throws IllegalAccessException, InvocationTargetException, InstantiationException { + super.init(); + compileScript("DumbLambdaClass"); + } + } + + @Benchmark + public Object dumbLambdaClassMethods(DumbLambdaState state) { + return state.testScript.exec(state.cx, state.scope); + } + + private static class DumbLambdaClass extends ScriptableObject { + + private static Object noop( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return Undefined.instance; + } + + private static Object setValue( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if (args.length < 1) { + throw ScriptRuntime.throwError(cx, scope, "Not enough args"); + } + DumbLambdaClass self = + LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + self.value = ScriptRuntime.toInt32(args[0]); + return Undefined.instance; + } + + private static Object getValue( + Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + DumbLambdaClass self = + LambdaConstructor.convertThisObject(thisObj, DumbLambdaClass.class); + return self.value; + } + + public static void init(Scriptable scope) { + LambdaConstructor cons = + new LambdaConstructor( + scope, + "DumbLambdaClass", + 0, + (Context cx, Scriptable s, Object[] args) -> new DumbLambdaClass()); + cons.definePrototypeMethod(scope, "one", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "two", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "three", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "four", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "five", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "six", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "seven", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "eight", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "nine", 0, DumbLambdaClass::noop); + cons.definePrototypeMethod(scope, "setValue", 1, DumbLambdaClass::setValue); + cons.definePrototypeMethod(scope, "getValue", 1, DumbLambdaClass::getValue); + ScriptableObject.putProperty(scope, "DumbLambdaClass", cons); + } + + @Override + public String getClassName() { + return "DumbLambdaClass"; + } + + private int value; + } +} diff --git a/build.gradle b/build.gradle index c2c787401e..d2a3dc52ea 100644 --- a/build.gradle +++ b/build.gradle @@ -108,10 +108,10 @@ task testBenchmark() {} testBenchmark.dependsOn sunSpiderBenchmark testBenchmark.dependsOn v8Benchmark -task microBenchmarks(type: JavaExec, description: 'JMH benchmarks') { +task builtinBenchmarks(type: JavaExec, description: 'JMH benchmarks') { classpath = sourceSets.jmh.runtimeClasspath main = 'org.openjdk.jmh.Main' - args '-f', '1', '-bm', 'avgt', '-tu', 'ns', 'ObjectCall' + args '-f', '1', '-bm', 'avgt', '-tu', 'ns', 'Builtin' } task jmhHelp(type: JavaExec, description: 'JMH benchmarks') { diff --git a/src/org/mozilla/javascript/BaseFunction.java b/src/org/mozilla/javascript/BaseFunction.java index e29a97234d..ed95eedd29 100644 --- a/src/org/mozilla/javascript/BaseFunction.java +++ b/src/org/mozilla/javascript/BaseFunction.java @@ -7,31 +7,29 @@ package org.mozilla.javascript; /** - * The base class for Function objects. That is one of two purposes. It is also - * the prototype for every "function" defined except those that are used - * as GeneratorFunctions via the ES6 "function *" syntax. + * The base class for Function objects. That is one of two purposes. It is also the prototype for + * every "function" defined except those that are used as GeneratorFunctions via the ES6 "function + * *" syntax. + * + *

See ECMA 15.3. * - * See ECMA 15.3. * @author Norris Boyd */ -public class BaseFunction extends IdScriptableObject implements Function -{ +public class BaseFunction extends IdScriptableObject implements Function { private static final long serialVersionUID = 5311394446546053859L; private static final Object FUNCTION_TAG = "Function"; private static final String FUNCTION_CLASS = "Function"; static final String GENERATOR_FUNCTION_CLASS = "__GeneratorFunction"; - static void init(Scriptable scope, boolean sealed) - { + static void init(Scriptable scope, boolean sealed) { BaseFunction obj = new BaseFunction(); // Function.prototype attributes: see ECMA 15.3.3.1 obj.prototypePropertyAttributes = DONTENUM | READONLY | PERMANENT; obj.exportAsJSClass(MAX_PROTOTYPE_ID, scope, sealed); } - static Object initAsGeneratorFunction(Scriptable scope, boolean sealed) - { + static Object initAsGeneratorFunction(Scriptable scope, boolean sealed) { BaseFunction obj = new BaseFunction(true); // Function.prototype attributes: see ECMA 15.3.3.1 obj.prototypePropertyAttributes = READONLY | PERMANENT; @@ -63,143 +61,142 @@ protected boolean isGeneratorFunction() { /** * Gets the value returned by calling the typeof operator on this object. + * * @see org.mozilla.javascript.ScriptableObject#getTypeOf() - * @return "function" or "undefined" if {@link #avoidObjectDetection()} returns true + * @return "function" or "undefined" if {@link #avoidObjectDetection()} returns true + * */ @Override - public String getTypeOf() - { + public String getTypeOf() { return avoidObjectDetection() ? "undefined" : "function"; } /** * Implements the instanceof operator for JavaScript Function objects. - *

- * + * + *

* foo = new Foo();
* foo instanceof Foo; // true
*
* - * @param instance The value that appeared on the LHS of the instanceof - * operator - * @return true if the "prototype" property of "this" appears in - * value's prototype chain - * + * @param instance The value that appeared on the LHS of the instanceof operator + * @return true if the "prototype" property of "this" appears in value's prototype chain */ @Override - public boolean hasInstance(Scriptable instance) - { + public boolean hasInstance(Scriptable instance) { Object protoProp = ScriptableObject.getProperty(this, "prototype"); if (protoProp instanceof Scriptable) { - return ScriptRuntime.jsDelegatesTo(instance, (Scriptable)protoProp); + return ScriptRuntime.jsDelegatesTo(instance, (Scriptable) protoProp); } - throw ScriptRuntime.typeErrorById("msg.instanceof.bad.prototype", - getFunctionName()); + throw ScriptRuntime.typeErrorById("msg.instanceof.bad.prototype", getFunctionName()); } -// #string_id_map# - - private static final int - Id_length = 1, - Id_arity = 2, - Id_name = 3, - Id_prototype = 4, - Id_arguments = 5, + // #string_id_map# - MAX_INSTANCE_ID = 5; + private static final int Id_length = 1, + Id_arity = 2, + Id_name = 3, + Id_prototype = 4, + Id_arguments = 5, + MAX_INSTANCE_ID = 5; @Override - protected int getMaxInstanceId() - { + protected int getMaxInstanceId() { return MAX_INSTANCE_ID; } @Override - protected int findInstanceIdInfo(String s) - { + protected int findInstanceIdInfo(String s) { int id; -// #generated# Last update: 2021-03-21 09:52:05 MEZ + // #generated# Last update: 2021-03-21 09:52:05 MEZ switch (s) { - case "length": - id = Id_length; - break; - case "arity": - id = Id_arity; - break; - case "name": - id = Id_name; - break; - case "prototype": - id = Id_prototype; - break; - case "arguments": - id = Id_arguments; - break; - default: - id = 0; - break; + case "length": + id = Id_length; + break; + case "arity": + id = Id_arity; + break; + case "name": + id = Id_name; + break; + case "prototype": + id = Id_prototype; + break; + case "arguments": + id = Id_arguments; + break; + default: + id = 0; + break; } -// #/generated# -// #/string_id_map# + // #/generated# + // #/string_id_map# if (id == 0) return super.findInstanceIdInfo(s); int attr; switch (id) { - case Id_length: - case Id_arity: - case Id_name: - attr = DONTENUM | READONLY | PERMANENT; - break; - case Id_prototype: - // some functions such as built-ins don't have a prototype property - if (!hasPrototypeProperty()) { - return 0; - } - attr = prototypePropertyAttributes; - break; - case Id_arguments: - attr = argumentsAttributes; - break; - default: throw new IllegalStateException(); + case Id_length: + case Id_arity: + case Id_name: + attr = DONTENUM | READONLY | PERMANENT; + break; + case Id_prototype: + // some functions such as built-ins don't have a prototype property + if (!hasPrototypeProperty()) { + return 0; + } + attr = prototypePropertyAttributes; + break; + case Id_arguments: + attr = argumentsAttributes; + break; + default: + throw new IllegalStateException(); } return instanceIdInfo(attr, id); } @Override - protected String getInstanceIdName(int id) - { + protected String getInstanceIdName(int id) { switch (id) { - case Id_length: return "length"; - case Id_arity: return "arity"; - case Id_name: return "name"; - case Id_prototype: return "prototype"; - case Id_arguments: return "arguments"; + case Id_length: + return "length"; + case Id_arity: + return "arity"; + case Id_name: + return "name"; + case Id_prototype: + return "prototype"; + case Id_arguments: + return "arguments"; } return super.getInstanceIdName(id); } @Override - protected Object getInstanceIdValue(int id) - { + protected Object getInstanceIdValue(int id) { switch (id) { - case Id_length: return ScriptRuntime.wrapInt(getLength()); - case Id_arity: return ScriptRuntime.wrapInt(getArity()); - case Id_name: return getFunctionName(); - case Id_prototype: return getPrototypeProperty(); - case Id_arguments: return getArguments(); + case Id_length: + return ScriptRuntime.wrapInt(getLength()); + case Id_arity: + return ScriptRuntime.wrapInt(getArity()); + case Id_name: + return getFunctionName(); + case Id_prototype: + return getPrototypeProperty(); + case Id_arguments: + return getArguments(); } return super.getInstanceIdValue(id); } @Override - protected void setInstanceIdValue(int id, Object value) - { + protected void setInstanceIdValue(int id, Object value) { switch (id) { case Id_prototype: if ((prototypePropertyAttributes & READONLY) == 0) { - prototypeProperty = (value != null) - ? value : UniqueTag.NULL_VALUE; + prototypeProperty = (value != null) ? value : UniqueTag.NULL_VALUE; } return; case Id_arguments: @@ -222,8 +219,7 @@ protected void setInstanceIdValue(int id, Object value) } @Override - protected void setInstanceIdAttributes(int id, int attr) - { + protected void setInstanceIdAttributes(int id, int attr) { switch (id) { case Id_prototype: prototypePropertyAttributes = attr; @@ -236,8 +232,7 @@ protected void setInstanceIdAttributes(int id, int attr) } @Override - protected void fillConstructorProperties(IdFunctionObject ctor) - { + protected void fillConstructorProperties(IdFunctionObject ctor) { // Fix up bootstrapping problem: getPrototype of the IdFunctionObject // can not return Function.prototype because Function object is not // yet defined. @@ -246,18 +241,36 @@ protected void fillConstructorProperties(IdFunctionObject ctor) } @Override - protected void initPrototypeId(int id) - { + protected void initPrototypeId(int id) { String s; int arity; switch (id) { - case Id_constructor: arity=1; s="constructor"; break; - case Id_toString: arity=0; s="toString"; break; - case Id_toSource: arity=1; s="toSource"; break; - case Id_apply: arity=2; s="apply"; break; - case Id_call: arity=1; s="call"; break; - case Id_bind: arity=1; s="bind"; break; - default: throw new IllegalArgumentException(String.valueOf(id)); + case Id_constructor: + arity = 1; + s = "constructor"; + break; + case Id_toString: + arity = 0; + s = "toString"; + break; + case Id_toSource: + arity = 1; + s = "toSource"; + break; + case Id_apply: + arity = 2; + s = "apply"; + break; + case Id_call: + arity = 1; + s = "call"; + break; + case Id_bind: + arity = 1; + s = "bind"; + break; + default: + throw new IllegalArgumentException(String.valueOf(id)); } initPrototypeMethod(FUNCTION_TAG, id, s, arity); } @@ -267,8 +280,8 @@ static boolean isApply(IdFunctionObject f) { } static boolean isApplyOrCall(IdFunctionObject f) { - if(f.hasTag(FUNCTION_TAG)) { - switch(f.methodId()) { + if (f.hasTag(FUNCTION_TAG)) { + switch (f.methodId()) { case Id_apply: case Id_call: return true; @@ -278,79 +291,74 @@ static boolean isApplyOrCall(IdFunctionObject f) { } @Override - public Object execIdCall(IdFunctionObject f, Context cx, Scriptable scope, - Scriptable thisObj, Object[] args) - { + public Object execIdCall( + IdFunctionObject f, Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { if (!f.hasTag(FUNCTION_TAG)) { return super.execIdCall(f, cx, scope, thisObj, args); } int id = f.methodId(); switch (id) { - case Id_constructor: - return jsConstructor(cx, scope, args); - - case Id_toString: { - BaseFunction realf = realFunction(thisObj, f); - int indent = ScriptRuntime.toInt32(args, 0); - return realf.decompile(indent, 0); - } - - case Id_toSource: { - BaseFunction realf = realFunction(thisObj, f); - int indent = 0; - int flags = Decompiler.TO_SOURCE_FLAG; - if (args.length != 0) { - indent = ScriptRuntime.toInt32(args[0]); - if (indent >= 0) { - flags = 0; - } else { - indent = 0; + case Id_constructor: + return jsConstructor(cx, scope, args); + + case Id_toString: + { + BaseFunction realf = realFunction(thisObj, f); + int indent = ScriptRuntime.toInt32(args, 0); + return realf.decompile(indent, 0); } - } - return realf.decompile(indent, flags); - } - case Id_apply: - case Id_call: - return ScriptRuntime.applyOrCall(id == Id_apply, - cx, scope, thisObj, args); + case Id_toSource: + { + BaseFunction realf = realFunction(thisObj, f); + int indent = 0; + int flags = Decompiler.TO_SOURCE_FLAG; + if (args.length != 0) { + indent = ScriptRuntime.toInt32(args[0]); + if (indent >= 0) { + flags = 0; + } else { + indent = 0; + } + } + return realf.decompile(indent, flags); + } - case Id_bind: - if ( !(thisObj instanceof Callable) ) { - throw ScriptRuntime.notFunctionError(thisObj); - } - Callable targetFunction = (Callable) thisObj; - int argc = args.length; - final Scriptable boundThis; - final Object[] boundArgs; - if (argc > 0) { - boundThis = ScriptRuntime.toObjectOrNull(cx, args[0], scope); - boundArgs = new Object[argc-1]; - System.arraycopy(args, 1, boundArgs, 0, argc-1); - } else { - boundThis = null; - boundArgs = ScriptRuntime.emptyArgs; - } - return new BoundFunction(cx, scope, targetFunction, boundThis, boundArgs); + case Id_apply: + case Id_call: + return ScriptRuntime.applyOrCall(id == Id_apply, cx, scope, thisObj, args); + + case Id_bind: + if (!(thisObj instanceof Callable)) { + throw ScriptRuntime.notFunctionError(thisObj); + } + Callable targetFunction = (Callable) thisObj; + int argc = args.length; + final Scriptable boundThis; + final Object[] boundArgs; + if (argc > 0) { + boundThis = ScriptRuntime.toObjectOrNull(cx, args[0], scope); + boundArgs = new Object[argc - 1]; + System.arraycopy(args, 1, boundArgs, 0, argc - 1); + } else { + boundThis = null; + boundArgs = ScriptRuntime.emptyArgs; + } + return new BoundFunction(cx, scope, targetFunction, boundThis, boundArgs); } throw new IllegalArgumentException(String.valueOf(id)); } - private static BaseFunction realFunction(Scriptable thisObj, IdFunctionObject f) - { + private static BaseFunction realFunction(Scriptable thisObj, IdFunctionObject f) { Object x = thisObj.getDefaultValue(ScriptRuntime.FunctionClass); if (x instanceof Delegator) { - x = ((Delegator)x).getDelegee(); + x = ((Delegator) x).getDelegee(); } return ensureType(x, BaseFunction.class, f); } - /** - * Make value as DontEnum, DontDelete, ReadOnly - * prototype property of this Function object - */ - public void setImmunePrototypeProperty(Object value) - { + /** Make value as DontEnum, DontDelete, ReadOnly prototype property of this Function object */ + public void setImmunePrototypeProperty(Object value) { if ((prototypePropertyAttributes & READONLY) != 0) { throw new IllegalStateException(); } @@ -358,8 +366,7 @@ public void setImmunePrototypeProperty(Object value) prototypePropertyAttributes = DONTENUM | PERMANENT | READONLY; } - protected Scriptable getClassPrototype() - { + protected Scriptable getClassPrototype() { Object protoVal = getPrototypeProperty(); if (protoVal instanceof Scriptable) { return (Scriptable) protoVal; @@ -367,24 +374,19 @@ protected Scriptable getClassPrototype() return ScriptableObject.getObjectPrototype(this); } - /** - * Should be overridden. - */ + /** Should be overridden. */ @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, - Object[] args) - { + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { return Undefined.instance; } @Override - public Scriptable construct(Context cx, Scriptable scope, Object[] args) - { + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { Scriptable result = createObject(cx, scope); if (result != null) { Object val = call(cx, scope, result, args); if (val instanceof Scriptable) { - result = (Scriptable)val; + result = (Scriptable) val; } } else { Object val = call(cx, scope, null, args); @@ -392,10 +394,12 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) // It is program error not to return Scriptable from // the call method if createObject returns null. throw new IllegalStateException( - "Bad implementaion of call as constructor, name=" - +getFunctionName()+" in "+getClass().getName()); + "Bad implementaion of call as constructor, name=" + + getFunctionName() + + " in " + + getClass().getName()); } - result = (Scriptable)val; + result = (Scriptable) val; if (result.getPrototype() == null) { Scriptable proto = getClassPrototype(); if (result != proto) { @@ -413,17 +417,13 @@ public Scriptable construct(Context cx, Scriptable scope, Object[] args) } /** - * Creates new script object. - * The default implementation of {@link #construct} uses the method to - * to get the value for thisObj argument when invoking - * {@link #call}. - * The methos is allowed to return null to indicate that - * {@link #call} will create a new object itself. In this case - * {@link #construct} will set scope and prototype on the result + * Creates new script object. The default implementation of {@link #construct} uses the method + * to to get the value for thisObj argument when invoking {@link #call}. The methos + * is allowed to return null to indicate that {@link #call} will create a new + * object itself. In this case {@link #construct} will set scope and prototype on the result * {@link #call} unless they are already set. */ - public Scriptable createObject(Context cx, Scriptable scope) - { + public Scriptable createObject(Context cx, Scriptable scope) { Scriptable newInstance = new NativeObject(); newInstance.setPrototype(getClassPrototype()); newInstance.setParentScope(getParentScope()); @@ -431,15 +431,12 @@ public Scriptable createObject(Context cx, Scriptable scope) } /** - * Decompile the source information associated with this js - * function/script back into a string. + * Decompile the source information associated with this js function/script back into a string. * * @param indent How much to indent the decompiled result. - * * @param flags Flags specifying format of decompilation output. */ - String decompile(int indent, int flags) - { + String decompile(int indent, int flags) { StringBuilder sb = new StringBuilder(); boolean justbody = (0 != (flags & Decompiler.ONLY_BODY_FLAG)); if (!justbody) { @@ -456,9 +453,13 @@ String decompile(int indent, int flags) return sb.toString(); } - public int getArity() { return 0; } + public int getArity() { + return 0; + } - public int getLength() { return 0; } + public int getLength() { + return 0; + } public String getFunctionName() { return ""; @@ -484,7 +485,11 @@ protected Object getPrototypeProperty() { return result; } - private synchronized Object setupDefaultPrototype() { + protected void setPrototypeProperty(Object prototype) { + this.prototypeProperty = prototype; + } + + protected synchronized Object setupDefaultPrototype() { if (prototypeProperty != null) { return prototypeProperty; } @@ -503,29 +508,25 @@ private synchronized Object setupDefaultPrototype() { return obj; } - private Object getArguments() - { - // .arguments is deprecated, so we use a slow - // way of getting it that doesn't add to the invocation cost. - // TODO: add warning, error based on version - Object value = defaultHas("arguments") ? defaultGet("arguments") : argumentsObj; - if (value != NOT_FOUND) { - // Should after changing .arguments its - // activation still be available during Function call? - // This code assumes it should not: - // defaultGet("arguments") != NOT_FOUND - // means assigned arguments - return value; - } - Context cx = Context.getContext(); - NativeCall activation = ScriptRuntime.findFunctionActivation(cx, this); - return (activation == null) - ? null - : activation.get("arguments", activation); - } - - private Object jsConstructor(Context cx, Scriptable scope, Object[] args) - { + private Object getArguments() { + // .arguments is deprecated, so we use a slow + // way of getting it that doesn't add to the invocation cost. + // TODO: add warning, error based on version + Object value = defaultHas("arguments") ? defaultGet("arguments") : argumentsObj; + if (value != NOT_FOUND) { + // Should after changing .arguments its + // activation still be available during Function call? + // This code assumes it should not: + // defaultGet("arguments") != NOT_FOUND + // means assigned arguments + return value; + } + Context cx = Context.getContext(); + NativeCall activation = ScriptRuntime.findFunctionActivation(cx, this); + return (activation == null) ? null : activation.get("arguments", activation); + } + + private Object jsConstructor(Context cx, Scriptable scope, Object[] args) { int arglen = args.length; StringBuilder sourceBuf = new StringBuilder(); @@ -567,8 +568,7 @@ private Object jsConstructor(Context cx, Scriptable scope, Object[] args) linep[0] = 1; } - String sourceURI = ScriptRuntime. - makeUrlForGeneratedScript(false, filename, linep[0]); + String sourceURI = ScriptRuntime.makeUrlForGeneratedScript(false, filename, linep[0]); Scriptable global = ScriptableObject.getTopLevelScope(scope); @@ -577,60 +577,55 @@ private Object jsConstructor(Context cx, Scriptable scope, Object[] args) Evaluator evaluator = Context.createInterpreter(); if (evaluator == null) { - throw new JavaScriptException("Interpreter not present", - filename, linep[0]); + throw new JavaScriptException("Interpreter not present", filename, linep[0]); } // Compile with explicit interpreter instance to force interpreter // mode. - return cx.compileFunction(global, source, evaluator, reporter, - sourceURI, 1, null); + return cx.compileFunction(global, source, evaluator, reporter, sourceURI, 1, null); } @Override - protected int findPrototypeId(String s) - { + protected int findPrototypeId(String s) { int id; -// #string_id_map# -// #generated# Last update: 2021-03-21 09:52:05 MEZ + // #string_id_map# + // #generated# Last update: 2021-03-21 09:52:05 MEZ switch (s) { - case "constructor": - id = Id_constructor; - break; - case "toString": - id = Id_toString; - break; - case "toSource": - id = Id_toSource; - break; - case "apply": - id = Id_apply; - break; - case "call": - id = Id_call; - break; - case "bind": - id = Id_bind; - break; - default: - id = 0; - break; + case "constructor": + id = Id_constructor; + break; + case "toString": + id = Id_toString; + break; + case "toSource": + id = Id_toSource; + break; + case "apply": + id = Id_apply; + break; + case "call": + id = Id_call; + break; + case "bind": + id = Id_bind; + break; + default: + id = 0; + break; } -// #/generated# + // #/generated# return id; } - private static final int - Id_constructor = 1, - Id_toString = 2, - Id_toSource = 3, - Id_apply = 4, - Id_call = 5, - Id_bind = 6, + private static final int Id_constructor = 1, + Id_toString = 2, + Id_toSource = 3, + Id_apply = 4, + Id_call = 5, + Id_bind = 6, + MAX_PROTOTYPE_ID = Id_bind; - MAX_PROTOTYPE_ID = Id_bind; - -// #/string_id_map# + // #/string_id_map# private Object prototypeProperty; private Object argumentsObj = NOT_FOUND; @@ -639,7 +634,6 @@ protected int findPrototypeId(String s) // For function object instances, attributes are // {configurable:false, enumerable:false}; // see ECMA 15.3.5.2 - private int prototypePropertyAttributes = PERMANENT|DONTENUM; - private int argumentsAttributes = PERMANENT|DONTENUM; + private int prototypePropertyAttributes = PERMANENT | DONTENUM; + private int argumentsAttributes = PERMANENT | DONTENUM; } - diff --git a/src/org/mozilla/javascript/Constructable.java b/src/org/mozilla/javascript/Constructable.java new file mode 100644 index 0000000000..9f3d53bc72 --- /dev/null +++ b/src/org/mozilla/javascript/Constructable.java @@ -0,0 +1,19 @@ +package org.mozilla.javascript; + +/** An interface that can be used to implement a constructor function as a lambda. */ +public interface Constructable { + + /** + * Call the function as a constructor. + * + *

This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. + * + * @param cx the current Context for this thread + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. + * @param args the array of arguments + * @return the allocated object + */ + Scriptable construct(Context cx, Scriptable scope, Object[] args); +} diff --git a/src/org/mozilla/javascript/Function.java b/src/org/mozilla/javascript/Function.java index 89c6d4b473..4d70231883 100644 --- a/src/org/mozilla/javascript/Function.java +++ b/src/org/mozilla/javascript/Function.java @@ -9,45 +9,40 @@ package org.mozilla.javascript; /** - * This is interface that all functions in JavaScript must implement. - * The interface provides for calling functions and constructors. + * This is interface that all functions in JavaScript must implement. The interface provides for + * calling functions and constructors. * * @see org.mozilla.javascript.Scriptable * @author Norris Boyd */ - -public interface Function extends Scriptable, Callable -{ +public interface Function extends Scriptable, Callable, Constructable { /** * Call the function. * - * Note that the array of arguments is not guaranteed to have - * length greater than 0. + *

Note that the array of arguments is not guaranteed to have length greater than 0. * * @param cx the current Context for this thread - * @param scope the scope to execute the function relative to. This is - * set to the value returned by getParentScope() except - * when the function is called from a closure. + * @param scope the scope to execute the function relative to. This is set to the value returned + * by getParentScope() except when the function is called from a closure. * @param thisObj the JavaScript this object * @param args the array of arguments * @return the result of the call */ @Override - public Object call(Context cx, Scriptable scope, Scriptable thisObj, - Object[] args); + Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args); /** * Call the function as a constructor. * - * This method is invoked by the runtime in order to satisfy a use - * of the JavaScript new operator. This method is - * expected to create a new object and return it. + *

This method is invoked by the runtime in order to satisfy a use of the JavaScript + * new operator. This method is expected to create a new object and return it. * * @param cx the current Context for this thread - * @param scope an enclosing scope of the caller except - * when the function is called from a closure. + * @param scope an enclosing scope of the caller except when the function is called from a + * closure. * @param args the array of arguments * @return the allocated object */ - public Scriptable construct(Context cx, Scriptable scope, Object[] args); + @Override + Scriptable construct(Context cx, Scriptable scope, Object[] args); } diff --git a/src/org/mozilla/javascript/LambdaConstructor.java b/src/org/mozilla/javascript/LambdaConstructor.java new file mode 100644 index 0000000000..914cb1094b --- /dev/null +++ b/src/org/mozilla/javascript/LambdaConstructor.java @@ -0,0 +1,134 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript; + +/** + * This class implements a JavaScript function that may be used as a constructor by delegating to an + * interface that can be easily implemented as a lambda. The LambdaFunction class may be used to add + * functions to the prototype that are also implemented as lambdas. + * + *

In micro benchmarks (as of 2021) using this class to implement a built-in class is about 15% + * more efficient than using IdScriptableObject, and about 25% faster than using reflection via the + * ScriptableObject.defineClass() family of methods. Furthermore, it results in code that more + * directly maps to JavaScript idioms than either methods, it is much easier to implement than + * IdScriptableObject, and the lambda pattern makes it easier to maintain state in various ways that + * don't always map directly to the existing concepts. + */ +public class LambdaConstructor extends LambdaFunction { + + private static final long serialVersionUID = 2691205302914111400L; + + /** If this flag is set, the constructor may be invoked as an ordinary function */ + public static final int CONSTRUCTOR_FUNCTION = 1; + /** If this flag is set, the constructor may be invoked using "new" */ + public static final int CONSTRUCTOR_NEW = 1 << 1; + /** By default, the constructor may be invoked either way */ + public static final int CONSTRUCTOR_DEFAULT = CONSTRUCTOR_FUNCTION | CONSTRUCTOR_NEW; + + // Lambdas should not be serialized. + private final transient Constructable targetConstructor; + private final int flags; + + /** + * Create a new function that may be used as a constructor. The new object will have the + * Function prototype and no parent. The caller is responsible for binding this object to the + * appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Constructable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaConstructor(Scriptable scope, String name, int length, Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = CONSTRUCTOR_DEFAULT; + } + + /** + * Create a new function and control whether it may be invoked using new, as a function, or + * both. + */ + public LambdaConstructor( + Scriptable scope, String name, int length, int flags, Constructable target) { + super(scope, name, length, null); + this.targetConstructor = target; + this.flags = flags; + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + if ((flags & CONSTRUCTOR_FUNCTION) == 0) { + throw ScriptRuntime.typeErrorById("msg.constructor.no.function", getFunctionName()); + } + return targetConstructor.construct(cx, scope, args); + } + + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + if ((flags & CONSTRUCTOR_NEW) == 0) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } + Scriptable obj = targetConstructor.construct(cx, scope, args); + obj.setPrototype(getClassPrototype()); + obj.setParentScope(scope); + return obj; + } + + /** + * Define a function property on the prototype of the constructor using a LambdaFunction under + * the covers. + */ + public void definePrototypeMethod(Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, f, 0); + } + + /** Define a property that may be of any type on the prototype of this constructor. */ + public void definePrototypeProperty(String name, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(name, value, 0); + } + + public void definePrototypeProperty(Symbol key, Object value, int attributes) { + ScriptableObject proto = getPrototypeScriptable(); + proto.defineProperty(key, value, 0); + } + + /** + * Define a function property directly on the constructor that is implemented under the covers + * by a LambdaFunction. + */ + public void defineConstructorMethod( + Scriptable scope, String name, int length, Callable target) { + LambdaFunction f = new LambdaFunction(scope, name, length, target); + defineProperty(name, f, DONTENUM); + } + + /** + * A convenience method to convert JavaScript's "this" object into a target class and throw a + * TypeError if it does not match. This is useful for implementing lambda functions, as "this" + * in JavaScript doesn't necessarily map to an instance of the class. + */ + @SuppressWarnings("unchecked") + public static T convertThisObject(Scriptable thisObj, Class targetClass) { + if (!targetClass.isInstance(thisObj)) { + throw ScriptRuntime.typeErrorById("msg.this.not.instance"); + } + return (T) thisObj; + } + + private ScriptableObject getPrototypeScriptable() { + Object prop = getPrototypeProperty(); + if (!(prop instanceof ScriptableObject)) { + throw ScriptRuntime.typeError("Not properly a lambda constructor"); + } + return (ScriptableObject) prop; + } +} diff --git a/src/org/mozilla/javascript/LambdaFunction.java b/src/org/mozilla/javascript/LambdaFunction.java new file mode 100644 index 0000000000..d27e54f2b8 --- /dev/null +++ b/src/org/mozilla/javascript/LambdaFunction.java @@ -0,0 +1,73 @@ +/* -*- Mode: java; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.javascript; + +/** + * This class implements a single JavaScript function that has the prototype of the built-in + * Function class, and which is implemented using a single function that can easily be implemented + * using a lambda expression. + */ +public class LambdaFunction extends BaseFunction { + + private static final long serialVersionUID = -8388132362854748293L; + + // The target is expected to be a lambda -- lambdas should not be serialized. + private final transient Callable target; + private final String name; + private final int length; + + /** + * Create a new function. The new object will have the Function prototype and no parent. The + * caller is responsible for binding this object to the appropriate scope. + * + * @param scope scope of the calling context + * @param name name of the function + * @param length the arity of the function + * @param target an object that implements the function in Java. Since Callable is a + * single-function interface this will typically be implemented as a lambda. + */ + public LambdaFunction(Scriptable scope, String name, int length, Callable target) { + this.target = target; + this.name = name; + this.length = length; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + setupDefaultPrototype(); + } + + /** Create a new built-in function, with no name, and no default prototype. */ + public LambdaFunction(Scriptable scope, int length, Callable target) { + this.target = target; + this.length = length; + this.name = ""; + ScriptRuntime.setFunctionProtoAndParent(this, scope); + } + + @Override + public Object call(Context cx, Scriptable scope, Scriptable thisObj, Object[] args) { + return target.call(cx, scope, thisObj, args); + } + + @Override + public Scriptable construct(Context cx, Scriptable scope, Object[] args) { + throw ScriptRuntime.typeErrorById("msg.no.new", getFunctionName()); + } + + @Override + public int getLength() { + return length; + } + + @Override + public int getArity() { + return length; + } + + @Override + public String getFunctionName() { + return name; + } +} diff --git a/src/org/mozilla/javascript/resources/Messages.properties b/src/org/mozilla/javascript/resources/Messages.properties index 9fe5a2305d..4971b64d4a 100644 --- a/src/org/mozilla/javascript/resources/Messages.properties +++ b/src/org/mozilla/javascript/resources/Messages.properties @@ -911,3 +911,8 @@ msg.no.new =\ msg.map.function.not =\ Map function is not actually a function + +msg.constructor.no.function \= + The constructor for {0} may not be invoked as a function + +msg.this.not.instance \= \"this\" is not an instance of the class \ No newline at end of file diff --git a/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java new file mode 100644 index 0000000000..2021f61ca5 --- /dev/null +++ b/testsrc/org/mozilla/javascript/tests/LambdaFunctionTest.java @@ -0,0 +1,286 @@ +package org.mozilla.javascript.tests; + +import static org.junit.Assert.assertThrows; + +import java.io.FileReader; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.LambdaConstructor; +import org.mozilla.javascript.LambdaFunction; +import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptRuntime; +import org.mozilla.javascript.Scriptable; +import org.mozilla.javascript.ScriptableObject; +import org.mozilla.javascript.SymbolKey; + +public class LambdaFunctionTest { + + private Context cx; + private Scriptable root; + + @Before + public void init() throws IOException { + cx = Context.enter(); + cx.setLanguageVersion(Context.VERSION_ES6); + root = cx.initStandardObjects(); + try (FileReader rdr = new FileReader("testsrc/assert.js")) { + cx.evaluateReader(root, rdr, "assert.js", 1, null); + } + } + + @After + public void cleanup() { + Context.exit(); + } + + private void eval(String source) { + cx.evaluateString(root, source, "test.js", 1, null); + } + + @Test + public void testNativeFunction() { + eval( + "function foo() { return 'Hello'; }\n" + + "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype !== undefined);\n" + + "assertTrue(foo.prototype.toString !== undefined);"); + } + + @Test + public void testNoArgLambdaFunction() { + LambdaFunction f = + new LambdaFunction( + root, + "foo", + 0, + (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> { + return "Hello"; + }); + ScriptableObject.putProperty(root, "foo", f); + eval( + "assertEquals(foo.name, 'foo');\n" + + "assertEquals(foo.length, 0);\n" + + "assertEquals(typeof foo, 'function');\n" + + "assertEquals(foo(), 'Hello');\n" + + "assertTrue(foo.toString().length > 0);\n" + + "assertTrue(foo.prototype.toString !== undefined);"); + } + + @Test + public void testConstructLambdaClass() { + TestClass.init(root); + eval( + "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "tc.value = 'bar';\n" + + "assertEquals(tc.value, 'bar');\n" + + "tc.anotherValue = 123;\n" + + "assertEquals(tc.anotherValue, 123);\n" + + "assertEquals(TestClass.name, 'TestClass');\n" + + "assertEquals(TestClass.length, 1);\n" + + "assertEquals(typeof TestClass, 'function');\n" + + "assertTrue(tc instanceof TestClass);\n"); + } + + @Test + public void testNativePrototypeFunctions() { + eval( + "function TestClass(v) { this.value = v; }\n" + + "TestClass.prototype.appendToValue = function(x) { return this.value + x; }\n" + + "let tc = new TestClass('foo');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar'), 'foobar');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');"); + } + + @Test + public void testLambdaPrototypeFunctions() { + TestClass.init(root); + eval( + "let tc = new TestClass('foo');\n" + + "assertEquals(typeof TestClass.prototype.appendToValue, 'function');\n" + + "assertEquals(tc.value, 'foo');\n" + + "assertEquals(tc.appendToValue('bar', 'baz'), 'foobarbaz');\n" + + "tc.value = 'x';\n" + + "assertEquals(tc.appendToValue('x'), 'xx');\n" + + "assertEquals(TestClass.prototype.appendToValue.length, 1);\n"); + } + + @Test + public void testLambdaPrototypeFunctionNotFound() { + TestClass.init(root); + assertThrows( + RhinoException.class, + () -> { + eval("let tc = new TestClass('foo');\n" + "tc.notFound();"); + }); + } + + @Test + public void testLambdaPrototypeFunctionInvalidThis() { + TestClass.init(root); + eval( + "let tc = new TestClass();\n" + + "assertThrows(function() { tc.appendToValue.call(null, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call(undefined, 'invalid'); }, TypeError);\n" + + "assertThrows(function() { tc.appendToValue.call({}, 'invalid'); }, TypeError);\n"); + } + + @Test + public void testLambdaConstructorFunctions() { + TestClass.init(root); + eval( + "assertEquals(TestClass.sayHello('World'), 'Hello, World!');\n" + + "assertEquals(TestClass.sayHello.name, 'sayHello');\n" + + "assertEquals(TestClass.sayHello.length, 1);\n" + + "assertEquals(typeof TestClass.sayHello, 'function');"); + } + + @Test + public void testLambdaConstructorValues() { + TestClass.init(root); + eval( + "let tc = new TestClass();\n" + + "assertEquals(tc.protoValue, 123);\n" + + "assertEquals(tc[Symbol.species], 456);\n"); + } + + @Test + public void testLambdaConstructorNewOnly() { + LambdaConstructor constructor = + new LambdaConstructor( + root, + "NewOnly", + 0, + LambdaConstructor.CONSTRUCTOR_NEW, + (Context cx, Scriptable scope, Object[] args) -> cx.newObject(scope)); + ScriptableObject.defineProperty(root, "NewOnly", constructor, 0); + eval( + "let o = new NewOnly();\n" + + "assertEquals('object', typeof o);\n" + + "assertThrows(() => { NewOnly(); }, TypeError);"); + } + + @Test + public void testLambdaConstructorFunctionOnly() { + LambdaConstructor constructor = + new LambdaConstructor( + root, + "NewOnly", + 0, + LambdaConstructor.CONSTRUCTOR_FUNCTION, + (Context cx, Scriptable scope, Object[] args) -> cx.newObject(scope)); + ScriptableObject.defineProperty(root, "NewOnly", constructor, 0); + eval( + "let o = NewOnly();\n" + + "assertEquals('object', typeof o);\n" + + "assertThrows(() => { new NewOnly(); }, TypeError);"); + } + + @Test + public void testLambdaFunctionNoNew() { + LambdaFunction func = + new LambdaFunction( + root, + 0, + (Context cx, Scriptable scope, Scriptable thisObj, Object[] args) -> true); + ScriptableObject.defineProperty(root, "noNewFunc", func, 0); + eval( + "let o = noNewFunc();\n" + + "assertEquals(true, o);\n" + + "assertThrows(() => { new noNewFunc(); }, TypeError)"); + } + + private static class TestClass extends ScriptableObject { + + private String instanceVal; + + public static void init(Scriptable scope) { + LambdaConstructor constructor = + new LambdaConstructor( + scope, + "TestClass", + 1, + (Context cx, Scriptable s, Object[] args) -> { + TestClass tc = new TestClass(); + if (args.length > 0) { + tc.instanceVal = ScriptRuntime.toString(args[0]); + } + return tc; + }); + constructor.defineConstructorMethod( + scope, + "sayHello", + 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> + TestClass.sayHello(args)); + constructor.definePrototypeMethod( + scope, + "appendToValue", + 1, + (Context cx, Scriptable s, Scriptable thisObj, Object[] args) -> { + TestClass self = + LambdaConstructor.convertThisObject(thisObj, TestClass.class); + return self.appendToValue(args); + }); + constructor.definePrototypeProperty("protoValue", 123, 0); + constructor.definePrototypeProperty(SymbolKey.SPECIES, 456, 0); + ScriptableObject.defineProperty(scope, "TestClass", constructor, PERMANENT); + } + + @Override + public String getClassName() { + return "TestClass"; + } + + @Override + public Object get(String name, Scriptable start) { + if ("value".equals(name)) { + return instanceVal; + } + return super.get(name, start); + } + + @Override + public boolean has(String name, Scriptable start) { + if ("value".equals(name)) { + return true; + } + return super.has(name, start); + } + + @Override + public void put(String name, Scriptable start, Object value) { + if ("value".equals(name)) { + instanceVal = ScriptRuntime.toString(value); + } else { + super.put(name, start, value); + } + } + + private Object appendToValue(Object[] args) { + StringBuilder sb = new StringBuilder(instanceVal); + for (Object arg : args) { + sb.append(ScriptRuntime.toString(arg)); + } + return sb.toString(); + } + + private static Object sayHello(Object[] args) { + if (args.length != 1) { + throw ScriptRuntime.typeError("Expected an argument"); + } + return "Hello, " + ScriptRuntime.toString(args[0]) + '!'; + } + } +}