commit 7b9ab846a3f373798772e13f9fd6e35453726d77
parent 739186d77d4815ead3989e862ef7d2e954e7cfd3
Author: André Bargull <andre.bargull@gmail.com>
Date: Tue, 21 Oct 2025 07:05:39 +0000
Bug 1991402 - Part 4: Use InlinableNativeIRGenerator for inlinable native getters. r=jandem
Allow to create `InlinableNativeIRGenerator` with `GetPropIRGenerator`, so we
can inline get-accessor calls to native functions through the existing inlining
machinery.
Parts 6-12 will convert currently inlinable native accessors to use this new
mechanism.
Differential Revision: https://phabricator.services.mozilla.com/D266596
Diffstat:
6 files changed, 300 insertions(+), 21 deletions(-)
diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-1.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-1.js
@@ -0,0 +1,91 @@
+// Test calling an inlinable native accessor property as a normal function.
+
+// Set.prototype.size is an inlinable getter accessor.
+var SetSize = Object.getOwnPropertyDescriptor(Set.prototype, "size").get;
+
+// Install "size" getter as a normal method on Set.prototype.
+Set.prototype.getSize = SetSize;
+
+var sets = [
+ new Set(),
+ new Set([1, 2, 3]),
+];
+
+// Call inlinable accessor as normal method.
+function testInlinableAccessorAsMethod() {
+ for (var i = 0; i < 100; ++i) {
+ var set = sets[i & 1];
+ assertEq(set.getSize(), set.size);
+ }
+}
+testInlinableAccessorAsMethod();
+
+// Call inlinable accessor as through FunCall.
+function testInlinableAccessorWithFunCall() {
+ for (var i = 0; i < 100; ++i) {
+ var set = sets[i & 1];
+ assertEq(SetSize.call(set), set.size);
+ }
+}
+testInlinableAccessorWithFunCall();
+
+// Call inlinable accessor as through FunApply.
+function testInlinableAccessorWithFunApply() {
+ for (var i = 0; i < 100; ++i) {
+ var set = sets[i & 1];
+ assertEq(SetSize.apply(set), set.size);
+ }
+}
+testInlinableAccessorWithFunApply();
+
+// Call inlinable accessor as through bound FunCall.
+function testInlinableAccessorWithBoundFunCall() {
+ var callSetSize = Function.prototype.call.bind(SetSize);
+
+ for (var i = 0; i < 100; ++i) {
+ var set = sets[i & 1];
+ assertEq(callSetSize(set), set.size);
+ }
+}
+testInlinableAccessorWithBoundFunCall();
+
+// Call inlinable accessor as through bound FunCall.
+function testInlinableAccessorWithBoundFunApply() {
+ var applySetSize = Function.prototype.apply.bind(SetSize);
+
+ for (var i = 0; i < 100; ++i) {
+ var set = sets[i & 1];
+ assertEq(applySetSize(set), set.size);
+ }
+}
+testInlinableAccessorWithBoundFunApply();
+
+// Call inlinable accessor as bound function.
+function testBoundInlinableAccessor() {
+ var boundSetSize = SetSize.bind(sets[0]);
+
+ for (var i = 0; i < 100; ++i) {
+ assertEq(boundSetSize(), sets[0].size);
+ }
+}
+testBoundInlinableAccessor();
+
+// Call inlinable accessor as bound function through FunCall.
+function testBoundInlinableAccessorWithFunCall() {
+ var boundSetSize = SetSize.bind(sets[0]);
+
+ for (var i = 0; i < 100; ++i) {
+ assertEq(boundSetSize.call(), sets[0].size);
+ }
+}
+testBoundInlinableAccessorWithFunCall();
+
+// Call inlinable accessor as bound function through FunApply.
+function testBoundInlinableAccessorWithFunApply() {
+ var boundSetSize = SetSize.bind(sets[0]);
+
+ for (var i = 0; i < 100; ++i) {
+ assertEq(boundSetSize.apply(), sets[0].size);
+ }
+}
+testInlinableAccessorWithBoundFunApply();
diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-2.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-2.js
@@ -0,0 +1,30 @@
+// Test calling an inlinable native function as an accessor property.
+
+// Install the inlinable Number.prototype.toString method as an accessor property.
+Object.defineProperty(Number.prototype, "tostr", {
+ get: Number.prototype.toString,
+});
+
+function testWithPrimitive() {
+ var key = "tostr";
+ for (var i = 0; i < 100; ++i) {
+ assertEq(i.tostr, i.toString());
+ assertEq(i[key], i.toString());
+ }
+}
+testWithPrimitive();
+
+// Install the inlinable Date.prototype.getTime method as an accessor property.
+Object.defineProperty(Date.prototype, "time", {
+ get: Date.prototype.getTime,
+});
+
+function testWithObject() {
+ var key = "time";
+ for (var i = 0; i < 100; ++i) {
+ var d = new Date(i);
+ assertEq(d.time, d.getTime());
+ assertEq(d[key], d.getTime());
+ }
+}
+testWithObject();
diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-3.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-3.js
@@ -0,0 +1,23 @@
+// Test calling an inlinable native function as an accessor on a WindowProxy.
+
+var window = newGlobal({useWindowProxy: true});
+
+// GetPropIRGenerator::tryAttachWindowProxy only attaches a stub if the current
+// script global matches the window proxy, therefore we have to evaluate the
+// test in |window|'s global environment.
+window.eval(`
+var window = this;
+
+// Use Math.random because it can be called with any |this| value.
+Object.defineProperty(window, "random", {
+ get: Math.random,
+});
+
+function testRandom() {
+ for (var i = 0; i < 100; ++i) {
+ var r = window.random;
+ assertEq(0 <= r && r < 1, true);
+ }
+}
+testRandom();
+`);
diff --git a/js/src/jit-test/tests/cacheir/inlinable-native-accessor-4.js b/js/src/jit-test/tests/cacheir/inlinable-native-accessor-4.js
@@ -0,0 +1,27 @@
+// Test calling an inlinable native function as an accessor when the native function uses alloc-sites.
+
+function testObject() {
+ var obj = Object.defineProperty({}, "object", {
+ get: Object,
+ });
+
+ for (var i = 0; i < 100; i++) {
+ var o = obj.object;
+ assertEq(typeof o, "object");
+ assertEq(o !== null, true);
+ }
+}
+testObject();
+
+function testArray() {
+ var obj = Object.defineProperty({}, "array", {
+ get: Array,
+ });
+
+ for (var i = 0; i < 100; i++) {
+ var a = obj.array;
+ assertEq(a.length, 0);
+ assertEq(Array.isArray(a), true);
+ }
+}
+testArray();
diff --git a/js/src/jit/CacheIR.cpp b/js/src/jit/CacheIR.cpp
@@ -1113,12 +1113,22 @@ void GetPropIRGenerator::emitCallGetterResultGuards(NativeObject* obj,
}
void GetPropIRGenerator::emitCallGetterResult(NativeGetPropKind kind,
- NativeObject* obj,
- NativeObject* holder, HandleId id,
- PropertyInfo prop,
+ Handle<NativeObject*> obj,
+ Handle<NativeObject*> holder,
+ HandleId id, PropertyInfo prop,
ObjOperandId objId,
ValOperandId receiverId) {
emitCallGetterResultGuards(obj, holder, id, prop, objId);
+
+ if (kind == NativeGetPropKind::NativeGetter &&
+ mode_ == ICState::Mode::Specialized) {
+ auto attached = tryAttachInlinableNativeGetter(holder, prop, receiverId);
+ if (attached != AttachDecision::NoAction) {
+ MOZ_ASSERT(attached == AttachDecision::Attach);
+ return;
+ }
+ }
+
emitCallGetterResultNoGuards(kind, obj, holder, prop, receiverId);
}
@@ -1251,10 +1261,10 @@ AttachDecision GetPropIRGenerator::tryAttachNative(HandleObject obj,
HandleId id,
ValOperandId receiverId) {
Maybe<PropertyInfo> prop;
- NativeObject* holder = nullptr;
+ Rooted<NativeObject*> holder(cx_);
NativeGetPropKind kind =
- CanAttachNativeGetProp(cx_, obj, id, &holder, &prop, pc_);
+ CanAttachNativeGetProp(cx_, obj, id, holder.address(), &prop, pc_);
switch (kind) {
case NativeGetPropKind::None:
return AttachDecision::NoAction;
@@ -1282,7 +1292,7 @@ AttachDecision GetPropIRGenerator::tryAttachNative(HandleObject obj,
}
case NativeGetPropKind::ScriptedGetter:
case NativeGetPropKind::NativeGetter: {
- auto* nobj = &obj->as<NativeObject>();
+ auto nobj = obj.as<NativeObject>();
MOZ_ASSERT(!IsWindow(nobj));
// If we're in megamorphic mode, we assume that a specialized
@@ -1383,11 +1393,11 @@ AttachDecision GetPropIRGenerator::tryAttachWindowProxy(HandleObject obj,
}
// Now try to do the lookup on the Window (the current global).
- GlobalObject* windowObj = cx_->global();
- NativeObject* holder = nullptr;
+ Handle<GlobalObject*> windowObj = cx_->global();
+ Rooted<NativeObject*> holder(cx_);
Maybe<PropertyInfo> prop;
NativeGetPropKind kind =
- CanAttachNativeGetProp(cx_, windowObj, id, &holder, &prop, pc_);
+ CanAttachNativeGetProp(cx_, windowObj, id, holder.address(), &prop, pc_);
switch (kind) {
case NativeGetPropKind::None:
return AttachDecision::NoAction;
@@ -2432,6 +2442,31 @@ AttachDecision GetPropIRGenerator::tryAttachObjectLength(HandleObject obj,
return AttachDecision::NoAction;
}
+AttachDecision GetPropIRGenerator::tryAttachInlinableNativeGetter(
+ Handle<NativeObject*> holder, PropertyInfo prop, ValOperandId receiverId) {
+ MOZ_ASSERT(mode_ == ICState::Mode::Specialized);
+
+ // Receiver should be the object.
+ if (isSuper()) {
+ return AttachDecision::NoAction;
+ }
+
+ Rooted<JSFunction*> target(cx_, &holder->getGetter(prop)->as<JSFunction>());
+ MOZ_ASSERT(target->isNativeWithoutJitEntry());
+
+ Handle<Value> thisValue = val_;
+
+ bool isSpread = false;
+ bool isSameRealm = cx_->realm() == target->realm();
+ bool isConstructing = false;
+ CallFlags flags(isConstructing, isSpread, isSameRealm);
+
+ // Check for specific native-function optimizations.
+ InlinableNativeIRGenerator nativeGen(*this, target, thisValue, flags,
+ receiverId);
+ return nativeGen.tryAttachStub();
+}
+
AttachDecision GetPropIRGenerator::tryAttachTypedArray(HandleObject obj,
ObjOperandId objId,
HandleId id) {
@@ -3012,10 +3047,10 @@ AttachDecision GetPropIRGenerator::tryAttachPrimitive(ValOperandId valId,
return AttachDecision::NoAction;
}
- NativeObject* holder = nullptr;
+ Rooted<NativeObject*> holder(cx_);
Maybe<PropertyInfo> prop;
NativeGetPropKind kind =
- CanAttachNativeGetProp(cx_, proto, id, &holder, &prop, pc_);
+ CanAttachNativeGetProp(cx_, proto, id, holder.address(), &prop, pc_);
if (kind == NativeGetPropKind::None) {
return AttachDecision::NoAction;
}
@@ -3027,7 +3062,7 @@ AttachDecision GetPropIRGenerator::tryAttachPrimitive(ValOperandId valId,
}
maybeEmitIdGuard(id);
- auto* nproto = &proto->as<NativeObject>();
+ Rooted<NativeObject*> nproto(cx_, &proto->as<NativeObject>());
ObjOperandId protoId = writer.loadObject(nproto);
switch (kind) {
@@ -6534,6 +6569,15 @@ ObjOperandId InlinableNativeIRGenerator::emitNativeCalleeGuard(
// native from a different realm.
MOZ_ASSERT(target_->isNativeWithoutJitEntry());
+ // Guards already emitted in GetPropIRGenerator::emitCallGetterResultGuards.
+ if (isAccessorOp()) {
+ MOZ_ASSERT(flags_.getArgFormat() == CallFlags::Standard);
+ MOZ_ASSERT(!flags_.isConstructing());
+ MOZ_ASSERT(!isCalleeBoundFunction());
+ MOZ_ASSERT(!isTargetBoundFunction());
+ return ObjOperandId();
+ }
+
ValOperandId calleeValId;
switch (flags_.getArgFormat()) {
case CallFlags::Standard:
@@ -6697,7 +6741,9 @@ ObjOperandId InlinableNativeIRGenerator::emitLoadArgsArray() {
}
MOZ_ASSERT(flags_.getArgFormat() == CallFlags::FunApplyArray);
- return generator_.emitFunApplyArgsGuard(flags_.getArgFormat()).ref();
+ return gen_.as<CallIRGenerator*>()
+ ->emitFunApplyArgsGuard(flags_.getArgFormat())
+ .ref();
}
ValOperandId InlinableNativeIRGenerator::loadBoundArgument(
@@ -6719,6 +6765,15 @@ ValOperandId InlinableNativeIRGenerator::loadBoundArgument(
}
ValOperandId InlinableNativeIRGenerator::loadThis(ObjOperandId calleeId) {
+ // Accessor operations use the receiver as their |this| value.
+ if (isAccessorOp()) {
+ MOZ_ASSERT(flags_.getArgFormat() == CallFlags::Standard);
+ MOZ_ASSERT(!isTargetBoundFunction());
+ MOZ_ASSERT(!isCalleeBoundFunction());
+ MOZ_ASSERT(receiverId_.valid());
+ return receiverId_;
+ }
+
switch (flags_.getArgFormat()) {
case CallFlags::Standard:
case CallFlags::Spread:
@@ -6798,6 +6853,7 @@ ValOperandId InlinableNativeIRGenerator::loadArgument(ObjOperandId calleeId,
flags_.getArgFormat() == CallFlags::FunApplyNullUndefined);
MOZ_ASSERT_IF(flags_.getArgFormat() == CallFlags::FunApplyNullUndefined,
isTargetBoundFunction() && hasBoundArguments());
+ MOZ_ASSERT(!isAccessorOp(), "get property operations don't have arguments");
// Check if the |this| value is stored in the bound arguments.
bool thisFromBoundArgs = flags_.getArgFormat() == CallFlags::FunCall &&
@@ -12061,6 +12117,11 @@ AttachDecision InlinableNativeIRGenerator::tryAttachObjectConstructor() {
gc::AllocSite* site = nullptr;
PlainObject* templateObj = nullptr;
if (args_.length() == 0) {
+ // Don't optimize if we can't create an alloc-site.
+ if (!BytecodeOpCanHaveAllocSite(op())) {
+ return AttachDecision::NoAction;
+ }
+
// Stub doesn't support metadata builder
if (cx_->realm()->hasAllocationMetadataBuilder()) {
return AttachDecision::NoAction;
@@ -12112,6 +12173,11 @@ AttachDecision InlinableNativeIRGenerator::tryAttachObjectConstructor() {
}
AttachDecision InlinableNativeIRGenerator::tryAttachArrayConstructor() {
+ // Don't optimize if we can't create an alloc-site.
+ if (!BytecodeOpCanHaveAllocSite(op())) {
+ return AttachDecision::NoAction;
+ }
+
// Only optimize the |Array()| and |Array(n)| cases (with or without |new|)
// for now. Note that self-hosted code calls this without |new| via std_Array.
if (args_.length() > 1) {
@@ -12925,7 +12991,8 @@ AttachDecision InlinableNativeIRGenerator::tryAttachStub() {
MOZ_ASSERT(target_->isNativeWithoutJitEntry());
// Special case functions are only optimized for normal calls.
- if (!BytecodeCallOpCanHaveInlinableNative(op())) {
+ if (!BytecodeCallOpCanHaveInlinableNative(op()) &&
+ !BytecodeGetOpCanHaveInlinableNative(op())) {
return AttachDecision::NoAction;
}
diff --git a/js/src/jit/CacheIRGenerator.h b/js/src/jit/CacheIRGenerator.h
@@ -166,8 +166,13 @@ class MOZ_RAII GetPropIRGenerator : public IRGenerator {
HandleValue val_;
HandleValue idVal_;
+ friend class InlinableNativeIRGenerator;
+
AttachDecision tryAttachNative(HandleObject obj, ObjOperandId objId,
HandleId id, ValOperandId receiverId);
+ AttachDecision tryAttachInlinableNativeGetter(Handle<NativeObject*> holder,
+ PropertyInfo prop,
+ ValOperandId receiverId);
AttachDecision tryAttachObjectLength(HandleObject obj, ObjOperandId objId,
HandleId id);
AttachDecision tryAttachTypedArray(HandleObject obj, ObjOperandId objId,
@@ -280,8 +285,8 @@ class MOZ_RAII GetPropIRGenerator : public IRGenerator {
void emitCallGetterResultGuards(NativeObject* obj, NativeObject* holder,
HandleId id, PropertyInfo prop,
ObjOperandId objId);
- void emitCallGetterResult(NativeGetPropKind kind, NativeObject* obj,
- NativeObject* holder, HandleId id,
+ void emitCallGetterResult(NativeGetPropKind kind, Handle<NativeObject*> obj,
+ Handle<NativeObject*> holder, HandleId id,
PropertyInfo prop, ObjOperandId objId,
ValOperandId receiverId);
void emitCallDOMGetterResult(NativeObject* obj, NativeObject* holder,
@@ -650,7 +655,8 @@ class MOZ_RAII CallIRGenerator : public IRGenerator {
};
class MOZ_RAII InlinableNativeIRGenerator {
- CallIRGenerator& generator_;
+ mozilla::Variant<CallIRGenerator*, GetPropIRGenerator*> gen_;
+ IRGenerator& generator_;
CacheIRWriter& writer;
JSContext* cx_;
@@ -663,6 +669,9 @@ class MOZ_RAII InlinableNativeIRGenerator {
CallFlags flags_;
uint32_t stackArgc_;
+ // |this| for inlined accesor operations.
+ ValOperandId receiverId_;
+
HandleScript script() const { return generator_.script_; }
JSObject* callee() const { return callee_; }
bool isFirstStub() const { return generator_.isFirstStub_; }
@@ -670,6 +679,9 @@ class MOZ_RAII InlinableNativeIRGenerator {
JSOp op() const { return generator_.jsop(); }
uint32_t stackArgc() const { return stackArgc_; }
+ // Inlined native accessor for GetProp or GetElem operations.
+ bool isAccessorOp() const { return !IsInvokeOp(op()); }
+
bool isCalleeBoundFunction() const;
BoundFunctionObject* boundCallee() const;
@@ -700,6 +712,10 @@ class MOZ_RAII InlinableNativeIRGenerator {
bool hasBoundArguments() const;
Int32OperandId initializeInputOperand() {
+ // Input operands are already initialized for inlined accessors.
+ if (isAccessorOp()) {
+ return Int32OperandId();
+ }
return Int32OperandId(writer.setInputOperandId(0));
}
@@ -880,7 +896,8 @@ class MOZ_RAII InlinableNativeIRGenerator {
#endif
void trackAttached(const char* name /* must be a C string literal */) {
- return generator_.trackAttached(name);
+ return gen_.match(
+ [&](auto* generator) { return generator->trackAttached(name); });
}
public:
@@ -889,7 +906,8 @@ class MOZ_RAII InlinableNativeIRGenerator {
HandleValue thisValue, HandleValueArray args,
CallFlags flags,
Handle<BoundFunctionObject*> boundTarget = nullptr)
- : generator_(generator),
+ : gen_(&generator),
+ generator_(generator),
writer(generator.writer),
cx_(generator.cx_),
callee_(callee),
@@ -899,7 +917,25 @@ class MOZ_RAII InlinableNativeIRGenerator {
args_(args),
boundTarget_(boundTarget),
flags_(flags),
- stackArgc_(generator.argc_) {}
+ stackArgc_(generator.argc_),
+ receiverId_() {}
+
+ InlinableNativeIRGenerator(GetPropIRGenerator& generator,
+ HandleFunction target, HandleValue thisValue,
+ CallFlags flags, ValOperandId receiverId)
+ : gen_(&generator),
+ generator_(generator),
+ writer(generator.writer),
+ cx_(generator.cx_),
+ callee_(target),
+ target_(target),
+ newTarget_(JS::NullHandleValue),
+ thisval_(thisValue),
+ args_(HandleValueArray::empty()),
+ boundTarget_(nullptr),
+ flags_(flags),
+ stackArgc_(0),
+ receiverId_(receiverId) {}
AttachDecision tryAttachStub();
};
@@ -1088,13 +1124,18 @@ class MOZ_RAII LambdaIRGenerator : public IRGenerator {
AttachDecision tryAttachFunctionClone();
};
-// Returns true for bytecode ops that can use InlinableNativeIRGenerator.
+// Returns true for bytecode call ops that can use InlinableNativeIRGenerator.
inline bool BytecodeCallOpCanHaveInlinableNative(JSOp op) {
return op == JSOp::Call || op == JSOp::CallContent || op == JSOp::New ||
op == JSOp::NewContent || op == JSOp::CallIgnoresRv ||
op == JSOp::SpreadCall;
}
+// Returns true for bytecode get ops that can use InlinableNativeIRGenerator.
+inline bool BytecodeGetOpCanHaveInlinableNative(JSOp op) {
+ return op == JSOp::GetProp || op == JSOp::GetElem;
+}
+
inline bool BytecodeOpCanHaveAllocSite(JSOp op) {
return BytecodeCallOpCanHaveInlinableNative(op) || op == JSOp::NewArray ||
op == JSOp::NewObject || op == JSOp::NewInit || op == JSOp::CallIter ||