Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
01df591
majit: evict per-loop side tables on retirement and harden MAJIT_BRID…
youknowone Jul 25, 2026
342c441
majit: allocate a headerless jitcode `new` from the interpreter's own…
youknowone Jul 25, 2026
348f3b4
majit: drop the loop side tables on the bulk eviction paths too
youknowone Jul 25, 2026
ce0ee4a
majit: log the bridge-path preview virtual-state mismatch
youknowone Jul 25, 2026
5fe9072
jit: name the portal driver's own frame_value_count decoder
youknowone Jul 25, 2026
9864237
majit: own the jitcode table and the portal jitcode from the static data
youknowone Jul 25, 2026
a17c9df
majit: pin the export preview's self-match, which keeps building_brid…
youknowone Jul 25, 2026
ae6d801
jit: resume the build-time liveness buffer instead of forking the pool
youknowone Jul 26, 2026
868a6d5
jit: seed the runtime assembler's insns from the build-time opcode table
youknowone Jul 26, 2026
cb8cf10
jit: bind the list-append store helpers and their jitcode shells as f…
youknowone Jul 26, 2026
9f14bec
majit: drop the bridge_only_parse tests orphaned by the MAJIT_BRIDGE_…
youknowone Jul 26, 2026
4e38cf6
jit: skip the finish_setup republish when neither writer buffer grew
youknowone Jul 27, 2026
4e5e2e0
jit: census how many build-time Field descrs converge with the get_fi…
youknowone Jul 26, 2026
dcc5602
jit: classify the field-descr identity census by miss reason and by r…
youknowone Jul 26, 2026
8a16a05
jit: mint every field descr through GcCache::get_field_descr
youknowone Jul 27, 2026
e54d22c
jit: back metainterp_sd.all_descrs with the process-wide descr registry
youknowone Jul 27, 2026
22be020
jit: ignore shrinking take_back_all_descrs write-backs
youknowone Jul 27, 2026
b655f6f
jit: carry EffectInfo raw descr sets across descrs.bin as gccache keys
youknowone Jul 27, 2026
4ba04cb
jit: list PyFrame.w_globals once and hold get_field_descr cache hits …
youknowone Jul 27, 2026
8a386b0
jit: allocate BC_NEW / bh_new structs through the GC and barrier the …
youknowone Jul 27, 2026
7150fa2
jit: build the global build-time descr pool inside the OnceLock initi…
youknowone Jul 27, 2026
d4c96fd
jit: resolve SizeDescr::w_class_obj through a frontend-registered dec…
youknowone Jul 27, 2026
d48c98c
jit: append in the orthodox list-append fold only when the sub-walk d…
youknowone Jul 27, 2026
6c84747
jit: resolve W_ListObject field descrs to the canonical group before …
youknowone Jul 27, 2026
1b2ea33
majit: register the headerless nursery-alloc hook in the cranelift an…
youknowone Jul 27, 2026
4493c3b
majit(cranelift): lower CallMallocNurseryHeaderless to an inline nurs…
youknowone Jul 27, 2026
71931c7
majit-translate: stride pointer array items by the target word in the…
youknowone Jul 28, 2026
817a853
majit-backend-wasm: implement bh_arraylen_gc, zero the old-gen JitFrame
youknowone Jul 28, 2026
1ca24be
majit: run rustfmt over resolve_w_class_obj and the metainterp re-exp…
youknowone Jul 28, 2026
3ecf7ab
majit-ir: keep the caller's struct-qualified field name in GcCache::g…
youknowone Jul 28, 2026
20d26f5
majit-metainterp: serialize the finish_setup_descrs publish across th…
youknowone Jul 28, 2026
1806815
jit: record the measured gap behind SetMemberLookup::AbsentContainer
youknowone Jul 28, 2026
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
201 changes: 171 additions & 30 deletions majit/majit-backend-cranelift/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,9 @@ fn register_active_hooks(supports_guard_gc_type: bool) {
supports_guard_gc_type,
});
majit_gc::set_active_alloc_nursery_typed(Some(alloc_nursery_typed_via_active_runtime));
majit_gc::set_active_alloc_nursery_headerless_no_collect(Some(
alloc_nursery_headerless_no_collect_via_active_runtime,
));
majit_gc::set_active_alloc_nursery_typed_with_placement(Some(
alloc_nursery_typed_with_placement_via_active_runtime,
));
Expand Down Expand Up @@ -1543,6 +1546,16 @@ fn alloc_nursery_typed_via_active_runtime(type_id: u32, size: usize) -> GcRef {
with_cranelift_gc(|gc| gc.try_alloc_nursery_no_collect_typed(type_id, size)).unwrap_or(GcRef(0))
}

