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
161 changes: 119 additions & 42 deletions majit/majit-backend-cranelift/src/compiler.rs

Large diffs are not rendered by default.

114 changes: 83 additions & 31 deletions majit/majit-gc/src/rewrite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,12 @@ pub struct GcRewriterImpl {
pub supports_load_effective_address: bool,
/// llsupport/gc.py:30-34 `malloc_zero_filled` parity.
///
/// `true` when the allocator zero-fills payload bytes on
/// allocation. pyre's `Nursery` uses `alloc_zeroed` (nursery.rs:68)
/// and `reset()` memsets to zero on recycle (nursery.rs:105-110),
/// so production is always `true`. Gates `clear_gc_fields` per
/// rewrite.py:499-500; a future non-zero-fill allocator path would
/// flip this to `false` and let the existing plumbing emit
/// explicit NULL-pointer stores at flush time
/// `true` when the allocation path itself guarantees zero-filled
/// payload bytes. Production backends set this to `false` whenever a
/// real collector is installed and to `true` for the Boehm/raw-calloc
/// fallback (compiler.rs:8303, runner.rs:1704). Gates
/// `clear_gc_fields` per rewrite.py:499-500; the non-zero-fill path
/// emits explicit NULL-pointer stores at flush time
/// (rewrite.py:761-766).
pub malloc_zero_filled: bool,
/// llsupport/gc.py:39 `self.memcpy_fn = memcpy_fn` cast to a Signed
Expand Down Expand Up @@ -1205,24 +1204,18 @@ impl GcRewriterImpl {
self.gen_initialize_vtable(obj_ref.clone(), vtable, vtable_fd_ref, st);
}
}
// Upstream rewrite.py:479-484 rewrites NEW_WITH_VTABLE into allocation
// plus full header initialization. Pyre's object layout carries a
// separate `w_class` Python-class pointer alongside the vtable;
// interpreter, blackhole, and deopt-materialize paths all write it,
// so compiled allocations must too or trace-time GuardValue(w_class)
// folds fail deterministically on trace-made objects.
if let Some(w_class) = descr.w_class_obj() {
if w_class != 0 {
if let Some(w_class_fd) =
descr.gc_fielddescrs().iter().find(|fd| fd.is_w_class())
{
self.gen_initialize_w_class(
obj_ref.clone(),
w_class,
w_class_fd.as_ref(),
st,
);
}
}
// Upstream rewrite.py:479-484 rewrites NEW_WITH_VTABLE into allocation
// plus full header initialization. Pyre's object layout carries a
// separate `w_class` Python-class pointer alongside the vtable. Honor
// that descriptor invariant for both fixed-size allocation opcodes:
// clear_gc_fields handles both, and the optimizer's force path may
// materialize either Virtual or VirtualStruct without a duplicate
// SETFIELD_GC for this header slot.
if let Some(w_class) = descr.w_class_obj() {
if w_class != 0 {
if let Some(w_class_fd) = descr.gc_fielddescrs().iter().find(|fd| fd.is_w_class()) {
self.gen_initialize_w_class(obj_ref.clone(), w_class, w_class_fd.as_ref(), st);
}
}
}
Expand Down Expand Up @@ -1958,12 +1951,11 @@ impl GcRewriterImpl {
/// (rewrite.py:761-766) does not re-zero a slot that this explicit
/// SETFIELD_GC is about to overwrite.
///
/// Under pyre's default zero-fill nursery configuration
/// (`malloc_zero_filled = true`), `clear_gc_fields` skips its
/// insertion path, so this is effectively a no-op. The body is
/// wired for parity so that a non-zero-fill allocator automatically
/// activates the delayed-zero tracking without further callsite
/// changes.
/// Under the Boehm/raw-calloc fallback (`malloc_zero_filled = true`),
/// `clear_gc_fields` skips its insertion path, so this is effectively a
/// no-op. With a real collector, production backends set the flag to
/// false (compiler.rs:8303, runner.rs:1704), activating the delayed-zero
/// tracking.
fn consider_setfield_gc(&self, op: &Op, st: &mut RewriteState) {
let Some(descr) = op.getdescr() else { return };
let Some(fd) = descr.as_field_descr() else {
Expand Down Expand Up @@ -4595,6 +4587,66 @@ mod tests {
);
}

#[test]
fn test_new_with_vtable_eagerly_initializes_w_class_without_trace_store() {
let mut rw = make_rewriter();
rw.fielddescr_vtable = None;
let w_class = 0xD00D;
let ops = vec![Op::with_descr(
OpCode::NewWithVtable,
&[],
size_descr_with_w_class(48, 3, 0, Some(w_class), vec![w_class_field_descr_at(8)]),
)];

let (result, _constants, gcrefs) = rw.rewrite_for_gc_with_constants(&ops, &ConstMap::new());

assert_eq!(gcrefs, vec![GcRef(w_class as usize)]);
assert_eq!(
result
.iter()
.filter(|op| {
op.opcode == OpCode::GcStore
&& op.arg(1).to_opref().inline_const_bits() == Some(8)
&& op.arg(2).to_opref().inline_const_bits() != Some(0)
})
.count(),
1,
"allocation lowering must remain the sole w_class writer: {result:?}"
);
}

#[test]
fn test_new_initializes_w_class_before_clear_gc_fields() {
let mut rw = make_rewriter();
rw.fielddescr_vtable = None;
rw.malloc_zero_filled = false;
let w_class = 0xD00D;
let ops = vec![
Op::with_descr(
OpCode::New,
&[],
size_descr_with_w_class(48, 3, 0, Some(w_class), vec![w_class_field_descr_at(8)]),
),
Op::new(OpCode::Jump, &[]),
];

let (result, _constants, gcrefs) = rw.rewrite_for_gc_with_constants(&ops, &ConstMap::new());

assert_eq!(gcrefs, vec![GcRef(w_class as usize)]);
let w_class_stores: Vec<_> = result
.iter()
.filter(|op| {
op.opcode == OpCode::GcStore && op.arg(1).to_opref().inline_const_bits() == Some(8)
})
.collect();
assert_eq!(w_class_stores.len(), 1, "{result:?}");
assert_ne!(
w_class_stores[0].arg(2).to_opref().inline_const_bits(),
Some(0),
"plain NEW must receive the eager class value, not delayed NULL"
);
}

#[test]
fn test_clear_gc_fields_zeros_w_class_without_init_value() {
let mut rw = make_rewriter();
Expand Down
23 changes: 12 additions & 11 deletions majit/majit-ir/src/descr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4247,11 +4247,11 @@ pub fn vable_array_descr(idx: u16) -> DescrRef {
/// (after `v_inst`) and `setfield_vable_<kind>` (after `v_inst,
/// v_value`).
///
/// Pyre's `PyFrame._virtualizable_` declaration (see
/// `pyre-interpreter/src/pyframe.rs:406` and `interp_jit.py:25-31`)
/// has 6 static fields in fixed order: `[last_instr, pycode,
/// valuestackdepth, debugdata, lastblock, w_globals]`, so legitimate
/// `idx` values are `0..=5`. The struct stores only the per-field
/// `interp_jit.py:25-30` has 5 scalar fields in fixed order:
/// `[last_instr, pycode, valuestackdepth, debugdata, w_globals]`, so
/// legitimate `idx` values are `0..=4`. The canonical table is
/// `pyre-jit-trace/src/virtualizable_spec.rs::PYFRAME_VABLE_FIELDS`.
/// The struct stores only the per-field
/// index; bytecode emission and runtime field access still go
/// through the field-idx-to-offset table maintained by
/// `virtualizable_spec.rs`.
Expand All @@ -4277,14 +4277,16 @@ impl Descr for VableStaticFieldDescr {

/// Number of `OnceLock<DescrRef>` slots reserved for
/// `vable_static_field_descr(idx)` singletons. Matches the exact
/// scalar-field count of pyre's PyFrame virtualizable
/// (`interp_jit.py:25-31`: `last_instr, pycode, valuestackdepth,
/// debugdata, lastblock, w_globals`), mirroring upstream
/// scalar-field count of PyFrame's virtualizable
/// (`interp_jit.py:25-30`: `last_instr, pycode, valuestackdepth,
/// debugdata, w_globals`), with the canonical table at
/// `pyre-jit-trace/src/virtualizable_spec.rs::PYFRAME_VABLE_FIELDS`.
/// This mirrors
/// `rpython/jit/metainterp/virtualizable.py:71`'s
/// `static_field_descrs = [... for name in static_fields]` which
/// is sized exactly to `len(static_fields)`. Bump this when the
/// PyFrame `_virtualizable_` declaration grows.
const VABLE_STATIC_FIELD_DESCR_SLOTS: usize = 6;
const VABLE_STATIC_FIELD_DESCR_SLOTS: usize = 5;

/// Singleton accessor for `static_field_descrs[idx]`.
///
Expand All @@ -4302,14 +4304,13 @@ pub fn vable_static_field_descr(idx: u16) -> DescrRef {
OnceLock::new(),
OnceLock::new(),
OnceLock::new(),
OnceLock::new(),
];
let i = idx as usize;
assert!(
i < VABLE_STATIC_FIELD_DESCR_SLOTS,
"vable_static_field_descr: idx={} exceeds VABLE_STATIC_FIELD_DESCR_SLOTS={}; \
pyre's PyFrame _virtualizable_ declares only {} static fields \
(interp_jit.py:25-31)",
(interp_jit.py:25-30)",
idx,
VABLE_STATIC_FIELD_DESCR_SLOTS,
VABLE_STATIC_FIELD_DESCR_SLOTS,
Expand Down
2 changes: 1 addition & 1 deletion majit/majit-macros/src/virtualizable/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ pub fn expand_sym(input: DeriveInput) -> TokenStream {
/// Flush virtualizable static fields from concrete values.
///
/// `values` is `[last_instr, pycode, valuestackdepth, ...]`
/// in VirtualizableInfo declared field order (interp_jit.py:25-31).
/// in VirtualizableInfo declared field order (interp_jit.py:25-30).
pub fn flush_vable_fields(
&mut self,
ctx: &mut majit_metainterp::TraceCtx,
Expand Down
4 changes: 2 additions & 2 deletions majit/majit-macros/src/virtualizable/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub(crate) struct VirtualizableMacroInput {
/// Frame pointer field name in the state struct (e.g., `frame`).
frame_field: Option<Ident>,
/// Vable scalar fields (= RPython `_virtualizable_` static fields,
/// `interp_jit.py:25-31`). Read/written via getfield_vable on the
/// `interp_jit.py:25-30`). Read/written via getfield_vable on the
/// vable heap object; included in extract_live / jump_args.
inputargs: Vec<InputArgField>,
/// Extra red inputargs that are NOT vable scalar fields (= RPython
Expand Down Expand Up @@ -486,7 +486,7 @@ fn generate_layout_helpers(
///
/// TODO: codegen-time constant equivalent to
/// `len(VABLEINFO.static_field_descrs) + 1` (frame ptr + N
/// `_virtualizable_` scalars from `interp_jit.py:25-31`). RPython
/// `_virtualizable_` scalars from `interp_jit.py:25-30`). RPython
/// derives the count dynamically by iterating
/// `range(len(self.static_field_descrs))` (`virtualizable.py:86`);
/// pyre crystallises it at proc-macro expansion time so the flat
Expand Down
Loading
Loading