diff --git a/majit/majit-rlib/src/rbigint.rs b/majit/majit-rlib/src/rbigint.rs index 7d13f6a1311..03ac7eed130 100644 --- a/majit/majit-rlib/src/rbigint.rs +++ b/majit/majit-rlib/src/rbigint.rs @@ -5468,7 +5468,7 @@ fn parse_string_from_binary_base( /// rbigint.py `gcd_binary`. #[majit_macros::jit_elidable] -fn gcd_binary(mut a: i64, mut b: i64) -> i64 { +pub fn gcd_binary(mut a: i64, mut b: i64) -> i64 { debug_assert!(a >= 0 && b >= 0); if a == 0 { return b; diff --git a/pyre/bench/synth/math_log_trig_hot.cranelift.jitstats b/pyre/bench/synth/builtin_folds_hot.cranelift.jitstats similarity index 85% rename from pyre/bench/synth/math_log_trig_hot.cranelift.jitstats rename to pyre/bench/synth/builtin_folds_hot.cranelift.jitstats index 59f22855e15..68b5ccdf447 100644 --- a/pyre/bench/synth/math_log_trig_hot.cranelift.jitstats +++ b/pyre/bench/synth/builtin_folds_hot.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=1 +guard_failures=6 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/math_log_trig_hot.dynasm.jitstats b/pyre/bench/synth/builtin_folds_hot.dynasm.jitstats similarity index 85% rename from pyre/bench/synth/math_log_trig_hot.dynasm.jitstats rename to pyre/bench/synth/builtin_folds_hot.dynasm.jitstats index 59f22855e15..68b5ccdf447 100644 --- a/pyre/bench/synth/math_log_trig_hot.dynasm.jitstats +++ b/pyre/bench/synth/builtin_folds_hot.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=1 +guard_failures=6 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/builtin_folds_hot.py b/pyre/bench/synth/builtin_folds_hot.py new file mode 100644 index 00000000000..44f72e60f63 --- /dev/null +++ b/pyre/bench/synth/builtin_folds_hot.py @@ -0,0 +1,133 @@ +# pyre-check: max-pypy-ratio=12 +# pyre-check: skip-cpython +# Fitted between the two arms. With every fold in place the three runners read +# 4.6x / 4.7x on darwin-arm64, 7.2x / 7.6x on ubuntu-24.04 and 9.2x / 10.0x on +# windows, at half the loop counts below. The windows pair was marked `?` +# there: pypy's execution-only time sat under FLOOR_GATE_MIN_BASELINE_S, which +# is 0.15625s on a host whose CPU accounting advances in 1/64s ticks, and the +# pair cleared the ceiling of 8 it carried then only because `_compare_buffer` +# grants two ticks per unit of limit. Doubling the work carries that baseline +# over the bar, so the number is judged rather than excused. With +# PYRE_FBW_NO_SPECIALIZE=builtin_fold1,builtin_fold2 putting the same loops back +# on the residual, darwin-arm64 reads 52.9x and 61.4x -- eleven times the folded +# arm on that host. The gate clears the highest folded reading by 20% and still +# sits an order of magnitude under the residual arm scaled to it. +# +# pyre-check: max-wasm-ratio=13 +# pyre-check: spec-folds=builtin_fold1,builtin_fold2 +# The wasm ceiling is fitted to the highest reading observed plus 15%. Two +# ubuntu-24.04 runs of the same code read 8.2x and 11.3x, because the +# denominator is small enough for startup subtraction to move it: dynasm's +# execution-only time came out 0.69s and then 0.44s, and the second run's own +# failure line said a dynasm startup estimate 0.68x larger would have erased +# the gap. The loop counts below are twice what they were for that reason -- +# the subtraction error is a fixed number of milliseconds, so doubling the work +# halves its share. Every recorded jit-stats counter is unchanged by the +# doubling. +# +# The structure is the host crossing. A JIT-emitted trace is its own wasm +# module, so a call leaving it reaches the interpreter through the +# `env.jit_call` trampoline, which marshals func_ptr and arguments through the +# frame call area in shared linear memory and dispatches through the main +# module's indirect function table. What the fold removes is the frame force, +# the argument rooting, the execution-context resolution and the gateway +# binding; the crossing itself stays, because every fold here still lowers to +# a call into a raw helper. The tree carries its own contrast: on the same +# ubuntu run `math_folds_hot`, whose folds lower to inline arithmetic instead, +# reads 3.3x. Every loop below is nothing but folded builtin calls, so the +# crossing is the whole measurement. The alternative to this allowance is to +# give the trace module direct imports for the raw helpers rather than one +# generic trampoline. +# +# `spec-folds` is the exact instrument neither ratio is. Six loops sum into +# one number, so retiring one channel moves it by less than the span this +# fixture reads across the runners; the census gates each fold's coverage +# instead, and it reads the same on every host. +# +# Every builtin the generic walker fold covers, one hot loop per channel. +# +# Without a fold, `hash(x)` / `ord(c)` / `abs(x)` / `min(a, b)` each reach the +# interpreter as `bh_call_fn(builtin, NULL, ...)`, and that residual costs the +# same for all of them: the frame force, the argument rooting, the execution +# context resolution and the gateway signature binding all run before the body +# does. The fold emits a direct call into the builtin's raw helper, a guard on +# that channel's decline sentinel, and an inline `wrapint` / `wrapfloat` the +# optimizer can keep virtual. +# +# The ratio is the detector here: losing a fold changes no jit-stats counter, +# because the residual it falls back to compiles the same loop. +# +# hash_int/hash_str the `Int1` channel — an `i64` result and the +# `INT_FOLD_DECLINE` guard. +# ord_str the same channel on an operand whose acceptance is a +# length, not a type. +# abs_int/abs_float one builtin holding two rows, one per result channel. +# min_max the `Ref2` channel — the helper returns one of its own +# arguments, so nothing is allocated at all. +HASH_N = 32000000 +ORD_N = 32000000 +ABS_N = 32000000 +MINMAX_N = 24000000 + + +def run_hash_int(): + # `hash(int)` is the value itself, so this total is the same everywhere. + total = 0 + x = 1234567 + for _ in range(HASH_N): + total += hash(x) + return total + + +def run_hash_str(): + # A string's hash is seeded per process, so the digest itself cannot be + # printed. Count the iterations that agree with the first one instead: + # the fold still has to produce the digest, and the count is invariant. + s = "specialize" + first = hash(s) + same = 0 + for _ in range(HASH_N): + if hash(s) == first: + same += 1 + return same + + +def run_ord(): + total = 0 + c = "q" + for _ in range(ORD_N): + total += ord(c) + return total + + +def run_abs_int(): + total = 0 + x = -7 + for _ in range(ABS_N): + total += abs(x) + return total + + +def run_abs_float(): + total = 0.0 + x = -7.5 + for _ in range(ABS_N): + total += abs(x) + return total + + +def run_min_max(): + total = 0 + a = 3 + b = 9 + for _ in range(MINMAX_N): + total += min(a, b) + max(a, b) + return total + + +print(run_hash_int()) +print(run_hash_str()) +print(run_ord()) +print(run_abs_int()) +print(round(run_abs_float(), 6)) +print(run_min_max()) diff --git a/pyre/bench/synth/math_log_trig_hot.wasm.jitstats b/pyre/bench/synth/builtin_folds_hot.wasm.jitstats similarity index 85% rename from pyre/bench/synth/math_log_trig_hot.wasm.jitstats rename to pyre/bench/synth/builtin_folds_hot.wasm.jitstats index 59f22855e15..68b5ccdf447 100644 --- a/pyre/bench/synth/math_log_trig_hot.wasm.jitstats +++ b/pyre/bench/synth/builtin_folds_hot.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=1 +guard_failures=6 internal_compile_panics=0 loops_aborted=0 -loops_compiled=1 +loops_compiled=6 +retraces_compiled=0 diff --git a/pyre/bench/synth/inline_freevar_after_mayforce.py b/pyre/bench/synth/inline_freevar_after_mayforce.py index 0e734281486..6012466e409 100644 --- a/pyre/bench/synth/inline_freevar_after_mayforce.py +++ b/pyre/bench/synth/inline_freevar_after_mayforce.py @@ -1,11 +1,20 @@ # pyre-check: max-pypy-ratio=86 -# pyre-check: jitstats-band=guard_failures=1 -# One tree, three runners, one CI run (`1d212895c6b`): macOS and ubuntu read -# 1003 dynasm / 1008 cranelift, windows 1004 / 1009. The loop and bridge counts -# agreed at six and five everywhere, so the split is carried entirely by this -# counter and is not a function of the tree. The baseline holds the pair the two -# agreeing runners read; the band is exactly the measured width, so anything -# wider than the split still gates. +# pyre-check: jitstats-band=guard_failures=8 +# `guard_failures` here is carried by two things this fixture is not about. +# One is the host: one tree read 1003 dynasm / 1008 cranelift on macOS and +# ubuntu and 1004 / 1009 on windows in a single CI run (`1d212895c6b`), with the +# loop and bridge counts agreeing everywhere. The other is the collection +# schedule -- one binary swept across nursery sizes read 1034 / 1014 / 1007 / +# 1007 at 2 / 4 / 6 / 8 MB, 27 counts, while `loops_compiled` and +# `bridges_compiled` did not move. Suppressing the whole trace-time fold table +# moved it by one count and suppressing the folds this branch adds by none, so +# it is not reading those either. +# +# Width 8 covers both: the one-count host split, and the several counts a tree +# that allocates differently picks up on top of it -- this branch read 1011 on +# all three runners against a baseline of 1008. Anything wider than a tree's own +# allocation behaviour still gates, and `loops_compiled` and `bridges_compiled` +# stay gated exactly. Only the loop count answers whether the arm compiles. # The ceiling is a function of N, so raising N refits it. pypy's execution here # is almost all fixed cost -- doubling N moved it 0.035s to 0.039s -- while this # backend pays roughly 27us per iteration, so the ratio tracks N nearly one for diff --git a/pyre/bench/synth/math_folds_hot.cranelift.jitstats b/pyre/bench/synth/math_folds_hot.cranelift.jitstats new file mode 100644 index 00000000000..093bd345ac5 --- /dev/null +++ b/pyre/bench/synth/math_folds_hot.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=7 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=7 +retraces_compiled=0 diff --git a/pyre/bench/synth/math_folds_hot.dynasm.jitstats b/pyre/bench/synth/math_folds_hot.dynasm.jitstats new file mode 100644 index 00000000000..093bd345ac5 --- /dev/null +++ b/pyre/bench/synth/math_folds_hot.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=7 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=7 +retraces_compiled=0 diff --git a/pyre/bench/synth/math_folds_hot.py b/pyre/bench/synth/math_folds_hot.py new file mode 100644 index 00000000000..5d7052baccf --- /dev/null +++ b/pyre/bench/synth/math_folds_hot.py @@ -0,0 +1,128 @@ +# pyre-check: max-pypy-ratio=5 +# pyre-check: skip-cpython +# pyre-check: max-wasm-ratio=13 +# Fitted to the highest reading observed plus 15%: darwin-arm64 reads +# 8.1-9.0x across five runs at load 11 and 11.3x during a load spike. The wasm +# side is slower for a structural reason rather than a regression: `pymath` +# reaches the platform libm on native and its pure-Rust `libm` fallback in the +# guest, and the guest also pays an errno-classifying wrapper the native build +# folds away. Measured on the same fold machinery and the same loop, wasm +# runs 2M folded `log` (which lowers to `x.ln()`) in 0.09s and 2M folded `exp` +# (which goes through `pymath`) in 0.24s. Both backends fold; only the +# operation underneath differs. +# The ratio is the detector here: losing a fold changes no jit-stats counter, +# because the residual it falls back to compiles the same loop. Measured on +# an idle darwin-arm64 box against pypy 0.33s — every fold 0.63s, the generic +# float/isclose folds suppressed 2.71s, all folds suppressed 33.4s. The gate +# sits between the first two. +# Every `math` primitive the walker folds, one hot loop per fold shape. A +# residual `bh_call_fn(builtin, NULL, x)` costs an argument tuple, a +# `W_FloatObject` allocation and a full builtin dispatch per iteration; each +# specialization instead unboxes the operands, emits the raw operation, and +# leaves the result box virtualizable. +# +# sqrt `try_walker_specialize_math_sqrt` — `x >= 0` and `isfinite(x)` +# pin the two `ll_math_sqrt` branches, then a pure +# `CALL_F(sqrt_nonneg_jit)` with no result guard. +# log/cos/sin `try_walker_specialize_math_log_trig` — same shape, one +# domain guard each. +# fabs `try_walker_specialize_math_fabs` — a single `FloatAbs`; +# `fabs` raises for no input, so it carries no domain guard. +# floor/ceil/ `try_walker_specialize_math_round_to_int` — guard the operand +# trunc into the signed machine range, round, `CastFloatToInt`. +# isclose `try_walker_specialize_math_isclose` — one pure `CALL_I` +# into a total helper, whose truth the branch's own guard +# already pins, so it carries no result guard. +# the rest `try_walker_specialize_math_float{1,2}` — one pure elidable +# `CALL_F` into the function's raw helper plus a finite-result +# guard. The helper reports every raising direction as NaN, so +# the guard alone carries the domain: `exp` overflowing, +# `atanh` outside (-1, 1) and `pow(0.0, -2.0)` all resume in +# the builtin and raise there. +# +# A rebound callable, a numeric subclass, or an operand outside the folded +# domain keeps the residual in every case. +import math + +# Sized so pypy's own execution clears the measurement floor: below it the +# ratio gate divides by the floor and declines the baseline as too small. +SQRT_N = 8000000 +LOG_TRIG_N = 1600000 +FABS_N = 10000000 +ROUND_N = 10000000 +UNARY_N = 1500000 +BINARY_N = 2000000 +ISCLOSE_N = 4000000 + + +def run_sqrt(): + total = 0.0 + for i in range(SQRT_N): + total += math.sqrt(float(i)) + return total + + +def run_log_trig(): + total = 0.0 + for i in range(LOG_TRIG_N): + x = 1.0 + float(i % 97) / 97.0 + total += math.log(x) + math.cos(x) + math.sin(x) + return total + + +def run_fabs(): + total = 0.0 + for i in range(FABS_N): + total += math.fabs(float(i) - 6000000.0) + return total + + +def run_round_to_int(): + total = 0 + for i in range(ROUND_N): + x = float(i) * 0.5 - 1000.0 + total += math.floor(x) + math.ceil(x) + math.trunc(x) + return total + + +def run_unary(): + total = 0.0 + for i in range(UNARY_N): + x = float(i % 71) / 128.0 + total += math.exp(x) + math.tan(x) + math.atan(x) + total += math.tanh(x) + math.log1p(x) + math.degrees(x) + return total + + +def run_binary(): + total = 0.0 + for i in range(BINARY_N): + x = 1.0 + float(i % 53) / 53.0 + y = 1.0 + float(i % 31) / 31.0 + total += math.pow(x, y) + math.fmod(x, y) + total += math.copysign(x, y) + math.atan2(x, y) + return total + + +def run_isclose(): + # The result has to decide a branch and nothing else: a bool that escapes + # keeps the residual, because pinning it would bail on every re-entry with + # the other truth. + # The perturbation stays well inside the default `rel_tol=1e-09`, so the + # branch resolves the same way every iteration and the trace measures the + # fold rather than a bridge. + hits = 0 + for i in range(ISCLOSE_N): + x = 1.0 + float(i % 97) / 1e12 + if math.isclose(x, 1.0): + hits += 1 + return hits + + +print(round(run_sqrt(), 6)) +print(round(run_log_trig(), 6)) +print(round(run_fabs(), 6)) +print(run_round_to_int()) +print(round(run_unary(), 6)) +print(round(run_binary(), 6)) +print(run_isclose()) diff --git a/pyre/bench/synth/math_folds_hot.wasm.jitstats b/pyre/bench/synth/math_folds_hot.wasm.jitstats new file mode 100644 index 00000000000..093bd345ac5 --- /dev/null +++ b/pyre/bench/synth/math_folds_hot.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=7 +internal_compile_panics=0 +loops_aborted=0 +loops_compiled=7 +retraces_compiled=0 diff --git a/pyre/bench/synth/math_log_trig_hot.py b/pyre/bench/synth/math_log_trig_hot.py deleted file mode 100644 index 6aff1783fcc..00000000000 --- a/pyre/bench/synth/math_log_trig_hot.py +++ /dev/null @@ -1,21 +0,0 @@ -# pyre-check: max-pypy-ratio=14 -# pyre-check: skip-cpython -# cpython 1.70s vs pyre 0.46s (3.7x on the ubuntu runner), and it is not -# gated on — only pypy is. -# RPython lowers the guarded `ll_math_log/cos/sin` bodies to raw float calls. -# Keep all inputs in their hot domains so the walker emits those CALL_F ops -# and the temporary W_FloatObject results can virtualize. -from math import cos, log, sin - -N = 6600000 - - -def run(): - total = 0.0 - for i in range(N): - x = 1.0 + float(i % 97) / 97.0 - total += log(x) + cos(x) + sin(x) - return total - - -print(round(run(), 6)) diff --git a/pyre/bench/synth/str_getitem_len_hot.py b/pyre/bench/synth/str_getitem_len_hot.py index 51c3ae33acc..5c5b967fb6d 100644 --- a/pyre/bench/synth/str_getitem_len_hot.py +++ b/pyre/bench/synth/str_getitem_len_hot.py @@ -1,5 +1,15 @@ # pyre-check: max-pypy-ratio=19 # pyre-check: spec-folds=builtin_len +# pyre-check: max-wasm-ratio=4.8 +# Fitted to the highest reading observed plus 15%. This fixture sits over the +# global 3.5x wasm ceiling on every branch that measures it, not only here: a +# census of eleven branch runs on 2026-08-22 read 3.6x six times, 3.7x twice, +# 3.8x once and 4.1x once, with the one remaining run not reaching the leg. +# The ceiling is an allowance and not a fix -- the leg is a residual +# STRGETITEM/UNICODEGETITEM loop, so on wasm every iteration crosses out of +# the trace module through `env.jit_call`, and closing the gap means giving +# the trace module a direct import for the item read rather than raising a +# number. # Hot-loop str/unicode subscript and length over every string kind: ASCII / # latin1 (1-byte code units), BMP (2-byte), and non-BMP astral (4-byte). The # subscripts emit residual STRGETITEM/UNICODEGETITEM and the lengths STRLEN/ diff --git a/pyre/extra_tests/parity_tests/float_subclass_unboxed_storage.py b/pyre/extra_tests/parity_tests/float_subclass_unboxed_storage.py new file mode 100644 index 00000000000..27e04b15074 --- /dev/null +++ b/pyre/extra_tests/parity_tests/float_subclass_unboxed_storage.py @@ -0,0 +1,119 @@ +# CPython-suite gap: the suite checks float-subclass arithmetic and coercion, +# not whether a subclass instance survives storage in a container. +# parity-tests reason: pyre's raw-f64 storage reboxes on read as an exact float, +# which only `type()` / `is` sees. + +# `FloatListStrategy.is_correct_type`, `AbstractAttribute._pick_unbox_type` and +# `makespecialisedtuple2` are all strict `type(w) is W_FloatObject`, so a float +# SUBCLASS instance must keep its box. pyre gives a subclass the same builtin +# `ob_type` and separates it only by `w_class`, so every JIT fast path that +# unboxes into raw-f64 storage needs a `w_class` guard, not just a class guard: +# a class guard alone lets a subclass reuse a trace recorded for exact floats. + +f = 1.5 + + +class F(float): + pass + + +s = F(2.5) + +assert type(s) is F +assert s == 2.5 +assert type(f) is float + +# --- arity-2 tuple: `makespecialisedtuple2` / Cls_ff ----------------------- +t = (s, s) +assert t[0] is s and t[1] is s + +# One subclass slot is enough — both slots are stored raw. +mixed = (f, s) +assert mixed[0] is f and mixed[1] is s + +# --- instance attribute: mapdict `UnboxedPlainAttribute` ------------------- +class C: + pass + + +c = C() +c.x = s +assert c.x is s and c.__dict__["x"] is s + +# An existing unboxed float slot must convert back to boxed storage. +c2 = C() +c2.y = f +c2.y = s +assert c2.y is s + +# --- list: FloatListStrategy / IntOrFloatListStrategy ---------------------- +lst = [s] +assert lst[0] is s + +lst2 = [1.0, 2.0] +lst2.append(s) +assert lst2[2] is s and type(lst2[0]) is float + +lst3 = [1, 2.0] +lst3.append(s) +assert lst3[2] is s + +lst4 = [1.0, 2.0] +lst4[0] = s +assert lst4[0] is s + +lst5 = [] +lst5.append(s) +assert lst5[0] is s + + +# Hand each finite-float trace a subclass instance on its final iteration. +# A guard failure exits the whole loop iteration, so each direct store needs +# its own loop. +def warm_attr(rounds): + obj = C() + obj.x = 0.5 + for i in range(rounds): + v = s if i == rounds - 1 else i * 0.5 + obj.x = v + return obj.x is s + + +def warm_setitem(rounds): + lst = [0.5] + for i in range(rounds): + v = s if i == rounds - 1 else i * 0.5 + lst[0] = v + return lst[0] is s + + +def warm_newlist(rounds): + for i in range(rounds): + v = s if i == rounds - 1 else i * 0.5 + got = [v] + return got[0] is s + + +def warm_append_empty(rounds): + for i in range(rounds): + v = s if i == rounds - 1 else i * 0.5 + got = [] + got.append(v) + return got[0] is s + + +def warm_append_float(rounds): + for i in range(rounds): + v = s if i == rounds - 1 else i * 0.5 + got = [0.5] + got.append(v) + return got[1] is s + + +assert warm_attr(3000) +assert warm_setitem(3000) +assert warm_newlist(3000) +assert warm_append_empty(3000) +assert warm_append_float(3000) + +print("OK") diff --git a/pyre/extra_tests/parity_tests/nan_unboxed_storage_identity.py b/pyre/extra_tests/parity_tests/nan_unboxed_storage_identity.py new file mode 100644 index 00000000000..1bfeee1d5c0 --- /dev/null +++ b/pyre/extra_tests/parity_tests/nan_unboxed_storage_identity.py @@ -0,0 +1,106 @@ +# CPython-suite gap: the suite checks NaN value semantics, not identity through containers. +# parity-tests reason: pyre's raw-f64 storage reboxes on read, which only `is` / `id()` sees. + +# Python 3.14 gives NaNs pointer identity. Raw-f64 list, tuple, and mapdict +# storage must therefore reject them instead of reboxing on read. These checks +# cover the interpreter and JIT paths; finite floats must remain eligible. + +n = float("nan") +m = float("nan") +f = 1.5 + +assert n is not m +assert id(n) != id(m) +assert n is n and n != n + +# --- arity-2 tuple: `makespecialisedtuple2` / Cls_ff ----------------------- +t = (n, n) +assert t[0] is n and t[1] is n + +# One NaN makes the whole pair ineligible for `Cls_ff`. +mixed = (f, n) +assert mixed[0] is f and mixed[1] is n + +# --- instance attribute: mapdict `UnboxedPlainAttribute` ------------------- +class C: + pass + + +c = C() +c.x = n +assert c.x is n and c.__dict__["x"] is n + +# An existing float slot must convert back to boxed storage. +c2 = C() +c2.y = f +c2.y = n +assert c2.y is n + +# --- list: FloatListStrategy / IntOrFloatListStrategy ---------------------- +lst = [n] +assert lst[0] is n + +lst2 = [1.0, 2.0] +lst2.append(n) +assert lst2[2] is n + +lst3 = [1, 2.0] +lst3.append(n) +assert lst3[2] is n + +# `list.index`/`count` compare by value; a NaN is found only via the identity +# shortcut `==` gets before the value compare. +assert lst.index(n) == 0 +assert lst.count(n) == 1 +assert n in lst + + +# Hand each finite-float trace a NaN on its final iteration. A guard failure +# exits the whole loop iteration, so each direct store needs its own loop. +def warm_attr(rounds): + obj = C() + obj.x = 0.5 + for i in range(rounds): + v = n if i == rounds - 1 else i * 0.5 + obj.x = v + return obj.x is n + + +def warm_setitem(rounds): + lst = [0.5] + for i in range(rounds): + v = n if i == rounds - 1 else i * 0.5 + lst[0] = v + return lst[0] is n + + +def warm_newlist(rounds): + for i in range(rounds): + v = n if i == rounds - 1 else i * 0.5 + got = [v] + return got[0] is n + + +def warm_append_empty(rounds): + for i in range(rounds): + v = n if i == rounds - 1 else i * 0.5 + got = [] + got.append(v) + return got[0] is n + + +def warm_append_float(rounds): + for i in range(rounds): + v = n if i == rounds - 1 else i * 0.5 + got = [0.5] + got.append(v) + return got[1] is n + + +assert warm_attr(3000) +assert warm_setitem(3000) +assert warm_newlist(3000) +assert warm_append_empty(3000) +assert warm_append_float(3000) + +print("OK") diff --git a/pyre/extra_tests/snippets/builtin_abs.py b/pyre/extra_tests/snippets/builtin_abs.py index 7add4f4bcf4..b33200381a1 100644 --- a/pyre/extra_tests/snippets/builtin_abs.py +++ b/pyre/extra_tests/snippets/builtin_abs.py @@ -2,3 +2,61 @@ assert abs(7) == 7 assert abs(-3.21) == 3.21 assert abs(6.25) == 6.25 + + +# `abs()` reaches `__abs__` through the type (`operation.py`'s `abs` hands the +# operand to `space.abs`), +# so a subtype that replaced the builtin one is dispatched to. +class AbsInt(int): + def __abs__(self): + return "custom int" + + +class AbsFloat(float): + def __abs__(self): + return "custom float" + + +class AbsComplex(complex): + def __abs__(self): + return "custom complex" + + +assert abs(AbsInt(-5)) == "custom int" +assert abs(AbsFloat(-1.5)) == "custom float" +assert abs(AbsComplex(3 + 4j)) == "custom complex" + + +# A subtype that inherits the builtin one keeps the structural answer, and the +# exact type that goes with it. +class PlainInt(int): + pass + + +assert abs(PlainInt(-5)) == 5 +assert type(abs(PlainInt(-5))) is int +assert abs(True) == 1 and type(abs(True)) is int + + +# `__abs__ = None` is a replacement too: the lookup finds it and the call fails. +class NoAbs(int): + __abs__ = None + + +try: + abs(NoAbs(-5)) +except TypeError: + pass +else: + raise AssertionError("abs() answered for a subtype that unset __abs__") + + +# The builtin slot stays structural, so an override delegating back to it +# terminates instead of re-entering the lookup that reached it. +class DelegatesToInt(int): + def __abs__(self): + return int.__abs__(self) + + +assert abs(DelegatesToInt(-5)) == 5 +assert int.__abs__(AbsInt(-5)) == 5 diff --git a/pyre/extra_tests/snippets/builtin_float.py b/pyre/extra_tests/snippets/builtin_float.py index f0fcae5d103..f9da14849f6 100644 --- a/pyre/extra_tests/snippets/builtin_float.py +++ b/pyre/extra_tests/snippets/builtin_float.py @@ -549,3 +549,37 @@ def _check_msg(call, exc_type, expected_msg): lambda: INF.__int__(), OverflowError, "cannot convert float infinity to integer" ) _check_msg(lambda: NAN.__floor__(), ValueError, "cannot convert float NaN to integer") + + +# `float()` reaches `__float__` through the type, so an `int` subtype that +# overrides it is dispatched to rather than converted from its layout. +class FloatsToStr(int): + def __float__(self): + return "not a float" + + +try: + float(FloatsToStr(-5)) +except TypeError: + pass +else: + raise AssertionError("float() ignored an int subtype's __float__") + + +# A subtype that does not override it resolves to `int.__float__`, which is +# structural -- so an override that delegates back to the slot terminates. +class PlainInt(int): + pass + + +class DelegatesToInt(int): + def __float__(self): + return int.__float__(self) + + +assert float(PlainInt(-5)) == -5.0 +assert type(float(PlainInt(-5))) is float +assert float(DelegatesToInt(-5)) == -5.0 +assert int.__float__(FloatsToStr(-5)) == -5.0 +assert float(True) == 1.0 and float(False) == 0.0 +assert float(2**70) == 2.0**70 diff --git a/pyre/extra_tests/snippets/builtin_jit_folds.py b/pyre/extra_tests/snippets/builtin_jit_folds.py new file mode 100644 index 00000000000..be5adc1cede --- /dev/null +++ b/pyre/extra_tests/snippets/builtin_jit_folds.py @@ -0,0 +1,202 @@ +"""The builtins the JIT walker folds out of their residual call. + +One file for the whole table rather than one per builtin: what is under test +is a single mechanism, and every row shares the same two obligations — the +folded answer equals the builtin's, and every operand the raw helper does not +implement still reaches the builtin. + +Each case runs in a loop long enough for the loop to compile, and the value is +read inside that loop; a check that only inspects the result afterwards never +gets the fold compiled at all. The assert is outside so the reads stay +ordinary consumers of the folded value. +""" + +ROUNDS = 400 + + +def _agrees(got, want): + # NaN is its own witness here: the float rows decline on it, so a NaN that + # survives means the residual answered, which is still correct. + if isinstance(got, float) and isinstance(want, float): + if got != got and want != want: + return True + return type(got) is type(want) and got == want + + +def _stable(fn, args): + """`fn(*args)` agrees with itself across a compiled loop, or raises the + same exception every time.""" + try: + want = fn(*args) + except Exception as exc: # noqa: BLE001 - the raising direction is under test + want = (type(exc), str(exc)) + bad = 0 + for _ in range(ROUNDS): + try: + fn(*args) + except Exception as again: # noqa: BLE001 + if (type(again), str(again)) != want: + bad += 1 + else: + bad += 1 + assert bad == 0, (fn, args, want) + return want + bad = 0 + for _ in range(ROUNDS): + if not _agrees(fn(*args), want): + bad += 1 + assert bad == 0, (fn, args, want) + return want + + +# --- hash ------------------------------------------------------------------- +# The exact scalar types the raw helper answers for, plus the operands that +# send it back to the builtin. +class _Hashable: + def __hash__(self): + return 4242 + + +class _StrSub(str): + pass + + +class _IntSub(int): + pass + + +# One NaN object, reused: its hash is the identity hash, so a fresh NaN per +# iteration would not be stable to compare against. This is the arm the raw +# helper declines rather than answers, because reaching the identity hash means +# wrapping a fresh int under a call that is emitted as unable to collect. +_NAN = float("nan") + +for _v in [0, 1, -1, 7, -7, 2**62, -(2**62), 2**70, -(2**70), + True, False, 1.5, -0.0, 0.0, float("inf"), _NAN, + "", "a", "abcdefgh", b"", b"a", b"abcdefgh"]: + _stable(hash, (_v,)) + +assert _stable(hash, (_Hashable(),)) == 4242 +assert _stable(hash, (_StrSub("abc"),)) == hash("abc") +assert _stable(hash, (_IntSub(9),)) == hash(9) +# Numeric equality still implies hash equality through the fold. +assert _stable(hash, (1,)) == _stable(hash, (1.0,)) == _stable(hash, (True,)) +_stable(hash, ([1, 2],)) # unhashable: the raising direction + +# --- ord -------------------------------------------------------------------- +for _v in ["a", "é", "中", "\U0001f600", chr(0xD800), b"\x00", b"\xff"]: + _stable(ord, (_v,)) + +_stable(ord, ("ab",)) # length != 1 raises +_stable(ord, ("",)) +_stable(ord, (65,)) # not a string at all +assert _stable(ord, (_StrSub("z"),)) == ord("z") + +# --- abs -------------------------------------------------------------------- +for _v in [0, 1, -1, 7, -7, 2**62, -(2**62), -(2**63), 2**63, -(2**70), + True, False, 0.0, -0.0, 1.5, -1.5, + float("inf"), float("-inf"), float("nan")]: + _stable(abs, (_v,)) + +# `-(2**63)` is exactly the int channel's decline sentinel *and* the operand +# whose absolute value does not fit a machine word; both directions leave +# through the same side exit, and the builtin promotes it to a long. +assert _stable(abs, (-(2**63),)) == 2**63 +assert type(_stable(abs, (-7,))) is int +assert type(_stable(abs, (True,))) is int and _stable(abs, (True,)) == 1 +assert _stable(abs, (_IntSub(-5),)) == 5 + + +class _Abs: + def __abs__(self): + return "custom" + + +assert _stable(abs, (_Abs(),)) == "custom" + +# --- min / max -------------------------------------------------------------- +for _pair in [(1, 2), (2, 1), (1, 1), (-3, 3), (2**62, 2**62 + 1), + (1.5, 2.5), (2.5, 1.5), (1.5, 1.5), (-0.0, 0.0), + (float("inf"), 1.0), (float("nan"), 1.0), (1.0, float("nan")), + (1, 1.0), (1.0, 1), (True, 0), (2**70, 1)]: + _stable(min, _pair) + _stable(max, _pair) + +# A bigint reads as an exact `int` by class while keeping its own storage +# layout, so a comparison that reads the payload as a machine word would +# compare the payload's address instead of its value. An address is always +# a large positive number, which is why (2**70, 1) above agrees by accident: +# the answer only diverges once the bigint is the operand that should lose. +# `(2**62, 2**62 + 1)` above reaches none of this: both fit a machine word. +# An address sits far below 2**62, so `(2**70, 2**62)` diverges too. +for _pair in [(-(2**70), 5), (5, -(2**70)), (-(2**70), -1), + (2**70, 2**62), (2**62, 2**70), + (2**70, 2**71), (-(2**70), -(2**71))]: + _stable(min, _pair) + _stable(max, _pair) +assert min(-(2**70), 5) == -(2**70) +assert max(-(2**70), 5) == 5 + +# A tie keeps the first argument, and the fold hands back one of its own +# operands rather than a fresh box. The signed zeros are the only tie whose +# operands stay distinguishable: `is` on two exact ints compares values, so an +# equal int pair is one object whatever the fold returns, while two exact +# floats compare bit patterns and `-0.0` is not `0.0`. The read is inside the +# loop because `_stable` reports the answer it took before the loop ran. +_neg, _pos = -0.0, 0.0 +assert _neg is not _pos +_ties = 0 +for _ in range(ROUNDS): + if min(_neg, _pos) is not _neg or max(_neg, _pos) is not _neg: + _ties += 1 + if min(_pos, _neg) is not _pos or max(_pos, _neg) is not _pos: + _ties += 1 +assert _ties == 0 +_stable(min, ("b", "a")) +_stable(max, ([1], [2])) +_stable(min, (1,)) # a single iterable argument, not the pair form +_stable(max, ([3, 1, 2],)) + + +class _Cmp: + def __init__(self, v): + self.v = v + + def __lt__(self, other): + return self.v < other.v + + def __gt__(self, other): + return self.v > other.v + + +assert _stable(min, (_Cmp(1), _Cmp(2))).v == 1 +assert _stable(max, (_Cmp(1), _Cmp(2))).v == 2 + +# --- rebound names ---------------------------------------------------------- +# The fold keys on the wrapped builtin code, not the name it is reachable +# under, so a shadowing definition must win. +_real_abs, _real_hash, _real_min = abs, hash, min + + +def abs(x): # noqa: A001 - shadowing is the point + return "shadowed" + + +def hash(x): # noqa: A001 + return -12345 + + +def min(*a): # noqa: A001 + return "smallest" + + +_bad = 0 +for _ in range(ROUNDS): + if abs(-1) != "shadowed" or hash(1) != -12345 or min(1, 2) != "smallest": + _bad += 1 +assert _bad == 0 + +abs, hash, min = _real_abs, _real_hash, _real_min +assert abs(-1) == 1 and min(1, 2) == 1 + +print("OK") diff --git a/pyre/extra_tests/snippets/builtin_list.py b/pyre/extra_tests/snippets/builtin_list.py index f33b21902d6..1d42ed31f75 100644 --- a/pyre/extra_tests/snippets/builtin_list.py +++ b/pyre/extra_tests/snippets/builtin_list.py @@ -242,6 +242,26 @@ def __eq__(self, x): assert sorted([(1, 2, 3), (0, 3, 6)], key=lambda x: x[1]) == [(1, 2, 3), (0, 3, 6)] assert sorted([(1, 2), (), (5,)], key=len) == [(), (5,), (1, 2)] +# The all-int sorter stands in for the integer list strategy, so it must +# accept exactly what that strategy does. A bigint is an exact `int` by class +# while storing a pointer where a machine int stores its value, so admitting +# one orders the list by that pointer -- always a large positive number, which +# is why only a negative bigint, or a bigint that should lose to a large +# machine int, tells the two orders apart. +_big = 2**70 +assert sorted([-_big, 5]) == [-_big, 5] +assert sorted([5, -_big]) == [-_big, 5] +assert sorted([_big, 2**62]) == [2**62, _big] +assert sorted([-_big, 0, _big]) == [-_big, 0, _big] +assert sorted([_big, -_big, 3, -4]) == [-_big, -4, 3, _big] +_l = [3, -_big, 7, -1] +_l.sort() +assert _l == [-_big, -1, 3, 7] +_l.sort(reverse=True) +assert _l == [7, 3, -1, -_big] +# A bool alongside a bigint keeps the same requirement. +assert sorted([True, -_big, False]) == [-_big, False, True] + lst = [3, 1, 5, 2, 4] diff --git a/pyre/extra_tests/snippets/builtin_round.py b/pyre/extra_tests/snippets/builtin_round.py index e94d9754204..a708bf21166 100644 --- a/pyre/extra_tests/snippets/builtin_round.py +++ b/pyre/extra_tests/snippets/builtin_round.py @@ -93,3 +93,22 @@ def __round__(self, ndigits=None): assert round(1.0, 1000) == 1.0 assert round(1.0, -1000) == 0.0 assert round(1.7976931348623157e308, 0) == 1.7976931348623157e308 + + +# `int.__round__` and `float.__round__` are structural, so an override that +# delegates back to the slot does not re-enter the lookup that reached it. +class DelegatesToInt(int): + def __round__(self, *ndigits): + return int.__round__(self, *ndigits) + + +class DelegatesToFloat(float): + def __round__(self, *ndigits): + return float.__round__(self, *ndigits) + + +assert round(DelegatesToInt(-5)) == -5 +assert round(DelegatesToInt(-5), 1) == -5 +assert round(DelegatesToFloat(-1.5)) == -2 +assert round(DelegatesToFloat(-1.25), 1) == -1.2 +assert int.__round__(DelegatesToInt(-5)) == -5 diff --git a/pyre/extra_tests/snippets/stdlib_math.py b/pyre/extra_tests/snippets/stdlib_math.py index a6bb0099c05..8876425569e 100644 --- a/pyre/extra_tests/snippets/stdlib_math.py +++ b/pyre/extra_tests/snippets/stdlib_math.py @@ -311,3 +311,243 @@ def assertAllNotClose(examples, *args, **kwargs): assert math.fmod(0.0, NINF) == 0.0 assert math.gamma(1) == 1.0 + + +# The `__float__` fallback of math.floor/ceil goes through newlong_from_float, +# so a value outside the machine-int range stays exact and a non-finite one +# raises rather than saturating to a machine bound. math.trunc has no such +# fallback and requires `__trunc__`. +class FloatLike: + def __init__(self, value): + self.value = value + + def __float__(self): + return self.value + + +for _unary in (math.floor, math.ceil): + assert _unary(FloatLike(1e300)) == int(1e300) + assert _unary(FloatLike(-1e300)) == int(-1e300) + assert_raises(ValueError, lambda f=_unary: f(FloatLike(NAN))) + assert_raises(OverflowError, lambda f=_unary: f(FloatLike(INF))) + assert_raises(OverflowError, lambda f=_unary: f(FloatLike(NINF))) + +assert math.floor(FloatLike(41.9)) == 41 +assert math.ceil(FloatLike(42.5)) == 43 +assert type(math.floor(FloatLike(41.9))) is int +assert_raises(TypeError, lambda: math.trunc(FloatLike(23.5))) + + +# gcd reduces a pair of machine words without going through rbigint. The +# machine-word arm must decline where |x| leaves the signed range, must read a +# bool and an int subclass as their raw value, and must never return the +# operand object itself. +_MIN64 = -(2**63) +assert math.gcd(_MIN64, 0) == 2**63 +assert math.gcd(_MIN64, 6) == 2 +assert math.gcd(_MIN64, _MIN64) == 2**63 +assert math.gcd(True, False) == 1 +assert type(math.gcd(True, False)) is int +assert math.gcd(-120, 84) == 12 + + +class _IndexingInt(int): + def __index__(self): + return 99 + + +class _AbsingInt(int): + def __abs__(self): + return 7 + + +assert math.gcd(_IndexingInt(10), 4) == 2 +assert math.gcd(_AbsingInt(-10), 4) == 2 + + +# The tolerance sanity check names the tolerances rather than reporting the +# generic domain error the underlying comparison raises. +assert_raises( + ValueError, + lambda: math.isclose(1, 2, rel_tol=-1), + _msg="tolerances must be non-negative", +) +assert_raises( + ValueError, + lambda: math.isclose(1, 2, abs_tol=-1), + _msg="tolerances must be non-negative", +) +# All four operands are converted before any of them is checked, so an +# operand that is not a number is reported even when a tolerance is also +# rejectable, and the conversions run left to right. +assert_raises( + TypeError, + lambda: math.isclose("x", 1.0, rel_tol=-1), + _msg="the operand is converted before the tolerance is checked", +) + + +class _Reports: + def __init__(self, name, seen): + self.name = name + self.seen = seen + + def __float__(self): + self.seen.append(self.name) + return 1.0 + + +_seen = [] +assert math.isclose( + _Reports("a", _seen), + _Reports("b", _seen), + rel_tol=_Reports("rel", _seen), + abs_tol=_Reports("abs", _seen), +) +assert _seen == ["a", "b", "rel", "abs"], _seen + +assert math.isclose(1.0, 1.0 + 1e-12) +assert not math.isclose(1.0, 2.0) + + +# The generic float folds answer through a raw helper that reports every +# raising direction as NaN and is guarded finite. Run each covered function +# hot on one operand at a time, so the loop compiles and either the fold or +# the decline it chose runs for every iteration, and check the answer against +# the one the interpreter gave before anything was compiled. +_FLOAT1 = ( + math.tan, math.asin, math.acos, math.atan, + math.sinh, math.cosh, math.tanh, math.asinh, math.acosh, math.atanh, + math.cbrt, math.exp, math.exp2, math.expm1, math.log1p, + math.erf, math.erfc, math.gamma, math.lgamma, + math.ulp, math.degrees, math.radians, +) +_FLOAT2 = (math.pow, math.fmod, math.copysign, math.remainder, math.atan2) + +_PROBE1 = (0.5, 1.0, -1.0, 0.0, -0.0, 2.0, 1e300, -1e300, 710.0, INF, NINF, NAN) +_PROBE2 = ( + (2.0, 3.0), (2.0, -3.0), (0.0, -2.0), (-1.0, 2.3), (1e300, 1e300), + (7.0, 3.0), (7.0, 0.0), (-1.0, 0.0), (INF, 2.0), (NAN, 1.0), (3, 2), +) + + +def _outcome(fn, args): + """The answer or the rejection, as a comparable value.""" + try: + return ("v", fn(*args)) + except (ValueError, OverflowError) as exc: + return (type(exc).__name__, str(exc)) + + +def _agrees(got, want): + if got == want: + return True + # A NaN result is never equal to itself. + return ( + got[0] == "v" and want[0] == "v" and got[1] != got[1] and want[1] != want[1] + ) + + +for _fn, _args in [(f, (x,)) for f in _FLOAT1 for x in _PROBE1] + [ + (f, p) for f in _FLOAT2 for p in _PROBE2 +]: + _want = _outcome(_fn, _args) + for _round in range(300): + assert _agrees(_outcome(_fn, _args), _want), (_fn, _args, _want) + + +# A float subclass and a rebound name both decline the fold; the answers must +# stay the interpreter's. +class _MyFloat(float): + pass + + +for _round in range(400): + assert math.exp(_MyFloat(0.0)) == 1.0 + assert math.pow(_MyFloat(2.0), _MyFloat(3.0)) == 8.0 + +_real_exp = math.exp +math.exp = lambda x: "rebound" +try: + for _round in range(400): + assert math.exp(1.0) == "rebound" +finally: + math.exp = _real_exp +assert math.exp(0.0) == 1.0 + + +# `isclose` folds only where the result decides a branch, and the folded +# helper re-implements the comparison rather than calling the module's own. +# Check both shapes against each other on both truths, on the infinities, and +# on NaN. +_CLOSE = ( + (1.0, 1.0, True), (1.0, 1.0 + 1e-12, True), (1.0, 2.0, False), + (INF, INF, True), (INF, NINF, False), (INF, 1.0, False), + (NAN, NAN, False), (NAN, 1.0, False), (0.0, -0.0, True), + (0.0, 1e-300, False), (1, 1, True), (True, 1.0, True), + (-1.0, -1.0 - 1e-12, True), (1e300, 1e300 + 1e280, True), +) +for _a, _b, _want in _CLOSE: + for _round in range(300): + # The branch shape, which folds. + if math.isclose(_a, _b): + assert _want, (_a, _b) + else: + assert not _want, (_a, _b) + # The escaping shape, which keeps the residual. + assert math.isclose(_a, _b) is _want, (_a, _b) + + +# comb reduces a machine-word pair without rbigint. The arm must decline +# where an intermediate leaves the range, must keep the rejection order, and +# must agree with the rbigint path everywhere the two overlap. +assert math.comb(0, 0) == 1 +assert math.comb(5, 2) == 10 +assert math.comb(5, 3) == 10 +assert math.comb(40, 20) == 137846528820 +assert math.comb(62, 31) == 465428353255261088 +assert math.comb(68, 34) == 28453041475240576740 +assert math.comb(100, 50) == 100891344545564193334812497256 +assert math.comb(10, 11) == 0 +assert math.comb(2**70, 2) == (2**70 * (2**70 - 1)) // 2 +assert math.comb(True, True) == 1 +assert type(math.comb(True, True)) is int +assert math.comb(_IndexingInt(10), 4) == 210 +assert_raises(ValueError, lambda: math.comb(-1, -1), _msg="n must be a non-negative integer") +assert_raises(ValueError, lambda: math.comb(1, -1), _msg="k must be a non-negative integer") +# A METH_VARARGS entry point takes no keywords at all, so one is rejected +# before the operands are read rather than reaching the body as an extra +# argument. Every arity is covered because the marker arrives appended. +assert_raises(TypeError, lambda: math.comb(5, k=2), _msg="comb takes no keywords") +assert_raises(TypeError, lambda: math.comb(n=5, k=2), _msg="comb takes no keywords") +assert_raises(TypeError, lambda: math.perm(5, k=2), _msg="perm takes no keywords") +assert_raises(TypeError, lambda: math.gcd(5, b=2), _msg="gcd takes no keywords") +assert_raises(TypeError, lambda: math.lcm(5, b=2), _msg="lcm takes no keywords") +# Every small pair against the same value built by repeated addition, which +# shares no code with either comb arm. +_pascal = [[1]] +for _n in range(1, 60): + _prev = _pascal[-1] + _pascal.append([1] + [_prev[_i] + _prev[_i + 1] for _i in range(_n - 1)] + [1]) +for _n in range(60): + for _k in range(_n + 1): + assert math.comb(_n, _k) == _pascal[_n][_k], (_n, _k) + +# perm shares comb's machine-word arm. `perm(n)` and `perm(n, None)` both +# mean k = n. +assert math.perm(0) == 1 +assert math.perm(5) == 120 +assert math.perm(5, None) == 120 +assert math.perm(5, 2) == 20 +assert math.perm(20) == 2432902008176640000 +assert math.perm(21) == 51090942171709440000 +assert math.perm(2**70, 2) == 2**70 * (2**70 - 1) +assert math.perm(10, 11) == 0 +assert math.perm(True, True) == 1 +assert type(math.perm(True, True)) is int +assert math.perm(_IndexingInt(10), 4) == 5040 +assert_raises(ValueError, lambda: math.perm(-1, -1), _msg="n must be a non-negative integer") +assert_raises(ValueError, lambda: math.perm(1, -1), _msg="k must be a non-negative integer") +for _n in range(30): + for _k in range(_n + 1): + assert math.perm(_n, _k) == math.comb(_n, _k) * math.factorial(_k), (_n, _k) diff --git a/pyre/pyre-interpreter/src/baseobjspace.rs b/pyre/pyre-interpreter/src/baseobjspace.rs index cf5edf5db07..1706291c20e 100644 --- a/pyre/pyre-interpreter/src/baseobjspace.rs +++ b/pyre/pyre-interpreter/src/baseobjspace.rs @@ -4580,33 +4580,24 @@ pub fn is_w(w_one: PyObjectRef, w_two: PyObjectRef) -> bool { } // `W_FloatObject.is_w` (floatobject.py): two plain // `float`s are identical when their bit patterns are equal - // (`float2longlong`), so `0.0 is -0.0` is false and a NaN is its - // own identity. `float` subclasses (`user_overridden_class`) keep - // pointer identity — the exact-type gate excludes them. + // (`float2longlong`), so `0.0 is -0.0` is false. `float` subclasses + // (`user_overridden_class`) keep pointer identity — the exact-type + // gate excludes them. // - // This has to stay in step with `function::immutable_unique_id`, which - // derives `id()` from the same bits, and with `FloatListStrategy`, - // which unboxes and reboxes list elements: pointer identity here would - // make `a is b` false while `id(a) == id(b)` stayed true, and would - // make `[a][0] is a` false. + // CPython 3.14 gives NaNs pointer identity; unlike finite floats they + // stay boxed (`cpython_differences.rst`, "Object Identity of Primitive + // Values, `is` and `id`"). if pyre_object::pyobject::is_exact_type(w_one, &pyre_object::pyobject::FLOAT_TYPE) && pyre_object::pyobject::is_exact_type(w_two, &pyre_object::pyobject::FLOAT_TYPE) { - return pyre_object::floatobject::w_float_get_value(w_one).to_bits() - == pyre_object::floatobject::w_float_get_value(w_two).to_bits(); - } - // `W_ComplexObject.is_w` (complexobject.py): two plain - // `complex`es are identical when both component bit patterns are - // equal (`float2longlong`). `complex` subclasses - // (`user_overridden_class`) keep pointer identity. - if pyre_object::pyobject::is_exact_type(w_one, &pyre_object::pyobject::COMPLEX_TYPE) - && pyre_object::pyobject::is_exact_type(w_two, &pyre_object::pyobject::COMPLEX_TYPE) - { - return pyre_object::complexobject::w_complex_get_real(w_one).to_bits() - == pyre_object::complexobject::w_complex_get_real(w_two).to_bits() - && pyre_object::complexobject::w_complex_get_imag(w_one).to_bits() - == pyre_object::complexobject::w_complex_get_imag(w_two).to_bits(); + let one = pyre_object::floatobject::w_float_get_value(w_one); + let two = pyre_object::floatobject::w_float_get_value(w_two); + if one.is_nan() || two.is_nan() { + return false; + } + return one.to_bits() == two.to_bits(); } + // CPython 3.14 gives complex objects pointer identity, handled above. // `W_AbstractTupleObject.is_w` (tupleobject.py): a `tuple` is // identical to another only when both are the empty tuple — "empty // tuples are unique-ified". Non-empty tuples keep pointer identity diff --git a/pyre/pyre-interpreter/src/builtins.rs b/pyre/pyre-interpreter/src/builtins.rs index 159d3642652..b6582db2bfb 100644 --- a/pyre/pyre-interpreter/src/builtins.rs +++ b/pyre/pyre-interpreter/src/builtins.rs @@ -4612,8 +4612,64 @@ pub fn builtin_abs(args: &[PyObjectRef]) -> Result }; builtin_abs_obj(obj) } +/// Whether the receiver's `__abs__` is still the one its builtin numeric type +/// installs, which is what makes the structural arms below correct for it. +/// `int` covers the bigint representation and `bool`, neither of which +/// registers its own. +/// +/// # Safety +/// `obj` must point to a valid object whose header is readable. +unsafe fn abs_uses_builtin(obj: PyObjectRef) -> bool { + let w_class = unsafe { (*obj).w_class }; + if w_class.is_null() { + return true; + } + // An exact receiver cannot carry a replacement, so the common call spends + // a pointer compare rather than a lookup. + for tp in [ + &pyre_object::INT_TYPE, + &pyre_object::FLOAT_TYPE, + &pyre_object::COMPLEX_TYPE, + ] { + if std::ptr::eq(w_class, pyre_object::get_instantiate(tp)) { + return true; + } + } + let Some((src, _)) = (unsafe { crate::baseobjspace::lookup_where_pair(w_class, "__abs__") }) + else { + return true; + }; + [ + &pyre_object::INT_TYPE, + &pyre_object::FLOAT_TYPE, + &pyre_object::COMPLEX_TYPE, + ] + .into_iter() + .any(|tp| std::ptr::eq(src, pyre_object::get_instantiate(tp))) +} fn builtin_abs_obj(obj: PyObjectRef) -> Result { + // `operation.py`'s `abs` hands the operand to `space.abs`, which reaches + // `__abs__` through the type, so a subtype that replaced the builtin one — + // `__abs__ = None` included — is dispatched by the lookup below rather than + // answered structurally. + if unsafe { abs_uses_builtin(obj) } { + return abs_structural(obj); + } + unsafe { + if let Some(tp) = crate::typedef::r#type(obj) + && let Some(method) = crate::baseobjspace::lookup_in_type(tp.as_ptr(), "__abs__") + { + return crate::baseobjspace::get_and_call_function(method, obj, tp.as_ptr(), &[]); + } + } + Err(abs_bad_operand(obj)) +} + +/// `int.__abs__` / `float.__abs__`: the layout arms on their own. These are +/// what the lookup in `builtin_abs_obj` dispatches to, so they must not repeat +/// it — a subtype whose `__abs__` calls back into one would not terminate. +fn abs_structural(obj: PyObjectRef) -> Result { unsafe { if is_bool(obj) { return Ok(w_int_new(w_bool_get_value(obj) as i64)); @@ -4628,10 +4684,11 @@ fn builtin_abs_obj(obj: PyObjectRef) -> Result { } if is_long(obj) { let val = w_long_get_value(obj); - // rbigint.py:1303-1308 returns `self` for nonnegative values; - // longobject.py then allocates a fresh W_LongObject around - // that same rbigint. Preserve both wrapper identity and payload - // sharing instead of translating `Clone` into another GC payload. + // `rbigint.abs` returns `self` for nonnegative values, and + // longobject.py's `_make_descr_unaryop('abs')` then allocates a + // fresh W_LongObject around that same rbigint. Preserve both + // wrapper identity and payload sharing instead of translating + // `Clone` into another GC payload. if val.get_sign() != -1 { return Ok(pyre_object::longobject::w_long_from_raw( pyre_object::longobject::w_long_get_raw_value(obj), @@ -4646,19 +4703,23 @@ fn builtin_abs_obj(obj: PyObjectRef) -> Result { return crate::objspace::descroperation::complex_abs(obj); } } - // Instance __abs__ — PyPy: baseobjspace.py abs - unsafe { - if pyre_object::is_instance(obj) { - let w_type = pyre_object::w_instance_get_type(obj); - if let Some(method) = crate::baseobjspace::lookup_in_type(w_type, "__abs__") { - return crate::call::call_function_impl_result(method, &[obj]); - } - } - } - Err(crate::PyError::type_error(format!( + Err(abs_bad_operand(obj)) +} + +/// `int.__abs__` / `float.__abs__` as gateway functions. +pub fn builtin_abs_dunder(args: &[PyObjectRef]) -> Result { + let obj = match args { + [val] => *val, + _ => parse_single_required(args, "self", "__abs__")?, + }; + abs_structural(obj) +} + +fn abs_bad_operand(obj: PyObjectRef) -> crate::PyError { + crate::PyError::type_error(format!( "bad operand type for abs(): '{}'", crate::baseobjspace::object_functionstr_type_name(obj) - ))) + )) } pub fn __pyre_wrap_builtin_abs(args: &[PyObjectRef]) -> Result { @@ -5167,12 +5228,12 @@ pub(crate) unsafe fn obj_to_bigint(obj: PyObjectRef) -> BigInt { /// - reject any kwargs other than `key` / `default` /// - reject `default=` paired with multiple positional args /// - require ≥1 positional arg -fn builtin_min(args: &[PyObjectRef]) -> Result { +pub(crate) fn builtin_min(args: &[PyObjectRef]) -> Result { min_max_dispatch(args, /* want_max= */ false, "min") } /// `max(a, b)` / `max(iterable)` — return the largest of two values or an iterable. -fn builtin_max(args: &[PyObjectRef]) -> Result { +pub(crate) fn builtin_max(args: &[PyObjectRef]) -> Result { min_max_dispatch(args, /* want_max= */ true, "max") } @@ -10367,6 +10428,41 @@ pub(crate) fn builtin_float_dunder(args: &[PyObjectRef]) -> Result Result { + let obj = args[0]; + unsafe { + if is_bool(obj) { + return Ok(floatobject::w_float_new(if w_bool_get_value(obj) { + 1.0 + } else { + 0.0 + })); + } + if is_int(obj) { + return Ok(floatobject::w_float_new(w_int_get_value(obj) as f64)); + } + if pyre_object::is_long(obj) { + let v = pyre_object::jit_bigint_to_f64_or_nan(pyre_object::w_long_get_value(obj)); + if !v.is_finite() { + return Err(crate::PyError::overflow_error( + "int too large to convert to float", + )); + } + return Ok(floatobject::w_float_new(v)); + } + } + Err(crate::PyError::type_error(format!( + "descriptor '__float__' requires an 'int' object but received a '{}'", + crate::type_methods::arg_type_name(obj) + ))) +} + /// `float(obj)` → convert to float pub(crate) fn builtin_float(args: &[PyObjectRef]) -> Result { if args.is_empty() { @@ -10391,7 +10487,13 @@ pub(crate) fn builtin_float(args: &[PyObjectRef]) -> Result Result i64 { /// `ord(c)` — PyPy: operation.py ord (dispatches to space.ord); /// `unicodeobject.py:155-160` raises TypeError on multi-char strings. -fn builtin_ord(args: &[PyObjectRef]) -> Result { +pub(crate) fn builtin_ord(args: &[PyObjectRef]) -> Result { if args.len() != 1 { return Err(crate::PyError::type_error( "ord() takes exactly one argument", @@ -15124,9 +15226,12 @@ impl crate::listsort::SortLt for SortCompare { /// upstream — so the test has to be `is_exact_type` (pyobject.rs), which /// compares the instance's `w_class` against the builtin's type object and so /// rejects a subclass, which retags `w_class` to its own. `is_int` / `is_str` -/// / `is_float` are NOT usable here: they are `py_type_check`, an `ob_type` -/// layout test a subclass instance also passes because it shares the builtin -/// vtable. A `key=` sort is `CustomKeySort`, generic upstream as well. +/// / `is_float` are NOT usable here ON THEIR OWN: they are `py_type_check`, an +/// `ob_type` layout test a subclass instance also passes because it shares the +/// builtin vtable. The int arm needs both tests, which is what +/// `is_plain_int1` is -- the exact-class test alone would let a bigint into a +/// comparator that reads a machine word. A `key=` sort is `CustomKeySort`, +/// generic upstream as well. fn sort_compare_for(base: usize, len: usize, keyed: bool) -> SortCompare { if keyed { return SortCompare::Generic(base); @@ -15138,7 +15243,15 @@ fn sort_compare_for(base: usize, len: usize, keyed: bool) -> SortCompare { // `bool` is admitted alongside `int` because it cannot be // subclassed, so it can never carry an overriding `__lt__`, and // `int_value` reads it the same way. - all_int &= pyre_object::is_exact_type(item, &pyre_object::INT_TYPE) + // `is_exact_type` answers on `w_class`, and a bigint's is wired + // to `int`'s so that `type(x) is int` holds for one -- so it alone + // admits a `W_LongObject`, whose `*mut BigInt` sits where + // `W_IntObject` keeps `intval`, and the `Int` arm's `int_value` + // would order the list by payload address. `is_plain_int1` is the + // strategy's own `is_correct_type`, which carries both halves; + // using it here is what makes this scan the strategy decision it + // stands in for. + all_int &= pyre_object::listobject::is_plain_int1(item) || pyre_object::is_exact_type(item, &pyre_object::BOOL_TYPE); all_float &= pyre_object::is_exact_type(item, &pyre_object::FLOAT_TYPE); all_str &= pyre_object::is_exact_type(item, &pyre_object::STR_TYPE); @@ -18389,6 +18502,18 @@ unsafe fn round_uses_builtin(obj: PyObjectRef) -> bool { } pub(crate) fn builtin_round(args: &[PyObjectRef]) -> Result { + round_receiver(args, false) +} + +/// `int.__round__` / `float.__round__`. These are what the lookup at the end +/// of `round_receiver` dispatches to, so they must not repeat it: a subtype +/// whose `__round__` delegates back to the slot would re-enter the lookup that +/// reached it and never terminate. +pub(crate) fn builtin_round_dunder(args: &[PyObjectRef]) -> Result { + round_receiver(args, true) +} + +fn round_receiver(args: &[PyObjectRef], slot: bool) -> Result { // `round(number, ndigits=None)`: both positional-or-keyword; at most two. let (pos, kwargs) = split_builtin_kwargs(args); let total = pos.len() + real_kwarg_count(kwargs); @@ -18404,11 +18529,12 @@ pub(crate) fn builtin_round(args: &[PyObjectRef]) -> Result Result(w_root: WRoot, _space: PyObjectRef) -> WRootF let _ = w_root; WRootFinalizerQueue } + +/// `interp_jit.py`'s `is_being_profiled` portal green, read from the running +/// execution context rather than from a frame. +/// +/// `setllprofile` sets the per-frame flag on every live frame +/// (`force_all_frames(is_being_profiled=True)`) and `call_trace` sets it on +/// each frame it enters, so "this frame is being profiled" and "a profile +/// function is installed" name the same state for every frame the portal can +/// reach. The portal has green-key sites with no frame in hand — a function +/// entry keys on `(pycode, 0)` — and the hash form and the typed form must +/// agree or they resolve to different cells, so both derive the green here. +pub fn current_is_being_profiled() -> bool { + let ec = crate::call::getexecutioncontext(); + !ec.is_null() && unsafe { (*ec).profilefunc.is_some() } +} diff --git a/pyre/pyre-interpreter/src/function.rs b/pyre/pyre-interpreter/src/function.rs index 806cc04c033..5e029736fb0 100644 --- a/pyre/pyre-interpreter/src/function.rs +++ b/pyre/pyre-interpreter/src/function.rs @@ -2887,7 +2887,6 @@ pub unsafe fn fdel_func_doc(obj: PyObjectRef) -> Result<(), crate::PyError> { const IDTAG_SHIFT: i64 = 4; const IDTAG_INT: i64 = 1; const IDTAG_FLOAT: i64 = 5; -const IDTAG_COMPLEX: i64 = 7; const IDTAG_SPECIAL: i64 = 11; #[inline] @@ -2913,25 +2912,19 @@ pub fn immutable_unique_id(obj: PyObjectRef) -> Option { if is_exact_type(obj, &FLOAT_TYPE) { // `float2longlong(float_w(self))` reinterprets the f64 bits as // a signed i64; the same `| IDTAG_FLOAT` == `+ IDTAG_FLOAT`. - let bits = pyre_object::floatobject::w_float_get_value(obj).to_bits() as i64; + // NaNs use the address uid, matching `is_w`'s pointer identity. + let value = pyre_object::floatobject::w_float_get_value(obj); + if value.is_nan() { + return None; + } + let bits = value.to_bits() as i64; let b = (majit_rlib::rbigint::RBigInt::from(bits) << IDTAG_SHIFT as usize) + majit_rlib::rbigint::RBigInt::from(IDTAG_FLOAT); return Some(pyre_object::functional::range_bigint_to_obj(b)); } - if is_exact_type(obj, &COMPLEX_TYPE) { - // `(real_b << 64 | imag_b) << IDTAG_SHIFT | IDTAG_COMPLEX` - // (complexobject.py:303-314): the real bits are signed - // (`float2longlong`), the imag bits unsigned (`r_ulonglong`); - // the high/low 64-bit halves don't overlap, so each `|` is a - // `+`. - let real_bits = pyre_object::complexobject::w_complex_get_real(obj).to_bits() as i64; - let imag_bits = pyre_object::complexobject::w_complex_get_imag(obj).to_bits(); - let combined = (majit_rlib::rbigint::RBigInt::from(real_bits) << 64usize) - + majit_rlib::rbigint::RBigInt::from(imag_bits); - let b = (combined << IDTAG_SHIFT as usize) - + majit_rlib::rbigint::RBigInt::from(IDTAG_COMPLEX); - return Some(pyre_object::functional::range_bigint_to_obj(b)); - } + // Unlike `W_ComplexObject.immutable_unique_id` (complexobject.py), + // CPython 3.14 complex identity is address-based, so there is no + // IDTAG_COMPLEX branch. if is_exact_type(obj, &TUPLE_TYPE) { // `W_AbstractTupleObject.immutable_unique_id` // (tupleobject.py:57-62): only the empty tuple is unique-ified diff --git a/pyre/pyre-interpreter/src/jit_builtin_folds.rs b/pyre/pyre-interpreter/src/jit_builtin_folds.rs new file mode 100644 index 00000000000..e638f12fbfe --- /dev/null +++ b/pyre/pyre-interpreter/src/jit_builtin_folds.rs @@ -0,0 +1,324 @@ +//! Raw entry points for the builtins the JIT walker folds out of a residual +//! call, and the identity table that names them. +//! +//! A traced `abs(x)` / `hash(x)` / `min(a, b)` reaches the interpreter through +//! `bh_call_fn`, which roots its arguments, resolves the execution context and +//! binds the gateway signature before the builtin body runs at all. That +//! preamble costs an order of magnitude more than the operation it guards, and +//! it is paid once per loop iteration. PyPy has no equivalent step: its +//! builtins are RPython, so the tracer walks straight into the body and the +//! result box is virtual. +//! +//! Each helper here is the body of one builtin, restricted to the operand +//! shapes it can answer without running Python and without allocating, and +//! reporting every other direction through a decline channel: +//! +//! * an `i64`-valued helper declines with [`INT_FOLD_DECLINE`]; +//! * an `f64`-valued helper declines with NaN, and the trace guards the result +//! finite; +//! * a reference-valued helper declines with `PY_NULL`, and the trace guards +//! the result non-null. +//! +//! A decline resumes in the interpreter at the same bytecode, which re-executes +//! the whole call — so a helper may decline for any reason at all, including a +//! result that happens to collide with its own sentinel. What it may not do is +//! answer where the builtin would have raised, returned something else, or run +//! app-level code. Nothing here allocates: the result box is emitted as +//! `wrapint` / `wrapfloat` / `newbool` in the trace instead, where the +//! optimizer can keep it virtual. + +use pyre_object::{PY_NULL, PyObjectRef}; + +// Every raw helper takes and returns `i64`, never `PyObjectRef`, and casts at +// its own boundary. That is the residual-call ABI the backends assume: the +// wasm backend lowers an all-Int/Ref call to a direct `call_indirect` typed +// `(i64 x n) -> i64`, and a `PyObjectRef` parameter is an `i32` on wasm32, so +// a helper spelled with pointer arguments traps the moment the compiled trace +// calls it. + +/// The `i64` an int-valued raw helper returns to decline its operand. +/// +/// A builtin whose real answer is this value declines too and takes the side +/// exit; the interpreter then computes the same answer the slow way. +pub const INT_FOLD_DECLINE: i64 = i64::MIN; + +/// How a row's raw helper is called and what its result means. +#[derive(Clone, Copy)] +pub enum BuiltinFoldRaw { + /// One argument, machine-int result, [`INT_FOLD_DECLINE`] declines. + Int1(extern "C" fn(i64) -> i64), + /// One argument, float result, NaN declines. + Float1(extern "C" fn(i64) -> f64), + /// Two arguments, reference result, `PY_NULL` declines. + Ref2(extern "C" fn(i64, i64) -> i64), +} + +impl BuiltinFoldRaw { + /// Positional argument count this helper answers for. A call with any + /// other count is not this row's shape and keeps the residual. + pub fn arity(&self) -> usize { + match self { + Self::Int1(_) | Self::Float1(_) => 1, + Self::Ref2(_) => 2, + } + } +} + +/// One folded builtin: the identity that recognizes it and the helper that +/// answers for it. +pub struct BuiltinFold { + /// Names the row in `PYRE_FBW_SPEC_CENSUS` output. + pub name: &'static str, + /// The `BuiltinCodeFn` the module namespace registered under this name. + /// A rebound global keeps the residual: the identity is the wrapped code, + /// not the name it is reachable under. + code_fn: crate::gateway::BuiltinCodeFn, + pub raw: BuiltinFoldRaw, +} + +/// `hash(x)` for the exact scalar types `try_hash_value` already answers +/// without a `__hash__` lookup. Every one of them is final in place, so an +/// exact instance can only reach the digest `hash_value` computes. +/// +/// A NaN `float` is the one arm excluded for its effects rather than its +/// answer: `hash_value` sends it to the identity hash, and a float's identity +/// is its bit pattern widened through `immutable_unique_id`, which wraps a +/// fresh `int`. The trace emits this call as one that cannot collect, so it +/// carries no gcmap and spills no reference registers; allocating under it +/// would leave the collector blind. Decline and let the interpreter allocate. +extern "C" fn jit_builtin_hash(obj: i64) -> i64 { + let obj = obj as PyObjectRef; + if obj.is_null() { + return INT_FOLD_DECLINE; + } + unsafe { + if pyre_object::is_exact_type(obj, &pyre_object::FLOAT_TYPE) + && pyre_object::w_float_get_value(obj).is_nan() + { + return INT_FOLD_DECLINE; + } + for tp in [ + &pyre_object::STR_TYPE, + &pyre_object::INT_TYPE, + &pyre_object::BOOL_TYPE, + &pyre_object::LONG_TYPE, + &pyre_object::FLOAT_TYPE, + &pyre_object::bytesobject::BYTES_TYPE, + ] { + if pyre_object::is_exact_type(obj, tp) { + return crate::builtins::hash_value(obj); + } + } + } + INT_FOLD_DECLINE +} + +/// `ord(x)` for a one-code-point string or a one-byte bytes-like. Any other +/// length or type is the arm that raises, so it declines. +extern "C" fn jit_builtin_ord(obj: i64) -> i64 { + let obj = obj as PyObjectRef; + if obj.is_null() { + return INT_FOLD_DECLINE; + } + unsafe { + if pyre_object::is_exact_type(obj, &pyre_object::STR_TYPE) { + if pyre_object::w_str_len(obj) != 1 { + return INT_FOLD_DECLINE; + } + let Some(cp) = pyre_object::w_str_get_wtf8(obj).code_points().next() else { + return INT_FOLD_DECLINE; + }; + return cp.to_u32() as i64; + } + if pyre_object::bytesobject::is_bytes(obj) { + let data = pyre_object::bytesobject::bytes_like_data(obj); + if data.len() != 1 { + return INT_FOLD_DECLINE; + } + return data[0] as i64; + } + } + INT_FOLD_DECLINE +} + +/// `abs(x)` for an exact machine int or bool. `i64::MIN` is the value +/// `builtin_abs` promotes to a long, and it is also the decline sentinel, so +/// both directions leave through the same side exit. +extern "C" fn jit_builtin_abs_int(obj: i64) -> i64 { + let obj = obj as PyObjectRef; + if obj.is_null() { + return INT_FOLD_DECLINE; + } + unsafe { + if pyre_object::is_exact_type(obj, &pyre_object::BOOL_TYPE) { + return pyre_object::w_bool_get_value(obj) as i64; + } + // `is_int` reads `ob_type`, which a subclass instance shares with the + // builtin, so it alone would answer for one whose `__abs__` override + // the fold has no operand-class guard to notice. `is_exact_type` + // reads `w_class`, which the subclass retags -- and it also excludes + // `bool`, whose own arm sits above. Neither test implies the other: + // `is_exact_type` alone would admit a bigint, whose `*mut BigInt` sits + // where `intval` does. + if pyre_object::is_exact_type(obj, &pyre_object::INT_TYPE) && pyre_object::is_int(obj) { + return pyre_object::w_int_get_value(obj) + .checked_abs() + .unwrap_or(INT_FOLD_DECLINE); + } + } + INT_FOLD_DECLINE +} + +/// `abs(x)` for an exact float. A NaN operand declines through the same +/// channel a NaN result would, which is what the trace's finite guard reads. +extern "C" fn jit_builtin_abs_float(obj: i64) -> f64 { + let obj = obj as PyObjectRef; + if obj.is_null() { + return f64::NAN; + } + unsafe { + if pyre_object::is_exact_type(obj, &pyre_object::FLOAT_TYPE) { + return pyre_object::w_float_get_value(obj).abs(); + } + } + f64::NAN +} + +/// The comparison `builtin_min` / `builtin_max` reach for two positional +/// arguments, restricted to the operand pairs that compare without a +/// `__lt__`/`__gt__` dispatch. Returns the winning *object*, so ties keep the +/// argument the iteration order keeps. +fn compare_pair(a: PyObjectRef, b: PyObjectRef, want_second_wins: bool) -> Option { + unsafe { + let exact = |obj, tp| pyre_object::is_exact_type(obj, tp); + // `is_exact_type` answers on `w_class`, and `w_long_from_raw` wires a + // bigint's `w_class` to `int`'s so that `type(x) is int` holds for one + // — so the exact-`int` gate alone admits a `W_LongObject`, whose + // `value: *mut BigInt` sits exactly where `W_IntObject` keeps + // `intval`. Reading one as the other would compare heap addresses + // instead of numbers. `is_int` reads `ob_type`, which still separates + // the two layouts; this is the conjunct the dict's builtin-key test + // uses for the same reason. + let machine_int = |obj| exact(obj, &pyre_object::INT_TYPE) && pyre_object::is_int(obj); + if machine_int(a) && machine_int(b) { + let (a, b) = ( + pyre_object::w_int_get_value(a), + pyre_object::w_int_get_value(b), + ); + return Some(if want_second_wins { b > a } else { b < a }); + } + if exact(a, &pyre_object::FLOAT_TYPE) && exact(b, &pyre_object::FLOAT_TYPE) { + let (a, b) = ( + pyre_object::w_float_get_value(a), + pyre_object::w_float_get_value(b), + ); + // A NaN operand makes every comparison false, and which argument + // that leaves standing depends on the scan order; decline instead + // of encoding it here. + if a.is_nan() || b.is_nan() { + return None; + } + return Some(if want_second_wins { b > a } else { b < a }); + } + } + None +} + +/// `min(a, b)` — `builtin_min`'s two-positional form: keep the first argument +/// unless the second compares strictly smaller. +extern "C" fn jit_builtin_min2(a: i64, b: i64) -> i64 { + let (a, b) = (a as PyObjectRef, b as PyObjectRef); + if a.is_null() || b.is_null() { + return PY_NULL as i64; + } + match compare_pair(a, b, false) { + Some(true) => b as i64, + Some(false) => a as i64, + None => PY_NULL as i64, + } +} + +/// `max(a, b)` — the `builtin_max` twin of [`jit_builtin_min2`]. +extern "C" fn jit_builtin_max2(a: i64, b: i64) -> i64 { + let (a, b) = (a as PyObjectRef, b as PyObjectRef); + if a.is_null() || b.is_null() { + return PY_NULL as i64; + } + match compare_pair(a, b, true) { + Some(true) => b as i64, + Some(false) => a as i64, + None => PY_NULL as i64, + } +} + +/// Every folded builtin. Adding a row adds a fold: the walker looks the +/// callable up here and emits the channel the row names. +static BUILTIN_FOLDS: &[BuiltinFold] = &[ + BuiltinFold { + name: "hash", + code_fn: crate::builtins::builtin_hash, + raw: BuiltinFoldRaw::Int1(jit_builtin_hash), + }, + BuiltinFold { + name: "ord", + code_fn: crate::builtins::builtin_ord, + raw: BuiltinFoldRaw::Int1(jit_builtin_ord), + }, + BuiltinFold { + name: "abs", + code_fn: crate::builtins::__pyre_wrap_builtin_abs, + raw: BuiltinFoldRaw::Int1(jit_builtin_abs_int), + }, + BuiltinFold { + name: "abs", + code_fn: crate::builtins::__pyre_wrap_builtin_abs, + raw: BuiltinFoldRaw::Float1(jit_builtin_abs_float), + }, + BuiltinFold { + name: "min", + code_fn: crate::builtins::builtin_min, + raw: BuiltinFoldRaw::Ref2(jit_builtin_min2), + }, + BuiltinFold { + name: "max", + code_fn: crate::builtins::builtin_max, + raw: BuiltinFoldRaw::Ref2(jit_builtin_max2), + }, +]; + +/// The rows `callable` could be, in table order, for a call carrying `argc` +/// positional arguments. +/// +/// One builtin may hold several rows — `abs` has an int one and a float one — +/// and the walker takes the first whose helper answers for the recorded +/// operands. +pub fn builtin_folds_for( + callable: PyObjectRef, + argc: usize, +) -> impl Iterator { + let code_fn = unsafe { builtin_code_fn_of(callable) }; + BUILTIN_FOLDS.iter().filter(move |fold| { + fold.raw.arity() == argc + && code_fn.is_some_and(|found| unsafe { + crate::gateway::builtin_code_fn_eq(found, fold.code_fn) + }) + }) +} + +/// The `BuiltinCodeFn` a callable's wrapped code holds, or `None` when the +/// callable is not a builtin-code function at all. +unsafe fn builtin_code_fn_of(callable: PyObjectRef) -> Option { + unsafe { + if callable.is_null() || !crate::is_function(callable) { + return None; + } + let code = crate::function_get_code(callable) as PyObjectRef; + if code.is_null() || !crate::gateway::is_builtin_code(code) { + return None; + } + Some(crate::gateway::builtin_code_get(code)) + } +} + +/// Row count, for the walker's census to size its per-row tally. +pub const BUILTIN_FOLD_COUNT: usize = BUILTIN_FOLDS.len(); diff --git a/pyre/pyre-interpreter/src/lib.rs b/pyre/pyre-interpreter/src/lib.rs index 5dc02f2f43a..512fc621467 100644 --- a/pyre/pyre-interpreter/src/lib.rs +++ b/pyre/pyre-interpreter/src/lib.rs @@ -145,6 +145,7 @@ pub mod host_seam { } } pub mod async_operation; +pub mod jit_builtin_folds; pub mod jit_fnaddr; pub mod launch_env; pub mod listobject; diff --git a/pyre/pyre-interpreter/src/module/math/interp_math.rs b/pyre/pyre-interpreter/src/module/math/interp_math.rs index d5be4a36c6e..137ae9880c0 100644 --- a/pyre/pyre-interpreter/src/module/math/interp_math.rs +++ b/pyre/pyre-interpreter/src/module/math/interp_math.rs @@ -229,6 +229,10 @@ static MATH_SIN_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new() static MATH_FREXP_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); static MATH_LDEXP_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); static MATH_ISQRT_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); +static MATH_FABS_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); +static MATH_FLOOR_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); +static MATH_CEIL_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); +static MATH_TRUNC_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); /// Record the checked-arity wrapper pointers installed by `py_module!`. /// @@ -246,6 +250,11 @@ pub fn register_jit_builtin_wrappers(ns: PyObjectRef) { ("frexp", &MATH_FREXP_WRAPPER), ("ldexp", &MATH_LDEXP_WRAPPER), ("isqrt", &MATH_ISQRT_WRAPPER), + ("fabs", &MATH_FABS_WRAPPER), + ("floor", &MATH_FLOOR_WRAPPER), + ("ceil", &MATH_CEIL_WRAPPER), + ("trunc", &MATH_TRUNC_WRAPPER), + ("isclose", &MATH_ISCLOSE_WRAPPER), ] { let callable = crate::module_ns_get(ns, name) .unwrap_or_else(|| panic!("math.{name} missing after module registration")); @@ -257,6 +266,23 @@ pub fn register_jit_builtin_wrappers(ns: PyObjectRef) { let installed = slot.get_or_init(|| wrapper); debug_assert_eq!(*installed, wrapper); } + // The generic float folds are identified the same way; they differ only in + // that one table entry stands for one raw helper rather than one probe fn. + for (name, slot) in MATH_FLOAT1_FOLDS + .iter() + .map(|fold| (fold.name, &fold.slot)) + .chain(MATH_FLOAT2_FOLDS.iter().map(|fold| (fold.name, &fold.slot))) + { + let callable = crate::module_ns_get(ns, name) + .unwrap_or_else(|| panic!("math.{name} missing after module registration")); + let wrapper = unsafe { + let code = crate::function_get_code(callable) as PyObjectRef; + debug_assert!(crate::gateway::is_builtin_code(code)); + crate::gateway::builtin_code_get(code) as usize + }; + let installed = slot.get_or_init(|| wrapper); + debug_assert_eq!(*installed, wrapper); + } } unsafe fn math_builtin_wrapper_matches( @@ -312,6 +338,34 @@ pub fn is_math_isqrt_function(callable: PyObjectRef) -> bool { unsafe { math_builtin_wrapper_matches(callable, &MATH_ISQRT_WRAPPER) } } +pub fn is_math_fabs_function(callable: PyObjectRef) -> bool { + unsafe { math_builtin_wrapper_matches(callable, &MATH_FABS_WRAPPER) } +} + +pub fn is_math_floor_function(callable: PyObjectRef) -> bool { + unsafe { math_builtin_wrapper_matches(callable, &MATH_FLOOR_WRAPPER) } +} + +pub fn is_math_ceil_function(callable: PyObjectRef) -> bool { + unsafe { math_builtin_wrapper_matches(callable, &MATH_CEIL_WRAPPER) } +} + +pub fn is_math_trunc_function(callable: PyObjectRef) -> bool { + unsafe { math_builtin_wrapper_matches(callable, &MATH_TRUNC_WRAPPER) } +} + +/// Raw counterparts of `ll_math_floor` / `ll_math_ceil` for a guarded JIT fast +/// path. Both are total on finite input and cannot raise, so the walker emits +/// them as pure elidable calls; the trace guards the rounded value into the +/// machine range before casting. +pub extern "C" fn jit_math_floor_raw(x: f64) -> f64 { + x.floor() +} + +pub extern "C" fn jit_math_ceil_raw(x: f64) -> f64 { + x.ceil() +} + /// Raw, allocation-free counterparts of RPython's `ll_math_frexp` result /// components. The translated PyPy trace carries the pair as two unboxed /// values before `space.newtuple2` virtualizes; pyre's walker emits one pure @@ -370,6 +424,205 @@ pub extern "C" fn jit_math_isqrt_i64(n: i64) -> i64 { root as i64 } +// ── raw helpers and identity table for the generic float folds ─────── + +/// One entry of the walker's generic `math` fold table: the module +/// attribute's checked-arity wrapper pointer and the raw helper that computes +/// the same value without boxing. +/// +/// Each helper answers exactly what the builtin body would compute for the +/// same `f64` operands, and reports the builtin's raising directions as NaN. +/// The walker guards the result finite, so a NaN — whether it came from a +/// raising direction or from a genuine NaN result — resumes in the builtin +/// and reproduces the interpreter's answer. That makes the fold sound for +/// every operand without the walker knowing any function's domain. +pub struct MathFloatFold { + name: &'static str, + slot: std::sync::OnceLock, + raw: Raw, +} + +pub type MathFloat1Fold = MathFloatFold f64>; +pub type MathFloat2Fold = MathFloatFold f64>; + +/// Raw counterpart of a `pm1!`/`pm1_edom!` body: the same `pymath` call, with +/// every error direction reported as NaN. +macro_rules! jit_raw1 { + ($helper:ident, $name:ident) => { + pub extern "C" fn $helper(x: f64) -> f64 { + match pymath::math::$name(x) { + Ok(v) => v, + Err(_) => f64::NAN, + } + } + }; +} + +/// Raw counterpart of a `pm1_plain!` body, which has no error direction. +macro_rules! jit_raw1_plain { + ($helper:ident, $name:ident) => { + pub extern "C" fn $helper(x: f64) -> f64 { + pymath::math::$name(x) + } + }; +} + +macro_rules! jit_raw2 { + ($helper:ident, $name:ident) => { + pub extern "C" fn $helper(x: f64, y: f64) -> f64 { + match pymath::math::$name(x, y) { + Ok(v) => v, + Err(_) => f64::NAN, + } + } + }; +} + +jit_raw1!(jit_math_tan, tan); +jit_raw1!(jit_math_asin, asin); +jit_raw1!(jit_math_acos, acos); +jit_raw1!(jit_math_atan, atan); +jit_raw1!(jit_math_sinh, sinh); +jit_raw1!(jit_math_cosh, cosh); +jit_raw1!(jit_math_tanh, tanh); +jit_raw1!(jit_math_asinh, asinh); +jit_raw1!(jit_math_acosh, acosh); +jit_raw1!(jit_math_atanh, atanh); +jit_raw1!(jit_math_cbrt, cbrt); +jit_raw1!(jit_math_exp, exp); +jit_raw1!(jit_math_exp2, exp2); +jit_raw1!(jit_math_expm1, expm1); +jit_raw1!(jit_math_log1p, log1p); +jit_raw1!(jit_math_erf, erf); +jit_raw1!(jit_math_erfc, erfc); +jit_raw1!(jit_math_gamma, gamma); +jit_raw1!(jit_math_lgamma, lgamma); +jit_raw1_plain!(jit_math_ulp, ulp); +jit_raw1_plain!(jit_math_degrees, degrees); +jit_raw1_plain!(jit_math_radians, radians); + +jit_raw2!(jit_math_pow, pow); +jit_raw2!(jit_math_fmod, fmod); +jit_raw2!(jit_math_copysign, copysign); +jit_raw2!(jit_math_remainder, remainder); +jit_raw2!(jit_math_atan2, atan2); + +/// Raw `math.isclose(a, b)` with both keyword tolerances left at their +/// defaults, `rel_tol=1e-09` and `abs_tol=0.0`. +/// +/// Spelled out rather than delegated to `pymath::math::isclose` so that it is +/// total: the only rejection the builtin has is a negative tolerance, which +/// no default is, so this form has no error direction to report and the +/// walker can read the answer as a plain truth value. Identical operands +/// (including two infinities of the same sign) compare close; a single +/// infinity and any NaN do not. +pub extern "C" fn jit_math_isclose_default(a: f64, b: f64) -> i64 { + const REL_TOL: f64 = 1e-09; + if a == b { + return 1; + } + if a.is_infinite() || b.is_infinite() { + return 0; + } + let diff = (b - a).abs(); + i64::from(diff <= (REL_TOL * b).abs() || diff <= (REL_TOL * a).abs()) +} + +static MATH_ISCLOSE_WRAPPER: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// True iff `callable` is the canonical builtin `math.isclose`. +pub fn is_math_isclose_function(callable: PyObjectRef) -> bool { + unsafe { math_builtin_wrapper_matches(callable, &MATH_ISCLOSE_WRAPPER) } +} + +macro_rules! math_fold_table { + ($table:ident: $entry:ty, $($name:literal => $helper:ident),* $(,)?) => { + // The length is spelled out from the entry list rather than left to a + // slice reference: a `static` holding `&[..]` extends the temporary's + // lifetime, and an entry's `OnceLock` makes that temporary interior + // mutable, which a static borrow may not refer to. + static $table: [$entry; [$(stringify!($helper)),*].len()] = [ + $(MathFloatFold { + name: $name, + slot: std::sync::OnceLock::new(), + raw: $helper, + }),* + ]; + }; +} + +/// `sqrt`, `log`, `cos`, `sin` and `fabs` are absent: each has a dedicated +/// specialization that lowers to a tighter shape (a domain-guarded call with +/// no result guard, or a single `FloatAbs`). +math_fold_table!( + MATH_FLOAT1_FOLDS: MathFloat1Fold, + "tan" => jit_math_tan, + "asin" => jit_math_asin, + "acos" => jit_math_acos, + "atan" => jit_math_atan, + "sinh" => jit_math_sinh, + "cosh" => jit_math_cosh, + "tanh" => jit_math_tanh, + "asinh" => jit_math_asinh, + "acosh" => jit_math_acosh, + "atanh" => jit_math_atanh, + "cbrt" => jit_math_cbrt, + "exp" => jit_math_exp, + "exp2" => jit_math_exp2, + "expm1" => jit_math_expm1, + "log1p" => jit_math_log1p, + "erf" => jit_math_erf, + "erfc" => jit_math_erfc, + "gamma" => jit_math_gamma, + "lgamma" => jit_math_lgamma, + "ulp" => jit_math_ulp, + "degrees" => jit_math_degrees, + "radians" => jit_math_radians, +); + +math_fold_table!( + MATH_FLOAT2_FOLDS: MathFloat2Fold, + "pow" => jit_math_pow, + "fmod" => jit_math_fmod, + "copysign" => jit_math_copysign, + "remainder" => jit_math_remainder, + "atan2" => jit_math_atan2, +); + +/// The wrapper pointer `py_module!` installed for `math.`, or `None` +/// for a callable that is not a builtin function. +unsafe fn builtin_wrapper_addr(callable: PyObjectRef) -> Option { + unsafe { + if callable.is_null() || !crate::is_function(callable) { + return None; + } + let code = crate::function_get_code(callable) as PyObjectRef; + if code.is_null() || !crate::gateway::is_builtin_code(code) { + return None; + } + Some(crate::gateway::builtin_code_get(code) as usize) + } +} + +/// The raw 1-arg helper for `callable`, or `None` when it is not one of the +/// canonical `math` builtins in [`MATH_FLOAT1_FOLDS`]. A value rebound under +/// the same name carries a different code object and declines here. +pub fn math_float1_fold_helper(callable: PyObjectRef) -> Option f64> { + let wrapper = unsafe { builtin_wrapper_addr(callable)? }; + MATH_FLOAT1_FOLDS + .iter() + .find(|fold| fold.slot.get() == Some(&wrapper)) + .map(|fold| fold.raw) +} + +pub fn math_float2_fold_helper(callable: PyObjectRef) -> Option f64> { + let wrapper = unsafe { builtin_wrapper_addr(callable)? }; + MATH_FLOAT2_FOLDS + .iter() + .find(|fold| fold.slot.get() == Some(&wrapper)) + .map(|fold| fold.raw) +} + pm1!(cbrt); pm1!(exp); pm1!(exp2); @@ -479,21 +732,26 @@ fn math_unary_int( "{fname}() takes exactly 1 argument", ))); } - // If the descriptor itself raises (e.g. BadDescr.__get__ → ValueError), - // propagate that error rather than silently falling back to float. - match unsafe { crate::baseobjspace::lookup_special(args[0], dunder) } { - Ok(Some(method)) => { - crate::call::clear_call_error(); - let result = crate::call_function(method, &[]); - if !result.is_null() { - return Ok(result); - } - if let Some(err) = crate::call::take_call_error() { - return Err(err); - } - } - Ok(None) => {} - Err(err) => return Err(err), + // `interp_math.py`'s `floor` / `ceil` / `trunc`: + // + // w_descr = space.lookup(w_x, '__floor__') + // if w_descr is not None: + // return space.get_and_call_function(w_descr, w_x) + // + // The unbound descriptor is called with the object leading the + // positionals, so a plain `float` argument does not pay for a bound method + // object per call. A descriptor whose `__get__` itself raises (e.g. + // BadDescr.__get__ → ValueError) still propagates that error: + // `get_and_call_function` binds through `get` for every descriptor other + // than a function or method descriptor, and neither of those runs user + // code to bind. `lookup` reads the type MRO only, so an instance + // attribute of the same name stays ignored. + if let Some(w_descr) = unsafe { crate::baseobjspace::lookup(args[0], dunder) } + && let Some(w_type) = crate::typedef::r#type(args[0]) + { + return unsafe { + crate::baseobjspace::get_and_call_function(w_descr, args[0], w_type.as_ptr(), &[]) + }; } if !fallback_float { return Err(crate::PyError::type_error(format!( @@ -507,11 +765,18 @@ fn math_unary_int( // `__float__`, or the `OverflowError` for an int too wide for an f64 — // propagates; relabelling here would swallow exactly those. let v = try_get_double(args[0])?; - Ok(w_int_new(match dunder { - "__ceil__" => v.ceil() as i64, - "__floor__" => v.floor() as i64, - _ => v.trunc() as i64, - })) + // `float_to_pyint` is `newlong_from_float`: NaN raises, an infinity raises, + // and a finite value outside the machine range becomes a long. A direct + // `as i64` saturates instead, so `floor(FloatLike(1e300))` answered + // `i64::MAX` and `floor(FloatLike(nan))` answered `0`. + crate::typedef::float_to_pyint( + v, + match dunder { + "__ceil__" => crate::typedef::FloatToIntMode::Ceil, + "__floor__" => crate::typedef::FloatToIntMode::Floor, + _ => crate::typedef::FloatToIntMode::Trunc, + }, + ) } pub fn floor(args: &[PyObjectRef]) -> PyResult { @@ -728,6 +993,14 @@ pub fn isclose(args: &[PyObjectRef]) -> PyResult { } // `rel_tol` and `abs_tol` are the only (keyword-only) parameters. crate::builtins::kwarg_reject_unknown(kwargs, &["rel_tol", "abs_tol"], "isclose")?; + // `interp_math.py`'s `isclose` — all four operands are converted, in this + // order, before anything about them is checked, so a non-numeric `a` is + // reported even when a tolerance is negative. An omitted tolerance + // arrives upstream as an already-wrapped float, so converting it can + // neither raise nor reach `__float__`; `None` stands in for that here and + // `pymath` supplies the same defaults. + let a = try_get_double(pos[0])?; + let b = try_get_double(pos[1])?; let read = |name: &str| -> Result, crate::PyError> { match crate::builtins::kwarg_get(kwargs, name) { Some(v) => Ok(Some(try_get_double(v)?)), @@ -736,12 +1009,16 @@ pub fn isclose(args: &[PyObjectRef]) -> PyResult { }; let rel_tol = read("rel_tol")?; let abs_tol = read("abs_tol")?; - match pymath::math::isclose( - try_get_double(pos[0])?, - try_get_double(pos[1])?, - rel_tol, - abs_tol, - ) { + // `isclose` — the sanity check on the tolerances runs + // after those conversions and before the comparison, and names them. + // `pymath` reports the same rejection as EDOM, which `map_int_err` + // relabels "math domain error". + if rel_tol.is_some_and(|t| t < 0.0) || abs_tol.is_some_and(|t| t < 0.0) { + return Err(crate::PyError::value_error( + "tolerances must be non-negative", + )); + } + match pymath::math::isclose(a, b, rel_tol, abs_tol) { Ok(v) => Ok(w_bool_from(v)), Err(e) => Err(map_int_err(e)), } @@ -876,8 +1153,28 @@ fn get_bigint(obj: PyObjectRef) -> Result { )) } +/// `space.abs(space.index(w))` in the machine-word domain. `None` is +/// `gcd_two`'s `except OverflowError` direction, which replays the +/// pair in the rbigint domain: `is_long` values never fit, and `i64::MIN` is +/// the one machine int whose absolute value leaves the range. +fn index_abs_machine_word(obj: PyObjectRef) -> Option { + if !unsafe { pyre_object::is_int(obj) } { + return None; + } + unsafe { pyre_object::w_int_get_value(obj) }.checked_abs() +} + pub fn gcd(args: &[PyObjectRef]) -> PyResult { let args = no_keywords(args, "gcd")?; + // `interp_math.py`'s `gcd_two` reads both operands as Signed and only + // falls back to rbigint when one overflows. Taking the pair through + // `get_bigint` unconditionally allocates five digit blocks and runs a + // divmod to reduce two machine words. + if let [a, b] = args + && let (Some(a), Some(b)) = (index_abs_machine_word(*a), index_abs_machine_word(*b)) + { + return Ok(w_int_new(majit_rlib::rbigint::gcd_binary(a, b))); + } // RPython's GC transform roots this running rbigint across the next // argument's potentially user-defined `__index__` call. let mut result = RBigIntGcRoot::new(BigInt::zero()); @@ -928,12 +1225,71 @@ fn bigint_to_pyint(b: &BigInt) -> PyObjectRef { } } +/// The value of an operand that is already a machine int. `None` covers +/// everything `space.index` would have to run to answer — a long, or an +/// object with `__index__`. +fn machine_word_int(obj: PyObjectRef) -> Option { + unsafe { pyre_object::is_int(obj).then(|| pyre_object::w_int_get_value(obj)) } +} + +/// `comb(n, k)` in the machine-word domain, for `0 <= k <= n`. +/// +/// `None` is the direction that replays the pair in the rbigint domain: an +/// intermediate that leaves the machine range. Each step is the exact +/// `C(n, i-1) * (n - i + 1) / i = C(n, i)`, so the running value is a real +/// binomial coefficient throughout and only the last multiplication before +/// the answer itself grows out of range can overflow. +fn comb_machine_word(n: i64, k: i64) -> Option { + let k = k.min(n - k); + let mut result: i64 = 1; + for i in 1..=k { + result = result.checked_mul(n - i + 1)?; + result /= i; + } + Some(result) +} + +/// `perm(n, k)` in the machine-word domain, for `0 <= k <= n`: the falling +/// factorial `n * (n-1) * ... * (n-k+1)`. `None` replays the pair in the +/// rbigint domain. +fn perm_machine_word(n: i64, k: i64) -> Option { + let mut result: i64 = 1; + for i in 0..k { + result = result.checked_mul(n - i)?; + } + Some(result) +} + pub fn comb(args: &[PyObjectRef]) -> PyResult { if args.len() != 2 { return Err(crate::PyError::type_error( "comb() takes exactly two arguments", )); } + // `get_bigint` allocates a digit block per operand before the reduction + // below allocates another per multiplication and per divmod. A pair of + // machine ints answers the same value with neither. The two rejections + // keep their order, so `comb(-1, -1)` still names `n`. + if let [n, k] = args + && let (Some(n), Some(k)) = (machine_word_int(*n), machine_word_int(*k)) + { + if n < 0 { + return Err(crate::PyError::value_error( + "n must be a non-negative integer", + )); + } + if k < 0 { + return Err(crate::PyError::value_error( + "k must be a non-negative integer", + )); + } + if k > n { + return Ok(w_int_new(0)); + } + if let Some(result) = comb_machine_word(n, k) { + return Ok(w_int_new(result)); + } + } // `n` is an unboxed rbigint local across `index(k)`, exactly the kind of // local rooted automatically by RPython's GC transform. let n_big = RBigIntGcRoot::new(get_bigint(args[0])?); @@ -995,6 +1351,34 @@ pub fn perm(args: &[PyObjectRef]) -> PyResult { "perm() takes at most 2 arguments", )); } + // `perm(n, k)` over machine ints whose falling factorial stays in range + // answers without a digit block per multiplication, the same way `comb` + // does. `perm(n)` and `perm(n, None)` mean k = n, which only fits for + // n <= 20 and otherwise falls through. + if let Some(n) = args.first().copied().and_then(machine_word_int) + && let Some(k) = match args.get(1).copied() { + None => Some(n), + Some(k) if unsafe { pyre_object::is_none(k) } => Some(n), + Some(k) => machine_word_int(k), + } + { + if n < 0 { + return Err(crate::PyError::value_error( + "n must be a non-negative integer", + )); + } + if k < 0 { + return Err(crate::PyError::value_error( + "k must be a non-negative integer", + )); + } + if k > n { + return Ok(w_int_new(0)); + } + if let Some(result) = perm_machine_word(n, k) { + return Ok(w_int_new(result)); + } + } // Keep `n` rooted while a non-None `k` invokes its `__index__`. let n_big = RBigIntGcRoot::new(get_bigint(args[0])?); if n_big.int_lt(0) { diff --git a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs index ce2487683a6..cad4365754f 100644 --- a/pyre/pyre-interpreter/src/objspace/std/mapdict.rs +++ b/pyre/pyre-interpreter/src/objspace/std/mapdict.rs @@ -3426,8 +3426,17 @@ unsafe fn is_unboxable_int(w_value: PyObjectRef) -> bool { /// Float half of `_pick_unbox_type`: PyPy uses /// `type(w_value) is space.FloatObjectCls`, so a float subclass must retain /// its boxed object and `w_class` too. -unsafe fn is_unboxable_float(w_value: PyObjectRef) -> bool { - if !unsafe { pyre_object::is_float(w_value) } { +/// +/// NaNs also stay boxed: raw-f64 storage reboxes on read and would lose their +/// CPython 3.14 pointer identity. Shared with the JIT STORE_ATTR fold and used +/// for both new and existing unboxed slots. +/// +/// # Safety +/// `w_value` must point to a live object. +pub unsafe fn is_unboxable_float(w_value: PyObjectRef) -> bool { + if !unsafe { pyre_object::is_float(w_value) } + || unsafe { pyre_object::w_float_get_value(w_value) }.is_nan() + { return false; } let exact = crate::typedef::gettypeobject(&pyre_object::FLOAT_TYPE); diff --git a/pyre/pyre-interpreter/src/type_methods.rs b/pyre/pyre-interpreter/src/type_methods.rs index fb7417a4eb8..c3cf6aff48f 100644 --- a/pyre/pyre-interpreter/src/type_methods.rs +++ b/pyre/pyre-interpreter/src/type_methods.rs @@ -313,7 +313,7 @@ pub(crate) fn arity_pow(args: &[PyObjectRef], name: &str) -> Result<(), crate::P pub fn number_dunder_round(args: &[PyObjectRef]) -> Result { require_receiver(args, "__round__")?; arity_at_most(args, "__round__", 1)?; - crate::builtins::builtin_round(args) + crate::builtins::builtin_round_dunder(args) } /// TypeError for an unbound method descriptor invoked with no receiver diff --git a/pyre/pyre-interpreter/src/typedef.rs b/pyre/pyre-interpreter/src/typedef.rs index dd8a866c564..b6629668fb7 100644 --- a/pyre/pyre-interpreter/src/typedef.rs +++ b/pyre/pyre-interpreter/src/typedef.rs @@ -18612,14 +18612,18 @@ fn init_int_type(ns: PyObjectRef) { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__float__", - make_builtin_function_with_arity("__float__", crate::builtins::builtin_float, 1), + make_builtin_function_with_arity( + "__float__", + crate::builtins::builtin_int_float_dunder, + 1, + ), ) }; unsafe { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__abs__", - make_builtin_function_with_arity("__abs__", crate::builtins::builtin_abs, 1), + make_builtin_function_with_arity("__abs__", crate::builtins::builtin_abs_dunder, 1), ) }; unsafe { @@ -19478,7 +19482,7 @@ fn init_float_type(ns: PyObjectRef) { pyre_object::dictmultiobject::w_dict_setitem_str_no_proxy( ns, "__abs__", - make_builtin_function_with_arity("__abs__", crate::builtins::builtin_abs, 1), + make_builtin_function_with_arity("__abs__", crate::builtins::builtin_abs_dunder, 1), ) }; unsafe { @@ -19759,6 +19763,16 @@ pub(crate) fn float_to_pyint(v: f64, mode: FloatToIntMode) -> Result v.floor(), FloatToIntMode::Ceil => v.ceil(), }; + // `floatobject.py`'s `newint_from_float` reaches for + // `ovfcheck_float_to_int` first and only materialises a long when that + // overflows. `2**63` is exactly representable while `i64::MAX` is not, so + // the upper bound is strict — the same pair the `int(x)` walker + // specialization uses. + const SIGNED_MIN_AS_FLOAT: f64 = -9223372036854775808.0; + const SIGNED_LIMIT_AS_FLOAT: f64 = 9223372036854775808.0; + if reduced >= SIGNED_MIN_AS_FLOAT && reduced < SIGNED_LIMIT_AS_FLOAT { + return Ok(pyre_object::w_int_new(reduced as i64)); + } use num_traits::FromPrimitive; let big = BigInt::from_f64(reduced).expect("finite already checked"); if pyre_object::jit_bigint_to_i64_fits(&big) != 0 { diff --git a/pyre/pyre-jit-trace/src/driver.rs b/pyre/pyre-jit-trace/src/driver.rs index 1545b52446e..5e0d1886dd1 100644 --- a/pyre/pyre-jit-trace/src/driver.rs +++ b/pyre/pyre-jit-trace/src/driver.rs @@ -7,16 +7,20 @@ use crate::callbacks; use crate::state::PyreJitState; /// RPython green_key = pypyjit greens `[next_instr, is_being_profiled, -/// pycode]` (interp_jit.py:67-70). pyre's portal greens are always exactly -/// `(PyCode*, next_instr)`, and the JIT path never runs under a profiler, -/// so `is_being_profiled` folds to 0 — the trace-side call sites have no -/// frame to read it from. The returned u64 is the full +/// pycode]` (interp_jit.py). `is_being_profiled` is read from the running +/// execution context (`current_is_being_profiled`) rather than folded to 0, +/// so installing a profiler selects a different cell instead of sharing the +/// unprofiled one. The returned u64 is the full /// `JitCell.get_uhash` over the typed green tuple (warmstate.py), /// so this legacy hash flow and the typed marker-path lookup /// (`lookup_chain_with_key`) agree on the same cell. #[inline(always)] pub fn make_green_key(code_ptr: *const (), pc: usize) -> u64 { - majit_ir::pypyjit_greenkey_uhash(pc, false, code_ptr as u64) + majit_ir::pypyjit_greenkey_uhash( + pc, + pyre_interpreter::executioncontext::current_is_being_profiled(), + code_ptr as u64, + ) } /// The typed form of [`make_green_key`]: the greens themselves, not a fold of @@ -28,7 +32,11 @@ pub fn make_green_key(code_ptr: *const (), pc: usize) -> u64 { /// without a comparekey, where no later typed lookup can find it. The u64 form /// stays correct for calls that only move a counter. pub fn make_green_key_typed(code_ptr: *const (), pc: usize) -> majit_ir::GreenKey { - majit_ir::pypyjit_greenkey(pc, false, code_ptr as u64) + majit_ir::pypyjit_greenkey( + pc, + pyre_interpreter::executioncontext::current_is_being_profiled(), + code_ptr as u64, + ) } /// Type alias for the JIT driver pair. Must match pyre-jit/eval.rs JitDriverPair. diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs index 34a34d886ed..028635155fb 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/diag.rs @@ -266,7 +266,7 @@ pub fn skip_python_trivia_forward(code: &pyre_interpreter::CodeObject, mut py_pc /// `parent` marks the second row as a split of the first so the reader does not /// sum them. #[rustfmt::skip] -pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 60] = [ +pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 69] = [ // (label, site, parent) ("truth_int", "residual_call", "-"), ("truth_bool", "residual_call", "-"), @@ -298,6 +298,15 @@ pub const SPEC_FOLD_ROWS: [(&str, &str, &str); 60] = [ ("math_frexp", "residual_call", "-"), ("math_ldexp", "residual_call", "-"), ("math_isqrt", "residual_call", "-"), + ("math_fabs", "residual_call", "-"), + ("math_float1", "residual_call", "-"), + ("math_float2", "residual_call", "-"), + ("math_isclose", "residual_call", "-"), + ("builtin_fold1", "residual_call", "-"), + ("builtin_fold2", "residual_call", "-"), + ("math_floor", "residual_call", "-"), + ("math_ceil", "residual_call", "-"), + ("math_trunc", "residual_call", "-"), ("int_call", "residual_call", "-"), ("float_call", "residual_call", "-"), ("builtin_divmod", "residual_call", "-"), @@ -458,10 +467,10 @@ pub fn fbw_depth_census_summary() -> String { /// selector tokens. The reserved `all` token turns off every /// `SPEC_FOLD_ROWS` row and nothing else: the `try_walker_fold_*` trio /// and the `try_walker_inline_*` descent entry points all stay live. -fn spec_suppression() -> &'static (u64, Vec) { - static SUPPRESSION: std::sync::OnceLock<(u64, Vec)> = std::sync::OnceLock::new(); +fn spec_suppression() -> &'static (SpecMask, Vec) { + static SUPPRESSION: std::sync::OnceLock<(SpecMask, Vec)> = std::sync::OnceLock::new(); SUPPRESSION.get_or_init(|| { - let mut mask = 0u64; + let mut mask = SpecMask::none(); let mut unknown = Vec::new(); if let Some(selectors) = std::env::var_os("PYRE_FBW_NO_SPECIALIZE") { for selector in selectors.to_string_lossy().split(',') { @@ -470,9 +479,9 @@ fn spec_suppression() -> &'static (u64, Vec) { continue; } if selector == "all" { - mask = (1u64 << SPEC_FOLD_COUNT) - 1; + mask = SpecMask::all(); } else if let Some(idx) = spec_row_index(selector) { - mask |= 1u64 << idx; + mask.set(idx); } else { unknown.push(selector.to_owned()); } @@ -482,7 +491,39 @@ fn spec_suppression() -> &'static (u64, Vec) { }) } -fn spec_suppressed_mask() -> u64 { +/// One bit per [`SPEC_FOLD_ROWS`] entry. A single `u64` held the whole table +/// until the row count reached its width; the shape is the same, spread over +/// as many words as the table needs. +#[derive(Clone, Copy)] +struct SpecMask([u64; SPEC_FOLD_COUNT.div_ceil(u64::BITS as usize)]); + +impl SpecMask { + fn none() -> Self { + Self([0; SPEC_FOLD_COUNT.div_ceil(u64::BITS as usize)]) + } + + fn all() -> Self { + let mut mask = Self::none(); + for idx in 0..SPEC_FOLD_COUNT { + mask.set(idx); + } + mask + } + + fn set(&mut self, idx: usize) { + self.0[idx / u64::BITS as usize] |= 1u64 << (idx % u64::BITS as usize); + } + + fn get(&self, idx: usize) -> bool { + self.0[idx / u64::BITS as usize] & (1u64 << (idx % u64::BITS as usize)) != 0 + } + + fn is_empty(&self) -> bool { + self.0.iter().all(|word| *word == 0) + } +} + +fn spec_suppressed_mask() -> SpecMask { spec_suppression().0 } @@ -494,7 +535,7 @@ fn spec_suppress_unknown() -> &'static [String] { /// pays one cached load and one predictable branch per consult. fn spec_instrumented() -> bool { static ON: std::sync::OnceLock = std::sync::OnceLock::new(); - *ON.get_or_init(|| fbw_spec_census_enabled() || spec_suppressed_mask() != 0) + *ON.get_or_init(|| fbw_spec_census_enabled() || !spec_suppressed_mask().is_empty()) } fn spec_row_index(name: &str) -> Option { @@ -531,7 +572,7 @@ pub(crate) fn spec_gate( SPEC_UNKNOWN.fetch_add(1, std::sync::atomic::Ordering::Relaxed); return call(); }; - if spec_suppressed_mask() & (1u64 << idx) != 0 { + if spec_suppressed_mask().get(idx) { SPEC_SUPPRESSED[idx].fetch_add(1, std::sync::atomic::Ordering::Relaxed); return Ok(None); } @@ -570,7 +611,7 @@ pub(super) fn spec_gate_store_attr( return call(); }; let mask = spec_suppressed_mask(); - if mask & ((1u64 << direct_idx) | (1u64 << residual_idx)) != 0 { + if mask.get(direct_idx) || mask.get(residual_idx) { SPEC_SUPPRESSED[direct_idx].fetch_add(1, std::sync::atomic::Ordering::Relaxed); SPEC_SUPPRESSED[residual_idx].fetch_add(1, std::sync::atomic::Ordering::Relaxed); return Ok(None); @@ -621,13 +662,13 @@ pub fn spec_census_summary() -> String { let consulted_total: u64 = rows.iter().map(|row| row.3).sum(); let fired_total: u64 = rows.iter().map(|row| row.4).sum(); let suppressed_total: u64 = rows.iter().map(|row| row.5).sum(); - let suppressed_names = if mask == 0 { + let suppressed_names = if mask.is_empty() { "-".to_owned() } else { SPEC_FOLD_ROWS .iter() .enumerate() - .filter_map(|(idx, (label, _, _))| (mask & (1u64 << idx) != 0).then_some(*label)) + .filter_map(|(idx, (label, _, _))| mask.get(idx).then_some(*label)) .collect::>() .join(",") }; diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs index f71c6b953aa..0397ab15752 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs @@ -8517,6 +8517,16 @@ fn walker_float_cmp_guard( walker_emit_guard_with_snapshot(ctx, op_pc, guard, &[c]) } +/// Guard the non-NaN precondition of raw-f64 storage. A class guard alone +/// admits NaNs; `raw != raw` sends them back to the boxed interpreter path. +fn walker_guard_float_not_nan( + ctx: &mut WalkContext<'_, '_, Sym>, + op_pc: usize, + raw: OpRef, +) -> Result<(), DispatchError> { + walker_float_cmp_guard(ctx, op_pc, OpCode::FloatNe, &[raw, raw], false) +} + /// Inline trace of `_pow` (floatobject.py, ported as /// `float_pow_inner`) for its fast paths: `y == 2.0` (`float_mul`), /// `y == 0.0` / `bx == 1.0` (constant result), and the mainstream 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 65f9715b7ff..d688d23f86f 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/residual_call.rs @@ -6109,6 +6109,117 @@ pub(crate) fn dispatch_residual_call_iRd_kind( { return Ok((DispatchOutcome::Continue, op.next_pc)); } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_fabs", || { + try_walker_specialize_math_fabs(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_float1", || { + try_walker_specialize_math_float1(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_float2", || { + try_walker_specialize_math_float2(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_isclose", || { + try_walker_specialize_math_isclose(ctx, code, op, &r_args, dst, dst_bank) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_fold1", || { + try_walker_specialize_builtin_fold1(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("builtin_fold2", || { + try_walker_specialize_builtin_fold2(ctx, code, op, &r_args, dst) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_floor", || { + try_walker_specialize_math_round_to_int( + ctx, + code, + op, + &r_args, + dst, + MathRoundMode::Floor, + ) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_ceil", || { + try_walker_specialize_math_round_to_int( + ctx, + code, + op, + &r_args, + dst, + MathRoundMode::Ceil, + ) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } + if ctx.is_authoritative_executor + && dst_bank == 'r' + && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn + && spec_gate("math_trunc", || { + try_walker_specialize_math_round_to_int( + ctx, + code, + op, + &r_args, + dst, + MathRoundMode::Trunc, + ) + })? + .is_some() + { + return Ok((DispatchOutcome::Continue, op.next_pc)); + } if ctx.is_authoritative_executor && dst_bank == 'r' && ei.pyre_helper == majit_ir::PyreHelperKind::CallFn diff --git a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs index 5b0203dab84..e26a53712c9 100644 --- a/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs +++ b/pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs @@ -3847,9 +3847,11 @@ pub(crate) fn try_walker_specialize_store_attr( } } pyre_interpreter::objspace::std::mapdict::UnboxType::Float => { - // A non-float changes the slot to boxed storage and freezes further - // unboxing (mapdict.py), so retain setattr. - if !unsafe { pyre_object::pyobject::is_float(concrete_value) } { + // Match mapdict.py `_direct_write` exactly: subclasses and + // NaNs convert the slot to boxed storage. + if !unsafe { + pyre_interpreter::objspace::std::mapdict::is_unboxable_float(concrete_value) + } { return Ok(None); } } @@ -3883,6 +3885,7 @@ pub(crate) fn try_walker_specialize_store_attr( let live_f = unsafe { pyre_object::w_float_get_value(concrete_value) }; ctx.trace_ctx .set_opref_concrete(raw, majit_ir::Value::Float(live_f)); + walker_guard_float_not_nan(ctx, op_pc, raw)?; ( crate::helpers::jit_mapdict_unboxed_write_f as *const (), raw, @@ -4332,11 +4335,16 @@ pub(crate) fn try_walker_specialize_newlist( Emit::Int(vals) } ListStrategy::Float => { - // `all_floats` is strict `type(w) is W_FloatObject`, so every - // element is an exact `W_FloatObject` (`walker_unbox_float`'s - // `&FLOAT_TYPE` guard holds). + // `list_strategy_for` admits only exact, non-NaN floats here. Its + // subclass term is enforced on replay by pinning each element's + // `w_class`; `is_plain_float_strict` also admits the null spelling + // of "exact float", which no pin can express, so decline such an + // element rather than emit a guard it would fail itself. let mut vals = Vec::with_capacity(len); for &p in &concretes { + if unsafe { walker_exact_builtin_class(p) }.is_none() { + return Ok(None); + } vals.push(unsafe { pyre_object::w_float_get_value(p) }); } Emit::Float(vals) @@ -4391,6 +4399,17 @@ pub(crate) fn try_walker_specialize_newlist( let raw = walker_unbox_float(ctx, op_pc, it, float_type_addr)?; ctx.trace_ctx .set_opref_concrete(raw, majit_ir::Value::Float(v)); + // `walker_unbox_float` guards `ob_type` only, which a float + // SUBCLASS instance shares; pin `w_class` so it side-exits + // instead of being unboxed into Float storage the interpreter + // would have declined (`all_floats` is strict). + walker_guard_exact_w_class( + ctx, + op_pc, + it, + pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::FLOAT_TYPE), + )?; + walker_guard_float_not_nan(ctx, op_pc, raw)?; raws.push(raw); } crate::helpers::emit_typed_list_inline( @@ -10036,6 +10055,837 @@ pub(crate) fn try_walker_specialize_int_call( Ok(Some(())) } +/// `math.fabs(x)` on an exact int/float argument. `interp_math.py`'s `fabs` is +/// `math1(space, math.fabs, w_x)`, and RPython lowers `ll_math_fabs` to a sign +/// mask, so the whole builtin is one `FloatAbs` once the operand is unboxed. +/// `fabs` is total — it raises for no input and needs no domain guard — so the +/// only guards are the operand's own class and exact-`w_class` checks. +/// Rebound callables, numeric subclasses, and non-numeric inputs retain the +/// ordinary residual call. +pub(crate) fn try_walker_specialize_math_fabs( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + if r_args.len() != 3 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(arg_obj), + ) = (arg_concretes[0], arg_concretes[1], arg_concretes[2]) + else { + return Ok(None); + }; + if concrete_callable.is_null() || !null_or_self.is_null() || arg_obj.is_null() { + return Ok(None); + } + if !pyre_interpreter::module::math::interp_math::is_math_fabs_function(concrete_callable) { + return Ok(None); + } + let (is_int, val) = unsafe { + if !pyre_object::is_exact_builtin_instance(arg_obj) { + return Ok(None); + } + if pyre_object::is_int(arg_obj) { + (true, pyre_object::w_int_get_value(arg_obj) as f64) + } else if pyre_object::is_float(arg_obj) { + (false, pyre_object::w_float_get_value(arg_obj)) + } else { + return Ok(None); + } + }; + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &[arg_obj]) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + + // `w_float_get_value` reads the payload without checking the box, and a + // non-float here would be read as one rather than rejected, so the value + // the trace records would be a number with no relation to the answer. + if !unsafe { pyre_object::is_float(boxed_result) } { + return Ok(None); + } + let result_val = unsafe { pyre_object::w_float_get_value(boxed_result) }; + // The trace computes `FloatAbs` over the coerced operand, so an int too + // large to be an exact `f64` would have the trace answering for a value + // the builtin never saw. Compare the two on this operand, by bits so the + // two zeroes stay distinct. + if val.abs().to_bits() != result_val.to_bits() { + return Ok(None); + } + 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 x = walker_coerce_operand_to_float(ctx, op.pc, r_args[2], arg_obj, is_int, val, false)?; + let raw = ctx.trace_ctx.record_op(OpCode::FloatAbs, &[x]); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Float(result_val)); + let boxed = crate::state::wrapfloat(ctx.trace_ctx, raw); + ctx.trace_ctx.set_opref_concrete( + boxed, + majit_ir::Value::Ref(majit_ir::GcRef(boxed_result as usize)), + ); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + Ok(Some(())) +} + +/// Which reduction `try_walker_specialize_math_round_to_int` is folding. +#[derive(Clone, Copy)] +pub(crate) enum MathRoundMode { + Floor, + Ceil, + Trunc, +} + +/// `math.floor(x)` / `math.ceil(x)` / `math.trunc(x)` on an exact float. +/// +/// `floor`/`ceil`/`trunc` look the dunder up on the type and call +/// it; for an exact float that resolves to `W_FloatObject`'s own reduction +/// followed by `newint_from_float`, whose `ovfcheck_float_to_int` arm is a +/// machine cast. Recreate that shape: unbox, guard the operand into the +/// signed range, round, and cast. +/// +/// Only an exact float is folded. An `int` argument reaches +/// `int.__floor__`, which returns the argument object itself rather than a +/// fresh box, and a float subclass may override the dunder — both keep the +/// residual. +/// +/// The range guard is on the operand rather than the rounded value, which is +/// sufficient for all three modes: `-2**63` is an integer so `floor` cannot +/// leave the range from below, `|trunc(x)| <= |x|`, and every float below +/// `2**63` large enough for `ceil` to move it is already integral (the ulp +/// there is 2048). +pub(crate) fn try_walker_specialize_math_round_to_int( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, + mode: MathRoundMode, +) -> Result, DispatchError> { + if r_args.len() != 3 { + return Ok(None); + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let ( + ConcreteValue::Ref(concrete_callable), + ConcreteValue::Ref(null_or_self), + ConcreteValue::Ref(arg_obj), + ) = (arg_concretes[0], arg_concretes[1], arg_concretes[2]) + else { + return Ok(None); + }; + if concrete_callable.is_null() || !null_or_self.is_null() || arg_obj.is_null() { + return Ok(None); + } + let is_this_builtin: fn(pyre_object::PyObjectRef) -> bool = match mode { + MathRoundMode::Floor => pyre_interpreter::module::math::interp_math::is_math_floor_function, + MathRoundMode::Ceil => pyre_interpreter::module::math::interp_math::is_math_ceil_function, + MathRoundMode::Trunc => pyre_interpreter::module::math::interp_math::is_math_trunc_function, + }; + if !is_this_builtin(concrete_callable) { + return Ok(None); + } + let value = unsafe { + if !pyre_object::is_exact_builtin_instance(arg_obj) || !pyre_object::is_float(arg_obj) { + return Ok(None); + } + pyre_object::w_float_get_value(arg_obj) + }; + // `2**63` is exactly representable while `i64::MAX` is not; use a strict + // upper bound, matching ovfcheck_float_to_int on a signed 64-bit target. + // NaN and both infinities fail these comparisons and keep the residual, + // which raises for them. + const SIGNED_MIN_AS_FLOAT: f64 = -9223372036854775808.0; + const SIGNED_LIMIT_AS_FLOAT: f64 = 9223372036854775808.0; + if !(value >= SIGNED_MIN_AS_FLOAT && value < SIGNED_LIMIT_AS_FLOAT) { + return Ok(None); + } + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &[arg_obj]) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + if !unsafe { pyre_object::is_int(boxed_result) } { + return Ok(None); + } + let result_value = unsafe { pyre_object::w_int_get_value(boxed_result) }; + + 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 arg_op = r_args[2]; + let float_type_addr = &pyre_object::pyobject::FLOAT_TYPE as *const _ as i64; + let raw_float = walker_unbox_float(ctx, op.pc, arg_op, float_type_addr)?; + walker_guard_exact_w_class( + ctx, + op.pc, + arg_op, + pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::FLOAT_TYPE), + )?; + ctx.trace_ctx + .set_opref_concrete(raw_float, majit_ir::Value::Float(value)); + let low = ctx + .trace_ctx + .const_float(SIGNED_MIN_AS_FLOAT.to_bits() as i64); + let high = ctx + .trace_ctx + .const_float(SIGNED_LIMIT_AS_FLOAT.to_bits() as i64); + walker_float_cmp_guard(ctx, op.pc, OpCode::FloatGe, &[raw_float, low], true)?; + walker_float_cmp_guard(ctx, op.pc, OpCode::FloatLt, &[raw_float, high], true)?; + + // `CastFloatToInt` already truncates toward zero, so only floor and ceil + // need a rounding step. Both are elidable and cannot raise. + let rounded = match mode { + MathRoundMode::Trunc => raw_float, + MathRoundMode::Floor | MathRoundMode::Ceil => { + let (helper, rounded_value) = match mode { + MathRoundMode::Floor => ( + pyre_interpreter::module::math::interp_math::jit_math_floor_raw as *const (), + value.floor(), + ), + _ => ( + pyre_interpreter::module::math::interp_math::jit_math_ceil_raw as *const (), + value.ceil(), + ), + }; + let rounded = ctx.trace_ctx.call_float_typed_with_effect( + helper, + &[raw_float], + &[majit_ir::Type::Float], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(rounded, majit_ir::Value::Float(rounded_value)); + rounded + } + }; + let raw_int = ctx.trace_ctx.record_op(OpCode::CastFloatToInt, &[rounded]); + ctx.trace_ctx + .set_opref_concrete(raw_int, majit_ir::Value::Int(result_value)); + let boxed = walker_box_int(ctx, op.pc, raw_int, result_value)?; + ctx.trace_ctx + .set_opref_concrete(boxed, box_int_concrete(result_value, boxed_result as i64)); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + Ok(Some(())) +} + +/// Read a plain `bh_call_fn(callable, PY_NULL, args…)` shape's concrete +/// operands. `None` means the call is not that shape — a bound receiver in +/// `null_or_self`, a NULL operand, or a non-`Ref` concrete. +fn plain_builtin_call_concretes( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + arity: usize, +) -> Option<(pyre_object::PyObjectRef, [pyre_object::PyObjectRef; 2])> { + if r_args.len() != arity + 2 { + return None; + } + let arg_concretes = read_ref_var_list_concrete(code, op, 1, ctx); + let (ConcreteValue::Ref(concrete_callable), ConcreteValue::Ref(null_or_self)) = + (arg_concretes[0], arg_concretes[1]) + else { + return None; + }; + if concrete_callable.is_null() || !null_or_self.is_null() { + return None; + } + let mut operands = [pyre_object::PY_NULL; 2]; + for (slot, concrete) in operands.iter_mut().zip(&arg_concretes[2..arity + 2]) { + let ConcreteValue::Ref(obj) = *concrete else { + return None; + }; + if obj.is_null() { + return None; + } + *slot = obj; + } + Some((concrete_callable, operands)) +} + +/// Classify a fold operand as an exact `int`/`bool`/`float` and read its +/// value as an `f64`. A numeric subclass keeps the builtin `ob_type` layout +/// but carries a Python-visible `w_class`, and the `guard_class` the coercion +/// emits reads `ob_type`, so it would not catch the subclass — decline here. +fn fold_float_operand(obj: pyre_object::PyObjectRef) -> Option<(bool, f64)> { + unsafe { + if !pyre_object::is_exact_builtin_instance(obj) { + return None; + } + if pyre_object::is_int(obj) { + Some((true, pyre_object::w_int_get_value(obj) as f64)) + } else if pyre_object::is_float(obj) { + Some((false, pyre_object::w_float_get_value(obj))) + } else { + None + } + } +} + +/// Pin a fold's callable identity. The module-attr fold usually makes it a +/// constant already; guard only when it is not. +fn walker_guard_fold_callable( + ctx: &mut WalkContext<'_, '_, Sym>, + pc: usize, + callable_op: OpRef, + concrete_callable: pyre_object::PyObjectRef, +) -> Result<(), DispatchError> { + if callable_op.is_constant() { + return Ok(()); + } + 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, pc)?; + ctx.trace_ctx + .heap_cache_mut() + .replace_box(callable_op, expected); + Ok(()) +} + +/// `raw - raw == 0` holds exactly for every finite value, including signed +/// zero; an infinity or a NaN bails to the builtin. This is the guard that +/// makes the generic float folds sound: the raw helper answers what the +/// builtin body computes and reports every raising direction as NaN, so a +/// finite result means the builtin returned this exact value. +fn walker_guard_float_result_finite( + ctx: &mut WalkContext<'_, '_, Sym>, + pc: usize, + raw: OpRef, +) -> Result<(), DispatchError> { + let diff = ctx.trace_ctx.record_op(OpCode::FloatSub, &[raw, raw]); + ctx.trace_ctx + .set_opref_concrete(diff, majit_ir::Value::Float(0.0)); + let zero = ctx.trace_ctx.const_float(0.0f64.to_bits() as i64); + walker_float_cmp_guard(ctx, pc, OpCode::FloatEq, &[diff, zero], true) +} + +/// The one-argument half of the generic `math` float fold. +/// +/// `interp_math`'s `pm1!` family is a single `pymath` call wrapped in the +/// boxing and error mapping the module needs; the walker otherwise sees only +/// the opaque `bh_call_fn(builtin, NULL, x)` residual, so a numeric loop pays +/// an argument tuple, a `W_FloatObject` allocation and a full builtin dispatch +/// per iteration. Emit instead the unboxed operand, one pure elidable +/// `CALL_F` into that function's raw helper, and an inline `wrapfloat`. +/// +/// The helper reports every raising direction as NaN, so the trailing +/// finite-result guard is what keeps this correct for arbitrary operands: it +/// deoptimizes into the builtin, which re-executes and raises or returns the +/// non-finite value itself. The fold therefore needs no per-function domain +/// knowledge, and adding a function to `MATH_FLOAT1_FOLDS` is all it takes to +/// cover it. Rebound callables, numeric subclasses and non-numeric operands +/// retain the generic residual path (SAFE). +pub(crate) fn try_walker_specialize_math_float1( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + let Some((concrete_callable, operands)) = + plain_builtin_call_concretes(ctx, code, op, r_args, 1) + else { + return Ok(None); + }; + let Some(raw_fn) = + pyre_interpreter::module::math::interp_math::math_float1_fold_helper(concrete_callable) + else { + return Ok(None); + }; + let Some((is_int, value)) = fold_float_operand(operands[0]) else { + return Ok(None); + }; + // Authentic boxed result, produced on the plain eval loop exactly as the + // skipped residual would. A raise, a non-float result, or a non-finite + // one all record a guard that would fail on the operand it was recorded + // from, so keep the residual for them. + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &[operands[0]]) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + let Some(result_value) = fold_finite_float_result(boxed_result) else { + return Ok(None); + }; + // The compiled loop calls the raw helper, not the function it stands for. + // Compare the two on this operand and keep the residual when they differ, + // so a helper that disagrees is not compiled into the loop. Bit equality + // rather than `==`, which cannot tell the two zeroes apart. + if raw_fn(value).to_bits() != result_value.to_bits() { + return Ok(None); + } + + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let x = + walker_coerce_operand_to_float(ctx, op.pc, r_args[2], operands[0], is_int, value, false)?; + let raw = ctx.trace_ctx.call_float_typed_with_effect( + raw_fn as *const (), + &[x], + &[majit_ir::Type::Float], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Float(result_value)); + walker_guard_float_result_finite(ctx, op.pc, raw)?; + let boxed = crate::state::wrapfloat(ctx.trace_ctx, raw); + ctx.trace_ctx.set_opref_concrete( + boxed, + majit_ir::Value::Ref(majit_ir::GcRef(boxed_result as usize)), + ); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + Ok(Some(())) +} + +/// The two-argument half of the generic `math` float fold — `pow`, `fmod`, +/// `copysign`, `remainder` and `atan2`. Same shape and same soundness +/// argument as [`try_walker_specialize_math_float1`]; the finite-result guard +/// carries `pow`'s `ValueError` (`pow(0.0, -2.0)`) and `OverflowError` +/// (`pow(1e100, 1e100)`) directions back to the builtin. +pub(crate) fn try_walker_specialize_math_float2( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + let Some((concrete_callable, operands)) = + plain_builtin_call_concretes(ctx, code, op, r_args, 2) + else { + return Ok(None); + }; + let Some(raw_fn) = + pyre_interpreter::module::math::interp_math::math_float2_fold_helper(concrete_callable) + else { + return Ok(None); + }; + let (Some((x_is_int, x_value)), Some((y_is_int, y_value))) = ( + fold_float_operand(operands[0]), + fold_float_operand(operands[1]), + ) else { + return Ok(None); + }; + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &operands) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + let Some(result_value) = fold_finite_float_result(boxed_result) else { + return Ok(None); + }; + // Same cross-check as the one-argument half: the compiled loop calls the + // raw helper, so a helper that disagrees with the function it stands for + // must not be compiled into it. + if raw_fn(x_value, y_value).to_bits() != result_value.to_bits() { + return Ok(None); + } + + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let x = walker_coerce_operand_to_float( + ctx, + op.pc, + r_args[2], + operands[0], + x_is_int, + x_value, + false, + )?; + let y = walker_coerce_operand_to_float( + ctx, + op.pc, + r_args[3], + operands[1], + y_is_int, + y_value, + false, + )?; + let raw = ctx.trace_ctx.call_float_typed_with_effect( + raw_fn as *const (), + &[x, y], + &[majit_ir::Type::Float, majit_ir::Type::Float], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Float(result_value)); + walker_guard_float_result_finite(ctx, op.pc, raw)?; + let boxed = crate::state::wrapfloat(ctx.trace_ctx, raw); + ctx.trace_ctx.set_opref_concrete( + boxed, + majit_ir::Value::Ref(majit_ir::GcRef(boxed_result as usize)), + ); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + Ok(Some(())) +} + +/// `math.isclose(a, b)` with both tolerances defaulted, in the shape +/// [`walker_newbool_guarded`] recognizes: the result decides one branch and +/// nothing else. +/// +/// The residual costs a builtin dispatch, a keyword split and two +/// `try_get_double` conversions to produce one of two prebuilt singletons. +/// Emit instead the two unboxed operands and a pure elidable `CALL_I` into +/// `jit_math_isclose_default`, whose truth the branch's own guard already +/// pins. That helper is total, so unlike the float folds this one needs no +/// result guard of its own. +/// +/// A keyword argument (which would reach `bh_call_fn_kw`, not this shape), a +/// third positional, a numeric subclass, a rebound callable, or a result that +/// escapes the branch all retain the generic residual path (SAFE). +pub(crate) fn try_walker_specialize_math_isclose( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, + dst_bank: char, +) -> Result, DispatchError> { + let Some((concrete_callable, operands)) = + plain_builtin_call_concretes(ctx, code, op, r_args, 2) + else { + return Ok(None); + }; + if !pyre_interpreter::module::math::interp_math::is_math_isclose_function(concrete_callable) { + return Ok(None); + } + // Settle the result's shape before emitting anything: everything below + // this point commits ops to the trace, and `walker_newbool_guarded` is + // what decides whether the branch's guard can stand in for the box. + if ctx.fbw_mode.snapshot_sym.is_null() + || dst_bank != 'r' + || !matches!( + classify_compare_box_use(ctx, op.pc, dst as u8, VablePublish::Tolerated), + CompareBoxUse::FeedsBranchOnly { .. } + ) + { + return Ok(None); + } + let (Some((a_is_int, a_value)), Some((b_is_int, b_value))) = ( + fold_float_operand(operands[0]), + fold_float_operand(operands[1]), + ) else { + return Ok(None); + }; + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &operands) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + // The helper reimplements the comparison rather than calling the module's + // own; compare the two answers on this operand pair before committing to + // it, so a divergence declines here instead of recording wrong code. + let observed = std::ptr::eq(boxed_result, pyre_object::w_bool_from(true)); + if !observed && !std::ptr::eq(boxed_result, pyre_object::w_bool_from(false)) { + return Ok(None); + } + let helper = pyre_interpreter::module::math::interp_math::jit_math_isclose_default; + if (helper(a_value, b_value) != 0) != observed { + return Ok(None); + } + + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let a = walker_coerce_operand_to_float( + ctx, + op.pc, + r_args[2], + operands[0], + a_is_int, + a_value, + false, + )?; + let b = walker_coerce_operand_to_float( + ctx, + op.pc, + r_args[3], + operands[1], + b_is_int, + b_value, + false, + )?; + let truth = ctx.trace_ctx.call_int_typed_with_effect( + helper as *const (), + &[a, b], + &[majit_ir::Type::Float, majit_ir::Type::Float], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(truth, majit_ir::Value::Int(i64::from(observed))); + // The shape check above already established what this re-tests, so the + // `None` arm is unreachable; keep it as the decline rather than assert. + let Some(boxed) = walker_newbool_guarded(ctx, op.pc, truth, observed, dst as u8, dst_bank)? + else { + return Ok(None); + }; + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + Ok(Some(())) +} + +/// The `f64` behind an exact-`float` fold result, or `None` when the builtin +/// answered something else or something the finite-result guard would reject. +fn fold_finite_float_result(boxed_result: pyre_object::PyObjectRef) -> Option { + unsafe { + if boxed_result.is_null() + || !pyre_object::is_exact_builtin_instance(boxed_result) + || !pyre_object::is_float(boxed_result) + { + return None; + } + let value = pyre_object::w_float_get_value(boxed_result); + value.is_finite().then_some(value) + } +} + +/// The machine int a folded builtin's authentic boxed result carries, or +/// `None` when that result is not an exact `int` at all. `True`/`False` are +/// excluded: a raw helper reporting `1` must not be accepted for a builtin +/// that returned the bool, because the trace boxes it with `wrapint`. +fn fold_boxed_int_value(boxed_result: pyre_object::PyObjectRef) -> Option { + unsafe { + if boxed_result.is_null() + || !pyre_object::is_exact_builtin_instance(boxed_result) + || !pyre_object::is_int(boxed_result) + || pyre_object::is_bool(boxed_result) + { + return None; + } + Some(pyre_object::w_int_get_value(boxed_result)) + } +} + +/// Guard an int-channel helper's result against its decline sentinel, so every +/// operand the helper does not answer for resumes in the builtin. +fn walker_guard_int_result_not_declined( + ctx: &mut WalkContext<'_, '_, Sym>, + pc: usize, + raw: OpRef, +) -> Result<(), DispatchError> { + let sentinel = ctx + .trace_ctx + .const_int(pyre_interpreter::jit_builtin_folds::INT_FOLD_DECLINE); + let answered = ctx.trace_ctx.record_op(OpCode::IntNe, &[raw, sentinel]); + ctx.trace_ctx + .set_opref_concrete(answered, majit_ir::Value::Int(1)); + walker_emit_fold_guard_with_snapshot(ctx, pc, OpCode::GuardTrue, &[answered]) +} + +/// The generic builtin fold, one argument. +/// +/// Every builtin that is not hand-specialized reaches the interpreter as +/// `bh_call_fn(builtin, NULL, x)`, and that residual costs the same regardless +/// of what the builtin does: the frame force, the argument rooting, the +/// execution-context resolution and the gateway signature binding all run +/// before the body does. Measured against pypy 7.3.20 the floor is an order +/// of magnitude on its own — `hash`, `ord` and `abs` all sit within a few +/// percent of each other because none of them is paying for its own work. +/// +/// `jit_builtin_folds` names, per builtin, a raw helper that is the body of +/// that builtin restricted to the operands it can answer without running +/// app-level code and without allocating. Emit a direct call into it, guard +/// the channel's decline sentinel, and box the result inline so the optimizer +/// can keep it virtual. A declined operand — a subclass instance, a shape the +/// helper does not implement, the argument that would have raised — resumes in +/// the builtin, which re-executes the call from scratch, so the fold needs no +/// per-builtin domain knowledge and adding a table row is all it takes to +/// cover another one. Rebound callables keep the residual (SAFE). +pub(crate) fn try_walker_specialize_builtin_fold1( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + use pyre_interpreter::jit_builtin_folds::{BuiltinFoldRaw, INT_FOLD_DECLINE}; + + let Some((concrete_callable, operands)) = + plain_builtin_call_concretes(ctx, code, op, r_args, 1) + else { + return Ok(None); + }; + let rows: Vec<_> = + pyre_interpreter::jit_builtin_folds::builtin_folds_for(concrete_callable, 1).collect(); + // Ask the raw helpers before the builtin runs. A call no row answers for + // is not this fold's shape, and the walker has to learn that without + // executing the builtin: the residual it falls back to executes the call + // again, so a decline taken afterwards would run a side-effecting + // `__hash__` or `__abs__` twice in one walk. + let answered = rows.iter().any(|fold| match fold.raw { + BuiltinFoldRaw::Int1(raw_fn) => raw_fn(operands[0] as i64) != INT_FOLD_DECLINE, + BuiltinFoldRaw::Float1(raw_fn) => !raw_fn(operands[0] as i64).is_nan(), + BuiltinFoldRaw::Ref2(_) => false, + }); + if !answered { + return Ok(None); + } + // Authentic boxed result, produced on the plain eval loop exactly as the + // skipped residual would. Every row cross-checks its helper against this, + // so a helper that disagrees with the builtin it stands for declines here + // rather than compiling the disagreement into the loop. + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &operands[..1]) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + + for fold in &rows { + match fold.raw { + BuiltinFoldRaw::Int1(raw_fn) => { + let value = raw_fn(operands[0] as i64); + if value == INT_FOLD_DECLINE || fold_boxed_int_value(boxed_result) != Some(value) { + continue; + } + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let raw = ctx.trace_ctx.call_int_typed_with_effect( + raw_fn as *const (), + &[r_args[2]], + &[majit_ir::Type::Ref], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Int(value)); + walker_guard_int_result_not_declined(ctx, op.pc, raw)?; + let boxed = walker_box_int(ctx, op.pc, raw, value)?; + ctx.trace_ctx + .set_opref_concrete(boxed, box_int_concrete(value, boxed_result as i64)); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + return Ok(Some(())); + } + BuiltinFoldRaw::Float1(raw_fn) => { + let Some(result_value) = fold_finite_float_result(boxed_result) else { + continue; + }; + let value = raw_fn(operands[0] as i64); + // By bits: `==` cannot tell `-0.0` from `0.0`, and which one + // the fold answers with is observable through `copysign`. + if value.to_bits() != result_value.to_bits() { + continue; + } + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let raw = ctx.trace_ctx.call_float_typed_with_effect( + raw_fn as *const (), + &[r_args[2]], + &[majit_ir::Type::Ref], + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Float(value)); + walker_guard_float_result_finite(ctx, op.pc, raw)?; + let boxed = crate::state::wrapfloat(ctx.trace_ctx, raw); + ctx.trace_ctx.set_opref_concrete( + boxed, + majit_ir::Value::Ref(majit_ir::GcRef(boxed_result as usize)), + ); + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', boxed)?; + return Ok(Some(())); + } + BuiltinFoldRaw::Ref2(_) => continue, + } + } + Ok(None) +} + +/// The two-argument half of the generic builtin fold — `min(a, b)` and +/// `max(a, b)`, whose helpers return one of their own arguments rather than +/// building anything. Same shape and same soundness argument as +/// [`try_walker_specialize_builtin_fold1`]; a `PY_NULL` is the decline the +/// trailing non-null guard carries back to the builtin. +pub(crate) fn try_walker_specialize_builtin_fold2( + ctx: &mut WalkContext<'_, '_, Sym>, + code: &[u8], + op: &DecodedOp, + r_args: &[OpRef], + dst: usize, +) -> Result, DispatchError> { + use pyre_interpreter::jit_builtin_folds::BuiltinFoldRaw; + + let Some((concrete_callable, operands)) = + plain_builtin_call_concretes(ctx, code, op, r_args, 2) + else { + return Ok(None); + }; + let rows: Vec<_> = + pyre_interpreter::jit_builtin_folds::builtin_folds_for(concrete_callable, 2).collect(); + // Same ordering as the one-argument half: no row may answer only after the + // builtin has already run, or the residual re-executes it. + let answered = rows.iter().any(|fold| match fold.raw { + BuiltinFoldRaw::Ref2(raw_fn) => { + !(raw_fn(operands[0] as i64, operands[1] as i64) as pyre_object::PyObjectRef).is_null() + } + BuiltinFoldRaw::Int1(_) | BuiltinFoldRaw::Float1(_) => false, + }); + if !answered { + return Ok(None); + } + let boxed_result = { + let _plain_guard = pyre_interpreter::call::force_plain_eval(); + pyre_interpreter::call::call_function_impl_result(concrete_callable, &operands) + }; + let Ok(boxed_result) = boxed_result else { + return Ok(None); + }; + + for fold in &rows { + let BuiltinFoldRaw::Ref2(raw_fn) = fold.raw else { + continue; + }; + let value = raw_fn(operands[0] as i64, operands[1] as i64) as pyre_object::PyObjectRef; + if value.is_null() || value != boxed_result { + continue; + } + walker_guard_fold_callable(ctx, op.pc, r_args[0], concrete_callable)?; + let raw = ctx.trace_ctx.call_ref_typed_with_effect( + raw_fn as *const (), + &[r_args[2], r_args[3]], + &[majit_ir::Type::Ref, majit_ir::Type::Ref], + // `min` / `max` compare two exact scalars and hand back one of + // their own arguments, so unlike the allocating ref helpers this + // one really cannot collect -- which drops the gcmap bracket the + // plain `CannotRaise` constructor would ask every backend for. + majit_metainterp::CANNOT_RAISE_NO_HEAP_EFFECT_INFO, + ); + // Concrete before the guard: the guard captures a resume snapshot, and + // a `raw` with no value yet is recorded into it without one. + ctx.trace_ctx + .set_opref_concrete(raw, majit_ir::Value::Ref(majit_ir::GcRef(value as usize))); + walker_emit_fold_guard_with_snapshot(ctx, op.pc, OpCode::GuardNonnull, &[raw])?; + write_residual_call_result_to_dst(ctx, op.pc, dst, 'r', raw)?; + return Ok(Some(())); + } + Ok(None) +} + /// `float(x)` on an exact int/float argument: inline the conversion /// (`W_IntObject.descr_float` → `space.newfloat`, or the identity /// `float(f) is f` for an exact float) instead of the opaque @@ -10672,7 +11522,8 @@ unsafe fn orthodox_list_append_recognize( let int_ok = pyre_object::is_plain_int1(value) && !(pyre_object::tagged_int::CAN_BE_TAGGED && pyre_object::tagged_int::is_tagged_int(value)); - let float_ok = !value.is_null() && pyre_object::is_plain_float_strict(value); + // NaNs select Object storage to preserve identity. + let float_ok = pyre_object::is_float_strategy_item(value); // switch_to_correct_strategy routes `is_plain_int1` (exact int or // fits-in-word long) -> Integer with no tagged exclusion. Exclude any // plain-int / float from the object fallback so a tagged-int DECLINES @@ -10680,7 +11531,7 @@ unsafe fn orthodox_list_append_recognize( // traced strategy from the concrete one the commit installs. let obj_ok = !value.is_null() && !pyre_object::is_plain_int1(value) - && !pyre_object::is_plain_float_strict(value); + && !pyre_object::is_float_strategy_item(value); if !int_ok && !float_ok && !obj_ok { return None; } @@ -10702,14 +11553,10 @@ unsafe fn orthodox_list_append_recognize( // object items block — no unboxing, so the value carries no type // precondition. let obj_ok = pyre_object::w_list_uses_object_storage(inner_self) && !value.is_null(); - // Float-storage specialization: a strict `W_FloatObject` stored - // unboxed. `FloatListStrategy.is_correct_type` (listobject.py) is - // `type(w_obj) is W_FloatObject`, the strict predicate the body's Float - // arm also uses. No fits-* long analogue (a float is never re-boxed - // across arithmetic, unlike a fits-int W_LongObject). + // Match `FloatListStrategy.is_correct_type`; NaNs take the residual path + // that converts the receiver to Object storage. let float_ok = pyre_object::w_list_uses_float_storage(inner_self) - && !value.is_null() - && pyre_object::is_plain_float_strict(value); + && pyre_object::is_float_strategy_item(value); if !int_ok && !obj_ok && !float_ok { return None; } @@ -10797,7 +11644,7 @@ pub(crate) fn orthodox_list_append_commit( && pyre_object::tagged_int::is_tagged_int(value)); if int_ok { ListStrategy::Integer - } else if !value.is_null() && pyre_object::is_plain_float_strict(value) { + } else if pyre_object::is_float_strategy_item(value) { ListStrategy::Float } else { ListStrategy::Object @@ -13097,6 +13944,7 @@ pub(crate) fn try_walker_specialize_store_subscr( // route through the generic path — PyPy's IntegerListStrategy rejects a // W_BoolObject (`is_correct_type` is exact-type), switching the list to // object storage, so the int-storage fast path would drop the bool type. + // Float subclasses and NaNs switch the list to Object storage. // EXACT list only: a list SUBCLASS instance shares `ob_type == // &LIST_TYPE` but retags `w_class` and may override `__setitem__`; // `is_exact_list` excludes it so it falls to the generic residual @@ -13120,8 +13968,16 @@ pub(crate) fn try_walker_specialize_store_subscr( { 1i64 } else if pyre_object::w_list_uses_float_storage(list_obj) - && pyre_object::is_float(value_obj) + && pyre_object::is_float_strategy_item(value_obj) { + // The subclass term of that predicate is enforced on replay by + // pinning `w_class` below. `is_plain_float_strict` also admits the + // null spelling of "exact float", which no pin can express, so + // decline such an operand here rather than emit a guard it would + // fail itself (see `walker_guard_exact_w_class`). + if walker_exact_builtin_class(value_obj).is_none() { + return Ok(None); + } 2i64 } else { return Ok(None); @@ -13226,6 +14082,19 @@ pub(crate) fn try_walker_specialize_store_subscr( let elem = unsafe { pyre_object::w_float_get_value(value_obj) }; ctx.trace_ctx .set_opref_concrete(raw, majit_ir::Value::Float(elem)); + // A float SUBCLASS instance shares `ob_type == &FLOAT_TYPE` (so it + // passes the unbox guard) but retags `w_class`; + // `FloatListStrategy.is_correct_type` rejects it, so the interpreter + // switches the list to Object storage instead of writing raw f64. + // Pin the canonical class the same way the list operand is pinned + // above, so such an instance side-exits to the generic residual. + walker_guard_exact_w_class( + ctx, + op_pc, + value_op, + pyre_object::pyobject::get_instantiate(&pyre_object::pyobject::FLOAT_TYPE), + )?; + walker_guard_float_not_nan(ctx, op_pc, raw)?; crate::state::trace_float_block_setitem_value(ctx.trace_ctx, block, raw_index, raw); } diff --git a/pyre/pyre-jit/src/call_jit.rs b/pyre/pyre-jit/src/call_jit.rs index c3d180d59fc..0ac995706a0 100644 --- a/pyre/pyre-jit/src/call_jit.rs +++ b/pyre/pyre-jit/src/call_jit.rs @@ -5202,24 +5202,22 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // rewrite these copied native parameters. Root them immediately and // dispatch only values reloaded from the forwarded slots. let _roots = pyre_object::gc_roots::push_roots(); - let root_base = pyre_object::gc_roots::shadow_stack_len(); - pyre_object::gc_roots::pin_root(callable); - pyre_object::gc_roots::pin_root(null_or_self); + let root_base = _roots.base(); + _roots.pin_root(callable); + _roots.pin_root(null_or_self); for &arg in args { - pyre_object::gc_roots::pin_root(arg); + _roots.pin_root(arg); } // `eval.rs`'s `PyFrame::call` — a non-null null_or_self is the method receiver // (load_method_fast_path pushes `[w_descr, w_obj]`); the call proceeds // as `callable(null_or_self, *args)`. let reload_args = || { - let null_or_self = pyre_object::gc_roots::shadow_stack_get(root_base + 1); + let null_or_self = _roots.get(root_base + 1); let mut values = Vec::with_capacity(args.len() + usize::from(!null_or_self.is_null())); if !null_or_self.is_null() { values.push(null_or_self); } - values.extend( - (0..args.len()).map(|i| pyre_object::gc_roots::shadow_stack_get(root_base + 2 + i)), - ); + values.extend((0..args.len()).map(|i| _roots.get(root_base + 2 + i))); values }; // `space.getexecutioncontext()` (`call.rs`'s `getexecutioncontext` → @@ -5237,7 +5235,7 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO getexecutioncontext(); the eval loop must pin the execution context \ before any residual call" ); - if pyre_object::gc_roots::shadow_stack_get(root_base).is_null() { + if _roots.get(root_base).is_null() { let mut err = pyre_interpreter::PyError::new( pyre_interpreter::PyErrorKind::TypeError, "call on null callable".to_string(), @@ -5263,9 +5261,7 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO unsafe { pyre_interpreter::eval::FrameAnchor::from_raw((*ec).gettopframe_raw()) }; for i in 0..args.len() { let parent_frame_ptr = frame_anchor.live(); - if !parent_frame_ptr.is_null() - && pyre_object::gc_roots::shadow_stack_get(root_base + 2 + i).is_null() - { + if !parent_frame_ptr.is_null() && _roots.get(root_base + 2 + i).is_null() { let frame = unsafe { &*parent_frame_ptr }; // A NULL here with a live fastlocal is an unbound blackhole // register (the resume never seeded the slot); a NULL fastlocal @@ -5295,8 +5291,8 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // take the same gateway call directly. Keep every signature-dependent // shape on the generic path: only a natural arity in the fixed range with // an exact argument count is equivalent to the positional fast path. - let rooted_callable = pyre_object::gc_roots::shadow_stack_get(root_base); - let rooted_null_or_self = pyre_object::gc_roots::shadow_stack_get(root_base + 1); + let rooted_callable = _roots.get(root_base); + let rooted_null_or_self = _roots.get(root_base + 1); let bound_builtin = unsafe { if !rooted_null_or_self.is_null() && pyre_interpreter::is_function(rooted_callable) { Some((rooted_callable, rooted_null_or_self)) @@ -5317,16 +5313,16 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO && unsafe { pyre_interpreter::builtin_code_get_fast_natural_arity(code) as usize } == positional_count; if exact_fixed_arity { - pyre_object::gc_roots::pin_root(code); - pyre_object::gc_roots::pin_root(receiver); + _roots.pin_root(code); + _roots.pin_root(receiver); let code_slot = root_base + 2 + args.len(); let receiver_slot = code_slot + 1; let mut call_args = [pyre_object::PY_NULL; 4]; - call_args[0] = pyre_object::gc_roots::shadow_stack_get(receiver_slot); + call_args[0] = _roots.get(receiver_slot); for (index, slot) in call_args[1..positional_count].iter_mut().enumerate() { - *slot = pyre_object::gc_roots::shadow_stack_get(root_base + 2 + index); + *slot = _roots.get(root_base + 2 + index); } - let code = pyre_object::gc_roots::shadow_stack_get(code_slot); + let code = _roots.get(code_slot); return match unsafe { pyre_interpreter::builtin_code_call(code, &call_args[..positional_count]) } { @@ -5346,18 +5342,35 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO // Cold path: type/method/staticmethod/classmethod/callable-instance are // delegated to call_function_impl_result under ForcePlainEvalGuard, which // mirrors baseobjspace.py:1155 dispatch without re-entering the JIT. - let callable = pyre_object::gc_roots::shadow_stack_get(root_base); + let callable = _roots.get(root_base); if unsafe { is_function(callable) } { let code = unsafe { pyre_interpreter::getcode(callable) }; if unsafe { pyre_interpreter::is_builtin_code(code as pyre_object::PyObjectRef) } { - let call_args = reload_args(); + // `reload_args` allocates a vector per residual builtin call. A + // call with no bound receiver and at most four positionals reads + // them straight out of the shadow slots instead — the same + // allocation-free shape the bound-receiver arm above takes. The + // slice contents are identical either way, so the dispatch below + // is unchanged. + let mut inline_args = [pyre_object::PY_NULL; 4]; + let spilled_args; + let call_args: &[pyre_object::PyObjectRef] = + if args.len() <= inline_args.len() && _roots.get(root_base + 1).is_null() { + for (index, slot) in inline_args[..args.len()].iter_mut().enumerate() { + *slot = _roots.get(root_base + 2 + index); + } + &inline_args[..args.len()] + } else { + spilled_args = reload_args(); + &spilled_args + }; // `call_args` are raw positionals; a HOPELESS-arity Signature // (`*args`, optional positional) needs `_match_signature` binding // before the body reads its slots. `builtin_code_call` never binds, // so route through the positional entry that does. return match pyre_interpreter::call::builtin_code_call_positional( code as pyre_object::PyObjectRef, - &call_args, + call_args, ) { Ok(result) if !result.is_null() => result as i64, Ok(_) => 0, @@ -5426,7 +5439,7 @@ fn bh_call_fn_impl(callable: PyObjectRef, null_or_self: PyObjectRef, args: &[PyO let saved_ctx = pyre_interpreter::call::take_last_exec_ctx(); pyre_interpreter::call::set_last_exec_ctx(ec); let _plain_guard = pyre_interpreter::call::force_plain_eval(); - let callable = pyre_object::gc_roots::shadow_stack_get(root_base); + let callable = _roots.get(root_base); let call_args = reload_args(); let result = pyre_interpreter::call::call_function_impl_result(callable, &call_args); pyre_interpreter::call::set_last_exec_ctx(saved_ctx); diff --git a/pyre/pyre-jit/src/eval.rs b/pyre/pyre-jit/src/eval.rs index d7fc53972e9..74b32d47231 100644 --- a/pyre/pyre-jit/src/eval.rs +++ b/pyre/pyre-jit/src/eval.rs @@ -5939,11 +5939,9 @@ fn green_key_from_pycode(next_instr: usize, w_pycode: pyre_object::PyObjectRef) /// warmstate.py:575-582). Callers that read a cell take this; callers that /// only tick a counter can keep the hash. /// -/// `is_being_profiled` is folded to `false` here **because -/// [`make_green_key`] folds it to `false`** — the two must agree or the typed -/// and hash paths would resolve to different cells. That fold is itself a -/// parity gap (upstream splits cells on the flag, pyre never does), tracked -/// separately; it is not this function's to change. +/// `is_being_profiled` comes from `current_is_being_profiled` here **because +/// [`make_green_key`] reads it from the same place** — the two must agree or +/// the typed and hash paths would resolve to different cells. fn green_key_typed_from_pycode( next_instr: usize, w_pycode: pyre_object::PyObjectRef, @@ -5956,7 +5954,7 @@ fn green_key_typed_from_pycode( } Some(majit_ir::pypyjit_greenkey( next_instr, - false, + pyre_interpreter::executioncontext::current_is_being_profiled(), code_ptr as u64, )) } @@ -6420,7 +6418,11 @@ pub fn make_green_key(code_ptr: *const (), pc: usize) -> u64 { // computed allocation-free. `is_being_profiled` folds to 0 (the JIT // path is never profiled), so this matches the typed marker-path key // and both lookups resolve to the same cell. - majit_ir::pypyjit_greenkey_uhash(pc, false, code_ptr as u64) + majit_ir::pypyjit_greenkey_uhash( + pc, + pyre_interpreter::executioncontext::current_is_being_profiled(), + code_ptr as u64, + ) } // JIT_CALL_DEPTH removed — pyre-interpreter::call::PY_RECURSION_DEPTH is the @@ -8263,6 +8265,12 @@ fn eval_with_jit_inner( // A traced frame runs interpreted: `call_trace` / `return_trace` / // `bytecode_trace` are driven from the plain eval path, so a frame the JIT // takes over reports no events at all. + // + // Narrowing this to line tracing alone is not enough on its own: measured, + // a profiled frame that reaches the portal still does not compile, and the + // calls a compiled trace inlines and the builtins it folds stop reporting + // (`call` 1042 against CPython's 3001, `c_call` 2085 against 6001). Both + // have to be answered before the profile half can be let through. if pyre_interpreter::pyframe::frame_tracing_active(frame) { return frame.execute_frame_plain(resume); } diff --git a/pyre/pyre-object/src/listobject.rs b/pyre/pyre-object/src/listobject.rs index 8b31c343809..8ec6d5716d7 100644 --- a/pyre/pyre-object/src/listobject.rs +++ b/pyre/pyre-object/src/listobject.rs @@ -591,8 +591,13 @@ fn all_ints(items: &[PyObjectRef]) -> bool { /// containers must retain the original NaN object so `[nan] == [nan]` is true /// for one shared object while two freshly-created NaNs still compare false. /// Keep NaNs in Object storage; ordinary exact floats retain PyPy's strategy. +/// +/// Shared with JIT list stores so traced and concrete strategies agree. +/// +/// # Safety +/// `item` must be null or point to a live object. #[inline] -unsafe fn is_float_strategy_item(item: PyObjectRef) -> bool { +pub unsafe fn is_float_strategy_item(item: PyObjectRef) -> bool { !item.is_null() && is_plain_float_strict(item) && !w_float_get_value(item).is_nan() } @@ -2707,16 +2712,7 @@ pub unsafe fn w_list_find_or_count_fast( let mut result: i64 = 0; let mut i = start.max(0); while i < stop { - // `FloatListStrategy._safe_find_or_count`: ordinary floats - // compare by value, while NaNs compare by their unwrapped - // bit pattern. The latter preserves the identity shortcut - // that `space.eq_w` would observe before the strategy erased - // the original W_FloatObject. - let matches = if target.is_nan() { - items[i as usize].to_bits() == target.to_bits() - } else { - items[i as usize] == target - }; + let matches = items[i as usize] == target; if matches { if count { result += 1; diff --git a/pyre/pyre-object/src/tupleobject.rs b/pyre/pyre-object/src/tupleobject.rs index d338d9fd7f3..1813b143c96 100644 --- a/pyre/pyre-object/src/tupleobject.rs +++ b/pyre/pyre-object/src/tupleobject.rs @@ -524,7 +524,8 @@ pub unsafe fn w_tuple_set_cached_hash(obj: PyObjectRef, hash: i64) { /// /// Predicates: `listobject.py is_plain_int1` accepts exact /// `W_IntObject` (not bool, not int subclass) AND fits-int -/// `W_LongObject`; `type(w) is W_FloatObject` is strict identity. +/// `W_LongObject`; `type(w) is W_FloatObject` is strict identity. NaNs are +/// excluded because `Cls_ff` would lose their pointer identity when reboxing. #[expect( clippy::not_unsafe_ptr_arg_deref, reason = "PyObjectRef is a GC-managed VM handle whose validity is established at the interpreter boundary; this item is the safe object-space facade" @@ -534,7 +535,11 @@ pub fn makespecialisedtuple2(w_arg1: PyObjectRef, w_arg2: PyObjectRef) -> PyObje if is_plain_int1(w_arg1) && is_plain_int1(w_arg2) { return w_specialised_tuple_ii_new(plain_int_w(w_arg1), plain_int_w(w_arg2)); } - if is_plain_float_strict(w_arg1) && is_plain_float_strict(w_arg2) { + if is_plain_float_strict(w_arg1) + && is_plain_float_strict(w_arg2) + && !w_float_get_value(w_arg1).is_nan() + && !w_float_get_value(w_arg2).is_nan() + { return w_specialised_tuple_ff_new( w_float_get_value(w_arg1), w_float_get_value(w_arg2), @@ -758,6 +763,40 @@ mod tests { } } + /// NaNs must decline the raw-f64 `Cls_ff` specialization. + #[test] + fn test_explicit_specialised_nan_pair_declines_ff() { + let nan = crate::floatobject::w_float_new(f64::NAN); + let tup = makespecialisedtuple2(nan, nan); + unsafe { + assert!(!is_specialised_tuple_ff(tup)); + assert!(is_specialised_tuple_oo(tup)); + assert_eq!(w_tuple_getitem(tup, 0).unwrap(), nan); + assert_eq!(w_tuple_getitem(tup, 1).unwrap(), nan); + } + + // One NaN slot is enough — both slots are stored raw. + let finite = crate::floatobject::w_float_new(1.5); + let mixed = makespecialisedtuple2(finite, nan); + unsafe { + assert!(!is_specialised_tuple_ff(mixed)); + assert_eq!(w_tuple_getitem(mixed, 0).unwrap(), finite); + assert_eq!(w_tuple_getitem(mixed, 1).unwrap(), nan); + } + } + + #[test] + fn test_arity2_nan_pair_preserves_boxed_identity() { + let nan = crate::floatobject::w_float_new(f64::NAN); + let tup = w_tuple_new(vec![nan, nan]); + unsafe { + assert!(!is_specialised_tuple_ff(tup)); + assert!(is_specialised_tuple_oo(tup)); + assert_eq!(w_tuple_getitem(tup, 0).unwrap(), nan); + assert_eq!(w_tuple_getitem(tup, 1).unwrap(), nan); + } + } + /// `specialisedtupleobject.py:176` checks `type(w) is W_FloatObject`. /// A float subclass keeps the W_FloatObject payload shape but has a /// different Python-level `w_class`, so it must fall through to `Cls_oo`.