/// `majit_gc::AllocNurseryHeaderlessNoCollectFn` installed by
/// `set_gc_allocator`. The metainterp's jitcode tracer allocates a `NEW` on a
/// `headerless` descr through here so the object lands in the interpreter's
/// own collected pool rather than the host heap, where its collector could not
/// see it. Returns `GcRef(0)` when no GC is bound, leaving the caller on its
/// own path.
fn alloc_nursery_headerless_no_collect_via_active_runtime(size: usize) -> GcRef {
with_cranelift_gc(|gc| gc.alloc_nursery_headerless_no_collect(size)).unwrap_or(GcRef(0))
}

/// Placement-reporting companion of
/// [`alloc_nursery_typed_via_active_runtime`].
///
Expand Down Expand Up @@ -11670,36 +11683,164 @@ impl CraneliftBackend {
&known_values,
op.arg(0).to_opref(),
);
let result = emit_collecting_gc_call(
&mut builder,
ptr_type,
call_conv,
jf_ptr,
&ref_root_slots,
&defined_ref_vars,
&stale_ref_vars,
&demoted_failarg_slots,
ref_root_base_ofs,
per_call_gcmap,
gc_alloc_nursery_headerless_shim as *const () as usize,
&[size],
Some(cl_types::I64),
)
.expect("headerless nursery allocation helper must return a value");
jf_ptr = emit_reload_frame_if_necessary(&mut builder, ptr_type, call_conv);
builder.ins().set_pinned_reg(jf_ptr);
builder.def_var(var(vi), result);
// malloc_cond parity: the headerless slow path
// (gc_alloc_nursery_headerless_shim) returns NULL on
// host/bounded out-of-memory; propagate before the
// following stores dereference it.
emit_memory_error_check(
&mut builder,
ptr_type,
result,
propagate_exception_descr_ptr,
preamble_phase,
);
// x86/assembler.py:2556-2565 malloc_cond parity, headerless
// variant — the same inline bump the `CallMallocNursery`
// arm below emits and the dynasm backends already emit for
// this opcode (`genop_call_malloc_nursery_headerless`).
// Three differences from the headered shape: the bump is by
// `size` alone (no `GcHeader::SIZE` reservation), no header
// word is zeroed, and the result is the old nursery base
// rather than `base + GcHeader::SIZE`.
//
// The raw bump is correct only while the active GC is
// headerless-aware, which is the same invariant
// `alloc_nursery_headerless`'s panicking default enforces on
// the overflow path — whoever declares `headerless_structs`
// upholds it. Without a bump surface (`nursery_free` /
// `nursery_top` reported as 0) the op stays on the helper.
let inline_bump = gc_nursery_addrs.filter(|&(nf, nt)| nf != 0 && nt != 0);
if let Some((nf_addr, nt_addr)) = inline_bump {
let flags = MemFlags::trusted();
let nf_ptr = builder.ins().iconst(ptr_type, nf_addr as i64);
let nt_ptr = builder.ins().iconst(ptr_type, nt_addr as i64);
let free = builder.ins().load(ptr_type, flags, nf_ptr, 0);
let new_free = builder.ins().iadd(free, size);
let top = builder.ins().load(ptr_type, flags, nt_ptr, 0);
// `nursery_top` is one-past-last, so the region is exhausted
// exactly when the bumped free pointer runs past it — the
// dynasm emitters' `b.hi` / `ja` slow-path edge.
let fits =
builder
.ins()
.icmp(IntCC::UnsignedLessThanOrEqual, new_free, top);

// Same block-param carry as the headered arm: only the slow
// path spills and reloads the ref roots, so the merge takes
// every live ref as a parameter instead of letting the two
// paths disagree on the variable's definition.
let live_refs: Vec<(u32, usize)> = ref_root_slots
.iter()
.filter(|(var_idx, _)| defined_ref_vars.contains(var_idx))
.copied()
.collect();

let fast_block = builder.create_block();
let slow_block = builder.create_block();
let merge_block = builder.create_block();
builder.append_block_param(merge_block, ptr_type); // result
builder.append_block_param(merge_block, ptr_type); // jf_ptr
for _ in &live_refs {
builder.append_block_param(merge_block, cl_types::I64);
}
builder.ins().brif(fits, fast_block, &[], slow_block, &[]);

// fast: publish the bumped free pointer and hand back the
// old base. Nothing here can collect, so no gcmap is pushed
// and no ref root is spilled.
builder.switch_to_block(fast_block);
builder.seal_block(fast_block);
builder.ins().store(flags, new_free, nf_ptr, 0);
let mut fast_args: Vec<BlockArg> =
vec![BlockArg::from(free), BlockArg::from(jf_ptr)];
for &(var_idx, _) in &live_refs {
fast_args.push(BlockArg::from(builder.use_var(var(var_idx))));
}
builder.ins().jump(merge_block, &fast_args);

// slow: aarch64 `_build_malloc_slowpath` parity — spill the
// ref roots and install the gcmap, then let the helper
// collect and re-bump.
builder.switch_to_block(slow_block);
builder.seal_block(slow_block);
builder.set_cold_block(slow_block);
spill_ref_roots(
&mut builder,
jf_ptr,
&ref_root_slots,
&defined_ref_vars,
&stale_ref_vars,
&demoted_failarg_slots,
ref_root_base_ofs,
);
emit_push_gcmap(&mut builder, jf_ptr, per_call_gcmap);
let slow_r = emit_host_call(
&mut builder,
ptr_type,
call_conv,
gc_alloc_nursery_headerless_shim as *const () as usize,
&[size],
Some(cl_types::I64),
)
.expect("headerless nursery allocation helper must return a value");
let jf_ptr_slow =
emit_reload_frame_if_necessary(&mut builder, ptr_type, call_conv);
emit_pop_gcmap(&mut builder, jf_ptr_slow, per_call_gcmap);
reload_ref_roots(
&mut builder,
jf_ptr_slow,
&ref_root_slots,
&defined_ref_vars,
&demoted_failarg_slots,
ref_root_base_ofs,
);
let mut slow_args: Vec<BlockArg> =
vec![BlockArg::from(slow_r), BlockArg::from(jf_ptr_slow)];
for &(var_idx, _) in &live_refs {
slow_args.push(BlockArg::from(builder.use_var(var(var_idx))));
}
builder.ins().jump(merge_block, &slow_args);

builder.switch_to_block(merge_block);
builder.seal_block(merge_block);
let params = builder.block_params(merge_block).to_vec();
let result = params[0];
jf_ptr = params[1];
builder.ins().set_pinned_reg(jf_ptr);
for (i, &(var_idx, _)) in live_refs.iter().enumerate() {
builder.def_var(var(var_idx), params[2 + i]);
}
builder.def_var(var(vi), result);
// malloc_cond parity: the headerless slow path
// (gc_alloc_nursery_headerless_shim) returns NULL on
// host/bounded out-of-memory; propagate before the
// following stores dereference it. The fast path cannot
// produce NULL, but the check is on the merged value so a
// slow-path NULL is caught on either edge.
emit_memory_error_check(
&mut builder,
ptr_type,
result,
propagate_exception_descr_ptr,
preamble_phase,
);
} else {
let result = emit_collecting_gc_call(
&mut builder,
ptr_type,
call_conv,
jf_ptr,
&ref_root_slots,
&defined_ref_vars,
&stale_ref_vars,
&demoted_failarg_slots,
ref_root_base_ofs,
per_call_gcmap,
gc_alloc_nursery_headerless_shim as *const () as usize,
&[size],
Some(cl_types::I64),
)
.expect("headerless nursery allocation helper must return a value");
jf_ptr = emit_reload_frame_if_necessary(&mut builder, ptr_type, call_conv);
builder.ins().set_pinned_reg(jf_ptr);
builder.def_var(var(vi), result);
emit_memory_error_check(
&mut builder,
ptr_type,
result,
propagate_exception_descr_ptr,
preamble_phase,
);
}
}
OpCode::CallMallocNursery => {
// x86/assembler.py:2556-2565 malloc_cond parity.
Expand Down
98 changes: 68 additions & 30 deletions majit/majit-backend-dynasm/src/runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@ fn register_active_hooks(supports_guard_gc_type: bool) {
supports_guard_gc_type,
});
majit_gc::set_active_alloc_nursery_typed(Some(dynasm_alloc_nursery_typed));
majit_gc::set_active_alloc_nursery_headerless_no_collect(Some(
dynasm_alloc_nursery_headerless_no_collect,
));
majit_gc::set_active_alloc_nursery_typed_with_placement(Some(
dynasm_alloc_nursery_typed_with_placement,
));
Expand Down Expand Up @@ -357,6 +360,26 @@ pub(crate) extern "C" fn dynasm_new_alloc(size: usize) -> *mut u8 {
})
}

