diff --git a/pyre/bench/synth/bound_method_builtin_fold.cranelift.jitstats b/pyre/bench/synth/bound_method_builtin_fold.cranelift.jitstats index 933245ec222..f700e308332 100644 --- a/pyre/bench/synth/bound_method_builtin_fold.cranelift.jitstats +++ b/pyre/bench/synth/bound_method_builtin_fold.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=458 +guard_failures=468 internal_compile_panics=0 -loops_aborted=0 -loops_compiled=7 +loops_aborted=1 +loops_compiled=9 +retraces_compiled=0 diff --git a/pyre/bench/synth/bound_method_builtin_fold.dynasm.jitstats b/pyre/bench/synth/bound_method_builtin_fold.dynasm.jitstats index 933245ec222..f700e308332 100644 --- a/pyre/bench/synth/bound_method_builtin_fold.dynasm.jitstats +++ b/pyre/bench/synth/bound_method_builtin_fold.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=458 +guard_failures=468 internal_compile_panics=0 -loops_aborted=0 -loops_compiled=7 +loops_aborted=1 +loops_compiled=9 +retraces_compiled=0 diff --git a/pyre/bench/synth/bound_method_builtin_fold.wasm.jitstats b/pyre/bench/synth/bound_method_builtin_fold.wasm.jitstats index e8df1a84ef0..f700e308332 100644 --- a/pyre/bench/synth/bound_method_builtin_fold.wasm.jitstats +++ b/pyre/bench/synth/bound_method_builtin_fold.wasm.jitstats @@ -8,8 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=458 +guard_failures=468 internal_compile_panics=0 -loops_aborted=0 -loops_compiled=7 +loops_aborted=1 +loops_compiled=9 retraces_compiled=0 diff --git a/pyre/bench/synth/exception_with_exit_self_null_slot.cranelift.jitstats b/pyre/bench/synth/exception_with_exit_self_null_slot.cranelift.jitstats new file mode 100644 index 00000000000..32c163496e6 --- /dev/null +++ b/pyre/bench/synth/exception_with_exit_self_null_slot.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +fbw_rolled_back_with_effects=0 +fbw_store_journal_rollback_failed=0 +field_pos_attached_misplaced=0 +field_pos_spec_misplaced=0 +guard_failures=1786 +internal_compile_panics=0 +loops_aborted=9 +loops_compiled=3 +retraces_compiled=0 diff --git a/pyre/bench/synth/exception_with_exit_self_null_slot.dynasm.jitstats b/pyre/bench/synth/exception_with_exit_self_null_slot.dynasm.jitstats new file mode 100644 index 00000000000..32c163496e6 --- /dev/null +++ b/pyre/bench/synth/exception_with_exit_self_null_slot.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +fbw_rolled_back_with_effects=0 +fbw_store_journal_rollback_failed=0 +field_pos_attached_misplaced=0 +field_pos_spec_misplaced=0 +guard_failures=1786 +internal_compile_panics=0 +loops_aborted=9 +loops_compiled=3 +retraces_compiled=0 diff --git a/pyre/bench/synth/exception_with_exit_self_null_slot.py b/pyre/bench/synth/exception_with_exit_self_null_slot.py new file mode 100644 index 00000000000..4b8e3648e98 --- /dev/null +++ b/pyre/bench/synth/exception_with_exit_self_null_slot.py @@ -0,0 +1,39 @@ +# A `with` block whose handler runs on most iterations of a hot `while` loop. +# LOAD_SPECIAL pushes the resolved `__exit__` and the call's `self_or_null` +# slot; that NULL is a live value, and the dense virtualizable shadow cannot +# tell it from an unwritten slot. Carried as a mirror hole rather than as a +# constant, the slot was omitted from the resume image and a blackhole +# resuming into WITH_EXCEPT_START read the bound `__exit__` the same opcode had +# pushed one slot below, calling it with the receiver twice +# ("__exit__() takes 4 positional arguments but 5 were given"). +# +# No FOR_ITER: this is the shape that reaches the defect with no opcode gate +# involved, so it stays a witness independently of what the FOR_ITER body scan +# admits. Kept at module scope, where it was first reproduced. +N = 20000 + + +class Context: + def __init__(self): + self.exits = 0 + + def __enter__(self): + return 3 + + def __exit__(self, exc_type, exc_value, traceback): + self.exits += 1 + return exc_type is ValueError + + +context = Context() +total = 0 +i = 0 +while i < N: + with context as value: + total += value + if i % 10 == 0: + raise ValueError + total += i + i += 1 +print(total, context.exits) +# Expected: 180060000 20000 diff --git a/pyre/bench/synth/exception_with_exit_self_null_slot.wasm.jitstats b/pyre/bench/synth/exception_with_exit_self_null_slot.wasm.jitstats new file mode 100644 index 00000000000..32c163496e6 --- /dev/null +++ b/pyre/bench/synth/exception_with_exit_self_null_slot.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +fbw_rolled_back_with_effects=0 +fbw_store_journal_rollback_failed=0 +field_pos_attached_misplaced=0 +field_pos_spec_misplaced=0 +guard_failures=1786 +internal_compile_panics=0 +loops_aborted=9 +loops_compiled=3 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_function_ex_body.cranelift.jitstats b/pyre/bench/synth/foriter_call_function_ex_body.cranelift.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_function_ex_body.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_function_ex_body.dynasm.jitstats b/pyre/bench/synth/foriter_call_function_ex_body.dynasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_function_ex_body.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_function_ex_body.py b/pyre/bench/synth/foriter_call_function_ex_body.py new file mode 100644 index 00000000000..d9dc175fe4b --- /dev/null +++ b/pyre/bench/synth/foriter_call_function_ex_body.py @@ -0,0 +1,19 @@ +# CALL_FUNCTION_EX in a hot FOR_ITER body. The starred call is the same +# MayForce boundary as CALL and CALL_KW for the whole-frame safety gate. +N = 20000 + + +def add(a, b): + return a + b + + +def main(): + total = 0 + for i in range(N): + args = (i, 1) + total += add(*args) + print(total) + + +main() +# Expected: 200010000 diff --git a/pyre/bench/synth/foriter_call_function_ex_body.wasm.jitstats b/pyre/bench/synth/foriter_call_function_ex_body.wasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_function_ex_body.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.cranelift.jitstats b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.cranelift.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.dynasm.jitstats b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.dynasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.py b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.py new file mode 100644 index 00000000000..758f73d4177 --- /dev/null +++ b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.py @@ -0,0 +1,14 @@ +# CALL_INTRINSIC_1/UnaryPositive in a hot FOR_ITER body. This is one of the two +# variants with a codewriter residual; the other variants remain gate declines. +N = 20000 + + +def main(): + total = 0 + for i in range(N): + total += +i + print(total) + + +main() +# Expected: 199990000 diff --git a/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.wasm.jitstats b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.wasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/foriter_call_intrinsic1_unary_positive.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_load_special_with.cranelift.jitstats b/pyre/bench/synth/foriter_load_special_with.cranelift.jitstats new file mode 100644 index 00000000000..ffc3592b8b6 --- /dev/null +++ b/pyre/bench/synth/foriter_load_special_with.cranelift.jitstats @@ -0,0 +1,15 @@ +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=0 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_load_special_with.dynasm.jitstats b/pyre/bench/synth/foriter_load_special_with.dynasm.jitstats new file mode 100644 index 00000000000..ffc3592b8b6 --- /dev/null +++ b/pyre/bench/synth/foriter_load_special_with.dynasm.jitstats @@ -0,0 +1,15 @@ +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=0 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_load_special_with.py b/pyre/bench/synth/foriter_load_special_with.py new file mode 100644 index 00000000000..ef708f15258 --- /dev/null +++ b/pyre/bench/synth/foriter_load_special_with.py @@ -0,0 +1,34 @@ +# A `with` block in a hot FOR_ITER body, whose handler runs every tenth +# iteration. The whole-frame FOR_ITER gate declines LOAD_SPECIAL, so this frame +# runs interpreted; the answer is recorded here for the day it is admitted. +# `exception_with_exit_self_null_slot` is the same shape written as a `while` +# loop, where no gate stands between it and the JIT. +N = 20000 + + +class Context: + def __init__(self): + self.exits = 0 + + def __enter__(self): + return 3 + + def __exit__(self, exc_type, exc_value, traceback): + self.exits += 1 + return exc_type is ValueError + + +def main(): + context = Context() + total = 0 + for i in range(N): + with context as value: + total += value + if i % 10 == 0: + raise ValueError + total += i + print(total, context.exits) + + +main() +# Expected: 180060000 20000 diff --git a/pyre/bench/synth/foriter_load_special_with.wasm.jitstats b/pyre/bench/synth/foriter_load_special_with.wasm.jitstats new file mode 100644 index 00000000000..ffc3592b8b6 --- /dev/null +++ b/pyre/bench/synth/foriter_load_special_with.wasm.jitstats @@ -0,0 +1,15 @@ +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=0 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_make_function_body.cranelift.jitstats b/pyre/bench/synth/foriter_make_function_body.cranelift.jitstats new file mode 100644 index 00000000000..7fed4742a70 --- /dev/null +++ b/pyre/bench/synth/foriter_make_function_body.cranelift.jitstats @@ -0,0 +1,15 @@ +bridges_compiled=47 +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=9480 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_make_function_body.dynasm.jitstats b/pyre/bench/synth/foriter_make_function_body.dynasm.jitstats new file mode 100644 index 00000000000..7fed4742a70 --- /dev/null +++ b/pyre/bench/synth/foriter_make_function_body.dynasm.jitstats @@ -0,0 +1,15 @@ +bridges_compiled=47 +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=9480 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/foriter_make_function_body.py b/pyre/bench/synth/foriter_make_function_body.py new file mode 100644 index 00000000000..e9df9683ad2 --- /dev/null +++ b/pyre/bench/synth/foriter_make_function_body.py @@ -0,0 +1,18 @@ +# MAKE_FUNCTION plus SET_FUNCTION_ATTRIBUTE in a hot FOR_ITER body. The default +# value forces the companion attribute initializer onto the definition path. +N = 20000 + + +def main(): + total = 0 + for i in range(N): + + def add(value=i): + return value + 1 + + total += add() + print(total) + + +main() +# Expected: 200010000 diff --git a/pyre/bench/synth/foriter_make_function_body.wasm.jitstats b/pyre/bench/synth/foriter_make_function_body.wasm.jitstats new file mode 100644 index 00000000000..7fed4742a70 --- /dev/null +++ b/pyre/bench/synth/foriter_make_function_body.wasm.jitstats @@ -0,0 +1,15 @@ +bridges_compiled=47 +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=9480 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_deque_backing_list.cranelift.jitstats b/pyre/bench/synth/gc_deque_backing_list.cranelift.jitstats index 794b6dbdecc..aeb098ea813 100644 --- a/pyre/bench/synth/gc_deque_backing_list.cranelift.jitstats +++ b/pyre/bench/synth/gc_deque_backing_list.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=203 +guard_failures=204 internal_compile_panics=0 loops_aborted=0 -loops_compiled=5 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_deque_backing_list.dynasm.jitstats b/pyre/bench/synth/gc_deque_backing_list.dynasm.jitstats index 794b6dbdecc..aeb098ea813 100644 --- a/pyre/bench/synth/gc_deque_backing_list.dynasm.jitstats +++ b/pyre/bench/synth/gc_deque_backing_list.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=203 +guard_failures=204 internal_compile_panics=0 loops_aborted=0 -loops_compiled=5 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_deque_backing_list.wasm.jitstats b/pyre/bench/synth/gc_deque_backing_list.wasm.jitstats index 794b6dbdecc..aeb098ea813 100644 --- a/pyre/bench/synth/gc_deque_backing_list.wasm.jitstats +++ b/pyre/bench/synth/gc_deque_backing_list.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=203 +guard_failures=204 internal_compile_panics=0 loops_aborted=0 -loops_compiled=5 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_iterator_source_drop.cranelift.jitstats b/pyre/bench/synth/gc_iterator_source_drop.cranelift.jitstats index d9af7a0ecb4..31a9aed9445 100644 --- a/pyre/bench/synth/gc_iterator_source_drop.cranelift.jitstats +++ b/pyre/bench/synth/gc_iterator_source_drop.cranelift.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=613 +guard_failures=1015 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_iterator_source_drop.dynasm.jitstats b/pyre/bench/synth/gc_iterator_source_drop.dynasm.jitstats index d9af7a0ecb4..31a9aed9445 100644 --- a/pyre/bench/synth/gc_iterator_source_drop.dynasm.jitstats +++ b/pyre/bench/synth/gc_iterator_source_drop.dynasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=613 +guard_failures=1015 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/gc_iterator_source_drop.wasm.jitstats b/pyre/bench/synth/gc_iterator_source_drop.wasm.jitstats index d9af7a0ecb4..31a9aed9445 100644 --- a/pyre/bench/synth/gc_iterator_source_drop.wasm.jitstats +++ b/pyre/bench/synth/gc_iterator_source_drop.wasm.jitstats @@ -1,4 +1,4 @@ -bridges_compiled=3 +bridges_compiled=5 descr_set_absent=0 descr_set_ambiguous=0 descr_set_stale_absent=0 @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=613 +guard_failures=1015 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/hash_subclass_disabled.cranelift.jitstats b/pyre/bench/synth/hash_subclass_disabled.cranelift.jitstats index c4e90ee6f4b..32b9f1fb455 100644 --- a/pyre/bench/synth/hash_subclass_disabled.cranelift.jitstats +++ b/pyre/bench/synth/hash_subclass_disabled.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=5 +retraces_compiled=0 diff --git a/pyre/bench/synth/hash_subclass_disabled.dynasm.jitstats b/pyre/bench/synth/hash_subclass_disabled.dynasm.jitstats index c4e90ee6f4b..32b9f1fb455 100644 --- a/pyre/bench/synth/hash_subclass_disabled.dynasm.jitstats +++ b/pyre/bench/synth/hash_subclass_disabled.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=5 +retraces_compiled=0 diff --git a/pyre/bench/synth/hash_subclass_disabled.wasm.jitstats b/pyre/bench/synth/hash_subclass_disabled.wasm.jitstats index c4e90ee6f4b..32b9f1fb455 100644 --- a/pyre/bench/synth/hash_subclass_disabled.wasm.jitstats +++ b/pyre/bench/synth/hash_subclass_disabled.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=5 +retraces_compiled=0 diff --git a/pyre/bench/synth/len_range_bignum_length.cranelift.jitstats b/pyre/bench/synth/len_range_bignum_length.cranelift.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/len_range_bignum_length.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/len_range_bignum_length.dynasm.jitstats b/pyre/bench/synth/len_range_bignum_length.dynasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/len_range_bignum_length.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/len_range_bignum_length.py b/pyre/bench/synth/len_range_bignum_length.py new file mode 100644 index 00000000000..260809d03b1 --- /dev/null +++ b/pyre/bench/synth/len_range_bignum_length.py @@ -0,0 +1,29 @@ +# `len()` on ranges whose stored length does and does not fit a machine word. +# +# `functional.py:496-497 W_Range.descr_len` returns the precomputed +# `self.w_length` field, and `descr_new` stores a bignum there whenever +# `compute_range_length` leaves the machine range. The fold reads that field +# behind a class guard, so the loaded box needs its own guard: the receiver +# being an exact range says nothing about what its length slot holds, and the +# hot small ranges below would otherwise let a bignum-length range take their +# recorded exit and hand the bignum straight to `len()`. +N = 20000 + +SMALL = range(10) +HUGE = range(-4611686018427387904, 4611686018427387904) + + +def main(): + total = 0 + for _ in range(N): + total += len(SMALL) + try: + len(HUGE) + huge = "no-error" + except OverflowError: + huge = "OverflowError" + print(total, huge) + + +main() +# Expected: 200000 OverflowError diff --git a/pyre/bench/synth/len_range_bignum_length.wasm.jitstats b/pyre/bench/synth/len_range_bignum_length.wasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/len_range_bignum_length.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_ctor_user_index_bound.cranelift.jitstats b/pyre/bench/synth/range_ctor_user_index_bound.cranelift.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_ctor_user_index_bound.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_ctor_user_index_bound.dynasm.jitstats b/pyre/bench/synth/range_ctor_user_index_bound.dynasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_ctor_user_index_bound.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_ctor_user_index_bound.py b/pyre/bench/synth/range_ctor_user_index_bound.py new file mode 100644 index 00000000000..5f4c168a013 --- /dev/null +++ b/pyre/bench/synth/range_ctor_user_index_bound.py @@ -0,0 +1,32 @@ +# `range()` with a bound whose type supplies `__index__`. +# `functional.py:461-474 W_Range.descr_new` converts each bound with +# `space.index`, and `descroperation.py:599-620 _index` carries no JIT hints, +# so the user method is traced into like any other call. The bound is live +# rather than trace-constant, which is what puts the emitted +# `compute_range_length` (functional.py:42-53) on the recorded path: the +# step-sign guard, the emptiness guard and the overflow guards all come from +# the source's own conditionals. +N = 20000 + + +class Index: + def __init__(self, value): + self.value = value + + def __index__(self): + return self.value + + +def main(): + total = 0 + stop = Index(0) + step = Index(2) + for i in range(N): + stop.value = i % 7 + total += len(range(stop)) + total += len(range(0, stop, step)) + print(total) + + +main() +# Expected: 94281 diff --git a/pyre/bench/synth/range_ctor_user_index_bound.wasm.jitstats b/pyre/bench/synth/range_ctor_user_index_bound.wasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_ctor_user_index_bound.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_user_index_side_effect.cranelift.jitstats b/pyre/bench/synth/range_user_index_side_effect.cranelift.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_user_index_side_effect.cranelift.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_user_index_side_effect.dynasm.jitstats b/pyre/bench/synth/range_user_index_side_effect.dynasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_user_index_side_effect.dynasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/range_user_index_side_effect.py b/pyre/bench/synth/range_user_index_side_effect.py new file mode 100644 index 00000000000..f9640e41a81 --- /dev/null +++ b/pyre/bench/synth/range_user_index_side_effect.py @@ -0,0 +1,32 @@ +# A `__index__` that writes back to its own instance, consumed by `range()`. +# +# `functional.py:465` applies `space.index` once per bound and +# `descroperation.py:607 _index` calls the user method exactly once, so the +# counter must equal the iteration count. The specialization runs the callee +# during recording while keeping the caller pinned at the `range` CALL, so a +# guard failure resumes by re-entering that CALL and calls `__index__` again -- +# the reason a body carrying a store is not admitted at all. The `& 1` makes +# the range alternate empty and non-empty, which is what makes the emptiness +# guard flip and the second call observable. +N = 20000 + + +class Alt: + def __init__(self): + self.n = 0 + + def __index__(self): + self.n += 1 + return self.n & 1 + + +def main(): + alt = Alt() + total = 0 + for _ in range(N): + total += len(range(alt)) + print(alt.n, total) + + +main() +# Expected: 20000 10000 diff --git a/pyre/bench/synth/range_user_index_side_effect.wasm.jitstats b/pyre/bench/synth/range_user_index_side_effect.wasm.jitstats new file mode 100644 index 00000000000..651a3eaf3e9 --- /dev/null +++ b/pyre/bench/synth/range_user_index_side_effect.wasm.jitstats @@ -0,0 +1,15 @@ +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=1 +retraces_compiled=0 diff --git a/pyre/bench/synth/str_encode_text_codec.cranelift.jitstats b/pyre/bench/synth/str_encode_text_codec.cranelift.jitstats index bf884686e62..4e1e41a7bed 100644 --- a/pyre/bench/synth/str_encode_text_codec.cranelift.jitstats +++ b/pyre/bench/synth/str_encode_text_codec.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/str_encode_text_codec.dynasm.jitstats b/pyre/bench/synth/str_encode_text_codec.dynasm.jitstats index bf884686e62..4e1e41a7bed 100644 --- a/pyre/bench/synth/str_encode_text_codec.dynasm.jitstats +++ b/pyre/bench/synth/str_encode_text_codec.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/str_encode_text_codec.wasm.jitstats b/pyre/bench/synth/str_encode_text_codec.wasm.jitstats index bf884686e62..4e1e41a7bed 100644 --- a/pyre/bench/synth/str_encode_text_codec.wasm.jitstats +++ b/pyre/bench/synth/str_encode_text_codec.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=0 +guard_failures=1 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=2 +retraces_compiled=0 diff --git a/pyre/bench/synth/subscr_user_getitem_inline.cranelift.jitstats b/pyre/bench/synth/subscr_user_getitem_inline.cranelift.jitstats index 6349d33b49a..83008be23b0 100644 --- a/pyre/bench/synth/subscr_user_getitem_inline.cranelift.jitstats +++ b/pyre/bench/synth/subscr_user_getitem_inline.cranelift.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=404 +guard_failures=406 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/subscr_user_getitem_inline.dynasm.jitstats b/pyre/bench/synth/subscr_user_getitem_inline.dynasm.jitstats index 6349d33b49a..83008be23b0 100644 --- a/pyre/bench/synth/subscr_user_getitem_inline.dynasm.jitstats +++ b/pyre/bench/synth/subscr_user_getitem_inline.dynasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=404 +guard_failures=406 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/subscr_user_getitem_inline.wasm.jitstats b/pyre/bench/synth/subscr_user_getitem_inline.wasm.jitstats index 6349d33b49a..83008be23b0 100644 --- a/pyre/bench/synth/subscr_user_getitem_inline.wasm.jitstats +++ b/pyre/bench/synth/subscr_user_getitem_inline.wasm.jitstats @@ -8,7 +8,8 @@ fbw_rolled_back_with_effects=0 fbw_store_journal_rollback_failed=0 field_pos_attached_misplaced=0 field_pos_spec_misplaced=0 -guard_failures=404 +guard_failures=406 internal_compile_panics=0 loops_aborted=0 -loops_compiled=4 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index cf49b5c2b9e..051d5a6ad17 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -13354,6 +13354,30 @@ pub(crate) unsafe fn int_as_base(obj: PyObjectRef) -> PyObjectRef { } } +/// Resolve the user `__index__` call performed by +/// `descroperation.py:599-620 _index` without executing it. This is the +/// trace-side admission counterpart of [`getitem_fast_path`]: the caller pins +/// the returned type and version tag before inlining the returned descriptor. +/// Exact int/long inputs return `None` because `_index` returns those directly +/// and has no user call to inline. +/// +/// # Safety +/// `w_obj` must be a live object. +pub unsafe fn index_fast_path(w_obj: PyObjectRef) -> Option<(PyObjectRef, u64, PyObjectRef)> { + unsafe { + if w_obj.is_null() || pyre_object::pyobject::is_int_or_long(w_obj) { + return None; + } + let w_type = crate::typedef::r#type(w_obj)?.as_ptr(); + let method = lookup_in_type_where(w_type, "__index__")?; + let version_tag = w_type_version_tag(w_type); + if version_tag == 0 { + return None; + } + Some((w_type, version_tag, method)) + } +} + pub fn space_index(obj: PyObjectRef) -> Result { if obj.is_null() { return Err(PyError::type_error("space.index: null object")); diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs index e97e135e722..cc2a150b8f0 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/inline_call.rs @@ -474,12 +474,14 @@ pub(crate) fn callee_body_owns_loop_header(body_code: &[u8]) -> bool { crate::jitcode_runtime::decoded_ops(body_code).any(|op| op.opname == "jit_merge_point") } -/// Whether sampling an exception string override before recording can have no -/// app-visible effect. Portal-frame vable traffic and constant/int boxing are -/// local; branches, other calls, and live-heap writes decline the sample. -pub(crate) fn exception_string_override_sample_safe( +/// Whether running a straight-line callee body before deciding to keep it can +/// have no app-visible effect: no branch, no live-heap write, and no residual +/// outside `pure_helpers`. Portal-frame vable traffic and constant/int boxing +/// are local, so both callers pass those. +fn body_sample_safe_with( body_code: &[u8], callee_descr_refs: &[DescrRef], + pure_helpers: &[majit_ir::PyreHelperKind], ) -> bool { let mut pc = 0usize; while pc < body_code.len() { @@ -491,10 +493,7 @@ pub(crate) fn exception_string_override_sample_safe( } if d.opname.starts_with("residual_call") { let kind = residual_call_helper_kind_in_body(body_code, &d, callee_descr_refs); - if !matches!( - kind, - Some(majit_ir::PyreHelperKind::LoadConst | majit_ir::PyreHelperKind::BoxInt) - ) { + if !kind.is_some_and(|kind| pure_helpers.contains(&kind)) { return false; } } else if d.opname.starts_with("setfield_gc") @@ -512,6 +511,48 @@ pub(crate) fn exception_string_override_sample_safe( true } +/// Whether sampling an exception string override before recording can have no +/// app-visible effect. Portal-frame vable traffic and constant/int boxing are +/// local; branches, other calls, and live-heap writes decline the sample. +pub(crate) fn exception_string_override_sample_safe( + body_code: &[u8], + callee_descr_refs: &[DescrRef], +) -> bool { + body_sample_safe_with( + body_code, + callee_descr_refs, + &[ + majit_ir::PyreHelperKind::LoadConst, + majit_ir::PyreHelperKind::BoxInt, + ], + ) +} + +/// The same question for the `_index` callee ([`prepare_walker_inline_index`]), +/// which additionally reads the value it returns off its own instance. +/// +/// `LOAD_ATTR` is admitted where the exception-override route refuses it. The +/// residual covers both a plain instance-attribute read, which runs no app-level +/// code, and a descriptor whose getter does, and the bytecode alone cannot tell +/// them apart — but the ones that matter here are separable at run time by the +/// executed-effect odometer, which is what [`try_walker_inline_index`] consults +/// once the body has run. `STORE_ATTR` is a distinct residual +/// ([`majit_ir::PyreHelperKind::StoreAttr`]) and stays rejected here, so the +/// ordinary writing `__index__` never runs at all: it has to be refused before +/// execution, because by the time the odometer could report it the write has +/// already happened once. +pub(crate) fn index_inline_sample_safe(body_code: &[u8], callee_descr_refs: &[DescrRef]) -> bool { + body_sample_safe_with( + body_code, + callee_descr_refs, + &[ + majit_ir::PyreHelperKind::LoadConst, + majit_ir::PyreHelperKind::BoxInt, + majit_ir::PyreHelperKind::LoadAttr, + ], + ) +} + /// The bounded builtin-dispatch route only admits a straight-line app-level /// override. A control-flow-bearing method stays on the original residual /// dispatch path, where the interpreter owns its frame and branch semantics. @@ -1601,6 +1642,7 @@ fn sub_jitcode_body_facts_for_code(code: *const ()) -> Option( } pub(crate) fn try_walker_inline_resolved_user_call( + ctx: &mut WalkContext<'_, '_, Sym>, + op: &DecodedOp, + code: &[u8], + funcptr: OpRef, + r_args: &[OpRef], + call_descr: &dyn majit_ir::descr::CallDescr, + dst_bank: char, + dst: usize, + callable: pyre_object::PyObjectRef, + callable_guard_op: OpRef, + callable_guard_value: pyre_object::PyObjectRef, + arg_concretes: Vec, + callee_args: Vec, + callee_arg_concretes: Vec, + method_form: bool, + bound_method: Option, + w_code: *const (), + nparams: usize, + has_closure: bool, + exception_receiver_guard: Option, + arg_class_guard: Option, + entry_is_call_boundary: bool, + require_str_result: bool, + constructor_result: Option<(OpRef, ConcreteValue)>, +) -> Result, DispatchError> { + try_walker_inline_resolved_user_call_inner( + ctx, + op, + code, + funcptr, + r_args, + call_descr, + dst_bank, + dst, + callable, + callable_guard_op, + callable_guard_value, + arg_concretes, + callee_args, + callee_arg_concretes, + method_form, + bound_method, + w_code, + nparams, + has_closure, + exception_receiver_guard, + arg_class_guard, + entry_is_call_boundary, + require_str_result, + constructor_result, + None, + false, + ) +} + +/// The resolved-call inliner with an optional receiver for a nested call's +/// result. RPython's `MIFrame.finishframe` hands a returned box to the +/// previous frame (`pyjitpl.py:1688-1698`) before that caller continues its +/// current operation. `intermediate_result` represents that hand-off for a +/// user call nested inside a specialized builtin: it deliberately leaves the +/// outer residual's destination untouched, so guards still snapshot the +/// caller at the builtin-call boundary. +#[allow(clippy::too_many_arguments)] +fn try_walker_inline_resolved_user_call_inner( ctx: &mut WalkContext<'_, '_, Sym>, op: &DecodedOp, code: &[u8], @@ -2651,6 +2757,8 @@ pub(crate) fn try_walker_inline_resolved_user_call( entry_is_call_boundary: bool, require_str_result: bool, constructor_result: Option<(OpRef, ConcreteValue)>, + intermediate_result: Option<&mut Option<(OpRef, ConcreteValue)>>, + require_exact_int_result: bool, ) -> Result, DispatchError> { // `_compute_flatcall` (`pycode.py:256-268`) leaves `fast_natural_arity` // HOPELESS for a `*args` / `**kwargs` / keyword-only callee. The general @@ -4786,6 +4894,25 @@ pub(crate) fn try_walker_inline_resolved_user_call( ); return Err(DispatchError::callee_inline_unsupported(op.pc)); } + if require_exact_int_result + && !matches!( + concrete_for_shadow, + ConcreteValue::Ref(obj) + if walker_is_exact_machine_int_concrete(obj) + ) + { + // `descroperation.py:608-620 _index` validates the app-level + // result before its caller continues, and a long, a bool or an + // int subclass are all legal there — only the machine-int + // arithmetic downstream cannot take them. Decline instead of + // aborting: the caller rewinds the emission and falls through + // to its residual, which re-runs the whole builtin. That is + // sound because the body is admitted only when re-running it + // observes and changes nothing (`exc_override_sample_safe`), + // and it keeps a legal program from killing the enclosing + // loop's trace, which `callee_inline_unsupported` would. + return Ok(None); + } // `descr_call` discards `__init__`'s result after checking it is // None and returns the instance instead (`check_init_returned_none`). // A non-None result is a TypeError the inlined body cannot raise, so @@ -4814,6 +4941,33 @@ pub(crate) fn try_walker_inline_resolved_user_call( } None => (value, concrete_for_shadow), }; + if let Some(result) = intermediate_result { + // The caller stays pinned at its own CALL boundary, so a guard + // it emits after this hand-off resumes by re-entering that CALL + // and running the callee a second time, and its rewinding + // declines cut the trace without undoing what the body already + // did. `FBW_EXECUTED_EFFECT_COUNT` is the odometer that + // answers whether that is survivable: "a nonzero count delta + // means the callee attempt cannot be discarded and re-executed + // without risking a double". Abort rather than decline — + // `latch_abort_call_resume` deliberately declines to latch the + // CALL when effects ran, so the interpreter resumes past it + // instead of re-running the effects. + if fbw_executed_effect_count() != executed_effects_before { + latch_abort_call_resume( + code, + op, + ctx, + is_top_inline, + unjournaled_before_subwalk, + executed_effects_before, + abort_flush_call_jitcode_coord, + ); + return Err(DispatchError::callee_inline_unsupported(op.pc)); + } + *result = Some((value, concrete_for_shadow)); + return Ok(Some((DispatchOutcome::Continue, op.next_pc))); + } match dst_bank { 'r' => write_ref_reg(ctx, op.pc, dst, value, concrete_for_shadow)?, 'i' => write_int_reg(ctx, op.pc, dst, value, concrete_for_shadow)?, @@ -5692,6 +5846,204 @@ pub(crate) fn try_walker_inline_property_set( ) } +/// Whether a concrete object is the canonical machine-word `int` layout that +/// the range virtualizer can unbox. Longs and int subclasses stay on the +/// residual path even though Python-level `type(x) is int` may hold for the +/// separate long payload layout. +pub(crate) fn walker_is_exact_machine_int_concrete(obj: pyre_object::PyObjectRef) -> bool { + if pyre_object::tagged_int::CAN_BE_TAGGED && pyre_object::tagged_int::is_tagged_int(obj) { + return true; + } + if obj.is_null() { + return false; + } + unsafe { + std::ptr::eq((*obj).ob_type, &pyre_object::pyobject::INT_TYPE) + && std::ptr::eq( + (*obj).w_class, + pyre_object::get_instantiate(&pyre_object::pyobject::INT_TYPE), + ) + } +} + +/// A fully preflighted user `__index__` call nested inside a builtin. +/// Keeping the resolved pieces together lets `range` preflight every bound +/// before the first conversion emits guards or enters a callee frame. +pub(crate) struct IndexInlineCandidate { + arg: OpRef, + concrete_arg: pyre_object::PyObjectRef, + w_type: pyre_object::PyObjectRef, + version_tag: u64, + method: pyre_object::PyObjectRef, + w_code: *const (), + nparams: usize, + has_closure: bool, +} + +/// Resolve and statically preflight the call made by +/// `descroperation.py:599-620 _index`. The body is held to the existing +/// strict straight-line inline shape; a non-user descriptor, loop/raise body, +/// or other unsupported function shape leaves the enclosing builtin on its +/// ordinary residual path without having emitted anything. +/// +/// The body must additionally pass [`index_inline_sample_safe`]. This call is +/// nested inside a builtin the caller keeps pinned at its own CALL boundary, so +/// every guard the caller emits afterwards resumes by re-entering that CALL, and +/// the interpreter then performs `space.index` again — a second `__index__` on +/// an object the inlined body already ran once. The caller also declines on +/// paths that only rewind the trace, which likewise cannot undo an executed +/// body. Both are harmless exactly when re-running the body changes nothing. +/// +/// That predicate settles it before the body runs for every shape it can read +/// off the bytecode, which is the only place a writing body can be caught: the +/// executed-effect odometer reports a write only after it has happened. It also +/// rejects a branch, so the odometer's verdict on the one recorded path speaks +/// for every later execution as well. +pub(crate) fn prepare_walker_inline_index( + ctx: &WalkContext<'_, '_, Sym>, + arg: OpRef, + concrete_arg: pyre_object::PyObjectRef, +) -> Option { + // Every other lever that inlines app-level Python out of a specializer + // declines once it is itself inside a callee sub-walk — `try_walker_inline_ + // type_call`, `..._property_get`, `..._property_set`, `..._subscr_getitem` + // and `try_walker_specialize_seqiter_getitem_next` all lead with this. + if !ctx.is_authoritative_executor || ctx.fbw_mode.inline_subwalk { + return None; + } + let (w_type, version_tag, method) = + unsafe { pyre_interpreter::baseobjspace::index_fast_path(concrete_arg) }?; + let (w_code, nparams, has_closure) = unsafe { resolve_inlinable_callee(method) }?; + // `get_and_call_function(w_impl, w_obj)` supplies exactly `self`. + if nparams != 1 || has_closure { + return None; + } + let body_facts = sub_jitcode_body_facts_for_code(w_code)?; + if body_facts.has_abort_permanent + || body_facts.owns_loop_header + || body_facts.contains_raise + || !body_facts.index_sample_safe + { + return None; + } + // `index_inline_sample_safe` admits `LOAD_ATTR` because the residual covers + // both a plain instance read and a descriptor whose getter runs app-level + // code, and the bytecode cannot tell them apart. Separate them here, while + // declining is still free: every name the body can name has to be a plain + // mapdict slot on this very receiver, which is the question + // `mapdict.py:1479-1537 LOAD_ATTR_caching` asks. A property, a slot this + // receiver does not carry, and a read chained through some other object all + // fail it. Deciding it after the body has run would be too late — a + // getter's effect has happened by then. Both storage shapes count: an + // `__index__` returning an int attribute holds it unboxed + // (`mapdict.py:600-601 _prim_direct_read`), which is the common case here. + let mut name_idx = 0usize; + while let Some(name) = + crate::jitcode_dispatch::walker_load_name_from_code(w_code as usize, name_idx) + { + let plain_slot = unsafe { + pyre_interpreter::objspace::std::mapdict::load_attr_fast_path(concrete_arg, &name) + .is_some() + || pyre_interpreter::objspace::std::mapdict::load_attr_unboxed_fast_path( + concrete_arg, + &name, + ) + .is_some() + }; + if !plain_slot { + return None; + } + name_idx += 1; + } + let body = crate::state::sub_jitcode_body_for_code(w_code)?; + let (callee_descr_refs, _, _) = crate::state::sub_jitcode_descr_pool_for_code(w_code)?; + let callee_frame_reg = crate::state::ensure_jitcode_index(w_code) + .filter(|&jc| crate::state::built_as_portal_at(jc)) + .map(|jc| crate::state::portal_red_regs_at(jc).0) + .unwrap_or(u16::MAX); + if !callee_fast_path_inlinable(body.code, callee_descr_refs, ctx, callee_frame_reg) { + return None; + } + Some(IndexInlineCandidate { + arg, + concrete_arg, + w_type, + version_tag, + method, + w_code, + nparams, + has_closure, + }) +} + +/// Inline one preflighted `_index` user call and return its box to the +/// enclosing builtin without writing the builtin residual's destination. +pub(crate) fn try_walker_inline_index( + ctx: &mut WalkContext<'_, '_, Sym>, + op: &DecodedOp, + code: &[u8], + funcptr: OpRef, + r_args: &[OpRef], + call_descr: &dyn majit_ir::descr::CallDescr, + dst: usize, + candidate: IndexInlineCandidate, +) -> Result, DispatchError> { + let IndexInlineCandidate { + arg, + concrete_arg, + w_type, + version_tag, + method, + w_code, + nparams, + has_closure, + } = candidate; + let arg_concretes = vec![ + ConcreteValue::Ref(method), + ConcreteValue::Null, + ConcreteValue::Ref(concrete_arg), + ]; + let method_const = ctx.trace_ctx.const_ref(method as i64); + let mut result = None; + let inlined = try_walker_inline_resolved_user_call_inner( + ctx, + op, + code, + funcptr, + r_args, + call_descr, + 'r', + dst, + method, + method_const, + method, + arg_concretes, + vec![arg], + vec![ConcreteValue::Ref(concrete_arg)], + true, + None, + w_code, + nparams, + has_closure, + Some((arg, concrete_arg, w_type, version_tag)), + None, + // This method call is nested inside `range(...)`, not represented by + // a caller bytecode CALL of its own. + false, + false, + None, + Some(&mut result), + true, + )?; + match (inlined, result) { + (Some((DispatchOutcome::Continue, next_pc)), Some(result)) if next_pc == op.next_pc => { + Ok(Some(result)) + } + (None, None) => Ok(None), + _ => Err(DispatchError::callee_inline_unsupported(op.pc)), + } +} + /// Inline `obj[key]` into the receiver type's Python `__getitem__`. /// /// `descroperation.py:356-381 DescrOperation.getitem` resolves `__getitem__` diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index c9692981fef..ec56453764e 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -6811,6 +6811,15 @@ enum VstackOpClass { /// sequence keep their mirror-tracked boxes (the hole-fill never /// overwrites a non-NONE slot). Never latches the mirror invalid. MultiResultFromShadow, + /// `LOAD_SPECIAL` (`__enter__` / `__exit__`): pop the context manager and + /// push the resolved special method followed by the call's `self_or_null` + /// slot. Same pop-then-push-a-group shape as + /// [`VstackOpClass::MultiResultFromShadow`], except that the top slot is a + /// live NULL the shadow cannot return — its source is a dense array where + /// an absent slot and a written NULL are the same word — so reconcile + /// stamps the NULL constant there instead, as [`Self::LoadGlobalMethod`] + /// does for the other callable/NULL pair. + LoadSpecialMethod, /// Anything that does not fit the shapes above — FOR_ITER or any opcode /// this classifier does not recognise. Latches `vstack_valid = false` /// so the overlay omits those slots, which resume re-materializes (zero diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs index 9f410ad10ce..43151e6cfa8 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -5508,7 +5508,7 @@ pub(crate) fn dispatch_residual_call_iRd_kind( && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn { if let Some(outcome) = spec_gate("builtin_range", || { - try_walker_specialize_builtin_range(ctx, code, op, &r_args, dst) + try_walker_specialize_builtin_range(ctx, code, op, funcptr, &r_args, call_descr, dst) })? { return Ok((outcome, op.next_pc)); } diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index 4d390f9fbee..b37c5182306 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -7001,13 +7001,16 @@ enum BuiltinLenSource { /// `tupleobject.py` carries no separate length field, so the length is /// `arraylen_gc(wrappeditems)`. TupleArrayLen, + /// `functional.py:496-497 W_Range.descr_len` returns the precomputed + /// wrapped `self.w_length` field unchanged. + RangeField, /// `specialisedtupleobject.py:54-55 length()` returns the constant /// `typelen`. PairArity, } /// `len(x)` on an exact canonical `W_ListObject` / `W_UnicodeObject` / -/// `W_TupleObject`, or on an arity-2 tuple specialisation: +/// `W_TupleObject` / `W_Range`, or on an arity-2 tuple specialisation: /// lower the opaque `bh_call_fn(len_builtin, PY_NULL, x)` residual to the /// inline length read the meta-tracer produces upstream /// (descroperation.py `_len`): `guard_value(callable)` + @@ -7048,7 +7051,7 @@ pub(crate) fn try_walker_specialize_builtin_len( if !pyre_interpreter::builtins::is_builtin_len_function(concrete_callable) { return Ok(None); } - // Exact canonical list / str / tuple, or one of the arity-2 tuple + // Exact canonical list / str / tuple / range, or one of the arity-2 tuple // specialisations. `arg_type_addr` pins the `guard_class` target; // `exact_w_class` is the subclass-`__len__` guard (see the doc comment), // absent for a specialisation because only `makespecialisedtuple2` builds @@ -7103,6 +7106,24 @@ pub(crate) fn try_walker_specialize_builtin_len( BuiltinLenSource::TupleArrayLen, pyre_object::w_tuple_len(list_obj), ) + } else if std::ptr::eq(ob_type, &pyre_object::functional::RANGE_TYPE) { + let exact = + pyre_object::pyobject::get_instantiate(&pyre_object::functional::RANGE_TYPE); + if !std::ptr::eq(w_class, exact) { + return Ok(None); + } + let Some(concrete_len) = pyre_object::functional::w_range_length_i64(list_obj) else { + return Ok(None); + }; + let Ok(concrete_len) = usize::try_from(concrete_len) else { + return Ok(None); + }; + ( + &pyre_object::functional::RANGE_TYPE as *const _ as i64, + Some(exact), + BuiltinLenSource::RangeField, + concrete_len, + ) } else if specialised_pair_kind(ob_type).is_some() { ( ob_type as i64, @@ -7152,6 +7173,38 @@ pub(crate) fn try_walker_specialize_builtin_len( if let Some(exact_w_class) = exact_w_class { walker_guard_exact_w_class(ctx, op.pc, list_op, exact_w_class)?; } + // `functional.py:496-497 W_Range.descr_len` is already a wrapped-field + // read. Reuse that box directly; unlike the scalar length sources below, + // there is nothing to unwrap and box again. A virtual range's cached + // field makes this fold to its existing virtual wrapped-int value. + if matches!(len_source, BuiltinLenSource::RangeField) { + let boxed = crate::state::opimpl_getfield_gc_r( + ctx.trace_ctx, + list_op, + crate::descr::range_length_descr(), + ); + // Admission read the field and required it to fit a machine word + // (`w_range_length_i64`), so the trace has to pin that too: the class + // guards above prove the receiver is a range, not what its length slot + // holds. `descr_new` stores a `W_LongObject` there whenever + // `compute_range_length` leaves the machine range, and without this + // guard a later entry carrying such a range would take the recorded + // exit and hand `len()` the long straight through. + walker_guard_class( + ctx, + op.pc, + boxed, + &pyre_object::pyobject::INT_TYPE as *const _ as i64, + )?; + walker_guard_exact_w_class( + ctx, + op.pc, + boxed, + pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::INT_TYPE), + )?; + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + return Ok(Some(())); + } // Length read. list: guard the storage strategy, then read that // strategy's length field (rlist.py inline field for object storage; // typed items-block length for int/float storage). str: a plain @@ -7209,6 +7262,7 @@ pub(crate) fn try_walker_specialize_builtin_len( list_op, crate::descr::str_len_descr(), ), + BuiltinLenSource::RangeField => unreachable!("range returned its wrapped length above"), // `specialisedtupleobject.py:54-55 length()` returns the constant // `typelen`; there is no field to read, so the class guard above is // the whole proof and the box below folds to a constant. @@ -7341,8 +7395,143 @@ pub(crate) fn try_walker_specialize_builtin_type_getattr( Ok(Some(())) } +/// Record an overflow-checked machine-int operation and guard it. +/// +/// [`record_int_ovf`] folds a both-constant operand pair to a constant without +/// recording anything, and `GuardNoOverflow` carries no operands — it reads the +/// flag of the operation immediately before it — so an unconditional guard +/// after a folded pair would attach to whatever was recorded last instead. +/// `None` means the operation cannot be represented and the caller must rewind. +fn record_int_ovf_guarded( + ctx: &mut WalkContext<'_, '_, Sym>, + op_pc: usize, + opcode: OpCode, + b1: OpRef, + b2: OpRef, +) -> Result, DispatchError> { + let (result, overflow) = record_int_ovf(ctx, op_pc, opcode, b1, b2)?; + if overflow { + return Ok(None); + } + if !result.is_constant() { + walker_emit_guard_with_snapshot(ctx, op_pc, OpCode::GuardNoOverflow, &[])?; + } + Ok(Some(result)) +} + +/// Fall back to the opaque `range` residual from a decline point that the +/// specializer only reaches after it has already emitted. +/// +/// Every decline in `try_walker_specialize_builtin_range` past `pre_emit_pos` +/// has to rewind: the callable `GuardValue`, the per-bound class guards and +/// `intval` reads, and — for a bound converted by a user `__index__` — that +/// callee's whole inlined body sit in the trace, and the residual the caller +/// falls through to recomputes all of it. Leaving them behind would pair the +/// residual with guards for a specialization that no longer exists. +fn walker_range_decline( + ctx: &mut WalkContext<'_, '_, Sym>, + pre_emit_pos: majit_metainterp::recorder::TracePosition, +) -> Result, DispatchError> { + ctx.trace_ctx.cut_trace_with_snapshots(pre_emit_pos); + ctx.trace_ctx.heap_cache_mut().reset(); + Ok(None) +} + +/// Emit the machine-int trace of `functional.py:42-53 compute_range_length` +/// for a path whose converted bounds all fit signed machine words. Each +/// source conditional becomes the guard chosen by the recording values; the +/// overflow guards side-exit to the interpreter's wrapped-int implementation. +fn walker_emit_range_length( + ctx: &mut WalkContext<'_, '_, Sym>, + op_pc: usize, + start: OpRef, + stop: OpRef, + step: OpRef, + concrete_start: i64, + concrete_stop: i64, + concrete_step: i64, +) -> Result, DispatchError> { + if concrete_step == 0 { + return Ok(None); + } + let (normalized_start, normalized_stop, normalized_step) = if concrete_step < 0 { + let Some(step) = concrete_step.checked_neg() else { + return Ok(None); + }; + (concrete_stop, concrete_start, step) + } else { + (concrete_start, concrete_stop, concrete_step) + }; + let concrete_length = if normalized_start < normalized_stop { + let Some(diff) = normalized_stop + .checked_sub(normalized_start) + .and_then(|diff| diff.checked_sub(1)) + else { + return Ok(None); + }; + let Some(length) = (diff / normalized_step).checked_add(1) else { + return Ok(None); + }; + length + } else { + 0 + }; + + let zero = ctx.trace_ctx.const_int(0); + let one = ctx.trace_ctx.const_int(1); + let step_has_recorded_sign = if concrete_step < 0 { + ctx.trace_ctx.record_op(OpCode::IntLt, &[step, zero]) + } else { + ctx.trace_ctx.record_op(OpCode::IntGt, &[step, zero]) + }; + ctx.trace_ctx + .set_opref_concrete(step_has_recorded_sign, majit_ir::Value::Int(1)); + walker_emit_guard_with_snapshot(ctx, op_pc, OpCode::GuardTrue, &[step_has_recorded_sign])?; + let (lo, hi, positive_step) = if concrete_step < 0 { + let Some(negated) = record_int_ovf_guarded(ctx, op_pc, OpCode::IntSubOvf, zero, step)? + else { + return Ok(None); + }; + (stop, start, negated) + } else { + (start, stop, step) + }; + + let nonempty = ctx.trace_ctx.record_op(OpCode::IntLt, &[lo, hi]); + ctx.trace_ctx.set_opref_concrete( + nonempty, + majit_ir::Value::Int((normalized_start < normalized_stop) as i64), + ); + if normalized_start >= normalized_stop { + walker_emit_guard_with_snapshot(ctx, op_pc, OpCode::GuardFalse, &[nonempty])?; + return Ok(Some(zero)); + } + walker_emit_guard_with_snapshot(ctx, op_pc, OpCode::GuardTrue, &[nonempty])?; + + let Some(span) = record_int_ovf_guarded(ctx, op_pc, OpCode::IntSubOvf, hi, lo)? else { + return Ok(None); + }; + let Some(diff) = record_int_ovf_guarded(ctx, op_pc, OpCode::IntSubOvf, span, one)? else { + return Ok(None); + }; + let quotient = ctx + .trace_ctx + .record_op(OpCode::IntFloorDiv, &[diff, positive_step]); + ctx.trace_ctx.set_opref_concrete( + quotient, + majit_ir::Value::Int((normalized_stop - normalized_start - 1) / normalized_step), + ); + let Some(length) = record_int_ovf_guarded(ctx, op_pc, OpCode::IntAddOvf, quotient, one)? else { + return Ok(None); + }; + ctx.trace_ctx + .set_opref_concrete(length, majit_ir::Value::Int(concrete_length)); + Ok(Some(length)) +} + /// `range(stop)` / `range(start, stop)` / `range(start, stop, step)` with -/// exact canonical machine-word ints: lower the opaque constructor residual +/// exact canonical machine-word ints or strict inlinable user `__index__` +/// conversions: lower the opaque constructor residual /// to a virtual `W_Range` and four virtual wrapped-int fields. This lets the /// existing GET_ITER specialization consume the range without forcing either /// allocation. All other callables and argument shapes fall through to the @@ -7351,7 +7540,9 @@ pub(crate) fn try_walker_specialize_builtin_range( ctx: &mut WalkContext<'_, '_, Sym>, code: &[u8], op: &DecodedOp, + funcptr: OpRef, r_args: &[OpRef], + call_descr: &dyn majit_ir::descr::CallDescr, dst: usize, ) -> Result, DispatchError> { if !(3..=5).contains(&r_args.len()) { @@ -7372,66 +7563,112 @@ pub(crate) fn try_walker_specialize_builtin_range( } let exact_int_class = pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::INT_TYPE); - let mut concrete_args = Vec::with_capacity(r_args.len() - 2); - let mut concrete_values = Vec::with_capacity(r_args.len() - 2); - for concrete in &arg_concretes[2..] { + enum BoundPlan { + Exact { + op: OpRef, + concrete: pyre_object::PyObjectRef, + }, + UserIndex(IndexInlineCandidate), + } + let mut plans = Vec::with_capacity(r_args.len() - 2); + let mut has_user_index = false; + for (&arg_op, concrete) in r_args[2..].iter().zip(&arg_concretes[2..]) { let ConcreteValue::Ref(arg_obj) = *concrete else { return Ok(None); }; - if arg_obj.is_null() - || unsafe { - !std::ptr::eq((*arg_obj).ob_type, &pyre_object::pyobject::INT_TYPE) - || !std::ptr::eq((*arg_obj).w_class, exact_int_class) - } - { + if walker_is_exact_machine_int_concrete(arg_obj) { + plans.push(BoundPlan::Exact { + op: arg_op, + concrete: arg_obj, + }); + } else if let Some(candidate) = prepare_walker_inline_index(ctx, arg_op, arg_obj) { + has_user_index = true; + plans.push(BoundPlan::UserIndex(candidate)); + } else { return Ok(None); } + } + + // Every non-int bound has been resolved and statically preflighted before + // this first emission. `functional.py:461-474 W_Range.descr_new` applies + // `space.index` independently to start/stop/step; mirror that order and + // retain each returned box as an intermediate feeding the constructor. + let pre_emit_pos = ctx.trace_ctx.get_trace_position(); + let callable_op = r_args[0]; + if !callable_op.is_constant() { + let expected = ctx.trace_ctx.const_ref(concrete_callable as i64); + ctx.trace_ctx + .record_guard(OpCode::GuardValue, &[callable_op, expected], 0); + walker_capture_snapshot_for_last_guard(ctx, op.pc)?; + ctx.trace_ctx + .heap_cache_mut() + .replace_box(callable_op, expected); + } + + let int_type_addr = &pyre_object::pyobject::INT_TYPE as *const _ as i64; + let mut concrete_args = Vec::with_capacity(plans.len()); + let mut concrete_values = Vec::with_capacity(plans.len()); + let mut raw_args = Vec::with_capacity(plans.len()); + for plan in plans { + let (arg_op, arg_obj) = match plan { + BoundPlan::Exact { op, concrete } => (op, concrete), + BoundPlan::UserIndex(candidate) => { + let Some((result, ConcreteValue::Ref(concrete))) = try_walker_inline_index( + ctx, op, code, funcptr, r_args, call_descr, dst, candidate, + )? + else { + return walker_range_decline(ctx, pre_emit_pos); + }; + (result, concrete) + } + }; + // A trace-constant bound carries its class in the constant itself, so + // record the class as known without proving it: `walker_guard_class` + // would emit a `GuardClass` that can never fail plus the tagged-operand + // low-bit test that guards a later entry's untagged arrival, and a + // constant has no later arrival. A bound returned by an inlined + // `__index__` is live and takes the full guard. + if arg_op.is_constant() { + ctx.trace_ctx + .heap_cache_mut() + .class_now_known(arg_op, int_type_addr); + } else { + walker_guard_class(ctx, op.pc, arg_op, int_type_addr)?; + } + walker_guard_exact_w_class(ctx, op.pc, arg_op, exact_int_class)?; + let concrete_value = unsafe { pyre_object::w_int_get_value(arg_obj) }; + let raw = crate::state::opimpl_getfield_gc_i( + ctx.trace_ctx, + arg_op, + crate::descr::int_intval_descr(), + ); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Int(concrete_value)); concrete_args.push(arg_obj); - concrete_values.push(unsafe { pyre_object::w_int_get_value(arg_obj) }); + concrete_values.push(concrete_value); + raw_args.push(raw); } - // Produce the authentic result before emitting IR, keeping every decline - // point side-effect-free with respect to the trace under construction. + + // Run only the remaining builtin range body on the converted exact ints; + // executing the original arguments here would call user `__index__` a + // second time during recording. let authentic_result = { let _plain_guard = pyre_interpreter::call::force_plain_eval(); pyre_interpreter::call::call_function_impl_result(concrete_callable, &concrete_args) }; if concrete_values.len() == 3 && concrete_values[2] == 0 { let Err(mut err) = authentic_result else { - return Ok(None); + return walker_range_decline(ctx, pre_emit_pos); }; let exc = err.to_exc_object(); let kind = pyre_object::interp_exceptions::ExcKind::ValueError; if !walker_recorded_builtin_raise_is_supported(exc, kind) { - return Ok(None); + return walker_range_decline(ctx, pre_emit_pos); } let Some(ec) = walker_ensure_execution_context(ctx) else { - return Ok(None); + return walker_range_decline(ctx, pre_emit_pos); }; - let callable_op = r_args[0]; - if !callable_op.is_constant() { - let expected = ctx.trace_ctx.const_ref(concrete_callable as i64); - ctx.trace_ctx - .record_guard(OpCode::GuardValue, &[callable_op, expected], 0); - walker_capture_snapshot_for_last_guard(ctx, op.pc)?; - ctx.trace_ctx - .heap_cache_mut() - .replace_box(callable_op, expected); - } - let int_type_addr = &pyre_object::pyobject::INT_TYPE as *const _ as i64; - let mut raw_args = Vec::with_capacity(concrete_values.len()); - for (&arg_op, &concrete_value) in r_args[2..].iter().zip(&concrete_values) { - walker_guard_class(ctx, op.pc, arg_op, int_type_addr)?; - walker_guard_exact_w_class(ctx, op.pc, arg_op, exact_int_class)?; - let raw = crate::state::opimpl_getfield_gc_i( - ctx.trace_ctx, - arg_op, - crate::descr::int_intval_descr(), - ); - ctx.trace_ctx - .set_opref_concrete(raw, majit_ir::Value::Int(concrete_value)); - raw_args.push(raw); - } let step_raw = raw_args[2]; let zero = ctx.trace_ctx.const_int(0); let is_zero = ctx.trace_ctx.record_op(OpCode::IntEq, &[step_raw, zero]); @@ -7441,7 +7678,7 @@ pub(crate) fn try_walker_specialize_builtin_range( return Ok(Some(walker_emit_recorded_builtin_raise(ctx, ec, exc, kind))); } let Ok(authentic_range) = authentic_result else { - return Ok(None); + return walker_range_decline(ctx, pre_emit_pos); }; let (authentic_start, authentic_stop, authentic_step) = unsafe { pyre_object::functional::w_range_fields(authentic_range) }; @@ -7456,7 +7693,7 @@ pub(crate) fn try_walker_specialize_builtin_range( !std::ptr::eq((*field).ob_type, &pyre_object::pyobject::INT_TYPE) || !std::ptr::eq((*field).w_class, exact_int_class) }) { - return Ok(None); + return walker_range_decline(ctx, pre_emit_pos); } let concrete_fields = authentic_fields.map(|field| unsafe { pyre_object::w_int_get_value(field) }); @@ -7467,45 +7704,6 @@ pub(crate) fn try_walker_specialize_builtin_range( concrete_length, ] = concrete_fields; - // The bound test below reads the unboxed `intval`, and that read is only - // safe behind the class guards emitted here, so the decline cannot be - // hoisted ahead of the emission — it rewinds instead. - let pre_emit_pos = ctx.trace_ctx.get_trace_position(); - - let callable_op = r_args[0]; - if !callable_op.is_constant() { - let expected = ctx.trace_ctx.const_ref(concrete_callable as i64); - ctx.trace_ctx - .record_guard(OpCode::GuardValue, &[callable_op, expected], 0); - walker_capture_snapshot_for_last_guard(ctx, op.pc)?; - ctx.trace_ctx - .heap_cache_mut() - .replace_box(callable_op, expected); - } - - let int_type_addr = &pyre_object::pyobject::INT_TYPE as *const _ as i64; - let int_type_const = ctx.trace_ctx.const_int(int_type_addr); - let mut raw_args = Vec::with_capacity(concrete_values.len()); - for (&arg_op, &concrete_value) in r_args[2..].iter().zip(&concrete_values) { - if !arg_op.is_constant() && !ctx.trace_ctx.heap_cache().is_class_known(arg_op) { - ctx.trace_ctx - .record_guard(OpCode::GuardClass, &[arg_op, int_type_const], 0); - walker_capture_snapshot_for_last_guard(ctx, op.pc)?; - } - ctx.trace_ctx - .heap_cache_mut() - .class_now_known(arg_op, int_type_addr); - walker_guard_exact_w_class(ctx, op.pc, arg_op, exact_int_class)?; - let raw = crate::state::opimpl_getfield_gc_i( - ctx.trace_ctx, - arg_op, - crate::descr::int_intval_descr(), - ); - ctx.trace_ctx - .set_opref_concrete(raw, majit_ir::Value::Int(concrete_value)); - raw_args.push(raw); - } - let zero = ctx.trace_ctx.const_int(0); let one = ctx.trace_ctx.const_int(1); let (start, stop, step) = match raw_args.as_slice() { @@ -7514,22 +7712,32 @@ pub(crate) fn try_walker_specialize_builtin_range( [start, stop, step] => (*start, *stop, *step), _ => unreachable!("range arity gate admitted an invalid argument count"), }; - // Trace-constant bounds only. This is what makes `length` sound as a - // record-time constant: a bound that varies per iteration would pair a - // stale length with fresh start/stop/step. The alternative — emitting - // `compute_range_length` — costs a division chain plus its overflow - // guards, and it regressed two shapes the gate pins: a bound that - // alternates empty and non-empty needs the emptiness folded in - // branchlessly (a guard side-exits every other call), and the resulting op - // run aborts the wasm trace when the `range` sits inside a self-recursive - // callee that is inlined per level. A variable bound keeps the residual - // until that is worked out. - if !start.is_constant() || !stop.is_constant() || !step.is_constant() { - ctx.trace_ctx.cut_trace_with_snapshots(pre_emit_pos); - ctx.trace_ctx.heap_cache_mut().reset(); - return Ok(None); - } - let length = ctx.trace_ctx.const_int(concrete_length); + // Trace-constant bounds retain the existing zero-op length. A bound + // produced by the user `_index` call follows PyPy's traced + // `compute_range_length` body above, so its live value feeds all four + // virtual fields instead of being paired with a stale record-time length. + // Other variable-bound sources keep the residual, preserving the existing + // admission boundary for recursive and alternating-range shapes. + let length = if start.is_constant() && stop.is_constant() && step.is_constant() { + ctx.trace_ctx.const_int(concrete_length) + } else if has_user_index { + let Some(length) = walker_emit_range_length( + ctx, + op.pc, + start, + stop, + step, + concrete_start, + concrete_stop, + concrete_step, + )? + else { + return walker_range_decline(ctx, pre_emit_pos); + }; + length + } else { + return walker_range_decline(ctx, pre_emit_pos); + }; let new = ctx.trace_ctx.record_op_with_descr( OpCode::NewWithVtable, diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs index 0d6a5a3d8ab..7d0e72319a1 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/vstack_mirror.rs @@ -222,7 +222,9 @@ pub(crate) fn classify_vstack_opcode( // LOAD_SPECIAL pops the context-manager object at `prev_depth - 1` // and pushes the special method followed by the call self/NULL slot // upward from that position. Clear that popped slot and the pushed - // range so each pushed slot is sourced from the virtualizable shadow. + // range so each pushed slot is sourced from the virtualizable shadow, + // except the trailing `self_or_null` the reconcile stamps as an + // explicit NULL constant. Instruction::LoadSpecial { method } if matches!( method.get(op_arg), @@ -230,7 +232,7 @@ pub(crate) fn classify_vstack_opcode( | pyre_interpreter::bytecode::SpecialMethod::Exit ) => { - VstackOpClass::MultiResultFromShadow + VstackOpClass::LoadSpecialMethod } // SWAP(i): exchange TOS with the box `i` positions below. A pure @@ -682,13 +684,29 @@ pub(crate) fn reconcile_vstack_at_boundary( ctx.vstack_boxes.clear(); ctx.vstack_boxes.resize(new_depth, OpRef::NONE); } - VstackOpClass::MultiResultFromShadow => { + VstackOpClass::MultiResultFromShadow | VstackOpClass::LoadSpecialMethod => { // UNPACK_* pops ONE sequence (at `prev_depth - 1`) and pushes its // elements upward. Clear only the affected range // `[pop_point .. new_depth)` to NONE so the hole-fill below // sources each pushed element from the shadow (all were written // through `setarrayitem_vable_r`); slots BELOW the popped sequence // keep their mirror-tracked boxes. + // + // LOAD_SPECIAL has that same shape but its second push is the + // call's `self_or_null` slot, a live NULL for the bound method it + // just resolved. The shadow cannot give that slot back: + // `reseed_vstack_from_shadow` reads a dense array in which an + // absent slot and a written NULL are the same word, so it rejects + // NULL and the slot stays a NONE hole `stack_sync` omits. A + // blackhole resuming into the `WITH_EXCEPT_START` that reads it + // then sees whatever the slot held before — the bound `__exit__` + // this same opcode pushed one slot below — and calls it with the + // receiver twice. Stamp the constant, as the method-form + // LOAD_GLOBAL arm above does for the other callable/NULL pair. + let trailing_null = match effective_class { + VstackOpClass::LoadSpecialMethod => Some(ctx.trace_ctx.const_null()), + _ => None, + }; let pop_point = ctx.vstack_depth.saturating_sub(1); ctx.vstack_boxes.truncate(new_depth); if ctx.vstack_boxes.len() < new_depth { @@ -697,6 +715,11 @@ pub(crate) fn reconcile_vstack_at_boundary( for s in pop_point..new_depth { ctx.vstack_boxes[s] = OpRef::NONE; } + if new_depth > 0 { + if let Some(null) = trailing_null { + ctx.vstack_boxes[new_depth - 1] = null; + } + } } VstackOpClass::Unmodeled => { ctx.vstack_valid = false; diff --git a/pyre/pyre-jit-trace/src/pyjitcode.rs b/pyre/pyre-jit-trace/src/pyjitcode.rs index 742530cfe16..f3e824434a3 100644 --- a/pyre/pyre-jit-trace/src/pyjitcode.rs +++ b/pyre/pyre-jit-trace/src/pyjitcode.rs @@ -309,6 +309,8 @@ pub(crate) struct InlineBodyFacts { pub(crate) exc_override_straight_line: bool, /// Effect-free enough to speculatively sample before emitting IR. pub(crate) exc_override_sample_safe: bool, + /// The same, admitting the instance-attribute read an `__index__` performs. + pub(crate) index_sample_safe: bool, /// Body performs a nested Python call. pub(crate) exc_override_has_nested_call: bool, /// Body carries a loop header of its own (a `jit_merge_point`). diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index 1db72e7ed32..c2df1642f51 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -6967,6 +6967,13 @@ fn for_iter_body_op_is_jit_safe(instr: pyre_interpreter::Instruction) -> bool { // FOR_ITER item is in-flight (try_walker_inline_user_call). | I::Call { .. } | I::CallKw { .. } + // CALL_FUNCTION_EX is the same MayForce call boundary as CALL and + // CALL_KW. The codewriter lowers it through + // PyreHelperKind::CallFunctionEx, and + // fbw_callee_body_replay_safety defers CallFn, CallKw, and + // CallFunctionEx together; omitting only the starred-call spelling + // here added no safety beyond the Layer 2 defense above. + | I::CallFunctionEx | I::LoadGlobal { .. } | I::Resume { .. } // container builders: produce new heap objects but do not mutate @@ -6974,6 +6981,17 @@ fn for_iter_body_op_is_jit_safe(instr: pyre_interpreter::Instruction) -> bool { | I::BuildList { .. } | I::BuildSet { .. } | I::BuildMap { .. } + // MAKE_FUNCTION is another fresh-object builder: codewriter.rs + // lowers PyreHelperKind::MakeFunction as a Plain allocation that + // runs no user code and cannot raise. SET_FUNCTION_ATTRIBUTE only + // initializes that newly-built function's typed fields, likewise + // without user code or an exception, so replay drops or rebuilds + // the incomplete function rather than mutating a pre-existing + // object. MakeCell belongs to the containing frame's prologue and + // CopyFreeVars to the nested function's prologue, not to this + // definition sequence. + | I::MakeFunction { .. } + | I::SetFunctionAttribute { .. } // In-frame exception raise / handling. These opcodes already trace // in the while-loop form (whose body bypasses this FOR_ITER-only // scan): the raised exception is virtualized and a walk abort @@ -6984,6 +7002,18 @@ fn for_iter_body_op_is_jit_safe(instr: pyre_interpreter::Instruction) -> bool { | I::CheckExcMatch | I::PopExcept | I::Reraise { .. } + // `LOAD_SPECIAL` and `WITH_EXCEPT_START` are NOT admitted. The + // opcodes themselves are safe — neither runs user code, and the + // calls they set up are ordinary `Call` boundaries admitted above — + // but admitting them puts `with` inside `for` in the JIT, and a + // `@contextmanager` generator used that way loses a `GeneratorExit` + // to the caller: `test.test_pow` `test_negative_exponent` (nested + // `for` around `with self.subTest(...)`) errors with a bare + // `GeneratorExit`, at a coordinate that moves between runs and is + // clean under `PYRE_JIT=0`. `bench/synth/foriter_load_special_with` + // records the shape's answer while the frame stays interpreted; + // `bench/synth/exception_with_exit_self_null_slot` is the `while` + // form, which never depended on this gate. // oparg prefix + inline-cache padding (no-ops in the body scan) | I::ExtendedArg | I::Cache @@ -7297,22 +7327,39 @@ fn for_iter_body_is_jit_safe_at(code: &pyre_interpreter::CodeObject, pc: usize) body_state = pyre_interpreter::OpArgState::default(); continue; } + // CALL_INTRINSIC_1 names several unrelated operations. UnaryPositive + // and ListToTuple are the two variants codewriter.rs actually lowers: + // the first follows the same implicit-dunder exact-resume path as + // UnaryNegative, while the second returns a fresh tuple. Keep counting + // the opcode in body_has_call so a unary-positive user frame still + // taints LIST_APPEND, but do not admit the def-time/import/error-path + // variants whose lowering deliberately aborts permanently. + let supported_call_intrinsic_1 = matches!( + body_instr, + I::CallIntrinsic1 { func } + if matches!( + func.get(body_arg), + pyre_interpreter::bytecode::IntrinsicFunction1::UnaryPositive + | pyre_interpreter::bytecode::IntrinsicFunction1::ListToTuple + ) + ); let permitted = for_iter_body_op_is_jit_safe(body_instr) + || supported_call_intrinsic_1 || matches!( body_instr, I::StoreSubscr - | I::StoreAttr { .. } - | I::StoreName { .. } - | I::StoreGlobal { .. } - | I::StoreDeref { .. } - | I::DeleteSubscr - | I::DeleteAttr { .. } - | I::LoadName { .. } - // `call_method(set, 'add', v)` / `setitem(d, k, v)` - // spelled as one opcode; void residuals, not folds. - | I::ListExtend { .. } - | I::SetAdd { .. } - | I::MapAdd { .. } + | I::StoreAttr { .. } + | I::StoreName { .. } + | I::StoreGlobal { .. } + | I::StoreDeref { .. } + | I::DeleteSubscr + | I::DeleteAttr { .. } + | I::LoadName { .. } + // `call_method(set, 'add', v)` / `setitem(d, k, v)` + // spelled as one opcode; void residuals, not folds. + | I::ListExtend { .. } + | I::SetAdd { .. } + | I::MapAdd { .. } ) || (!body_has_call && matches!(body_instr, I::ListAppend { .. })); if !permitted {