Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion pyre/bench/synth/classmethod_protocol_hot.cranelift.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
5 changes: 4 additions & 1 deletion pyre/bench/synth/classmethod_protocol_hot.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
5 changes: 4 additions & 1 deletion pyre/bench/synth/classmethod_protocol_hot.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
5 changes: 4 additions & 1 deletion pyre/bench/synth/type_metatype_method_call.dynasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
5 changes: 4 additions & 1 deletion pyre/bench/synth/type_metatype_method_call.wasm.jitstats
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=1
internal_compile_panics=0
loops_aborted=0
loops_compiled=2
loops_compiled=1
14 changes: 14 additions & 0 deletions pyre/bench/synth/type_name_attr_fold.cranelift.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=4
internal_compile_panics=0
loops_aborted=1
loops_compiled=5
14 changes: 14 additions & 0 deletions pyre/bench/synth/type_name_attr_fold.dynasm.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=4
internal_compile_panics=0
loops_aborted=1
loops_compiled=5
125 changes: 125 additions & 0 deletions pyre/bench/synth/type_name_attr_fold.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# pyre-check: max-pypy-ratio=18
N = 60000
M = 4000

# `cls.__name__` read from inside a method the enclosing `for` loop inlines.
# The read folds to the class's name slot under two guards -- the receiver is a
# type, and its metaclass is `type` -- so what this pins is the ways that can
# be wrong: a rename must be seen, because the slot is read live rather than
# baked, and its object identity must survive; two classes sharing the trace
# must each report their own name, since the fold deliberately does not pin
# which class arrived; a class dict entry of the same name must lose, because
# the metatype descriptor is consulted first; and any metaclass other than
# `type` must not be folded past at all.


class Base:
@classmethod
def tag(cls, i):
return i + len(cls.__name__)


class Derived(Base):
pass


class Longer(Base):
pass


class Shadowed(Base):
# `type.__name__` is a data descriptor on the metatype, so this entry is
# what an INSTANCE of the class reads and never what the class does.
__name__ = 'shadow-entry'


class GetattrMeta(type):
def __getattribute__(cls, name):
# 8 characters, which no class here is named, so the total says whether
# this ran.
if name == '__name__':
return 'via-meta'
return type.__getattribute__(cls, name)


class NameMeta(type):
# A metaclass may define `__name__` itself, and then it is the one that
# answers -- also 8 characters.
@property
def __name__(cls):
return 'metaname'


class PlainMeta(type):
# Answers exactly as `type` would, and still must not be folded past: the
# fold's precondition is the metaclass, not what it happens to do.
pass


class ByGetattr(metaclass=GetattrMeta):
@classmethod
def tag(cls, i):
return i + len(cls.__name__)


class ByNameProp(metaclass=NameMeta):
pass


class ByPlain(metaclass=PlainMeta):
pass


class Box:
def __init__(self, v):
self.v = v

# The instance-attribute twin: the same inline the type-name fold reaches,
# over the mapdict fold instead.
def at(self, i):
return self.v + i


def folded():
box = Box(4)
total = 0
for i in range(N):
# Three classes down one trace; each must answer with its own name.
total = total + Derived.tag(i) - i
total = total + Longer.tag(i) - i
total = total + Shadowed.tag(i) - i
total = total + box.at(i) - i
# Renaming mid-loop, after the loop is compiled: the fold reads the
# name slot rather than a constant, so the shorter name is seen from
# the next iteration on.
if i == N // 2:
Derived.__name__ = 'D'
return total


def declined():
# Every receiver here has a metaclass that is not `type`, so the fold must
# not run. Long enough to compile the loop and reach that decision, and no
# longer -- what this pins is the decision, not a speed.
total = 0
for i in range(M):
total = total + ByGetattr.tag(i) - i
total = total + len(ByNameProp.__name__)
total = total + len(ByPlain.__name__)
return total


print(folded())
print(declined())
print(Derived.__name__, Longer.__name__, Shadowed.__name__)
print(Shadowed.__dict__['__name__'], Shadowed().__name__)
print(ByGetattr.__name__, ByNameProp.__name__, ByPlain.__name__)

# A rename between two compiled loops, rather than inside one: the second loop
# reads the slot the first one's trace was built against, so a baked name would
# survive here too.
Box.__name__ = 'Renamed'
seen = None
for _ in range(M):
seen = Box.__name__
print(seen)
14 changes: 14 additions & 0 deletions pyre/bench/synth/type_name_attr_fold.wasm.jitstats
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
bridges_compiled=0
descr_set_absent=0
descr_set_ambiguous=0
descr_set_stale_absent=0
fbw_blackhole_adopted_multi_frame=0
fbw_blackhole_adopted_single_frame=0
fbw_rolled_back_with_effects=0
fbw_store_journal_rollback_failed=0
field_pos_attached_misplaced=0
field_pos_spec_misplaced=0
guard_failures=4
internal_compile_panics=0
loops_aborted=1
loops_compiled=5
82 changes: 82 additions & 0 deletions pyre/extra_tests/parity_tests/type_name_attr_identity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Reading `Cls.__name__` hands back the object the rename was given.