/// Headerless no-collect nursery trampoline for backend-agnostic callers.
///
/// The metainterp's jitcode tracer allocates a `NEW` on a `headerless` descr
/// through here so the object lands in the interpreter's own collected pool
/// rather than the host heap, where its collector could not see it. Returns
/// null when no GC is bound, leaving the caller on its own path.
fn dynasm_alloc_nursery_headerless_no_collect(size: usize) -> GcRef {
if let Some(r) = DYNASM_ACTIVE_GC.with(|c| {
c.borrow_mut()
.as_deref_mut()
.map(|g| g.alloc_nursery_headerless_no_collect(size))
}) {
return r;
}
if majit_gc::gc_sync::is_initialized() {
return majit_gc::gc_sync::gc_op(|g| g.alloc_nursery_headerless_no_collect(size));
}
GcRef::NULL
}

/// Host-side nursery allocation trampoline. Published via
/// `majit_gc::set_active_alloc_nursery_typed` from `set_gc_allocator`
/// so backend-agnostic callers (e.g. pyre-object `w_int_new`) can
Expand Down Expand Up @@ -465,6 +488,49 @@ fn dynasm_alloc_oldgen_typed(type_id: u32, size: usize) -> GcRef {
majit_gc::gc_sync::gc_op(|g| g.alloc_oldgen_typed(type_id, size))
}

