diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java index b75ce28c0a..5e67c7eb0b 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/Bootstrapper.java @@ -10,7 +10,7 @@ import jdk.dynalink.Operation; import jdk.dynalink.StandardNamespace; import jdk.dynalink.StandardOperation; -import jdk.dynalink.support.SimpleRelinkableCallSite; +import jdk.dynalink.support.ChainedCallSite; import org.mozilla.classfile.ByteCode; import org.mozilla.classfile.ClassFileWriter; @@ -35,7 +35,7 @@ public class Bootstrapper { static { // Set up the linkers that will be invoked whenever a call site needs to be resolved. DynamicLinkerFactory factory = new DynamicLinkerFactory(); - factory.setPrioritizedLinkers(new DefaultLinker()); + factory.setPrioritizedLinkers(new ConstAwareLinker(), new DefaultLinker()); linker = factory.createLinker(); } @@ -45,8 +45,7 @@ public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, Metho throws NoSuchMethodException, IllegalAccessException { Operation op = parseOperation(name); // For now, use a very simple call site. - // When we change to add more linkers, we will likely switch to ChainedCallSite. - return linker.link(new SimpleRelinkableCallSite(new CallSiteDescriptor(lookup, op, mType))); + return linker.link(new ChainedCallSite(new CallSiteDescriptor(lookup, op, mType))); } /** diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/ConstAwareLinker.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/ConstAwareLinker.java new file mode 100644 index 0000000000..87908eaa70 --- /dev/null +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/ConstAwareLinker.java @@ -0,0 +1,100 @@ +package org.mozilla.javascript.optimizer; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import jdk.dynalink.NamedOperation; +import jdk.dynalink.NamespaceOperation; +import jdk.dynalink.Operation; +import jdk.dynalink.StandardNamespace; +import jdk.dynalink.StandardOperation; +import jdk.dynalink.linker.GuardedInvocation; +import jdk.dynalink.linker.GuardingDynamicLinker; +import jdk.dynalink.linker.LinkRequest; +import jdk.dynalink.linker.LinkerServices; +import jdk.dynalink.linker.support.Guards; +import org.mozilla.javascript.NativeWith; +import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptableObject; + +@SuppressWarnings("AndroidJdkLibsChecker") +class ConstAwareLinker implements GuardingDynamicLinker { + @Override + public GuardedInvocation getGuardedInvocation(LinkRequest req, LinkerServices svc) + throws Exception { + if (req.isCallSiteUnstable()) { + if (DefaultLinker.DEBUG) { + System.out.println( + req.getCallSiteDescriptor().getOperation() + ": unstable call site"); + } + return null; + } + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + Operation rootOp = req.getCallSiteDescriptor().getOperation(); + MethodType mType = req.getCallSiteDescriptor().getMethodType(); + String name = DefaultLinker.getName(rootOp); + Operation op = NamedOperation.getBaseOperation(rootOp); + Object target = req.getReceiver(); + + if (NamespaceOperation.contains(op, StandardOperation.GET, RhinoNamespace.NAME) + || NamespaceOperation.contains( + op, StandardOperation.GET, StandardNamespace.PROPERTY) + || NamespaceOperation.contains( + op, RhinoOperation.GETNOWARN, StandardNamespace.PROPERTY)) { + Object constValue = getConstValue(target, name); + if (constValue != null) { + // The guard returns boolean and compares the first argument to the + // target here. This works because the target is always our first argument. + MethodHandle guard = Guards.asType(Guards.getIdentityGuard(target), mType); + // Replace the actual method invocation with one that just returns a constant. + // Works because we can drop all arguments here. + MethodHandle mh = + MethodHandles.dropArguments( + MethodHandles.constant(Object.class, constValue), + 0, + mType.parameterList()); + if (DefaultLinker.DEBUG) { + System.out.println(rootOp + " constant"); + } + return new GuardedInvocation(mh, guard); + } + } + + return null; + } + + /** + * Return the value of the specified property, but only if it's found in the root object that we + * search, and only if it's a constant. Return null otherwise, which means that we can't handle + * constants with a value of "null," which should not be a big loss. + */ + private Object getConstValue(Object t, String name) { + if (t instanceof NativeWith) { + // Support constants referenced from inside functions + return getConstValue(((NativeWith) t).getPrototype(), name); + } + if (!(t instanceof ScriptableObject)) { + return null; + } + try { + ScriptableObject target = (ScriptableObject) t; + // Just look in the root of the object -- don't mess around with + // nested scopes and things, to keep this simple and foolproof. + if (target.has(name, target)) { + int attributes = target.getAttributes(name); + if ((attributes & ScriptableObject.READONLY) != 0 + && (attributes & ScriptableObject.PERMANENT) != 0 + && (attributes & ScriptableObject.UNINITIALIZED_CONST) == 0) { + // If we get here then this object's value will not change for the + // lifetime of the target object. + return target.get(name, target); + } + } + } catch (RhinoException re) { + // Some implementations of ScriptableObject will fail on this operation with + // an exception, so treat that as "not found". + } + return null; + } +} diff --git a/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java b/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java index cdeaa4c575..46679d0392 100644 --- a/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java +++ b/rhino/src/main/java/org/mozilla/javascript/optimizer/DefaultLinker.java @@ -167,7 +167,7 @@ private GuardedInvocation getNameInvocation( } /** If the operation is a named operation, then return the name, */ - private static String getName(Operation op) { + static String getName(Operation op) { Object nameObj = NamedOperation.getName(op); if (nameObj instanceof String) { return (String) nameObj; diff --git a/tests/src/test/java/org/mozilla/javascript/tests/ConstWhiteBoxTest.java b/tests/src/test/java/org/mozilla/javascript/tests/ConstWhiteBoxTest.java new file mode 100644 index 0000000000..008b866c95 --- /dev/null +++ b/tests/src/test/java/org/mozilla/javascript/tests/ConstWhiteBoxTest.java @@ -0,0 +1,10 @@ +package org.mozilla.javascript.tests; + +import org.mozilla.javascript.Context; +import org.mozilla.javascript.drivers.LanguageVersion; +import org.mozilla.javascript.drivers.RhinoTest; +import org.mozilla.javascript.drivers.ScriptTestsBase; + +@RhinoTest("testsrc/jstests/const-white-box.js") +@LanguageVersion(Context.VERSION_ES6) +public class ConstWhiteBoxTest extends ScriptTestsBase {} diff --git a/tests/testsrc/jstests/const-white-box.js b/tests/testsrc/jstests/const-white-box.js new file mode 100644 index 0000000000..b2f15f6849 --- /dev/null +++ b/tests/testsrc/jstests/const-white-box.js @@ -0,0 +1,63 @@ +load("testsrc/assert.js"); + +// These tests deliberately exercise the optimizations in ConstAwareLinker. + +// Verify that things that aren't constant aren't constant +var notConstant = 1; +assertEquals(1, notConstant); +notConstant = 2; +assertEquals(2, notConstant); + +// Verify that things that are constant stay constant +const constant = 1; +assertEquals(1, constant); +constant = 2; +assertEquals(1, constant); + +// Verify that this works in a loop +function checkConstantness() { + constant++; + assertEquals(1, constant); +} +const ITERATIONS = 10; +for (let i = 0; i < ITERATIONS; i++) { + checkConstantness(); +} + +// Verify that we can set a local constant in a function +function localConstantness() { + const localConst = 1; + assertEquals(1, localConst); + localConst = 2; + assertEquals(1, localConst); +} +for (let i = 0; i < ITERATIONS; i++) { + localConstantness(); +} + +// Set up an object with a const field and try it out +const o = { + notConst: 1, +}; +Object.defineProperty(o, "const", { + value: 1, + configurable: false, + writable: false, +}); +assertEquals(1, o.notConst); +assertEquals(1, o.const); +o.notConst = 2; +o.const = 2; +assertEquals(2, o.notConst); +assertEquals(1, o.const); + +// Verify that it works in a function +function objectConstness() { + o.const++; + assertEquals(1, o.const); +} +for (let i = 0; i < ITERATIONS; i++) { + objectConstness(); +} + +'success'; \ No newline at end of file