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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 119 additions & 0 deletions pyre/extra_tests/parity_tests/float_subclass_unboxed_storage.py
Original file line number Diff line number Diff line change
@@ -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")
106 changes: 106 additions & 0 deletions pyre/extra_tests/parity_tests/nan_unboxed_storage_identity.py
Original file line number Diff line number Diff line change
@@ -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")
34 changes: 12 additions & 22 deletions pyre/pyre-interpreter/src/baseobjspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4121,33 +4121,23 @@ pub fn is_w(w_one: PyObjectRef, w_two: PyObjectRef) -> bool {
}
// `W_FloatObject.is_w` (floatobject.py:196-204): 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:290-301`).
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:287-301): 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;
Comment on lines +4135 to +4136

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep NaNs out of all unboxed float storage

Once NaNs fall back to pointer identity here, every path that erases the original W_FloatObject has to reject them, not just FloatListStrategy. makespecialisedtuple2 still builds W_SpecialisedTupleObject_ff for exact NaNs and mapdict still picks UnboxType::Float; both store only the raw f64 and rebox on read, so cases like n = float('nan'); t = (n, n); t[0] is n or a NaN instance attribute now become false / get fresh id() values even though Python attribute and tuple storage should retain the original object. Please apply the same NaN exclusion to those unboxed float paths before switching is_w/id to address identity.

Useful? React with 👍 / 👎.

}
return one.to_bits() == two.to_bits();
}
// CPython 3.14 gives complex objects pointer identity, handled above.
// `W_AbstractTupleObject.is_w` (tupleobject.py:47-55): a `tuple` is
// identical to another only when both are the empty tuple — "empty
// tuples are unique-ified". Non-empty tuples keep pointer identity
Expand Down
24 changes: 8 additions & 16 deletions pyre/pyre-interpreter/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2530,7 +2530,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]
Expand All @@ -2556,25 +2555,18 @@ pub fn immutable_unique_id(obj: PyObjectRef) -> Option<PyObjectRef> {
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 PyPy's `complexobject.py:303-314`, 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
Expand Down
13 changes: 11 additions & 2 deletions pyre/pyre-interpreter/src/objspace/std/mapdict.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3226,8 +3226,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);
Expand Down
12 changes: 11 additions & 1 deletion pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8008,6 +8008,16 @@ fn walker_float_cmp_guard<Sym: WalkSym>(
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<Sym: WalkSym>(
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
Expand Down Expand Up @@ -8573,7 +8583,7 @@ enum WalkerStoreAttrSpecialization {
///
/// # Safety
/// `obj` must be a non-null, untagged heap object.
unsafe fn walker_exact_builtin_class(
pub(crate) unsafe fn walker_exact_builtin_class(
obj: pyre_object::PyObjectRef,
) -> Option<pyre_object::PyObjectRef> {
unsafe {
Expand Down
Loading
Loading