/// Allocate the struct a `bh_new` / `bh_new_with_vtable` descr describes
/// (`llmodel.py:775-786`).
///
/// A GC-managed struct (real `type_id`) MUST be allocated through the GC so the
/// collector can trace its pointer fields: a resume-materialized virtual (e.g.
/// an inlined-callee `PyFrame`) holds a `locals_cells_stack` ref to its arrays,
/// and a raw `libc::malloc` block is invisible to the GC, so a minor collection
/// during the blackhole forward run frees those arrays out from under the
/// frame. Allocate in the non-moving old generation (mark-sweep), mirroring
/// `w_int_new`/`w_float_new`: the blackhole register file and the deep forward
/// recursion capture raw pointers to the materialized struct that the resume
/// path does not re-root across the minor collections it triggers, so a moving
/// nursery object would leave those captures stale. Old-gen keeps every
/// materialized pointer stable for the lifetime of the resume.
///
/// A headerless struct lives in the interpreter's own `headerless_structs` pool
/// and carries no `type_id` word at `ref - 8`, so it takes the headerless
/// nursery allocator instead: `alloc_oldgen_typed` returns
/// `base + GcHeader::SIZE`, which would shift every field offset the descr
/// carries.
///
/// Non-GC descrs (`type_id == 0`, raw buffers) and a runtime with no allocator
/// hook installed (unit tests) keep the plain zeroed malloc.
fn bh_alloc_struct(sizedescr: &majit_translate::jitcode::BhDescr) -> *mut libc::c_void {
let size = sizedescr.as_size();
let gc_ptr = if sizedescr.is_headerless() {
majit_gc::alloc_nursery_headerless_no_collect(size).0
} else {
match sizedescr.resolve_gc_tid() {
0 => 0,
type_id => dynasm_alloc_oldgen_typed(type_id, size).0,
}
};
if gc_ptr != 0 {
return gc_ptr as *mut libc::c_void;
}
let ptr = unsafe { libc::malloc(size) };
if !ptr.is_null() {
unsafe { libc::memset(ptr, 0, size) };
}
ptr
}

