-
Notifications
You must be signed in to change notification settings - Fork 19
jit: widen the FOR_ITER callee inline; fix a silent dunder-dispatch abort #919
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a5e8c4b
976924c
115d37a
4619278
659bb16
aa4100d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| # A metaclass resolves `Cls.name()` before the class's own MRO does: | ||
| # `type.__getattribute__` lets a metatype DATA descriptor win outright, and a | ||
| # metatype `__getattribute__` override produces the value itself. Either way the | ||
| # call must use what the metaclass returned, not rebind the class onto it. | ||
|
|
||
|
|
||
| class MetaProp(type): | ||
| @property | ||
| def where(cls): | ||
| return lambda: 'meta-prop' | ||
|
|
||
|
|
||
| class ByProp(metaclass=MetaProp): | ||
| @classmethod | ||
| def where(cls): | ||
| return 'own-classmethod' | ||
|
|
||
|
|
||
| class MetaGetattr(type): | ||
| def __getattribute__(cls, name): | ||
| if name == 'ping': | ||
| return lambda: 'meta-getattr' | ||
| return type.__getattribute__(cls, name) | ||
|
|
||
|
|
||
| class ByGetattr(metaclass=MetaGetattr): | ||
| @classmethod | ||
| def ping(cls): | ||
| return 'own-classmethod' | ||
|
|
||
|
|
||
| class Plain: | ||
| @classmethod | ||
| def tag(cls): | ||
| return cls.__name__ | ||
|
|
||
|
|
||
| def main(): | ||
| prop = getattr_ = plain = None | ||
| for _ in range(20000): | ||
| prop = ByProp.where() | ||
| getattr_ = ByGetattr.ping() | ||
| # an ordinary class still binds its classmethod's cls | ||
| plain = Plain.tag() | ||
| print('prop', prop) | ||
| print('getattr', getattr_) | ||
| print('plain', plain) | ||
|
|
||
|
|
||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2681,7 +2681,17 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>( | |
| let legacy_admit = match safety { | ||
| CalleeReplaySafety::Clean => true, | ||
| CalleeReplaySafety::DeferredCall => { | ||
| foriter_deferred_admit = !fbw_foriter_deferred_call_denied(callee_code_key); | ||
| // The deferred promise rests on the abort REWINDING to the | ||
| // enclosing CALL and re-executing it from scratch. A binop | ||
| // dunder dispatch (the only entry carrying an | ||
| // `arg_class_guard`) reaches this lever from a `BINARY_OP` | ||
| // instead, and that opcode is not a call boundary the rewind | ||
| // can name: the flush resumes one operand short and the whole | ||
| // iteration's contribution is dropped, silently. A `Clean` | ||
| // body is still admitted from there — it has nothing that can | ||
| // abort. | ||
| foriter_deferred_admit = | ||
| arg_class_guard.is_none() && !fbw_foriter_deferred_call_denied(callee_code_key); | ||
|
Comment on lines
+2684
to
+2694
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs --items all
rg -n -C 8 --type rust \
'FBW_FORITER_DEFERRED_DENY|fbw_foriter_deferred_call_denied|foriter_deferred.*den' \
pyreRepository: youknowone/pyre Length of output: 13081 🏁 Script executed: #!/bin/bash
set -euo pipefail
python3 - <<'PY'
import re
from pathlib import Path
fbw = Path('pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs').read_text()
inline = Path('pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs').read_text()
needles = {
'FBW_FORITER_DEFERRED_DENY': None,
'FBW_FORITER_DEFERRED_INLINE': None,
'FBW_HAZARDOUS_INLINE_DENY': None,
}
for m in re.finditer(r'(?s)\s*const\s+(\w+)_STORAGE', fbw):
for var_name in needles:
if var_name in fbw[m.end():]:
needles[var_name] = (
m.start(),
m.group(1),
re.search(r'(?s)\b' + var_name + r'\s*=', fbw[m.end():]).group(0)
)
break
print('TLS/storage declarations:')
for name, result in needles.items():
if result:
start, storage_prefix, ref = result
# Print the containing const storage declaration if the variable belongs to it
prev = fbw[:start].rfind('pub(crate) const ')
decl = fbw[prev:start+ref.find(name)+1+len(name)]
print(f'--- {name}')
print(decl)
else:
print(f'--- {name}: not found')
print('\ndeferrals functions context:')
for fn in [
'fbw_foriter_deferred_call_denied',
'fbw_foriter_deny_deferred_call',
'fbw_foriter_deferred_inline_outermost',
]:
i = fbw.find(f'fn {fn}')
if i != -1:
print(f'--- {fn}')
print(fbw[i:i+fbw.find('\npub(crate) fn', i+14)-i] if fbw.find('\npub(crate) fn', i+14) != -1 else fbw[i:i+500])
print('\ninline usage context:')
i = max(inline.find('fbw_foriter_deferred_call_denied'), inline.find('ForiterDeferredInlineGuard'), inline.find('CalleeReplaySafety'))
print(inline[max(0,i-800):i+900])
checks = [
('FBW_FORITER_DEFERRED_DENY declared as TLS_STORAGE', any('TLS_STORAGE<' in (ne or fbw) and 'FBW_FORITER_DEFERRED_DENY' in ne for ne,(_,_,fbw) in [] )), # placeholder
]
PYRepository: youknowone/pyre Length of output: 2778 🏁 Script executed: #!/bin/bash
set -euo pipefail
sed -n '1060,1210p' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
printf '\nTop-level FBW_* declarations (near file start):\n'
rg -n --type rust 'pub *static|static .*FBW_|TLS_STORAGE|thread_local' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs | sed -n '1,220p'
printf '\nTLS_STORAGE type definitions:\n'
rg -n --type rust 'macro_rules! TLS_STORAGE|struct TLS_STORAGE|pub *struct TLS_STORAGE|thread_local!|const .*TLS_STORAGE' pyre/pyre-jit-trace/src -C 6Repository: youknowone/pyre Length of output: 40878 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n --type rust 'fbw_foriter_deferred_deny_deferred_call|fbw_foriter_deferred_call_denied|CalleeReplaySafety::DeferredCall|fn fbw_callee_body_replay_safety|CalleeReplaySafety' pyre/pyre-jit-trace/src/jitcode_dispatch -C 5
printf '\nRelevant replay safety implementation:\n'
sed -n '1800,1930p' pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
printf '\nRelevant inline admission and sub-walk gating:\n'
sed -n '2620,2710p' pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs
sed -n '2710,2785p' pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rsRepository: youknowone/pyre Length of output: 32222 **Move the deferred-callee deny registry out of
🤖 Prompt for AI AgentsSource: Coding guidelines |
||
| foriter_deferred_admit | ||
| } | ||
| CalleeReplaySafety::Dirty => { | ||
|
|
@@ -3196,18 +3206,19 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>( | |
| // Stored bound methods carry their explicit receiver and callee frame, | ||
| // so their Ref operands remain available to the resume path. | ||
| // | ||
| // This is the one precondition here that still aborts instead of | ||
| // returning `Ok(None)`, and deliberately so. Residualizing it does | ||
| // work — `bench/synth/_pending/gc_bug_bridge_flavor_traceback_names` | ||
| // goes from 98 aborts to 2 and | ||
| // `_pending/exception_nested_exc_info_restore` from 5 aborts to 0, | ||
| // both compiling loops they never compiled before — but the loops it | ||
| // newly compiles then print traceback tuples missing their outermost | ||
| // frame, diverging from the interpreter (that fixture pins its | ||
| // expected output in its header). The abort was masking a lost | ||
| // `PyTraceback` node on the compiled exception path, not preventing | ||
| // one. Restore `Ok(None)` here once that node is recorded; it is the | ||
| // largest single win left in this function. | ||
| // This precondition used to abort the enclosing trace rather than | ||
| // decline the inline, because residualizing it let loops compile that | ||
| // then printed traceback tuples missing their OUTERMOST frame. That | ||
| // node is now recorded — the two bridge handler-entry arms attach the | ||
| // catching frame's own node — so the decline joins every other | ||
| // precondition here and returns `Ok(None)`. | ||
| // | ||
| // The abort was expensive out of all proportion to the inline it was | ||
| // protecting: a callee that walks a traceback (`while tb is not None`) | ||
| // lowers to exactly this instruction, so any handler calling such a | ||
| // helper aborted every retrace of the enclosing loop. The guard whose | ||
| // bridge the retrace was building therefore never got one and deopted | ||
| // on every delivery. | ||
| if bound_method.is_none() | ||
| && (0..callee_code.instructions.len()).any(|pc| { | ||
| matches!( | ||
|
|
@@ -3221,7 +3232,7 @@ pub(crate) fn try_walker_inline_resolved_user_call<Sym: WalkSym>( | |
| }) | ||
| { | ||
| if try_multiframe { | ||
| return Err(DispatchError::callee_inline_unsupported(op.pc)); | ||
| return Ok(None); | ||
| } | ||
| break 'seed; | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Duplicate exact-metaclass check across two files. Both sites independently implement the same "receiver's actual metaclass is exactly
type" predicate (crate::typedef::r#type(obj)followed bystd::ptr::eq(..., crate::typedef::w_type())). This predicate is the core correctness fix for this PR; keeping it in two places risks future divergence.pyre/pyre-interpreter/src/eval.rs#L2982-L2995: extract this check into a shared helper (for examplecrate::typedef::has_exact_type_metaclass) and call it here.pyre/pyre-interpreter/src/baseobjspace.rs#L8925-L8936: call the same shared helper here instead of reimplementing the pointer-identity check.📍 Affects 2 files
pyre/pyre-interpreter/src/eval.rs#L2982-L2995(this comment)pyre/pyre-interpreter/src/baseobjspace.rs#L8925-L8936🤖 Prompt for AI Agents