Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c38a39f
jit(fbw): emit ExecutionContext.enter/leave at the inlined-call push
youknowone Jul 25, 2026
e02a744
jit(fbw): Codex parity review fixes for the inlined-call enter/leave
youknowone Jul 25, 2026
d0307d2
majit, jit(fbw): second Codex parity review round
youknowone Jul 25, 2026
efaf811
jit: seed w_class from SizeDescr::w_class_obj in the resume materiali…
youknowone Jul 25, 2026
24fe833
jit(fbw): force the vref in gettopframe_raw; balance ec.topframeref a…
youknowone Jul 25, 2026
3a70fe0
jit(fbw): replay ExecutionContext.leave for bridge-resumed frames
youknowone Jul 26, 2026
393cd25
majit(virtualref): guard alloc_virtual_ref on the real unset sentinel
youknowone Jul 26, 2026
5eebf36
jit, majit: write-barrier the blackhole resume ref stores and the vre…
youknowone Jul 27, 2026
783eea4
jit, majit: close every bridge-carrier vref scope; force vrefs throug…
youknowone Jul 27, 2026
646c78c
jit, majit: PR #796 review round — vref finish, carrier drain scope, …
youknowone Jul 27, 2026
369c2f8
majit: memclear bh_new_array's payload; barrier the vable array ref s…
youknowone Jul 28, 2026
baffb18
jit: rustfmt the hoisted EC descr group field list
youknowone Jul 28, 2026
bb66472
jit, majit: restore the inlined-callee topframeref publish; make the …
youknowone Jul 28, 2026
5b9a3b1
majit(gc): restrict rewriter position reservation to typed body variants
youknowone Jul 29, 2026
77445c0
jit, majit: correct the JitVirtualRef doc comments to the current layout
youknowone Jul 29, 2026
16d0080
majit(gc): keep every non-Void payload in the rewriter high-water mark
youknowone Jul 29, 2026
21d053b
majit(dynasm): stop the write-barrier emitters from assembling nothing
youknowone Jul 29, 2026
f06af59
majit: cover the aarch64 immediate card arm and the unmanaged-frame b…
youknowone Jul 29, 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
24 changes: 22 additions & 2 deletions majit/majit-backend-cranelift/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16667,9 +16667,29 @@ impl majit_backend::Backend for CraneliftBackend {
type_id != 0,
"bh_new_array requires ArrayDescr.tid (descr.py:340) — got 0"
);
active_runtime_alloc_varsize_typed_and_set_len(
let obj = active_runtime_alloc_varsize_typed_and_set_len(
type_id, base_size, itemsize, len_offset, length,
) as i64
);
// The nursery is not zero-filled (`incminimark.py:211
// malloc_zero_filled = False`), so the fresh block still holds the
// recycled bytes of whatever lived there before. `framework.py:1058-1079
// gct_do_malloc_varsize_clear` compensates by memclearing the fixed and
// the variable part and only then storing the length, which is what a
// GC-traced array needs: the tracer visits every item slot, including
// the ones past `valuestackdepth` that nobody has written yet.
//
// This belongs here rather than in the shared allocation helper: the
// inline allocators in compiled code get their clearing from the
// rewriter's ZERO_ARRAY (`rewrite.py:499`, `:521`) and must stay
// memclear-free on the fast path.
if obj != 0 {
unsafe {
let p = obj as *mut u8;
std::ptr::write_bytes(p, 0, base_size + itemsize * length);
*(p.add(len_offset) as *mut usize) = length;
}
}
obj as i64
}