`type.__name__` is a getset on the metatype, and its setter stores the value
it was handed rather than a copy of the text: a `str` subclass assigned to a
heap type's `__name__` comes back as that same object, so a caller that put
an annotated string there can read its annotation off again. PyPy answers
with an exact `str` here; this is the CPython behaviour.

The reads run hot enough to compile, because the JIT folds this attribute to
the name slot and a fold that baked the name instead of reading it would keep
answering with the name the trace was built against.
"""

N = 30000


class Name(str):
def __new__(cls, value, note):
self = str.__new__(cls, value)
self.note = note
return self


class Heap:
pass


original = Heap.__name__
assert original == "Heap", original
assert type(original) is str, type(original)
# The object is stable across reads, not rebuilt per access.
assert Heap.__name__ is Heap.__name__

tagged = Name("Renamed", "from-parity-test")
Heap.__name__ = tagged
assert Heap.__name__ is tagged, Heap.__name__
assert type(Heap.__name__) is Name, type(Heap.__name__)
assert Heap.__name__.note == "from-parity-test"

# Hot enough to compile, and the identity has to survive that.
seen = None
for _ in range(N):
seen = Heap.__name__
assert seen is tagged, seen

# A rename after the loop is compiled is seen by it.
again = Name("Third", "second-rename")
Heap.__name__ = again
for _ in range(N):
seen = Heap.__name__
assert seen is again, seen
assert seen.note == "second-rename"

# `__qualname__` is a separate slot and the renames did not touch it.
assert Heap.__qualname__ == "Heap", Heap.__qualname__

# The class dict never gains a `__name__` entry from any of this -- the getset
# on the metatype owns the name, so the class's own mapping stays untouched.
assert "__name__" not in Heap.__dict__

# An instance reads its class's dict, not the metatype getset, so a class-level
# `__name__` entry is what an instance sees while the class keeps its own name.
class Shadowed:
__name__ = "entry"


for _ in range(N):
pair = (Shadowed.__name__, Shadowed().__name__)
assert pair == ("Shadowed", "entry"), pair

# Immutable types refuse the rename and keep answering with their own name.
for immutable in (int, str, type):
before = immutable.__name__
try:
immutable.__name__ = "nope"
except TypeError:
pass
else:
raise AssertionError("renamed the immutable type %r" % (immutable,))
assert immutable.__name__ == before, immutable.__name__

print("OK")
34 changes: 34 additions & 0 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9573,6 +9573,40 @@ pub unsafe fn classmethod_on_type_fast_path(
Some((w_type, version_tag, w_func))
}

/// `Cls.__name__` read fast path: when `w_obj` is a class whose metaclass is
/// exactly `type`, return that metaclass and the class's name object.
///
/// `__name__` is a `GetSetProperty` on `type`, so it is a DATA descriptor on
/// the metatype, and `descr_getattribute` (typeobject.py:814-819) hands the
/// read to it before consulting the class's own MRO — no `__name__` entry in
/// any base's dict can shadow it, and the MRO does not have to be walked at
/// all. Pinning the metatype to `type` itself is therefore the whole
/// precondition: `type` is immutable, so the getter cannot be replaced, and
/// what it returns is the `w_name` slot.
///
/// The slot is reported rather than its contents because the walker must read
/// it live: `descr_set__name__` (typeobject.py:1046) replaces the name without
/// going through `mutated()`, so the version tag does not move when a class is
/// renamed and a baked name would outlive the rename. A slot that has not
/// been materialised yet (`PY_NULL`) declines rather than filling it in, since
/// filling it in means allocating.
///
/// # Safety
/// `w_obj` must be a valid object pointer (null tolerated).
pub unsafe fn type_name_obj_fast_path(w_obj: PyObjectRef) -> Option<(PyObjectRef, PyObjectRef)> {
if w_obj.is_null() || !pyre_object::typeobject::is_type(w_obj) {
return None;
}
// `is_type` answers for the physical layout, which every type object
// shares; the metaclass is the `w_class` header read `getclass()` performs.
let metatype = crate::typedef::r#type(w_obj)?.as_ptr();
if !std::ptr::eq(metatype, crate::typedef::w_type()) {
return None;
}
let w_name = pyre_object::typeobject::w_type_peek_name_obj(w_obj);
(!w_name.is_null()).then_some((metatype, w_name))
}

/// `callmethod.py`'s `w_obj.getdictvalue(space, name)` shadowing check,
/// restricted to a probe that neither allocates nor runs Python.
///
Expand Down
Loading
Loading