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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 32 additions & 43 deletions majit/majit-backend-wasm/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,7 @@ pub fn build_wasm_module(
g.is_finish && !crate::failguard::meta_descr_is_exit_frame_with_exception(&g.meta_descr)
})
.map(|g| g.fail_index)
.unwrap_or(0);
.unwrap_or(crate::failguard::WASM_CA_FINISH_FI_UNKNOWN);
// CA frames execute the source loop and this bridge on the same frozen
// geometry. `compile_bridge` rejects a bridge that needs more slots, so
// no global floor or speculative slack is needed here.
Expand Down Expand Up @@ -2001,18 +2001,16 @@ fn build_function(
guard_idx += 1;
}
OpCode::GuardValue => {
let arg0 = op.arg(0).to_opref();
let is_float =
!arg0.is_constant() && value_types[arg0.raw() as usize] == ValType::F64;
if is_float {
emit_resolve_f64(&mut sink, constants, value_types, arg0);
emit_resolve_f64(&mut sink, constants, value_types, op.arg(1).to_opref());
sink.f64_ne();
} else {
emit_resolve(&mut sink, constants, value_types, arg0);
emit_resolve(&mut sink, constants, value_types, op.arg(1).to_opref());
sink.i64_ne();
}
// GUARD_VALUE checks bit-equality against the promoted constant:
// Value::eq (value.rs) compares floats by to_bits() (0.0 != -0.0,
// NaN == same-bit NaN, per history.py same_constant), which the
// dynasm/cranelift siblings implement as an integer bit-compare.
// emit_resolve pushes an F64 operand's i64 bits, so i64_ne is the
// correct compare for both int and float — an IEEE f64.ne would
// wrongly pass -0.0 == +0.0 (and fail NaN == same-bit NaN).
emit_resolve(&mut sink, constants, value_types, op.arg(0).to_opref());
emit_resolve(&mut sink, constants, value_types, op.arg(1).to_opref());
sink.i64_ne();
emit_guard_if_exit(
&mut sink,
constants,
Expand Down Expand Up @@ -2674,34 +2672,20 @@ fn build_function(
}

// ── String/Unicode ops (direct memory access) ──
OpCode::Strlen | OpCode::Unicodelen => {
let vi = op.pos.get().raw();
if !OpRef::raw_is_constant(vi) {
emit_resolve(&mut sink, constants, value_types, op.arg(0).to_opref());
sink.i32_wrap_i64();
// Length at offset 8 (after ob_type pointer on wasm32)
sink.i64_load(mem64(8));
sink.local_set(1 + vi);
}
}
OpCode::Strgetitem | OpCode::Unicodegetitem => {
let vi = op.pos.get().raw();
if !OpRef::raw_is_constant(vi) {
// str[index]: base + header_size + index
emit_resolve(&mut sink, constants, value_types, op.arg(0).to_opref());
sink.i32_wrap_i64();
emit_resolve(&mut sink, constants, value_types, op.arg(1).to_opref()); // index
sink.i32_wrap_i64();
sink.i32_add();
// String data starts after header (assume 16 bytes: ob_type + length)
sink.i32_load8_u(MemArg {
offset: 16,
align: 0,
memory_index: 0,
});
sink.i64_extend_i32_u();
sink.local_set(1 + vi);
}
// strlen/strgetitem/unicodelen/unicodegetitem were lowered with a
// hardcoded layout (length as an 8-byte load of a 4-byte word field;
// item as a 1-byte, stride-1 read at a fixed offset) that is wrong for
// UNICODE (4-byte code units, stride 4) and folds garbage into a str
// length's high bits — a silent wrong value on wasm, where offset is
// valid linear memory and does not trap. pyre models strings/unicode
// as Array(Char) and routes these through the descr-driven
// GETARRAYITEM/ARRAYLEN paths, so no producer emits these ops; decline
// them (interpreter fallback) rather than ship a wrong hardcoded read.
OpCode::Strlen | OpCode::Unicodelen | OpCode::Strgetitem | OpCode::Unicodegetitem => {
return Err(BackendError::Unsupported(format!(
"wasm codegen: string/unicode direct-memory op {:?} (no descr-driven layout)",
op.opcode
)));
}

// ── GC memory ops ──
Expand Down Expand Up @@ -2950,7 +2934,12 @@ fn build_function(
sink.i32_wrap_i64();
sink.i64_load(mem64(vtable_off as u64));
sink.i32_wrap_i64();
sink.i64_load(mem64(offset2 as u64));
emit_sized_int_load(
&mut sink,
offset2 as u64,
std::mem::size_of::<usize>(),
true,
Comment on lines +2937 to +2941

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 Load the full subclass range field

On wasm32 this emits a 4-byte load because size_of::<usize>() is 4, but the runtime layouts read by this guard are PyType::subclassrange_min / ClassTypeLayout::subclassrange_min, both 8-byte i64 fields. If a range value ever needs the upper 32 bits, GuardSubclass truncates/sign-extends the object's min and can accept or reject the guard incorrectly; use the actual field width for both this vtable path and the gcremovetypeptr path below.

AGENTS.md reference: AGENTS.md:L15-L18

Useful? React with 👍 / 👎.

);
} else {
// assembler.py:1957-1969 gcremovetypeptr path.
// MOV32 loc_tmp, mem(loc_object, 0)
Expand All @@ -2974,7 +2963,7 @@ fn build_function(
sink.i64_const((guard_gc_type_info.sizeof_ti + offset2) as i64);
sink.i64_add();
sink.i32_wrap_i64();
sink.i64_load(mem64(0));
emit_sized_int_load(&mut sink, 0, std::mem::size_of::<usize>(), true);
}
// Stack: [..., loc_tmp (i64)]

Expand Down
2 changes: 1 addition & 1 deletion majit/majit-backend-wasm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1818,7 +1818,7 @@ impl majit_backend::Backend for WasmBackend {
&& !failguard::meta_descr_is_exit_frame_with_exception(&descr.meta_descr)
})
.map(|descr| descr.fail_index)
.unwrap_or(0);
.unwrap_or(failguard::WASM_CA_FINISH_FI_UNKNOWN);
// For a pending self target this is the exact map already embedded in
// the module's CA arm. Reuse it for the published metadata so the
// loop and its self-callee have demonstrably identical geometry. A
Expand Down
28 changes: 28 additions & 0 deletions pyre/bench/synth/comprehension_param_range_call_flush.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# A module-scope hot loop calling a helper that returns an inlined list
# comprehension over `range(n)` with `n` a parameter. Once the loop reaches the
# trace threshold it records the `len(f(<const>))` body; recording the CALL to
# `f` aborts and the walk forward-flushes the caller (module) frame at the CALL
# boundary so the interpreter re-runs the call from there. That flush rebuilds
# the caller's operand stack from the walk's live/shadow sources — but the
# CALL's `LOAD_CONST`'d argument has no concrete Ref shadow, so its slot
# resolves to NULL. The flush must decline (fall back to the legacy replay)
# rather than commit the NULL; committing it left the next call's argument slot
# unbound, so `f` raised `UnboundLocalError` on its parameter (`n`).
#
# The trigger is specific: the caller loop must be at MODULE scope (its CALL
# operands come from LOAD_NAME / LOAD_CONST, not LOAD_FAST), the inner
# `range(n)` must be large enough to compile + bridge the comprehension loop,
# and the outer loop must run enough to reach the trace threshold so the two
# transitions coincide.


def f(n):
return [i for i in range(n)]


t = 0
k = 0
while k < 220:
t += len(f(300))
k += 1
print(t)
27 changes: 27 additions & 0 deletions pyre/bench/synth/int_mul_ovf_bignum_promote.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# pyre-check: max-pypy-ratio=30

# Overflow-crossing int multiply on a JIT-hot path. The inner loop is traced
# while `scale` is small (a*a stays in machine-int range, so the recorded
# GUARD_NO_OVERFLOW passes), then a large `scale` makes a*a overflow a 64-bit
# int and it must promote to a big int. A backend that drops the overflow check
# silently wraps the product instead of promoting, giving a wrong answer.
def hot(scale, n):
acc = 0
i = 0
while i < n:
a = scale + (i & 1) # loop-variant: cannot fold to a constant
acc = acc + a * a
i = i + 1
return acc


def main():
warm = 0
for _ in range(120):
warm = warm + hot(3, 20000) # a in {3,4}; a*a tiny, never overflows
# Big scale: a ~ 5e9, a*a = 2.5e19 overflows int64 (and uint64) -> big int.
print(hot(5000000000, 20000))
print(warm)


main()
33 changes: 33 additions & 0 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/fbw_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -741,6 +741,39 @@ pub(crate) fn fbw_store_journal_commit() {
FBW_FORITER_INFLIGHT.with(|c| c.borrow_mut().clear());
}

/// Record a bridge/retrace recording walk's range-iterator cursor before its
/// eager advance, so the abort path can restore it ([`FBW_BRIDGE_ITER_JOURNAL`]).
/// Called from the range FOR_ITER specialization ONLY while `is_bridge_trace`.
pub(crate) fn fbw_bridge_iter_journal_push(
iter: pyre_object::PyObjectRef,
pre_current: i64,
pre_remaining: i64,
) {
FBW_BRIDGE_ITER_JOURNAL.with(|j| j.borrow_mut().push((iter, pre_current, pre_remaining)));
}

/// Non-commit epilogue for a bridge/retrace recording walk: restore each
/// range iterator to the cursor it held before the walk advanced it, in
/// reverse push order. The interpreter resume then re-consumes the item the
/// aborted recording had taken, so the iteration is executed exactly once.
pub(crate) fn fbw_bridge_iter_journal_rollback() {
FBW_BRIDGE_ITER_JOURNAL.with(|j| {
let mut entries = j.borrow_mut();
while let Some((iter, pre_current, pre_remaining)) = entries.pop() {
unsafe {
pyre_object::functional::w_range_iter_set_cursor(iter, pre_current, pre_remaining);
}
}
});
}

/// Commit epilogue: a committed bridge recording keeps its advanced cursor
/// (the compiled bridge adopts it as the authoritative continuation), so drop
/// the undo log without restoring.
pub(crate) fn fbw_bridge_iter_journal_clear() {
FBW_BRIDGE_ITER_JOURNAL.with(|j| j.borrow_mut().clear());
}

/// Record the in-flight FOR_ITER continuation (#57 Option C): the consumed
/// item the `for_iter_next` residual produced and its FOR_ITER body coordinate.
/// Called from the residual
Expand Down
11 changes: 11 additions & 0 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4249,6 +4249,17 @@ thread_local! {
static FBW_FORITER_INFLIGHT: std::cell::RefCell<Vec<InflightForiter>> =
const { std::cell::RefCell::new(Vec::new()) };

/// Undo log for a bridge/retrace recording walk's eager range-iterator
/// cursor advance. The main walk leaves the advance unjournaled and relies
/// on in-flight FOR_ITER forward-delivery to recover the consumed item on
/// abort; the bridge/retrace abort path has no such delivery, so a bridge
/// walk records `(iter, pre_current, pre_remaining)` here and restores the
/// cursor when it does NOT commit — leaving the recording side-effect
/// neutral so the interpreter resume re-consumes the item exactly once.
/// Only populated while `is_bridge_trace`; empty (no-op) on the main walk.
static FBW_BRIDGE_ITER_JOURNAL: std::cell::RefCell<Vec<(pyre_object::PyObjectRef, i64, i64)>> =
const { std::cell::RefCell::new(Vec::new()) };
Comment on lines +4260 to +4261

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Root bridge iterator journal entries

This new TLS stores raw PyObjectRef iterators across the rest of an authoritative bridge walk, but capture_fbw_store_journal_root_area / fbw_store_journal_root_walker_area still visit the existing store/append/sys_exc/foriter journals only, not this one. If a minor collection runs after w_range_iter_next before rollback, the live frame's iterator pointer is forwarded while this journal slot is not, so w_range_iter_set_cursor can write through a stale moved pointer instead of restoring the iterator; add this journal to the root area/walker lifecycle like the other FBW journals.

AGENTS.md reference: AGENTS.md:L153-L155

Useful? React with 👍 / 👎.


static FBW_UNJOURNALED_VALUE_UNAVAILABLE: std::cell::Cell<bool> =
const { std::cell::Cell::new(false) };
static FBW_UNJOURNALED_SYMBOLIC: std::cell::Cell<bool> =
Expand Down
24 changes: 19 additions & 5 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/resume_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1132,10 +1132,11 @@ pub(crate) fn collect_call_stack_overrides<Sym: WalkSym>(
}
}
// Use the virtualizable shadow only after the live register/vstack
// sources. A mid-opcode shadow stack slot can be NULL because it was not
// mirrored, while the corresponding live color still has the value. The
// remaining NULL is the CALL's real null-or-self sentinel and must be
// preserved as an explicit slot override.
// sources, and only for slots those sources left unresolved. A mid-opcode
// shadow stack slot can be NULL because it was not mirrored, while the
// corresponding live color still holds the value — so a NULL reaching this
// fallback is an UNRESOLVED slot, not a proven null (see the per-slot note
// below), and is dropped rather than committed.
let base = ctx
.trace_ctx
.virtualizable_info()
Expand All @@ -1147,7 +1148,20 @@ pub(crate) fn collect_call_stack_overrides<Sym: WalkSym>(
}
if let Some((_opref, Value::Ref(value))) = ctx.trace_ctx.virtualizable_entry_at(base + slot)
{
overrides.push((slot, value.as_usize() as pyre_object::PyObjectRef));
// Only a positively-resolved (non-null) shadow value is a faithful
// stack slot. The live vstack/color sources above already emit
// every genuine null-or-self sentinel they can resolve
// (`concrete_ref_for_opref` yields an explicit null Ref for a
// PUSH_NULL box). A slot that reaches this shadow fallback with a
// NULL Ref is one the walk could not resolve — e.g. an
// unmaterialized `LOAD_CONST` operand whose concrete value was
// never mirrored — not a real null. Leaving it ABSENT makes the
// outer-call flush validation decline, so the legacy replay
// rebuilds the frame from its start state instead of resuming the
// interpreter over a NULL where a live object belongs.
if value.as_usize() != 0 {
overrides.push((slot, value.as_usize() as pyre_object::PyObjectRef));
}
}
}
overrides
Expand Down
6 changes: 6 additions & 0 deletions pyre/pyre-jit-trace/src/jitcode_dispatch/specialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5022,6 +5022,12 @@ pub(crate) fn try_walker_specialize_for_iter_next<Sym: WalkSym>(
ctx.trace_ctx
.set_opref_concrete(current, Value::Int(concrete_current));

if ctx.trace_ctx.is_bridge_trace {
// A bridge/retrace recording walk has no in-flight forward-delivery on
// abort, so journal the pre-advance cursor for restore if the walk does
// not commit (keeps the aborted recording side-effect neutral).
fbw_bridge_iter_journal_push(iter_obj, concrete_current, concrete_remaining);

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 Roll back bridge iterator journals on subwalk abort

When this specialization runs inside drive_bridge_carrier_subwalk, ctx.trace_ctx.is_bridge_trace is true, so it records the pre-advance range cursor here, but that subwalk does not return through run_perfn_walk's new epilogue; its failure path only calls fbw_store_journal_rollback() at pyre/pyre-jit-trace/src/trace.rs:1184 and other safe-abort paths reset at trace.rs:1455. For a bridge-carrier subwalk that consumes a range item and then declines, the journal is never rolled back, leaving the live iterator advanced while the interpreter/blackhole replays from the guard and drops that iteration.

Useful? React with 👍 / 👎.

}
fbw_foriter_inflight_capture(concrete_item_ptr, body);
// Range iteration stays at the C level, so the operand-stack mirror
// remains valid and must receive the item produced by FOR_ITER. Its
Expand Down
7 changes: 7 additions & 0 deletions pyre/pyre-jit-trace/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2688,8 +2688,15 @@ fn run_perfn_walk<Sym: WalkSym>(
let journal = crate::jitcode_dispatch::fbw_store_journal_len();
if committed {
crate::jitcode_dispatch::fbw_store_journal_commit();
// A committed bridge recording keeps its advanced iterator cursor (the
// compiled bridge / adopted end state owns the iteration count).
crate::jitcode_dispatch::fbw_bridge_iter_journal_clear();
} else {
crate::jitcode_dispatch::fbw_store_journal_rollback();
// A bridge/retrace recording that does not commit restores the
// iterator cursor it eagerly advanced, so the interpreter resume
// re-consumes the in-flight item exactly once (no drop).
crate::jitcode_dispatch::fbw_bridge_iter_journal_rollback();
}
if authoritative && std::env::var_os("PYRE_FBW_CENSUS").is_some() {
let mut end = match &walk_result {
Expand Down
Loading
Loading