/// llmodel.py:790 bh_new_array_clear = bh_new_array.
Expand Down
252 changes: 227 additions & 25 deletions majit/majit-backend-dynasm/src/aarch64/assembler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6067,28 +6067,39 @@ impl<'a> AssemblerARM64<'a> {

/// aarch64/opassembler.py:912 _write_barrier_fastpath parity.
fn emit_write_barrier_fastpath(&mut self, op: &Op, arglocs: &[Loc]) {
// opassembler.py:934 `mc.LDRB_ri(r.ip0.value, loc_base.value, ...)`
// indexes the base as a core register; upstream guarantees that in
// `ARMRegisterManager.return_constant` (aarch64/regalloc.py:70), which
// materializes every Const into a scratch register. The shared
// `RegisterManager::return_constant` follows the llsupport spelling
// (llsupport/regalloc.py:625) and can hand back a bare `Loc::Immed`,
// so state the contract instead of emitting nothing — a barrier that
// assembles to zero bytes stays invisible until it corrupts memory.
let loc_base = match arglocs.first() {
Some(Loc::Reg(r)) => *r,
_ => return,
other => {
panic!("write barrier base loc must be Loc::Reg (regalloc contract), got {other:?}")
}
};
let is_array = op.opcode == majit_ir::OpCode::CondCallGcWbArray;
let loc_index = match arglocs.get(1) {
Some(Loc::Reg(r)) => Some(*r),
_ => None,
};
// opassembler.py:996 `loc_index = arglocs[1]` — the location kind is
// discriminated at the card-marking block, not here.
let loc_index = arglocs.get(1).copied();
self.emit_write_barrier_fastpath_for_base(loc_base, is_array, loc_index);
}

fn emit_write_barrier_fastpath_for_base(
&mut self,
loc_base: crate::regloc::RegLoc,
is_array: bool,
loc_index: Option<crate::regloc::RegLoc>,
loc_index: Option<Loc>,
) {
let wb = match crate::runner::dynasm_write_barrier_descr() {
Some(wb) => wb,
None => return,
};
// opassembler.py:917-919 asserts the descriptor is the collector's
// write-barrier class. `COND_CALL_GC_WB` only exists because the GC
// rewriter emitted it, so a missing descriptor here means the two
// disagree; returning would drop the barrier without a trace.
let wb = crate::runner::dynasm_write_barrier_descr()
.expect("COND_CALL_GC_WB emitted without a write barrier descriptor");
let card_marking = is_array && wb.jit_wb_cards_set != 0;

// opassembler.py:922-929: build mask
Expand Down Expand Up @@ -6137,20 +6148,46 @@ impl<'a> AssemblerARM64<'a> {

// opassembler.py:996-1015: card marking inline
dynasm!(self.mc ; .arch aarch64 ; =>card_mark);
if let Some(loc_index) = loc_index {
let shift = 3 + wb.jit_wb_card_page_shift;
dynasm!(self.mc ; .arch aarch64
; lsr x16, X(loc_index.value), shift
; mvn x30, x16
; lsr x16, X(loc_index.value), wb.jit_wb_card_page_shift
; and x17, x16, 7
; mov x16, 1
; lsl x17, x16, x17
; sub x30, x30, majit_gc::header::GcHeader::SIZE as u32
; ldrb w16, [X(loc_base.value), x30]
; orr w16, w16, w17
; strb w16, [X(loc_base.value), x30]
);
match loc_index {
// opassembler.py:997 `assert loc_index.is_core_reg()`
Some(Loc::Reg(loc_index)) => {
let shift = 3 + wb.jit_wb_card_page_shift;
dynasm!(self.mc ; .arch aarch64
; lsr x16, X(loc_index.value), shift
; mvn x30, x16
; lsr x16, X(loc_index.value), wb.jit_wb_card_page_shift
; and x17, x16, 7
; mov x16, 1
; lsl x17, x16, x17
; sub x30, x30, majit_gc::header::GcHeader::SIZE as u32
; ldrb w16, [X(loc_base.value), x30]
; orr w16, w16, w17
; strb w16, [X(loc_base.value), x30]
);
}
// x86/assembler.py:2382-2386 `elif isinstance(loc_index, ImmedLoc)`:
// byte offset and bit mask are both assembly-time constants, so
// the sequence collapses to one load/or/store at a fixed
// displacement. A64 has no or-to-memory form, so the OR8 there
// becomes ldrb/orr/strb here. `byte_ofs` carries the same
// `- GcHeader::SIZE` bias the register form applies with
// `sub x30, x30, ...`: the base addresses the payload while the
// card bytes sit before the header.
Some(Loc::Immed(loc_index)) => {
let byte_index = loc_index.value >> wb.jit_wb_card_page_shift;
let byte_ofs = !(byte_index >> 3) - majit_gc::header::GcHeader::SIZE as i64;
let byte_val = (1_i64 << (byte_index & 7)) as u32;
self.emit_mov_imm64(30, byte_ofs);
dynasm!(self.mc ; .arch aarch64
; mov w17, byte_val
; ldrb w16, [X(loc_base.value), x30]
; orr w16, w16, w17
; strb w16, [X(loc_base.value), x30]
);
}
// x86/assembler.py:2387-2388
// `raise AssertionError("index is neither RegLoc nor ImmedLoc")`
_ => panic!("index is neither RegLoc nor ImmedLoc"),
}
} else {
// opassembler.py:968-976: non-array slow path
Expand Down Expand Up @@ -7494,8 +7531,12 @@ mod tests {
use std::time::Duration;

use majit_backend::{Backend, JitCellToken};
use majit_ir::forwarding::bound_operand_from_opref;
use majit_ir::operand::Operand;
use majit_ir::{Op, OpCode, OpRef, Type, make_array_descr_signed, make_loop_target_descr};
use majit_ir::{
GcRef, InputArg, Op, OpCode, OpRef, Type, Value, make_array_descr_signed,
make_loop_target_descr,
};

use crate::runner::DynasmBackend;

Expand Down Expand Up @@ -7719,4 +7760,165 @@ mod tests {
.expect("the compiled-loop worker must resume cleanly");
worker.join().unwrap();
}

// ── COND_CALL_GC_WB_ARRAY inline card marking ──────────────────────

/// Array length large enough that indices land in more than one card
/// byte at the default `card_page_indices = 128` (incminimark.py:275):
/// eight cards per byte means index 1024 is the first index in card
/// byte 1.
const CARD_ARRAY_LENGTH: usize = 2048;

/// incminimark.py:1017-1030 `external_malloc` with card bits: an
/// old-gen varsize array whose items are GC pointers gets GCFLAG_HAS_CARDS
/// and a run of zeroed card bytes in front of the header.
fn alloc_old_card_array(gc: &mut majit_gc::collector::MiniMarkGC, type_id: u32) -> GcRef {
let item_size = std::mem::size_of::<GcRef>();
let total_size = majit_gc::header::GcHeader::SIZE + 8 + item_size * CARD_ARRAY_LENGTH;
let obj = gc.alloc_in_oldgen_with_cards(type_id, total_size, CARD_ARRAY_LENGTH, true);
// `dirty_cards` reads the length out of the array's own length field.
unsafe { *(obj.0 as *mut usize) = CARD_ARRAY_LENGTH };
obj
}

/// Compile and run a one-operation trace holding a single
/// `COND_CALL_GC_WB_ARRAY` against `obj`.
///
/// `index_in_register` selects which argloc kind the emitter sees:
/// a non-constant `InputArg` is forced into a core register, while a
/// `ConstInt` reaches `RegisterManager::return_constant`
/// (llsupport/regalloc.py:625) with no selected register and comes back
/// as a bare `Loc::Immed`.
fn run_cond_call_gc_wb_array(trace_id: u64, obj: GcRef, index: i64, index_in_register: bool) {
let mut backend = DynasmBackend::new();
backend.attach_default_test_descrs();

let mut inputargs = vec![InputArg::new_ref(0)];
let mut values = vec![Value::Ref(obj)];
let index_operand = if index_in_register {
inputargs.push(InputArg::new_int(1));
values.push(Value::Int(index));
bound_operand_from_opref(OpRef::input_arg_int(1))
} else {
bound_operand_from_opref(OpRef::const_int(index))
};

let barrier = Op::new(
OpCode::CondCallGcWbArray,
&[
bound_operand_from_opref(OpRef::input_arg_ref(0)),
index_operand,
],
);
barrier.pos.set(OpRef::void_op(2));

let finish = Op::new(OpCode::Finish, &[]);
finish.pos.set(OpRef::void_op(3));
finish.set_fail_arg_types(vec![]);
finish.setfailargs(vec![].into());

let mut token = JitCellToken::new(trace_id);
backend
.compile_loop(&inputargs, &[Rc::new(barrier), Rc::new(finish)], &mut token)
.expect("compile COND_CALL_GC_WB_ARRAY trace");
let frame = backend.execute_token(&token, &values);
assert!(
backend.get_latest_descr(&frame).is_finish(),
"the barrier trace must run to its FINISH"
);
}

/// opassembler.py:996-1015 inline card marking, immediate-index arm.
///
/// The register arm shifts the index at runtime; the immediate arm folds
/// the same two quantities — card byte displacement and card bit — at
/// assembly time (x86/assembler.py:2382-2386). Both must dirty exactly
/// the card `mark_card` (incminimark.py:1574-1598) would dirty.
///
/// Regression cover: while the whole card sequence sat under a match that
/// only admitted `Loc::Reg`, an immediate index assembled to zero bytes
/// and the array kept a clean card across a barrier that was supposed to
/// dirty one.
#[test]
fn cond_call_gc_wb_array_immed_index_marks_same_card_as_reg_index() {
// gc.py:273 JIT_WB_CARDS_SET — zero means the backend emits no card
// sequence at all, which would leave this test asserting nothing.
let wb = crate::runner::dynasm_write_barrier_descr()
.expect("a write barrier descriptor must be resolvable");
assert_ne!(
wb.jit_wb_cards_set, 0,
"card marking must be enabled for this test to exercise the card arms"
);
let card_page_shift = wb.jit_wb_card_page_shift;

let mut gc = majit_gc::collector::MiniMarkGC::new();
let item_size = std::mem::size_of::<GcRef>();
let type_id = gc.register_type(majit_gc::TypeInfo::varsize(
8,
item_size,
0,
true,
Vec::new(),
));
let obj_immed = alloc_old_card_array(&mut gc, type_id);
let obj_reg = alloc_old_card_array(&mut gc, type_id);
let obj_interp = alloc_old_card_array(&mut gc, type_id);

// opassembler.py:943-949 branches straight to the inline card block
// when GCFLAG_CARDS_SET is already set, so the compiled arms never
// reach the `jit_remember_young_pointer_from_array` helper.
// `mark_card` sets the same flag on the interpreter's object itself.
for obj in [obj_immed, obj_reg] {
unsafe {
(*majit_gc::header::header_of(obj.0)).set_flag(majit_gc::flags::CARDS_SET);
}
}
for obj in [obj_immed, obj_reg, obj_interp] {
assert!(
gc.dirty_cards(obj).is_empty(),
"a freshly allocated card array starts with every card clean"
);
}

// Indices chosen to span two card bytes and several bits within them.
const INDICES: [i64; 5] = [0, 5, 200, 1152, 2047];
for (n, &index) in INDICES.iter().enumerate() {
let trace_id = 9100 + 2 * n as u64;
run_cond_call_gc_wb_array(trace_id, obj_immed, index, false);
run_cond_call_gc_wb_array(trace_id + 1, obj_reg, index, true);
gc.do_write_barrier_card(obj_interp, index as usize, card_page_shift);
// Compare after every index, not only at the end: an aggregate
// comparison would accept two arms that dirty the same set of
// cards while pairing them with different indices.
assert_eq!(
gc.dirty_cards(obj_immed),
gc.dirty_cards(obj_reg),
"index {index} must dirty the same cards through both arms"
);
}

let mut expected: Vec<usize> = INDICES
.iter()
.map(|&index| (index as usize) >> card_page_shift)
.collect();
expected.sort_unstable();
expected.dedup();

let immed_cards = gc.dirty_cards(obj_immed);
let reg_cards = gc.dirty_cards(obj_reg);
let interp_cards = gc.dirty_cards(obj_interp);

assert_eq!(
immed_cards, reg_cards,
"an immediate index must dirty the same cards as the register arm"
);
assert_eq!(
immed_cards, interp_cards,
"the compiled card bits must match remember_young_pointer_from_array2"
);
assert_eq!(
immed_cards, expected,
"each index must dirty exactly its own card, and nothing else"
);
}
}
25 changes: 19 additions & 6 deletions majit/majit-backend-dynasm/src/x86/assembler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7617,13 +7617,24 @@ impl<'a> Assembler386<'a> {
}

fn emit_write_barrier_fastpath_kind(&mut self, arglocs: &[Loc], is_array: bool) {
let wb = match crate::runner::dynasm_write_barrier_descr() {
Some(wb) => wb,
None => return,
};
// x86/assembler.py:2399-2401 asserts the descriptor is the collector's
// write-barrier class. `COND_CALL_GC_WB` only exists because the GC
// rewriter emitted it, so a missing descriptor here means the two
// disagree; returning would drop the barrier without a trace.
let wb = crate::runner::dynasm_write_barrier_descr()
.expect("COND_CALL_GC_WB emitted without a write barrier descriptor");
// x86/assembler.py:2415-2420 feeds `loc_base = arglocs[0]` into
// `addr_add_const`, and `AddressLoc` (x86/regloc.py:213) accepts an
// immediate base, so upstream needs no assertion here. This backend
// addresses the flag byte only through a core register, and the paired
// lowered `GcStore` already contracts for one, so state the contract
// instead of emitting nothing — a barrier that assembles to zero bytes
// stays invisible until it corrupts memory.
let loc_base = match arglocs.first() {
Some(Loc::Reg(r)) => *r,
_ => return,
other => {
panic!("write barrier base loc must be Loc::Reg (regalloc contract), got {other:?}")
}
};
let card_marking = is_array && wb.jit_wb_cards_set != 0;
let mut mask = wb.jit_wb_if_flag_singlebyte as i64;
Expand Down Expand Up @@ -7719,7 +7730,9 @@ impl<'a> Assembler386<'a> {
; or BYTE [Rq(loc_base.value as u8) + byte_ofs as i32], byte_val as i8
);
}
_ => {}
// x86/assembler.py:2387-2388
// `raise AssertionError("index is neither RegLoc nor ImmedLoc")`
_ => panic!("index is neither RegLoc nor ImmedLoc"),
}
} else {
// Non-array: generic barrier
Expand Down
Loading
Loading