Skip to content

Commit e8ea0e0

Browse files
committed
ZJIT: Split entry points into separate Cranelift functions
Each HIR entry block (interpreter entry, JIT entries) compiles to a separate Cranelift function that materializes merge block params and calls the body function. This allows multiple entry points per function, enabling JIT-to-JIT calls with optional parameters. The body function takes (EC, CFP, SP, merge_params...) and contains all non-entry blocks. Entry functions call the body via call_indirect.
1 parent e453dce commit e8ea0e0

2 files changed

Lines changed: 201 additions & 60 deletions

File tree

zjit/src/backend/cranelift_backend.rs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,24 +77,35 @@ pub struct CraneliftBuilder {
7777
}
7878

7979
impl CraneliftBuilder {
80-
/// Create a new CraneliftBuilder for a function with the ZJIT calling convention:
81-
/// `(EC: i64, CFP: i64, arg0: i64, arg1: i64, ...) -> i64`
82-
///
83-
/// `num_args` is the number of JIT entry arguments (LoadArg/Param in entry blocks).
80+
/// Create a new CraneliftBuilder with the default calling convention.
81+
/// Signature: `(EC, CFP, arg0..argN) -> VALUE`
8482
pub fn new(num_args: usize) -> Self {
83+
Self::new_with_calling_conv(num_args, {
84+
let shared_builder = settings::builder();
85+
let shared_flags = settings::Flags::new(shared_builder);
86+
isa::lookup(target_lexicon::Triple::host())
87+
.expect("Failed to look up target ISA")
88+
.finish(shared_flags)
89+
.expect("Failed to finish ISA")
90+
.default_call_conv()
91+
})
92+
}
93+
94+
/// Create a new CraneliftBuilder with a specific calling convention.
95+
/// Signature: `(EC, CFP, arg0..argN) -> VALUE`
96+
pub fn new_with_calling_conv(num_args: usize, call_conv: CallConv) -> Self {
8597
let shared_builder = settings::builder();
8698
let shared_flags = settings::Flags::new(shared_builder);
8799
let isa = isa::lookup(target_lexicon::Triple::host())
88100
.expect("Failed to look up target ISA")
89101
.finish(shared_flags)
90102
.expect("Failed to finish ISA");
91103

92-
let call_conv = isa.default_call_conv();
93104
let mut sig = Signature::new(call_conv);
94105
sig.params.push(AbiParam::new(types::I64)); // EC
95106
sig.params.push(AbiParam::new(types::I64)); // CFP
96107
for _ in 0..num_args {
97-
sig.params.push(AbiParam::new(types::I64)); // JIT entry args
108+
sig.params.push(AbiParam::new(types::I64)); // extra args
98109
}
99110
sig.returns.push(AbiParam::new(types::I64)); // VALUE return
100111

zjit/src/codegen.rs

Lines changed: 184 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -286,85 +286,88 @@ struct CLPatchPointInfo {
286286
state: FrameState,
287287
}
288288

289-
/// Compile a function using the Cranelift backend
289+
/// Compile a function using the Cranelift backend.
290+
///
291+
/// Each HIR function has entry blocks (interpreter entry, JIT entries) that Jump
292+
/// to a merge block. We compile:
293+
/// - The **body** (merge block + everything after) as one Cranelift function with
294+
/// signature `(EC, CFP, SP, merge_param0..N) -> VALUE` using the `tail` calling convention.
295+
/// - Each **entry point** as a separate tiny Cranelift function that materializes
296+
/// the merge params and `return_call_indirect`s the body.
290297
fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, function: &Function) -> Result<(IseqCodePtrs, Vec<CodePtr>, Vec<IseqCallRef>), CompileError> {
291-
// Count max params across all blocks to determine function ABI params
292-
let num_jit_args = max_num_params(function);
293-
let mut cl = CraneliftBuilder::new(num_jit_args);
298+
let reverse_post_order = function.rpo();
299+
300+
// Find the merge block — first non-entry, non-entries_super block in RPO
301+
let merge_block_id = reverse_post_order.iter().copied()
302+
.find(|&bid| bid != function.entries_block && !function.is_entry_block(bid))
303+
.expect("Function must have a merge block");
304+
let num_merge_params = function.block(merge_block_id).params().count();
305+
306+
// === Step 1: Compile the body function ===
307+
// Signature: (EC, CFP, SP, merge_param0..N) -> VALUE, tail calling convention
308+
// num_args = extra params beyond EC, CFP = SP + merge_params
309+
let body_extra_args = 1 + num_merge_params; // SP, then merge params
310+
let mut cl = CraneliftBuilder::new(body_extra_args);
294311

295312
// Cranelift operands indexed by HIR InsnId
296313
let mut cl_opnds: Vec<Option<CLValue>> = vec![None; function.num_insns()];
297314
let iseq_calls: Vec<IseqCallRef> = Vec::new();
298-
// Patch points to emit after Cranelift compilation
299315
let mut patch_points: Vec<CLPatchPointInfo> = Vec::new();
300-
// Call target cells for lazy JIT-to-JIT stubs: (cell_ptr, num_jit_args)
301316
let mut cl_call_cells: Vec<(*const CLCallTargetCell, usize)> = Vec::new();
302317

303-
cl.build(|builder, isa, side_exit_blocks, value_pool, ec_var, cfp_var, sp_var, next_var| {
304-
// Map HIR blocks → Cranelift blocks
305-
let reverse_post_order = function.rpo();
318+
cl.build(|builder, isa, side_exit_blocks, value_pool, ec_var, cfp_var, sp_var, _next_var| {
306319
let mut hir_to_cl: Vec<Option<CLBlock>> = vec![None; function.num_blocks()];
307320

308-
// Create Cranelift Variables for each ABI arg so LoadArg can use them
309-
// from any block (not just the entry block).
310-
let arg_vars: Vec<Variable> = (0..num_jit_args).map(|i| {
311-
let var = Variable::from_u32((*next_var + i) as u32);
312-
builder.declare_var(var, cl_types::I64);
313-
var
314-
}).collect();
315-
*next_var += num_jit_args;
316-
317-
// Create all Cranelift blocks for HIR blocks
321+
// Create Cranelift blocks for non-entry HIR blocks (skip entries_block and entry blocks)
318322
for &block_id in reverse_post_order.iter() {
319-
if block_id == function.entries_block { continue; }
323+
if block_id == function.entries_block || function.is_entry_block(block_id) { continue; }
320324
let cl_block = builder.create_block();
321325
hir_to_cl[block_id.0] = Some(cl_block);
322326

323-
// Add block parameters for HIR block params (SSA phi values)
324-
let block = function.block(block_id);
325-
for _ in block.params() {
326-
builder.append_block_param(cl_block, cl_types::I64);
327+
// For the merge block, don't add block params — its params come from ABI.
328+
// For other blocks, add block params as SSA phi values.
329+
if block_id != merge_block_id {
330+
let block = function.block(block_id);
331+
for _ in block.params() {
332+
builder.append_block_param(cl_block, cl_types::I64);
333+
}
327334
}
328335
}
329336

330-
// Emit each HIR block
331-
let mut is_first_block = true;
337+
// Emit each non-entry HIR block
338+
let mut is_first_body_block = true;
332339
for &block_id in reverse_post_order.iter() {
333-
if block_id == function.entries_block { continue; }
340+
if block_id == function.entries_block || function.is_entry_block(block_id) { continue; }
334341

335342
let cl_block = hir_to_cl[block_id.0].unwrap();
336343
builder.switch_to_block(cl_block);
337344

338345
let block = function.block(block_id);
339346

340-
// On the first block (function entry), append the ABI params
341-
// and store EC, CFP, SP, and arg values into Variables.
342-
if is_first_block {
347+
// The merge block (first body block) gets its params from function ABI
348+
if is_first_body_block {
343349
builder.append_block_params_for_function_params(cl_block);
344350
let all_params: Vec<CLValue> = builder.block_params(cl_block).to_vec();
345-
// Layout: [block_param_0..N, EC, CFP, arg0, arg1, ...]
346-
let num_block_params = block.params().count();
347-
let ec_val = all_params[num_block_params];
348-
let cfp_val = all_params[num_block_params + 1];
351+
// ABI layout: (EC, CFP, SP, merge_param_0, merge_param_1, ...)
352+
let ec_val = all_params[0];
353+
let cfp_val = all_params[1];
354+
let sp_val = all_params[2];
349355
builder.def_var(ec_var, ec_val);
350356
builder.def_var(cfp_var, cfp_val);
351-
352-
// Load SP from [CFP + RUBY_OFFSET_CFP_SP]
353-
let sp_val = builder.ins().load(cl_types::I64, MemFlags::trusted(), cfp_val, Offset32::new(RUBY_OFFSET_CFP_SP));
354357
builder.def_var(sp_var, sp_val);
355358

356-
// Store ABI args into Variables so any block can access them
357-
for (i, &var) in arg_vars.iter().enumerate() {
358-
builder.def_var(var, all_params[num_block_params + 2 + i]);
359+
// Map merge block params from ABI params [3..]
360+
for (idx, &insn_id) in block.params().enumerate() {
361+
cl_opnds[insn_id.0] = Some(all_params[3 + idx]);
359362
}
360363

361-
is_first_block = false;
362-
}
363-
364-
// Map block parameters (SSA phi values)
365-
let block_params: Vec<CLValue> = builder.block_params(cl_block).to_vec();
366-
for (idx, &insn_id) in block.params().enumerate() {
367-
cl_opnds[insn_id.0] = Some(block_params[idx]);
364+
is_first_body_block = false;
365+
} else {
366+
// Non-merge blocks: map block params from Cranelift block params
367+
let block_params: Vec<CLValue> = builder.block_params(cl_block).to_vec();
368+
for (idx, &insn_id) in block.params().enumerate() {
369+
cl_opnds[insn_id.0] = Some(block_params[idx]);
370+
}
368371
}
369372

370373
// Compile all instructions in this block
@@ -422,9 +425,7 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
422425

423426
// === Parameters ===
424427
Insn::Param => {} // handled above via block params
425-
Insn::LoadArg { idx, .. } => {
426-
cl_opnds[insn_id.0] = Some(builder.use_var(arg_vars[idx as usize]));
427-
}
428+
Insn::LoadArg { .. } => {} // handled in entry point functions
428429

429430
// === Snapshots (no-op) ===
430431
Insn::Snapshot { .. } => {}
@@ -1264,8 +1265,137 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
12641265
}
12651266
});
12661267

1267-
// Compile and copy to CodeBlock
1268-
let (start_ptr, gc_offsets) = cl.compile(cb)?;
1268+
// Compile body and copy to CodeBlock
1269+
let (body_ptr, gc_offsets) = cl.compile(cb)?;
1270+
let body_addr = body_ptr.raw_ptr(cb) as u64;
1271+
1272+
// === Step 2: Compile entry point functions ===
1273+
// Each entry block becomes a tiny function that materializes params and
1274+
// return_call_indirect's the body function.
1275+
let mut jit_entry_ptrs: Vec<CodePtr> = Vec::new();
1276+
let mut interpreter_entry_ptr: Option<CodePtr> = None;
1277+
1278+
for &block_id in reverse_post_order.iter() {
1279+
if block_id == function.entries_block { continue; }
1280+
if !function.is_entry_block(block_id) { continue; }
1281+
1282+
let block = function.block(block_id);
1283+
let is_interpreter_entry = !block.insns().any(|&id| matches!(function.find(id), Insn::LoadArg { .. }));
1284+
1285+
// Count the number of ABI args this entry expects (for JIT entries: LoadArg count)
1286+
let entry_num_args = block.insns().filter(|&&id| matches!(function.find(id), Insn::LoadArg { .. })).count();
1287+
1288+
// Entry function signature: (EC, CFP, args...) -> VALUE
1289+
// Use the default calling convention so the entry trampoline can call us.
1290+
let mut entry_cl = CraneliftBuilder::new(entry_num_args);
1291+
1292+
entry_cl.build(|builder, isa, _side_exit_blocks, _value_pool, ec_var, cfp_var, sp_var, _next_var| {
1293+
let entry = builder.create_block();
1294+
builder.append_block_params_for_function_params(entry);
1295+
builder.switch_to_block(entry);
1296+
1297+
let params: Vec<CLValue> = builder.block_params(entry).to_vec();
1298+
let ec_val = params[0];
1299+
let cfp_val = params[1];
1300+
builder.def_var(ec_var, ec_val);
1301+
builder.def_var(cfp_var, cfp_val);
1302+
1303+
// Load SP from CFP
1304+
let sp_val = builder.ins().load(cl_types::I64, MemFlags::trusted(), cfp_val, Offset32::new(RUBY_OFFSET_CFP_SP));
1305+
builder.def_var(sp_var, sp_val);
1306+
1307+
// Build the merge params by executing this entry block's instructions
1308+
let mut entry_opnds: Vec<Option<CLValue>> = vec![None; function.num_insns()];
1309+
1310+
for &insn_id in block.insns() {
1311+
match function.find(insn_id) {
1312+
Insn::LoadArg { idx, .. } => {
1313+
// JIT entry: arg comes from ABI param at position 2+idx
1314+
entry_opnds[insn_id.0] = Some(params[2 + idx as usize]);
1315+
}
1316+
Insn::LoadSelf => {
1317+
let v = builder.ins().load(cl_types::I64, MemFlags::trusted(), cfp_val, Offset32::new(RUBY_OFFSET_CFP_SELF));
1318+
entry_opnds[insn_id.0] = Some(v);
1319+
}
1320+
Insn::LoadSP => {
1321+
entry_opnds[insn_id.0] = Some(sp_val);
1322+
}
1323+
Insn::LoadEC => {
1324+
entry_opnds[insn_id.0] = Some(ec_val);
1325+
}
1326+
Insn::Const { val: Const::Value(val) } => {
1327+
entry_opnds[insn_id.0] = Some(builder.ins().iconst(cl_types::I64, val.as_i64()));
1328+
}
1329+
Insn::Const { val: Const::CPtr(ptr) } => {
1330+
entry_opnds[insn_id.0] = Some(builder.ins().iconst(cl_types::I64, ptr as i64));
1331+
}
1332+
Insn::LoadField { recv, id: _, offset, .. } => {
1333+
let base = entry_opnds[recv.0].unwrap();
1334+
let v = builder.ins().load(cl_types::I64, MemFlags::trusted(), base, Offset32::new(offset));
1335+
entry_opnds[insn_id.0] = Some(v);
1336+
}
1337+
Insn::GetEP { level } => {
1338+
let mut ep = builder.ins().load(cl_types::I64, MemFlags::trusted(), cfp_val, Offset32::new(RUBY_OFFSET_CFP_EP));
1339+
for _ in 0..level {
1340+
let specval_offset = SIZEOF_VALUE_I32 * VM_ENV_DATA_INDEX_SPECVAL;
1341+
ep = builder.ins().load(cl_types::I64, MemFlags::trusted(), ep, Offset32::new(specval_offset));
1342+
let mask = builder.ins().iconst(cl_types::I64, !0x03i64);
1343+
ep = builder.ins().band(ep, mask);
1344+
}
1345+
entry_opnds[insn_id.0] = Some(ep);
1346+
}
1347+
Insn::EntryPoint { .. } => {} // no-op
1348+
Insn::Snapshot { .. } => {} // no-op
1349+
Insn::Jump(target) => {
1350+
// This is the jump to the merge block — build the tail call
1351+
let mut body_args = vec![ec_val, cfp_val, sp_val];
1352+
for &arg_id in target.args.iter() {
1353+
body_args.push(entry_opnds[arg_id.0].unwrap_or_else(|| {
1354+
panic!("Missing entry opnd for {arg_id} in entry block {block_id}")
1355+
}));
1356+
}
1357+
1358+
// Call the body function and return its result
1359+
let body_sig_params = 3 + num_merge_params; // EC, CFP, SP, merge_params
1360+
let mut sig = cranelift_codegen::ir::Signature::new(isa.default_call_conv());
1361+
for _ in 0..body_sig_params {
1362+
sig.params.push(cranelift_codegen::ir::AbiParam::new(cl_types::I64));
1363+
}
1364+
sig.returns.push(cranelift_codegen::ir::AbiParam::new(cl_types::I64));
1365+
let sig_ref = builder.import_signature(sig);
1366+
let body_addr_val = builder.ins().iconst(cl_types::I64, body_addr as i64);
1367+
let call = builder.ins().call_indirect(sig_ref, body_addr_val, &body_args);
1368+
let ret = builder.inst_results(call)[0];
1369+
builder.ins().return_(&[ret]);
1370+
}
1371+
other => {
1372+
// Skip unknown instructions in entry blocks
1373+
}
1374+
}
1375+
}
1376+
1377+
builder.seal_block(entry);
1378+
});
1379+
1380+
let (entry_ptr, _) = entry_cl.compile(cb)?;
1381+
1382+
if is_interpreter_entry {
1383+
interpreter_entry_ptr = Some(entry_ptr);
1384+
}
1385+
1386+
// JIT entries: record the pointer
1387+
// The jit_entry_idx is determined by the EntryPoint instruction
1388+
for &insn_id in block.insns() {
1389+
if let Insn::EntryPoint { jit_entry_idx: Some(idx) } = function.find(insn_id) {
1390+
while jit_entry_ptrs.len() <= idx {
1391+
jit_entry_ptrs.push(entry_ptr);
1392+
}
1393+
jit_entry_ptrs[idx] = entry_ptr;
1394+
}
1395+
}
1396+
}
1397+
1398+
let start_ptr = interpreter_entry_ptr.unwrap_or(body_ptr);
12691399

12701400
// Generate lazy compile stubs for SendDirect call cells
12711401
for &(cell_ptr, num_args) in cl_call_cells.iter() {
@@ -1340,8 +1470,8 @@ fn gen_function(cb: &mut CodeBlock, iseq: IseqPtr, version: IseqVersionRef, func
13401470
}
13411471
}
13421472

1343-
// TODO: support jit_entry_ptrs for JIT-to-JIT calls
1344-
Ok((IseqCodePtrs { start_ptr, jit_entry_ptrs: vec![] }, gc_offsets, iseq_calls))
1473+
// Entry points compiled above, jit_entry_ptrs populated
1474+
Ok((IseqCodePtrs { start_ptr, jit_entry_ptrs }, gc_offsets, iseq_calls))
13451475
}
13461476

13471477
/// Save SP to CFP for Cranelift builder

0 commit comments

Comments
 (0)