/// User-level `gc.collect()` trampoline — drives `GcAllocator::collect_full`
/// on the active dynasm-owned GC. PyPy's `pypy/module/gc/interp_gc.py:7-26`
/// runs `rgc.collect()` from app-level `gc.collect`; this is the dynasm
Expand Down Expand Up @@ -2965,40 +3031,12 @@ impl Backend for DynasmBackend {
}

fn bh_new(&self, sizedescr: &majit_translate::jitcode::BhDescr) -> i64 {
let size = sizedescr.as_size();
let ptr = unsafe { libc::malloc(size) };
if !ptr.is_null() {
unsafe { libc::memset(ptr, 0, size) };
}
ptr as i64
bh_alloc_struct(sizedescr) as i64
}

fn bh_new_with_vtable(&self, sizedescr: &majit_translate::jitcode::BhDescr) -> i64 {
let size = sizedescr.as_size();
let vtable = sizedescr.get_vtable();
// A GC-managed struct (real `type_id`) MUST be allocated through the GC
// so the collector can trace its pointer fields: a resume-materialized
// virtual (e.g. an inlined-callee `PyFrame`) holds a `locals_cells_stack`
// ref to its arrays, and a raw `libc::malloc` block is invisible to the
// GC, so a minor collection during the blackhole forward run frees those
// arrays out from under the frame. Allocate in the non-moving old
// generation (mark-sweep), mirroring `w_int_new`/`w_float_new`: the
// blackhole register file and the deep forward recursion capture raw
// pointers to the materialized struct that the resume path does not
// re-root across the minor collections it triggers, so a moving nursery
// object would leave those captures stale. Old-gen keeps every
// materialized pointer stable for the lifetime of the resume. Non-GC
// descrs (`type_id == 0`, raw buffers) keep the plain malloc.
let type_id = sizedescr.resolve_gc_tid();
let ptr = if type_id != 0 {
dynasm_alloc_oldgen_typed(type_id, size).0 as *mut libc::c_void
} else {
let ptr = unsafe { libc::malloc(size) };
if !ptr.is_null() {
unsafe { libc::memset(ptr, 0, size) };
}
ptr
};
let ptr = bh_alloc_struct(sizedescr);
if !ptr.is_null() {
unsafe {
// llmodel.py:780-782: if self.vtable_offset is not None:
Expand Down
Loading
Loading