use luna_core::jit::send_compat::{TArc, TCellBool, TCellPtr, TCellU32, TRefLock};
use luna_core::runtime::Gc;
use luna_core::runtime::function::Proto;
use luna_core::vm::isa::{Inst, Op};
pub use luna_core::jit::trace_types::*;
use cranelift::prelude::*;
use cranelift_codegen::ir::UserFuncName;
use cranelift_codegen::settings;
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{DataDescription, DataId, FuncId, Linkage, Module};
fn emit_str_key_arg<M: Module>(
module: &mut M,
bcx: &mut FunctionBuilder<'_>,
key_v: luna_core::runtime::Gc<luna_core::runtime::LuaStr>,
aot: bool,
defined_aot_data: &mut std::collections::HashSet<DataId>,
) -> Value {
if !aot {
return bcx.ins().iconst(types::I64, key_v.as_ptr() as i64);
}
let bytes = key_v.as_bytes();
let hex = strkey_hex_label(bytes);
let slot_name = format!("__luna_aot_strkey_slot_{hex}");
let slot_id = module
.declare_data(&slot_name, Linkage::Export, true, false)
.expect("declare_data slot");
if defined_aot_data.insert(slot_id) {
let mut desc = DataDescription::new();
desc.define(Box::new([0u8; 8]));
let _ = module.define_data(slot_id, &desc);
}
let bytes_name = format!("__luna_aot_strkey_bytes_{hex}");
let bytes_id = module
.declare_data(&bytes_name, Linkage::Export, false, false)
.expect("declare_data bytes");
if defined_aot_data.insert(bytes_id) {
let mut payload = Vec::with_capacity(8 + bytes.len());
payload.extend_from_slice(&(bytes.len() as u64).to_le_bytes());
payload.extend_from_slice(bytes);
let mut desc = DataDescription::new();
desc.define(payload.into_boxed_slice());
let _ = module.define_data(bytes_id, &desc);
}
let idx_name = format!("__luna_aot_strkey_idx_{hex}");
let idx_id = module
.declare_data(&idx_name, Linkage::Local, false, false)
.expect("declare_data idx");
if defined_aot_data.insert(idx_id) {
let mut desc = DataDescription::new();
desc.define(Box::new([0u8; 16]));
let (idx_seg, idx_sect) = if cfg!(target_os = "windows") {
("", ".lt_skix")
} else {
("__DATA", "luna_strkey_idx")
};
desc.set_segment_section(idx_seg, idx_sect);
desc.set_align(8);
let bytes_gv = module.declare_data_in_data(bytes_id, &mut desc);
let slot_gv = module.declare_data_in_data(slot_id, &mut desc);
desc.write_data_addr(0, bytes_gv, 0);
desc.write_data_addr(8, slot_gv, 0);
let _ = module.define_data(idx_id, &desc);
}
let slot_gv = module.declare_data_in_func(slot_id, bcx.func);
let slot_addr = bcx.ins().symbol_value(types::I64, slot_gv);
bcx.ins()
.load(types::I64, MemFlags::trusted(), slot_addr, 0)
}
fn strkey_hex_label(bytes: &[u8]) -> String {
let mut h: u64 = 0xcbf29ce484222325;
for &b in bytes {
h ^= b as u64;
h = h.wrapping_mul(0x100000001b3);
}
format!("{h:016x}")
}
fn emit_chain_ptr_arg<M: Module>(
module: &mut M,
bcx: &mut FunctionBuilder<'_>,
chain: &[FrameMaterializeInfo],
chain_rc_first_ptr: i64,
aot: bool,
defined_aot_data: &mut std::collections::HashSet<DataId>,
) -> Value {
if !aot {
return bcx.ins().iconst(types::I64, chain_rc_first_ptr);
}
let mut packed: Vec<u8> = Vec::with_capacity(chain.len() * 12);
for fm in chain {
packed.extend_from_slice(&fm.base_offset.to_le_bytes());
packed.extend_from_slice(&fm.pc.to_le_bytes());
packed.extend_from_slice(&fm.nresults.to_le_bytes());
}
let hex = strkey_hex_label(&packed);
let slot_name = format!("__luna_aot_inline_chain_slot_{hex}");
let slot_id = module
.declare_data(&slot_name, Linkage::Export, true, false)
.expect("declare_data inline chain slot");
if defined_aot_data.insert(slot_id) {
let mut desc = DataDescription::new();
desc.define(Box::new([0u8; 8]));
let _ = module.define_data(slot_id, &desc);
}
let bytes_name = format!("__luna_aot_inline_chain_bytes_{hex}");
let bytes_id = module
.declare_data(&bytes_name, Linkage::Export, false, false)
.expect("declare_data inline chain bytes");
if defined_aot_data.insert(bytes_id) {
let mut payload = Vec::with_capacity(8 + packed.len());
payload.extend_from_slice(&(chain.len() as u64).to_le_bytes());
payload.extend_from_slice(&packed);
let mut desc = DataDescription::new();
desc.define(payload.into_boxed_slice());
let _ = module.define_data(bytes_id, &desc);
}
let idx_name = format!("__luna_aot_inline_chain_idx_{hex}");
let idx_id = module
.declare_data(&idx_name, Linkage::Local, false, false)
.expect("declare_data inline chain idx");
if defined_aot_data.insert(idx_id) {
let mut desc = DataDescription::new();
desc.define(Box::new([0u8; 16]));
let (idx_seg, idx_sect) = if cfg!(target_os = "windows") {
("", ".lt_chai")
} else {
("__DATA", "luna_inline_chnx")
};
desc.set_segment_section(idx_seg, idx_sect);
desc.set_align(8);
let bytes_gv = module.declare_data_in_data(bytes_id, &mut desc);
let slot_gv = module.declare_data_in_data(slot_id, &mut desc);
desc.write_data_addr(0, bytes_gv, 0);
desc.write_data_addr(8, slot_gv, 0);
let _ = module.define_data(idx_id, &desc);
}
let slot_gv = module.declare_data_in_func(slot_id, bcx.func);
let slot_addr = bcx.ins().symbol_value(types::I64, slot_gv);
bcx.ins()
.load(types::I64, MemFlags::trusted(), slot_addr, 0)
}
const MATH_LIBM_FNS: &[(&[u8], &str)] = &[
(b"sin", "sin"),
(b"cos", "cos"),
(b"tan", "tan"),
(b"asin", "asin"),
(b"acos", "acos"),
(b"atan", "atan"),
(b"exp", "exp"),
(b"log", "log"),
(b"sqrt", "sqrt"),
(b"floor", "floor"),
(b"ceil", "ceil"),
];
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FoldKind {
Libm1,
Min2,
Max2,
}
#[derive(Clone, Copy, Debug)]
enum FoldArgSrc {
Reg { reg: u32 },
}
#[derive(Clone, Copy, Debug)]
struct TraceMathFold {
start_idx: usize,
fn_name: &'static str,
kind: FoldKind,
arg_src: Option<FoldArgSrc>,
call_idx: usize,
dst_reg: u32,
arg1_reg: u32,
arg2_reg: u32,
}
const MINMAX_FOLD_ARG_PREP_MAX: usize = 6;
fn try_match_trace_math_fold(
record: &TraceRecord,
i: usize,
head_proto: Gc<Proto>,
) -> Option<TraceMathFold> {
if i + 1 >= record.ops.len() {
return None;
}
for k in 0..=1 {
if !std::ptr::eq(record.ops[i + k].proto.as_ptr(), head_proto.as_ptr()) {
return None;
}
if record.ops[i + k].inline_depth != 0 {
return None;
}
}
let i0 = record.ops[i].inst;
let i1 = record.ops[i + 1].inst;
if !matches!(i0.op(), Op::GetTabUp) {
return None;
}
if !matches!(i1.op(), Op::GetField) {
return None;
}
let a = i0.a();
if i0.b() != 0 {
return None;
}
let k_math = head_proto.consts.get(i0.c() as usize).copied()?;
let luna_core::runtime::Value::Str(s) = k_math else {
return None;
};
if s.as_bytes() != b"math" {
return None;
}
if i1.a() != a || i1.b() != a {
return None;
}
let k_fn = head_proto.consts.get(i1.c() as usize).copied()?;
let luna_core::runtime::Value::Str(fname) = k_fn else {
return None;
};
let fname_bytes = fname.as_bytes();
if let Some(fn_name) = MATH_LIBM_FNS
.iter()
.find_map(|&(needle, name)| (needle == fname_bytes).then_some(name))
{
if i + 3 >= record.ops.len() {
return None;
}
for k in 2..=3 {
if !std::ptr::eq(record.ops[i + k].proto.as_ptr(), head_proto.as_ptr()) {
return None;
}
if record.ops[i + k].inline_depth != 0 {
return None;
}
}
let i2 = record.ops[i + 2].inst;
let i3 = record.ops[i + 3].inst;
if !matches!(i2.op(), Op::Move) {
return None;
}
if !matches!(i3.op(), Op::Call) {
return None;
}
if i2.a() != a + 1 {
return None;
}
let arg_reg = i2.b();
if i3.a() != a || i3.b() != 2 || i3.c() != 2 {
return None;
}
return Some(TraceMathFold {
start_idx: i,
fn_name,
kind: FoldKind::Libm1,
arg_src: Some(FoldArgSrc::Reg { reg: arg_reg }),
call_idx: i + 3,
dst_reg: a,
arg1_reg: 0,
arg2_reg: 0,
});
}
let kind = match fname_bytes {
b"min" => FoldKind::Min2,
b"max" => FoldKind::Max2,
_ => return None,
};
let mut call_idx: Option<usize> = None;
let search_limit = (i + 2 + MINMAX_FOLD_ARG_PREP_MAX + 1).min(record.ops.len());
for j in (i + 2)..search_limit {
let rop_j = &record.ops[j];
if !std::ptr::eq(rop_j.proto.as_ptr(), head_proto.as_ptr()) || rop_j.inline_depth != 0 {
return None;
}
let inst_j = rop_j.inst;
if matches!(inst_j.op(), Op::Call) {
if inst_j.a() != a || inst_j.b() != 3 || inst_j.c() != 2 {
return None;
}
call_idx = Some(j);
break;
}
if inst_j.a() == a {
return None;
}
}
let call_idx = call_idx?;
let diag_name = if matches!(kind, FoldKind::Min2) {
"min"
} else {
"max"
};
Some(TraceMathFold {
start_idx: i,
fn_name: diag_name,
kind,
arg_src: None,
call_idx,
dst_reg: a,
arg1_reg: a + 1,
arg2_reg: a + 2,
})
}
fn infer_getx_exit_inst(getx_a: u32, next: Inst) -> Option<ExitTag> {
let na = next.a();
let nb = next.b();
let nc = next.c();
match next.op() {
Op::Add | Op::Sub | Op::Mul => {
if nb == getx_a || nc == getx_a {
Some(ExitTag::Int)
} else {
None
}
}
Op::Lt | Op::Le => {
if na == getx_a || nb == getx_a {
Some(ExitTag::Int)
} else {
None
}
}
Op::GetI | Op::GetTable | Op::GetField => {
if nb == getx_a {
Some(ExitTag::Table)
} else {
None
}
}
Op::SetI | Op::SetTable | Op::SetList | Op::SetField => {
if na == getx_a {
Some(ExitTag::Table)
} else {
None
}
}
Op::Len => {
if nb == getx_a {
Some(ExitTag::Table)
} else {
None
}
}
_ => None,
}
}
fn infer_getx_exit_lookahead(getx_a: u32, ops_after: &[RecordedOp]) -> Option<ExitTag> {
const MAX_LOOKAHEAD: usize = 4;
for rop in ops_after.iter().take(MAX_LOOKAHEAD) {
let inst = rop.inst;
let op = inst.op();
let a = inst.a();
if let Some(tag) = infer_getx_exit_inst(getx_a, inst) {
return Some(tag);
}
if matches!(op, Op::LoadI | Op::LoadF | Op::LoadK) {
if a == getx_a {
return None;
}
continue;
}
return None;
}
None
}
fn infer_upval_exit(getupval_a: u32, ops_after: &[RecordedOp]) -> Option<ExitTag> {
for rop in ops_after {
let next = rop.inst;
if next.op() == Op::Call && next.a() == getupval_a {
return Some(ExitTag::Closure);
}
let writes_a = !matches!(
next.op(),
Op::Lt
| Op::Le
| Op::Eq
| Op::EqK
| Op::Jmp
| Op::SetI
| Op::SetTable
| Op::SetList
| Op::Return0
| Op::Return1
| Op::Return
);
if writes_a && next.a() == getupval_a {
return None;
}
}
None
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RegKind {
Unset,
Int,
Float,
Table,
Closure,
Nil,
Str,
}
impl RegKind {
fn from_entry_tag(tag: u8) -> Option<Self> {
match tag {
luna_core::runtime::value::raw::INT => Some(RegKind::Int),
luna_core::runtime::value::raw::FLOAT => Some(RegKind::Float),
luna_core::runtime::value::raw::TABLE => Some(RegKind::Table),
luna_core::runtime::value::raw::CLOSURE => Some(RegKind::Closure),
luna_core::runtime::value::raw::NIL => Some(RegKind::Nil),
luna_core::runtime::value::raw::STR => Some(RegKind::Str),
_ => None,
}
}
}
pub(crate) fn verify_depth_invariant(items: &[(u8, bool)]) -> bool {
if items.is_empty() {
return true;
}
if items[0].0 != 0 {
return false;
}
let mut prev_depth = items[0].0;
let mut prev_was_call = items[0].1;
for &(d, is_call) in &items[1..] {
if d > prev_depth {
if d != prev_depth + 1 {
return false;
}
if !prev_was_call {
return false;
}
}
if d > MAX_INLINE_DEPTH {
return false;
}
prev_depth = d;
prev_was_call = is_call;
}
true
}
fn compute_op_offsets(record: &TraceRecord) -> (Vec<u32>, Vec<Option<u8>>) {
let n = record.ops.len();
let mut offsets = Vec::with_capacity(n);
let mut enclosing_call_a = Vec::with_capacity(n);
let mut offset_stack: Vec<u32> = vec![0u32];
let mut call_a_stack: Vec<u8> = vec![0u8];
for (i, rop) in record.ops.iter().enumerate() {
let d = rop.inline_depth as usize;
if d >= offset_stack.len() {
debug_assert!(i > 0, "first recorded op cannot be at depth > 0");
debug_assert!(
d == offset_stack.len(),
"depth jumped more than 1 in a single transition"
);
let caller_idx = i - 1;
let caller = &record.ops[caller_idx];
debug_assert!(
matches!(caller.inst.op(), Op::Call),
"depth bump must follow Op::Call"
);
let caller_offset = offset_stack[offset_stack.len() - 1];
let caller_a = caller.inst.a();
let new_offset = caller_offset + caller_a + 1;
offset_stack.push(new_offset);
call_a_stack.push(caller_a as u8);
} else {
while offset_stack.len() > d + 1 {
offset_stack.pop();
call_a_stack.pop();
}
}
offsets.push(offset_stack[d]);
enclosing_call_a.push(if d == 0 { None } else { Some(call_a_stack[d]) });
}
(offsets, enclosing_call_a)
}
fn kinds_to_exit_tags(kinds: &[RegKind]) -> Vec<ExitTag> {
kinds
.iter()
.map(|k| match k {
RegKind::Unset => ExitTag::Untouched,
RegKind::Int => ExitTag::Int,
RegKind::Float => ExitTag::Float,
RegKind::Table => ExitTag::Table,
RegKind::Closure => ExitTag::Closure,
RegKind::Nil => ExitTag::Nil,
RegKind::Str => ExitTag::Str,
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EscapeState {
Sinkable,
Escaped,
}
#[derive(Debug, Clone)]
pub struct AllocSite {
pub op_idx: usize,
pub pc: u32,
pub a: u32,
pub inline_depth: u8,
pub array_cap: u32,
pub hash_keys: Vec<u32>,
pub state: EscapeState,
}
#[derive(Debug, Clone, Copy)]
pub enum OpAction {
NewTableSite {
site_idx: u32,
},
SetListWrite {
site_idx: u32,
},
GetIRead {
site_idx: u32,
key: u32,
},
SetISunkWrite {
site_idx: u32,
key: u32,
},
SetTableSunkWrite {
site_idx: u32,
key: u32,
},
SetFieldSunkWrite {
site_idx: u32,
hash_slot: u32,
},
GetFieldSunkRead {
site_idx: u32,
hash_slot: u32,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BufferState {
Bufferable,
NonBuffered,
}
#[derive(Debug, Clone)]
pub struct AccumSite {
pub op_idx: usize,
pub pc: u32,
pub accum_slot: u32,
pub piece_slot: u32,
pub inline_depth: u8,
pub state: BufferState,
}
#[derive(Debug, Default)]
pub struct EscapeAnalysis {
pub sites: Vec<AllocSite>,
pub op_actions: Vec<Option<OpAction>>,
pub live_at_op: Vec<Vec<u32>>,
pub accum_sites: Vec<AccumSite>,
pub accum_live_at_op: Vec<Vec<u32>>,
}
impl EscapeAnalysis {
pub fn sinkable_count(&self) -> u32 {
self.sites
.iter()
.filter(|s| s.state == EscapeState::Sinkable)
.count() as u32
}
}
fn kind_to_raw_tag(k: RegKind) -> u8 {
use luna_core::runtime::value::raw;
match k {
RegKind::Int => raw::INT,
RegKind::Float => raw::FLOAT,
RegKind::Table => raw::TABLE,
RegKind::Closure => raw::CLOSURE,
RegKind::Str => raw::STR,
RegKind::Unset | RegKind::Nil => raw::NIL,
}
}
fn emit_materialize_live_sunk<M: Module>(
bcx: &mut FunctionBuilder<'_>,
module: &mut M,
mat_sunk_id: cranelift_module::FuncId,
escape: &EscapeAnalysis,
virt_vars: &[Option<Vec<Variable>>],
virt_kinds: &[Option<Vec<RegKind>>],
regs_full: &[Variable],
op_offsets: &[u32],
cmp_op_idx: usize,
kinds_snapshot: &mut [RegKind],
head_proto: Gc<Proto>,
aot: bool,
defined_aot_data: &mut std::collections::HashSet<DataId>,
) -> u32 {
let mut count: u32 = 0;
let empty: &[u32] = &[];
let live: &[u32] = escape
.live_at_op
.get(cmp_op_idx)
.map(|v| v.as_slice())
.unwrap_or(empty);
for &sid32 in live {
let sid = sid32 as usize;
if sid >= escape.sites.len() {
continue;
}
let site = &escape.sites[sid];
if site.state != EscapeState::Sinkable {
continue;
}
let Some(vars) = virt_vars[sid].as_ref() else {
continue;
};
let Some(kinds) = virt_kinds[sid].as_ref() else {
continue;
};
let cap = site.array_cap as usize;
if cap == 0 {
continue;
}
let off = op_offsets[site.op_idx] as usize;
let reg_idx = off + site.a as usize;
if reg_idx >= regs_full.len() {
continue;
}
if reg_idx >= kinds_snapshot.len() {
continue;
}
let n_hash = site.hash_keys.len();
let (raws_addr, kinds_addr) = if cap > 0 {
let raws_ss = bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
(cap * 8) as u32,
3,
));
let kinds_ss = bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
cap as u32,
0,
));
for vi in 0..cap {
let v = bcx.use_var(vars[vi]);
bcx.ins().stack_store(v, raws_ss, (vi * 8) as i32);
let tag = kind_to_raw_tag(kinds[vi]);
let k = bcx.ins().iconst(types::I8, tag as i64);
bcx.ins().stack_store(k, kinds_ss, vi as i32);
}
(
bcx.ins().stack_addr(types::I64, raws_ss, 0),
bcx.ins().stack_addr(types::I64, kinds_ss, 0),
)
} else {
(
bcx.ins().iconst(types::I64, 0),
bcx.ins().iconst(types::I64, 0),
)
};
let (hash_keys_addr, hash_raws_addr, hash_kinds_addr) = if n_hash > 0 {
let hash_keys_ss =
bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
(n_hash * 8) as u32,
3,
));
let hash_raws_ss =
bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
(n_hash * 8) as u32,
3,
));
let hash_kinds_ss =
bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
n_hash as u32,
0,
));
for vi in 0..n_hash {
let slot = cap + vi;
let v = bcx.use_var(vars[slot]);
bcx.ins().stack_store(v, hash_raws_ss, (vi * 8) as i32);
let tag = kind_to_raw_tag(kinds[slot]);
let k = bcx.ins().iconst(types::I8, tag as i64);
bcx.ins().stack_store(k, hash_kinds_ss, vi as i32);
let const_idx = site.hash_keys[vi] as usize;
let key_str = match head_proto.consts[const_idx] {
luna_core::runtime::Value::Str(s) => s,
_ => unreachable!(
"hash_keys must point at Str consts (validated by escape sweep)"
),
};
let key_ptr_v = emit_str_key_arg(module, bcx, key_str, aot, defined_aot_data);
bcx.ins()
.stack_store(key_ptr_v, hash_keys_ss, (vi * 8) as i32);
}
(
bcx.ins().stack_addr(types::I64, hash_keys_ss, 0),
bcx.ins().stack_addr(types::I64, hash_raws_ss, 0),
bcx.ins().stack_addr(types::I64, hash_kinds_ss, 0),
)
} else {
(
bcx.ins().iconst(types::I64, 0),
bcx.ins().iconst(types::I64, 0),
bcx.ins().iconst(types::I64, 0),
)
};
let cap_val = bcx.ins().iconst(types::I64, cap as i64);
let n_hash_val = bcx.ins().iconst(types::I64, n_hash as i64);
let mat_ref = module.declare_func_in_func(mat_sunk_id, bcx.func);
let call = bcx.ins().call(
mat_ref,
&[
cap_val,
raws_addr,
kinds_addr,
n_hash_val,
hash_keys_addr,
hash_raws_addr,
hash_kinds_addr,
],
);
let table_bits = bcx.inst_results(call)[0];
bcx.def_var(regs_full[reg_idx], table_bits);
kinds_snapshot[reg_idx] = RegKind::Table;
count += 1;
}
count
}
fn writes_target_a(op: Op) -> bool {
matches!(
op,
Op::Move
| Op::LoadI
| Op::LoadF
| Op::LoadK
| Op::LoadNil
| Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::IDiv
| Op::Mod
| Op::Pow
| Op::BAnd
| Op::BOr
| Op::BXor
| Op::Shl
| Op::Shr
| Op::Unm
| Op::BNot
| Op::Len
| Op::NewTable
| Op::GetI
| Op::GetTable
| Op::GetUpval
| Op::GetField
| Op::GetTabUp
| Op::Closure
)
}
fn const_fold_int_key(
record: &TraceRecord,
set_table_idx: usize,
reg: u32,
cap: u32,
) -> Option<u32> {
const MAX_STEPS: usize = 8;
let depth = record.ops[set_table_idx].inline_depth;
let mut cur_reg = reg;
let mut steps = 0;
let mut j = set_table_idx;
while j > 0 && steps < MAX_STEPS {
j -= 1;
steps += 1;
let rop = &record.ops[j];
if rop.inline_depth != depth {
continue;
}
let inst = rop.inst;
if inst.a() != cur_reg || !writes_target_a(inst.op()) {
continue;
}
match inst.op() {
Op::LoadI => {
let sbx = inst.sbx();
if sbx >= 1 && (sbx as u32) <= cap {
return Some(sbx as u32);
}
return None;
}
Op::Move => {
cur_reg = inst.b();
continue;
}
_ => return None,
}
}
None
}
fn escape_analyze(
record: &TraceRecord,
effective_end: usize,
end_kind: Option<TraceEnd>,
head_proto: Gc<Proto>,
) -> EscapeAnalysis {
let max_stack = head_proto.max_stack as usize;
let max_depth = (MAX_INLINE_DEPTH as usize) + 1;
if max_stack == 0 {
let upper0 = effective_end.min(record.ops.len());
return EscapeAnalysis {
sites: Vec::new(),
op_actions: vec![None; upper0],
live_at_op: vec![Vec::new(); upper0],
accum_sites: Vec::new(),
accum_live_at_op: Vec::new(),
};
}
let mut bindings: Vec<Vec<Option<usize>>> = vec![vec![None; max_stack]; max_depth];
let mut sites: Vec<AllocSite> = Vec::new();
let upper = effective_end.min(record.ops.len());
let mut op_actions: Vec<Option<OpAction>> = vec![None; upper];
let mut live_at_op: Vec<Vec<u32>> = vec![Vec::new(); upper];
fn mark_escape(sites: &mut [AllocSite], sid: usize) {
if sites[sid].state == EscapeState::Sinkable {
sites[sid].state = EscapeState::Escaped;
}
}
fn lookup(bindings: &[Vec<Option<usize>>], depth: u8, reg: u32) -> Option<usize> {
let d = depth as usize;
let r = reg as usize;
if d < bindings.len() && r < bindings[d].len() {
bindings[d][r]
} else {
None
}
}
fn unbind(bindings: &mut [Vec<Option<usize>>], depth: u8, reg: u32) {
let d = depth as usize;
let r = reg as usize;
if d < bindings.len() && r < bindings[d].len() {
bindings[d][r] = None;
}
}
fn escape_all_live(bindings: &[Vec<Option<usize>>], sites: &mut [AllocSite]) {
for row in bindings.iter() {
for &slot in row.iter() {
if let Some(sid) = slot {
mark_escape(sites, sid);
}
}
}
}
for i in 0..upper {
let cur_depth = record.ops[i].inline_depth as usize;
for d in (cur_depth + 1)..bindings.len() {
for slot in bindings[d].iter_mut() {
*slot = None;
}
}
let mut live_snap: Vec<u32> = Vec::new();
for row in bindings.iter() {
for &slot in row.iter() {
if let Some(sid) = slot {
live_snap.push(sid as u32);
}
}
}
live_at_op[i] = live_snap;
let rop = &record.ops[i];
let depth = rop.inline_depth;
let ins = rop.inst;
let a = ins.a();
let op = ins.op();
if (depth as usize) >= max_depth || (a as usize) >= max_stack {
continue;
}
match op {
Op::NewTable => {
let cap = ins.b();
let _c_hash = ins.c();
unbind(&mut bindings, depth, a);
let sid = sites.len();
sites.push(AllocSite {
op_idx: i,
pc: rop.pc,
a,
inline_depth: depth,
array_cap: cap,
hash_keys: Vec::new(),
state: EscapeState::Sinkable,
});
bindings[depth as usize][a as usize] = Some(sid);
op_actions[i] = Some(OpAction::NewTableSite {
site_idx: sid as u32,
});
}
Op::SetList => {
let b_bytecode = ins.b();
let c = ins.c();
let effective_b = if b_bytecode == 0 {
record.ops[i].var_count.unwrap_or_default()
} else {
b_bytecode
};
if let Some(sid) = lookup(&bindings, depth, a) {
if c != 0 || ins.k() || sites[sid].array_cap != effective_b {
mark_escape(&mut sites, sid);
} else {
op_actions[i] = Some(OpAction::SetListWrite {
site_idx: sid as u32,
});
}
for off in 1..=effective_b {
let src = a.wrapping_add(off);
if (src as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, src)
{
mark_escape(&mut sites, src_sid);
}
}
}
}
Op::SetI => {
let c = ins.c();
if (c as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, c)
{
mark_escape(&mut sites, src_sid);
}
if let Some(sid) = lookup(&bindings, depth, a) {
let key = ins.b();
if key >= 1 && key <= sites[sid].array_cap {
op_actions[i] = Some(OpAction::SetISunkWrite {
site_idx: sid as u32,
key,
});
} else {
mark_escape(&mut sites, sid);
}
}
}
Op::SetField => {
let c = ins.c();
let key_const_idx = ins.b();
if (c as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, c)
{
mark_escape(&mut sites, src_sid);
}
if let Some(sid) = lookup(&bindings, depth, a) {
let slot_opt = sites[sid]
.hash_keys
.iter()
.position(|&k| k == key_const_idx);
let slot = match slot_opt {
Some(s) => s,
None => {
let s = sites[sid].hash_keys.len();
sites[sid].hash_keys.push(key_const_idx);
s
}
};
op_actions[i] = Some(OpAction::SetFieldSunkWrite {
site_idx: sid as u32,
hash_slot: slot as u32,
});
}
}
Op::GetField => {
let b_reg = ins.b();
let key_const_idx = ins.c();
if (b_reg as usize) < max_stack
&& let Some(sid) = lookup(&bindings, depth, b_reg)
{
let slot_opt = sites[sid]
.hash_keys
.iter()
.position(|&k| k == key_const_idx);
match slot_opt {
Some(s) => {
op_actions[i] = Some(OpAction::GetFieldSunkRead {
site_idx: sid as u32,
hash_slot: s as u32,
});
}
None => {
mark_escape(&mut sites, sid);
}
}
}
unbind(&mut bindings, depth, a);
}
Op::SetTable => {
let c = ins.c();
if (c as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, c)
{
mark_escape(&mut sites, src_sid);
}
if let Some(sid) = lookup(&bindings, depth, a) {
let cap = sites[sid].array_cap;
let key_reg = ins.b();
if let Some(key) =
const_fold_int_key(record, i, key_reg, cap)
{
op_actions[i] = Some(OpAction::SetTableSunkWrite {
site_idx: sid as u32,
key,
});
} else {
mark_escape(&mut sites, sid);
}
}
}
Op::GetI => {
let b = ins.b();
let c = ins.c();
if (b as usize) < max_stack
&& let Some(sid) = lookup(&bindings, depth, b)
{
if c >= 1 && c <= sites[sid].array_cap {
op_actions[i] = Some(OpAction::GetIRead {
site_idx: sid as u32,
key: c,
});
} else {
mark_escape(&mut sites, sid);
}
}
unbind(&mut bindings, depth, a);
}
Op::GetTable | Op::Len => {
let b = ins.b();
if (b as usize) < max_stack
&& let Some(sid) = lookup(&bindings, depth, b)
{
mark_escape(&mut sites, sid);
}
unbind(&mut bindings, depth, a);
}
Op::Move => {
let b = ins.b();
let src_sid = if (b as usize) < max_stack {
lookup(&bindings, depth, b)
} else {
None
};
unbind(&mut bindings, depth, a);
if let Some(sid) = src_sid {
bindings[depth as usize][a as usize] = Some(sid);
}
}
Op::Call => {
let b = ins.b();
if b > 0 {
for off in 1..b {
let src = a.wrapping_add(off);
if (src as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, src)
{
mark_escape(&mut sites, src_sid);
}
}
}
if let Some(fn_sid) = lookup(&bindings, depth, a) {
mark_escape(&mut sites, fn_sid);
}
unbind(&mut bindings, depth, a);
}
Op::Return1 => {
if let Some(sid) = lookup(&bindings, depth, a) {
mark_escape(&mut sites, sid);
}
}
Op::Return0 => {}
Op::Lt | Op::Le | Op::Eq | Op::EqK => {
}
Op::Add | Op::Sub | Op::Mul | Op::Div | Op::IDiv | Op::Mod
| Op::Pow | Op::BAnd | Op::BOr | Op::BXor | Op::Shl | Op::Shr
| Op::Unm | Op::BNot
| Op::LoadI | Op::LoadF | Op::LoadK
| Op::GetUpval | Op::GetTabUp | Op::Concat
| Op::Closure => {
unbind(&mut bindings, depth, a);
}
Op::LoadNil => {
let b = ins.b();
for k in 0..=b {
let r = a.wrapping_add(k);
if (r as usize) < max_stack {
unbind(&mut bindings, depth, r);
}
}
}
Op::Close => {
escape_all_live(&bindings, &mut sites);
}
Op::Jmp | Op::ForLoop | Op::Return => {}
_ => {
unbind(&mut bindings, depth, a);
}
}
}
if let Some(end) = end_kind {
if effective_end < record.ops.len() {
let term = &record.ops[effective_end];
let depth = term.inline_depth;
let a = term.inst.a();
let op = term.inst.op();
let in_range = (depth as usize) < max_depth && (a as usize) < max_stack;
match end {
TraceEnd::Call => {
if in_range {
let b = term.inst.b();
if b > 0 {
for off in 1..b {
let src = a.wrapping_add(off);
if (src as usize) < max_stack
&& let Some(src_sid) = lookup(&bindings, depth, src)
{
mark_escape(&mut sites, src_sid);
}
}
}
if let Some(fn_sid) = lookup(&bindings, depth, a) {
mark_escape(&mut sites, fn_sid);
}
}
}
TraceEnd::InlineAbort => {
escape_all_live(&bindings, &mut sites);
}
TraceEnd::ForLoop => {
}
TraceEnd::Return => {
if matches!(op, Op::Return1)
&& in_range
&& let Some(sid) = lookup(&bindings, depth, a)
{
mark_escape(&mut sites, sid);
}
}
TraceEnd::SelfLink(_) => {
escape_all_live(&bindings, &mut sites);
}
TraceEnd::DownRec { .. } => {
escape_all_live(&bindings, &mut sites);
}
}
}
}
let accum_sites = detect_accumulators(record, effective_end, head_proto);
let accum_live_at_op = vec![Vec::new(); op_actions.len()];
EscapeAnalysis {
sites,
op_actions,
live_at_op,
accum_sites,
accum_live_at_op,
}
}
fn detect_accumulators(record: &TraceRecord, end: usize, _head_proto: Gc<Proto>) -> Vec<AccumSite> {
use luna_core::vm::isa::Op;
let upper = end.min(record.ops.len());
if upper < 4 {
return Vec::new();
}
let mut candidates: Vec<(AccumSite, usize, usize, usize, usize)> = Vec::new();
for ci in 2..upper.saturating_sub(1) {
let concat_rop = &record.ops[ci];
if !matches!(concat_rop.inst.op(), Op::Concat) {
continue;
}
if concat_rop.inline_depth != 0 {
continue;
}
if concat_rop.inst.b() != 2 {
continue;
}
let tmp = concat_rop.inst.a();
let pre1 = &record.ops[ci - 2];
if pre1.inline_depth != 0 || !matches!(pre1.inst.op(), Op::Move) || pre1.inst.a() != tmp {
continue;
}
let s_slot = pre1.inst.b();
let pre2 = &record.ops[ci - 1];
if pre2.inline_depth != 0 || !matches!(pre2.inst.op(), Op::Move) || pre2.inst.a() != tmp + 1
{
continue;
}
let v_slot = pre2.inst.b();
let post = &record.ops[ci + 1];
if post.inline_depth != 0
|| !matches!(post.inst.op(), Op::Move)
|| post.inst.a() != s_slot
|| post.inst.b() != tmp
{
continue;
}
let entry_tags = &record.entry_tags;
let s_tag = entry_tags.get(s_slot as usize).copied();
let v_tag = entry_tags.get(v_slot as usize).copied();
if s_tag != Some(luna_core::runtime::value::raw::STR)
|| v_tag != Some(luna_core::runtime::value::raw::STR)
{
continue;
}
candidates.push((
AccumSite {
op_idx: ci,
pc: concat_rop.pc,
accum_slot: s_slot,
piece_slot: v_slot,
inline_depth: 0,
state: BufferState::Bufferable,
},
ci - 2, ci - 1, ci, ci + 1, ));
}
if candidates.is_empty() {
return Vec::new();
}
if !record.closed || record.is_call_triggered {
return Vec::new();
}
let mut out: Vec<AccumSite> = Vec::with_capacity(candidates.len());
for (mut site, pre1, pre2, concat, post) in candidates {
for i in 0..upper {
if i == pre1 || i == pre2 || i == concat || i == post {
continue;
}
let rop = &record.ops[i];
if rop.inline_depth != 0 {
continue;
}
let ins = rop.inst;
let op = ins.op();
let a = ins.a();
let b = ins.b();
let c = ins.c();
let reads_slot = match op {
Op::Move => b == site.accum_slot,
Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::Pow
| Op::IDiv
| Op::BAnd
| Op::BOr
| Op::BXor
| Op::Shl
| Op::Shr => b == site.accum_slot || c == site.accum_slot,
Op::Unm | Op::BNot | Op::Not | Op::Len => b == site.accum_slot,
Op::Eq | Op::Lt | Op::Le => a == site.accum_slot || b == site.accum_slot,
Op::EqK | Op::Test | Op::TestSet => a == site.accum_slot,
Op::Concat => {
let n = ins.b();
let end_op = a.saturating_add(n);
(a..end_op).any(|r| r == site.accum_slot)
}
Op::Call | Op::TailCall => {
let lo = a;
let hi = lo.saturating_add(b.max(1));
site.accum_slot >= lo && site.accum_slot < hi
}
Op::Return | Op::Return1 => a == site.accum_slot,
Op::Return0 => false,
Op::SetI | Op::SetTable | Op::SetField | Op::SetUpval | Op::SetTabUp => {
a == site.accum_slot || c == site.accum_slot
}
Op::GetI | Op::GetTable | Op::GetField | Op::GetTabUp | Op::GetUpval => {
b == site.accum_slot
}
Op::SetList => {
let lo = a;
let hi = lo.saturating_add(b.max(1));
site.accum_slot >= lo && site.accum_slot < hi
}
_ => false,
};
let writes_slot = match op {
Op::LoadNil => {
let lo = a;
let hi = lo.saturating_add(b);
site.accum_slot >= lo && site.accum_slot <= hi
}
Op::Move
| Op::LoadI
| Op::LoadF
| Op::LoadK
| Op::LoadKx
| Op::LoadFalse
| Op::LFalseSkip
| Op::LoadTrue
| Op::GetUpval
| Op::GetTabUp
| Op::GetTable
| Op::GetI
| Op::GetField
| Op::NewTable
| Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Mod
| Op::Pow
| Op::IDiv
| Op::BAnd
| Op::BOr
| Op::BXor
| Op::Shl
| Op::Shr
| Op::Unm
| Op::BNot
| Op::Not
| Op::Len
| Op::Concat
| Op::Call
| Op::TailCall
| Op::TestSet
| Op::ForLoop
| Op::ForPrep
| Op::TForCall => a == site.accum_slot,
_ => false,
};
if reads_slot || writes_slot {
site.state = BufferState::NonBuffered;
break;
}
}
out.push(site);
}
out
}
pub fn op_reads_writes(inst: luna_core::vm::isa::Inst) -> (Vec<u32>, Vec<u32>) {
use luna_core::vm::isa::Op;
let a = inst.a();
let b = inst.b();
let c = inst.c();
let k = inst.k();
match inst.op() {
Op::Move => (vec![b], vec![a]),
Op::LoadI | Op::LoadF | Op::LoadK | Op::LoadKx => (vec![], vec![a]),
Op::LoadFalse | Op::LoadTrue | Op::LFalseSkip => (vec![], vec![a]),
Op::LoadNil => {
let mut w = Vec::with_capacity((b + 1) as usize);
for i in 0..=b {
w.push(a + i);
}
(vec![], w)
}
Op::GetUpval => (vec![], vec![a]),
Op::SetUpval => (vec![a], vec![]),
Op::GetTabUp => (vec![], vec![a]),
Op::GetTable => (vec![b, c], vec![a]),
Op::GetI => (vec![b], vec![a]),
Op::GetField => (vec![b], vec![a]),
Op::SetTabUp => {
let mut r = Vec::new();
if !k {
r.push(c);
}
(r, vec![])
}
Op::SetTable => {
let mut r = vec![a, b];
if !k {
r.push(c);
}
(r, vec![])
}
Op::SetI => {
let mut r = vec![a];
if !k {
r.push(c);
}
(r, vec![])
}
Op::SetField => {
let mut r = vec![a];
if !k {
r.push(c);
}
(r, vec![])
}
Op::NewTable => (vec![], vec![a]),
Op::SelfOp => (vec![b], vec![a, a + 1]),
Op::Add
| Op::Sub
| Op::Mul
| Op::Mod
| Op::Pow
| Op::Div
| Op::IDiv
| Op::BAnd
| Op::BOr
| Op::BXor
| Op::Shl
| Op::Shr => (vec![b, c], vec![a]),
Op::Unm | Op::BNot | Op::Not | Op::Len => (vec![b], vec![a]),
Op::Concat => {
let mut r = Vec::with_capacity(b as usize);
for i in 0..b {
r.push(a + i);
}
(r, vec![a])
}
Op::Close | Op::Tbc => (vec![], vec![]),
Op::Jmp | Op::ExtraArg => (vec![], vec![]),
Op::Eq | Op::Lt | Op::Le => (vec![a, b], vec![]),
Op::EqK => (vec![a], vec![]),
Op::Test => (vec![a], vec![]),
Op::TestSet => (vec![b], vec![a]),
Op::Call => {
let nargs = if b == 0 { 0 } else { b - 1 };
let nres = if c == 0 { 0 } else { c - 1 };
let mut r = vec![a];
for i in 1..=nargs {
r.push(a + i);
}
let mut w = Vec::with_capacity(nres as usize);
for i in 0..nres {
w.push(a + i);
}
(r, w)
}
Op::TailCall => {
let nargs = if b == 0 { 0 } else { b - 1 };
let mut r = vec![a];
for i in 1..=nargs {
r.push(a + i);
}
(r, vec![])
}
Op::Return => {
let n = if b == 0 { 1 } else { b - 1 };
let mut r = Vec::with_capacity(n as usize);
for i in 0..n {
r.push(a + i);
}
(r, vec![])
}
Op::Return0 => (vec![], vec![]),
Op::Return1 => (vec![a], vec![]),
Op::ForLoop => {
(vec![a, a + 1, a + 2], vec![a, a + 1, a + 3])
}
Op::ForPrep => {
(vec![a, a + 1, a + 2], vec![a, a + 1, a + 3])
}
Op::TForPrep => (vec![], vec![]),
Op::TForCall => {
let mut w = Vec::with_capacity(c as usize);
for i in 0..c {
w.push(a + 4 + i);
}
(vec![a, a + 1, a + 2], w)
}
Op::TForLoop => {
(vec![a + 4], vec![a + 2])
}
Op::SetList => {
let n = if b == 0 { 0 } else { b };
let mut r = vec![a];
for i in 1..=n {
r.push(a + i);
}
(r, vec![])
}
Op::Closure => (vec![], vec![a]),
Op::Vararg | Op::GetVarg => {
(vec![], vec![a])
}
Op::VargIdx => (vec![c], vec![a]),
Op::ErrNNil => (vec![a], vec![]),
}
}
fn op_writes_at_offset(rop: &RecordedOp, op_offset: u32) -> Vec<u32> {
let (_r, w) = op_reads_writes(rop.inst);
w.into_iter().map(|s| op_offset + s).collect()
}
fn op_reads_at_offset(rop: &RecordedOp, op_offset: u32) -> Vec<u32> {
let (r, _w) = op_reads_writes(rop.inst);
r.into_iter().map(|s| op_offset + s).collect()
}
pub fn compute_body_writes(record: &TraceRecord, op_offsets: &[u32]) -> Vec<u32> {
let mut s: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
for (i, rop) in record.ops.iter().enumerate() {
let off = op_offsets.get(i).copied().unwrap_or(0);
for w in op_writes_at_offset(rop, off) {
s.insert(w);
}
}
s.into_iter().collect()
}
pub fn compute_live_in_slots(record: &TraceRecord, op_offsets: &[u32]) -> Vec<u32> {
let mut written: std::collections::HashSet<u32> = std::collections::HashSet::new();
let mut live_in: std::collections::BTreeSet<u32> = std::collections::BTreeSet::new();
for (i, rop) in record.ops.iter().enumerate() {
let off = op_offsets.get(i).copied().unwrap_or(0);
for r in op_reads_at_offset(rop, off) {
if !written.contains(&r) {
live_in.insert(r);
}
}
for w in op_writes_at_offset(rop, off) {
written.insert(w);
}
}
live_in.into_iter().collect()
}
pub struct TraceHandle {
_module: super::SendJitModule,
_entry_raw: *const u8,
}
unsafe impl Send for TraceHandle {}
impl TraceHandle {
#[doc(hidden)]
#[inline]
pub fn __j_d_module(&self) -> &super::SendJitModule {
&self._module
}
}
#[derive(Clone, Copy, Debug)]
enum TraceEnd {
Call,
ForLoop,
InlineAbort,
Return,
SelfLink(SelfRecKind),
DownRec {
return_pc: u32,
target_proto_id: usize,
depth_delta: u8,
},
}
#[derive(Clone, Copy, Debug)]
enum CmpDir {
TookJmp,
SkippedJmp,
}
fn is_whitelisted_step5(op: Op) -> bool {
matches!(
op,
Op::Move
| Op::Add
| Op::Sub
| Op::Mul
| Op::Div
| Op::Pow
| Op::IDiv
| Op::Mod
| Op::BAnd
| Op::BOr
| Op::BXor
| Op::Shl
| Op::Shr
| Op::Unm
| Op::BNot
| Op::Jmp
| Op::Lt
| Op::Le
| Op::Eq
| Op::EqK
| Op::NewTable
| Op::GetI
| Op::GetTable
| Op::SetI
| Op::SetTable
| Op::SetList
| Op::Len
| Op::Call
| Op::ForLoop
| Op::LoadI
| Op::LoadF
| Op::LoadK
| Op::LoadNil
| Op::Closure
| Op::Close
| Op::GetUpval
| Op::GetTabUp
| Op::GetField
| Op::SetField
| Op::Test
| Op::TestSet
| Op::TForPrep
| Op::TForCall
| Op::TForLoop
| Op::Concat
)
}
fn k_op(current_kinds: &[RegKind], reg: u32) -> RegKind {
*current_kinds.get(reg as usize).unwrap_or(&RegKind::Unset)
}
fn use_var_f64(bcx: &mut FunctionBuilder<'_>, regs: &[Variable], reg: u32) -> Value {
let raw = bcx.use_var(regs[reg as usize]);
bcx.ins().bitcast(types::F64, MemFlags::new(), raw)
}
fn def_var_f64(bcx: &mut FunctionBuilder<'_>, var: Variable, val_f64: Value) {
let bits = bcx.ins().bitcast(types::I64, MemFlags::new(), val_f64);
bcx.def_var(var, bits);
}
fn emit_table_set<M: Module>(
bcx: &mut FunctionBuilder<'_>,
module: &mut M,
set_int_id: cranelift_module::FuncId,
set_nil_id: cranelift_module::FuncId,
set_raw_id: cranelift_module::FuncId,
t: Value,
key: Value,
val_kind: RegKind,
val_var: Variable,
) {
use luna_core::runtime::value::raw;
match val_kind {
RegKind::Nil => {
let f = module.declare_func_in_func(set_nil_id, bcx.func);
bcx.ins().call(f, &[t, key]);
}
RegKind::Int | RegKind::Unset => {
let v = bcx.use_var(val_var);
let f = module.declare_func_in_func(set_int_id, bcx.func);
bcx.ins().call(f, &[t, key, v]);
}
other => {
let tag = match other {
RegKind::Float => raw::FLOAT,
RegKind::Table => raw::TABLE,
RegKind::Closure => raw::CLOSURE,
RegKind::Str => raw::STR,
RegKind::Nil | RegKind::Int | RegKind::Unset => unreachable!(),
};
let v = bcx.use_var(val_var);
let tag_v = bcx.ins().iconst(types::I64, tag as i64);
let f = module.declare_func_in_func(set_raw_id, bcx.func);
bcx.ins().call(f, &[t, key, v, tag_v]);
}
}
}
#[derive(Clone, Copy)]
struct FlushCtx {
buf_var: Variable,
accum_slot: u32,
intern_ref: cranelift_codegen::ir::FuncRef,
release_ref: cranelift_codegen::ir::FuncRef,
}
#[allow(dead_code)]
pub(crate) fn current_base_addr(
bcx: &mut FunctionBuilder<'_>,
base_var: Variable,
op_offset_bytes: i32,
slot: u32,
) -> (Value, i32) {
let base_now = bcx.use_var(base_var);
let total_offset = op_offset_bytes.saturating_add((slot as i32).saturating_mul(8));
(base_now, total_offset)
}
fn emit_flush_buf(bcx: &mut FunctionBuilder<'_>, ctx: &FlushCtx, regs: &[Variable]) {
let buf_ptr = bcx.use_var(ctx.buf_var);
let call_inst = bcx.ins().call(ctx.intern_ref, &[buf_ptr]);
let str_ptr = bcx.inst_results(call_inst)[0];
if let Some(&accum_var) = regs.get(ctx.accum_slot as usize) {
bcx.def_var(accum_var, str_ptr);
}
bcx.ins().call(ctx.release_ref, &[buf_ptr]);
}
fn emit_side_trace_or_return(
bcx: &mut FunctionBuilder<'_>,
reg_state: Value,
side_trace_cell_addr: i64,
trace_fn_sig_ref: cranelift_codegen::ir::SigRef,
sentinel_code: u32,
normal_return: impl FnOnce(&mut FunctionBuilder<'_>),
) {
if side_trace_cell_addr == 0 {
normal_return(bcx);
return;
}
let cell_addr = bcx.ins().iconst(types::I64, side_trace_cell_addr);
let fn_ptr = bcx
.ins()
.load(types::I64, MemFlags::trusted(), cell_addr, 0);
let null = bcx.ins().iconst(types::I64, 0);
let has_side = bcx.ins().icmp(IntCC::NotEqual, fn_ptr, null);
let do_side_blk = bcx.create_block();
let do_exit_blk = bcx.create_block();
bcx.ins().brif(has_side, do_side_blk, &[], do_exit_blk, &[]);
bcx.switch_to_block(do_side_blk);
bcx.seal_block(do_side_blk);
let call_inst = bcx
.ins()
.call_indirect(trace_fn_sig_ref, fn_ptr, &[reg_state]);
let body = bcx.inst_results(call_inst)[0];
let mask_u64: u64 = (1u64 << 63) | ((sentinel_code as u64 & 0x7F) << 56);
let mask_v = bcx.ins().iconst(types::I64, mask_u64 as i64);
let masked = bcx.ins().bor(body, mask_v);
bcx.ins().return_(&[masked]);
bcx.switch_to_block(do_exit_blk);
bcx.seal_block(do_exit_blk);
normal_return(bcx);
}
fn emit_store_back_and_return_pc(
bcx: &mut FunctionBuilder<'_>,
regs: &[Variable],
reg_state: Value,
pc: u32,
flush_ctx: Option<&FlushCtx>,
side_trace_cell_addr: i64,
trace_fn_sig_ref: cranelift_codegen::ir::SigRef,
sentinel_code: u32,
) {
if let Some(ctx) = flush_ctx {
emit_flush_buf(bcx, ctx, regs);
}
for (idx, v) in regs.iter().copied().enumerate() {
let val = bcx.use_var(v);
let offset = (idx as i32) * 8;
bcx.ins().store(MemFlags::new(), val, reg_state, offset);
}
emit_side_trace_or_return(
bcx,
reg_state,
side_trace_cell_addr,
trace_fn_sig_ref,
sentinel_code,
|bcx| {
let pc_val = bcx.ins().iconst(types::I64, pc as i64);
bcx.ins().return_(&[pc_val]);
},
);
}
fn emit_store_back_and_return_site(
bcx: &mut FunctionBuilder<'_>,
regs: &[Variable],
reg_state: Value,
site_idx: u32,
cont_pc: u32,
flush_ctx: Option<&FlushCtx>,
side_trace_cell_addr: i64,
trace_fn_sig_ref: cranelift_codegen::ir::SigRef,
) {
if let Some(ctx) = flush_ctx {
emit_flush_buf(bcx, ctx, regs);
}
for (idx, v) in regs.iter().copied().enumerate() {
let val = bcx.use_var(v);
let offset = (idx as i32) * 8;
bcx.ins().store(MemFlags::new(), val, reg_state, offset);
}
let sentinel = encode_side_sentinel(SIDE_SENT_KIND_INLINE, site_idx);
emit_side_trace_or_return(
bcx,
reg_state,
side_trace_cell_addr,
trace_fn_sig_ref,
sentinel,
|bcx| {
let encoded = (((site_idx as u64) + 1) << 32) | (cont_pc as u64);
let v = bcx.ins().iconst(types::I64, encoded as i64);
bcx.ins().return_(&[v]);
},
);
}
pub fn try_compile_trace(
storage: &mut dyn luna_core::jit::JitStorage,
record: &TraceRecord,
) -> Option<CompiledTrace> {
try_compile_trace_with_options(storage, record, CompileOptions::default())
}
thread_local! {
pub(crate) static LAST_COMPILE_CHECKPOINT: std::cell::Cell<&'static str> =
const { std::cell::Cell::new("not-entered") };
pub(crate) static LAST_OP_ID: std::cell::Cell<u8> =
const { std::cell::Cell::new(255) };
pub(crate) static BASE_VAR_SCAFFOLD_DECLARED: std::cell::Cell<u64> =
const { std::cell::Cell::new(0) };
}
fn checkpoint(s: &'static str) {
LAST_COMPILE_CHECKPOINT.with(|c| c.set(s));
}
fn set_last_op_id(id: u8) {
LAST_OP_ID.with(|c| c.set(id));
}
pub fn last_compile_checkpoint() -> &'static str {
LAST_COMPILE_CHECKPOINT.with(|c| c.get())
}
pub fn last_op_id() -> u8 {
LAST_OP_ID.with(|c| c.get())
}
pub fn base_var_scaffold_declared_count() -> u64 {
BASE_VAR_SCAFFOLD_DECLARED.with(|c| c.get())
}
pub fn reset_base_var_scaffold_declared_count() {
BASE_VAR_SCAFFOLD_DECLARED.with(|c| c.set(0));
}
fn build_trace_jit_module() -> Option<JITModule> {
let mut flag_builder = settings::builder();
flag_builder.set("use_colocated_libcalls", "false").ok()?;
flag_builder.set("is_pic", "false").ok()?;
flag_builder.set("opt_level", "speed").ok()?;
let isa = cranelift_native::builder()
.ok()?
.finish(settings::Flags::new(flag_builder))
.ok()?;
let mut builder = JITBuilder::with_isa(isa, cranelift_module::default_libcall_names());
builder.symbol("luna_jit_new_table", super::luna_jit_new_table as *const u8);
builder.symbol(
"luna_jit_table_set_int",
super::luna_jit_table_set_int as *const u8,
);
builder.symbol(
"luna_jit_table_set_nil",
super::luna_jit_table_set_nil as *const u8,
);
builder.symbol(
"luna_jit_table_set_raw",
super::luna_jit_table_set_raw as *const u8,
);
builder.symbol(
"luna_jit_table_set_field",
super::luna_jit_table_set_field as *const u8,
);
builder.symbol(
"luna_jit_table_get_field",
super::luna_jit_table_get_field as *const u8,
);
builder.symbol(
"luna_jit_op_get_tab_up",
super::luna_jit_op_get_tab_up as *const u8,
);
builder.symbol(
"luna_jit_table_get_int",
super::luna_jit_table_get_int as *const u8,
);
builder.symbol("luna_jit_table_len", super::luna_jit_table_len as *const u8);
builder.symbol("luna_jit_upval_get", super::luna_jit_upval_get as *const u8);
builder.symbol(
"luna_jit_trace_materialize_frames",
super::luna_jit_trace_materialize_frames as *const u8,
);
builder.symbol(
"luna_jit_materialize_sunk_table",
super::luna_jit_materialize_sunk_table as *const u8,
);
builder.symbol(
"luna_jit_op_closure",
super::luna_jit_op_closure as *const u8,
);
builder.symbol(
"luna_jit_spill_to_stack",
super::luna_jit_spill_to_stack as *const u8,
);
builder.symbol("luna_jit_op_close", super::luna_jit_op_close as *const u8);
builder.symbol(
"luna_jit_op_tforcall",
super::luna_jit_op_tforcall as *const u8,
);
builder.symbol(
"luna_jit_stack_load",
super::luna_jit_stack_load as *const u8,
);
builder.symbol("luna_jit_stack_tag", super::luna_jit_stack_tag as *const u8);
builder.symbol("luna_jit_op_concat", super::luna_jit_op_concat as *const u8);
builder.symbol(
"luna_jit_stack_update_raw",
super::luna_jit_stack_update_raw as *const u8,
);
builder.symbol(
"luna_jit_str_buf_acquire",
super::luna_jit_str_buf_acquire as *const u8,
);
builder.symbol(
"luna_jit_str_buf_release",
super::luna_jit_str_buf_release as *const u8,
);
builder.symbol(
"luna_jit_str_buf_extend",
super::luna_jit_str_buf_extend as *const u8,
);
builder.symbol(
"luna_jit_str_buf_intern",
super::luna_jit_str_buf_intern as *const u8,
);
Some(JITModule::new(builder))
}
unsafe extern "C" fn placeholder_trace_fn(_reg_state: *mut i64) -> i64 {
panic!(
"placeholder_trace_fn called: CompiledTrace.entry must be patched by the JIT finalize wrapper before dispatch"
);
}
pub fn try_compile_trace_with_options(
storage: &mut dyn luna_core::jit::JitStorage,
record: &TraceRecord,
opts: CompileOptions,
) -> Option<CompiledTrace> {
let mut module = build_trace_jit_module()?;
let (fn_id, mut compiled) = lower_trace_into(&mut module, record, opts)?;
module.finalize_definitions().ok()?;
let ptr = module.get_finalized_function(fn_id);
let entry_fn: TraceFn = unsafe { std::mem::transmute::<*const u8, TraceFn>(ptr) };
compiled.entry = entry_fn;
let cs = crate::jit_backend::storage::from_storage(storage).ok()?;
cs.trace_handles.push(TraceHandle {
_module: super::SendJitModule::new(module),
_entry_raw: ptr,
});
Some(compiled)
}
pub fn lower_trace_into<M: Module>(
module: &mut M,
record: &TraceRecord,
opts: CompileOptions,
) -> Option<(FuncId, CompiledTrace)> {
lower_trace_into_named(module, record, opts, None)
}
pub fn lower_trace_into_named<M: Module>(
mut module: &mut M,
record: &TraceRecord,
opts: CompileOptions,
aot_fn_name: Option<&str>,
) -> Option<(FuncId, CompiledTrace)> {
checkpoint("enter");
if !record.closed {
checkpoint("bail:not-closed");
return None;
}
checkpoint("post:closed-check");
let mut defined_aot_data: std::collections::HashSet<DataId> = std::collections::HashSet::new();
let head_proto = record.head_proto;
let max_stack = head_proto.max_stack as usize;
let n = record.ops.len();
if let Some(first) = record.ops.first() {
if first.inline_depth != 0 || !std::ptr::eq(first.proto.as_ptr(), head_proto.as_ptr()) {
checkpoint("bail:first-op-shape");
return None;
}
}
checkpoint("post:first-op-check");
let depth_items: Vec<(u8, bool)> = record
.ops
.iter()
.map(|r| (r.inline_depth, matches!(r.inst.op(), Op::Call)))
.collect();
if !verify_depth_invariant(&depth_items) {
checkpoint("bail:depth-invariant");
return None;
}
checkpoint("post:depth-invariant");
let (op_offsets, enclosing_call_a) = compute_op_offsets(record);
let mut window_size: u32 = op_offsets
.iter()
.map(|&off| off + max_stack as u32)
.max()
.unwrap_or(max_stack as u32);
if record.self_link_kind.is_some() {
let mut last_call_idx: Option<usize> = None;
for (i, rop) in record.ops.iter().enumerate() {
if matches!(rop.inst.op(), Op::Call) {
last_call_idx = Some(i);
}
}
if let Some(idx) = last_call_idx {
let bump_off = op_offsets[idx] + record.ops[idx].inst.a() + 1;
let needed = bump_off + max_stack as u32;
if needed > window_size {
window_size = needed;
}
}
}
let window_size_us = window_size as usize;
if let Some((parent_proto, parent_head_pc, _)) = record.side_trace_parent {
let has_back_edge = record.ops.iter().any(|op| match op.inst.op() {
luna_core::vm::isa::Op::ForLoop | luna_core::vm::isa::Op::TForLoop => true,
luna_core::vm::isa::Op::Jmp => op.inst.sbx() < 0,
_ => false,
});
if has_back_edge {
let has_impure = record.ops.iter().any(|op| {
use luna_core::vm::isa::Op;
matches!(
op.inst.op(),
Op::Call
| Op::TailCall
| Op::TForCall
| Op::SetTable
| Op::SetI
| Op::SetField
| Op::SetUpval
| Op::SetTabUp
| Op::Closure
| Op::Close
| Op::Tbc
)
});
if has_impure {
checkpoint("bail:side-trace-back-edge-with-impure");
return None;
}
let child_live_in = compute_live_in_slots(record, &op_offsets);
if !child_live_in.is_empty() {
let parent_writes_opt = {
let traces = parent_proto.traces.borrow();
traces
.iter()
.find(|t| t.head_pc == parent_head_pc)
.map(|pct| pct.body_writes.clone())
};
if let Some(parent_writes) = parent_writes_opt {
let mut i = 0;
let mut j = 0;
let pw = &parent_writes[..];
let cl_li = &child_live_in[..];
while i < pw.len() && j < cl_li.len() {
match pw[i].cmp(&cl_li[j]) {
std::cmp::Ordering::Equal => {
checkpoint("bail:side-trace-live-in-overlap");
return None;
}
std::cmp::Ordering::Less => i += 1,
std::cmp::Ordering::Greater => j += 1,
}
}
} else {
checkpoint("bail:side-trace-parent-ct-missing");
return None;
}
}
}
}
checkpoint("post:side-trace-v2e-smart-gate");
for (i, rop) in record.ops.iter().enumerate() {
if !matches!(rop.inst.op(), Op::Call) {
continue;
}
let depth = rop.inline_depth as usize;
let Some(next) = record.ops.get(i + 1) else {
continue;
};
if (next.inline_depth as usize) != depth + 1 {
continue;
}
if !std::ptr::eq(next.proto.as_ptr(), head_proto.as_ptr()) {
continue;
}
let c = rop.inst.c();
if c == 2 {
} else if c == 0 && rop.var_count == Some(1) {
} else {
checkpoint("bail:self-rec-Call-c-not-1");
return None;
}
}
checkpoint("post:self-rec-Call-validate");
if head_proto.is_vararg {
for r in &record.ops {
if r.inline_depth > 0 {
checkpoint("bail:vararg-head-with-depth");
return None;
}
}
}
checkpoint("post:vararg-check");
let mut folded_ops: Vec<bool> = vec![false; n];
let mut math_folds: Vec<TraceMathFold> = Vec::new();
{
let mut i = 0;
while i < n {
if let Some(fold) = try_match_trace_math_fold(record, i, head_proto) {
match fold.kind {
FoldKind::Libm1 => {
for k in 0..4 {
folded_ops[i + k] = true;
}
i += 4;
}
FoldKind::Min2 | FoldKind::Max2 => {
folded_ops[fold.start_idx] = true;
folded_ops[fold.start_idx + 1] = true;
folded_ops[fold.call_idx] = true;
i = fold.call_idx + 1;
}
}
math_folds.push(fold);
} else {
i += 1;
}
}
}
let end_idx_opt: Option<(usize, TraceEnd)> = if let Some(dr) = record.downrec_close {
let mut natural_end = record.ops.len();
for (i, r) in record.ops.iter().enumerate() {
if folded_ops[i] {
continue;
}
let depth = r.inline_depth as usize;
if depth != 0 {
continue;
}
match r.inst.op() {
Op::Call | Op::ForLoop | Op::TForLoop | Op::Return0 | Op::Return1 => {
natural_end = i;
break;
}
_ => {}
}
}
Some((
natural_end,
TraceEnd::DownRec {
return_pc: dr.return_pc,
target_proto_id: dr.target_proto.as_ptr() as usize,
depth_delta: dr.depth_delta,
},
))
} else if let Some(kind) = record.self_link_kind {
Some((record.ops.len(), TraceEnd::SelfLink(kind)))
} else {
let mut found: Option<(usize, TraceEnd)> = None;
for (i, r) in record.ops.iter().enumerate() {
if folded_ops[i] {
continue;
}
let depth = r.inline_depth as usize;
if depth > MAX_INLINE_DEPTH as usize
|| !std::ptr::eq(r.proto.as_ptr(), head_proto.as_ptr())
{
found = Some((i, TraceEnd::InlineAbort));
break;
}
match r.inst.op() {
Op::Call => {
let nxt = record.ops.get(i + 1);
let is_self_recursive = nxt
.map(|n_op| {
n_op.inline_depth as usize == depth + 1
&& depth < MAX_INLINE_DEPTH as usize
&& std::ptr::eq(n_op.proto.as_ptr(), head_proto.as_ptr())
})
.unwrap_or(false);
if is_self_recursive {
continue;
}
if depth == 0 {
found = Some((i, TraceEnd::Call));
} else {
found = Some((i, TraceEnd::InlineAbort));
}
break;
}
Op::ForLoop => {
if depth == 0 {
found = Some((i, TraceEnd::ForLoop));
} else {
found = Some((i, TraceEnd::InlineAbort));
}
break;
}
Op::TForLoop => {
if depth == 0 {
found = Some((i, TraceEnd::ForLoop));
} else {
found = Some((i, TraceEnd::InlineAbort));
}
break;
}
Op::Return0 | Op::Return1 if depth == 0 => {
found = Some((i, TraceEnd::Return));
break;
}
_ => {}
}
}
found
};
let effective_end = end_idx_opt.map(|(i, _)| i).unwrap_or(n);
checkpoint("post:end-idx-found");
let mut escape = escape_analyze(
record,
effective_end,
end_idx_opt.map(|(_, k)| k),
head_proto,
);
checkpoint("post:escape-analyze");
let mut flush_ctx: Option<FlushCtx> = None;
#[derive(Clone, Copy, Debug)]
struct BufferedAccum {
accum_slot: u32,
piece_slot: u32,
pre1_idx: usize,
pre2_idx: usize,
concat_idx: usize,
post_idx: usize,
}
let active_accum: Option<BufferedAccum> = escape
.accum_sites
.iter()
.find(|s| s.state == BufferState::Bufferable)
.map(|s| BufferedAccum {
accum_slot: s.accum_slot,
piece_slot: s.piece_slot,
pre1_idx: s.op_idx - 2,
pre2_idx: s.op_idx - 1,
concat_idx: s.op_idx,
post_idx: s.op_idx + 1,
});
let call_idx_opt = match end_idx_opt {
Some((i, TraceEnd::Call)) => Some(i),
_ => None,
};
let for_loop_idx_opt = match end_idx_opt {
Some((i, TraceEnd::ForLoop)) => Some(i),
_ => None,
};
let inline_abort_idx_opt = match end_idx_opt {
Some((i, TraceEnd::InlineAbort)) => Some(i),
_ => None,
};
let return_idx_opt = match end_idx_opt {
Some((i, TraceEnd::Return)) => Some(i),
_ => None,
};
let self_link_idx_opt: Option<(usize, SelfRecKind)> = match end_idx_opt {
Some((i, TraceEnd::SelfLink(kind))) => Some((i, kind)),
_ => None,
};
let downrec_idx_opt: Option<(usize, u32, usize, u8)> = match end_idx_opt {
Some((
i,
TraceEnd::DownRec {
return_pc,
target_proto_id,
depth_delta,
},
)) => Some((i, return_pc, target_proto_id, depth_delta)),
_ => None,
};
let mut consumed_by_cmp = vec![false; effective_end];
let mut cmp_dirs: Vec<Option<CmpDir>> = vec![None; effective_end];
checkpoint("pre:cmp-dirs-loop");
for (i, rop) in record.ops[..effective_end].iter().enumerate() {
if folded_ops[i] {
continue;
}
set_last_op_id(rop.inst.op() as u8);
if !std::ptr::eq(rop.proto.as_ptr(), head_proto.as_ptr()) {
checkpoint("bail:cmp-dirs-cross-proto-op");
return None;
}
let op = rop.inst.op();
if matches!(op, Op::Call) {
continue;
}
if rop.inline_depth > 0 && matches!(op, Op::Return0 | Op::Return1) {
if matches!(op, Op::Return1) && (rop.inst.a() as usize) >= max_stack {
checkpoint("bail:cmp-dirs-Return1-a-oob");
return None;
}
continue;
}
if !is_whitelisted_step5(op) {
checkpoint("bail:cmp-dirs-op-not-whitelisted");
return None;
}
if matches!(op, Op::GetTabUp) {
let cx = rop.inst.c() as usize;
if cx >= head_proto.consts.len()
|| !matches!(head_proto.consts[cx], luna_core::runtime::Value::Str(_))
{
checkpoint("bail:cmp-dirs-GetTabUp-key-not-str");
return None;
}
}
if matches!(op, Op::SetField) {
let bx = rop.inst.b() as usize;
if bx >= head_proto.consts.len()
|| !matches!(head_proto.consts[bx], luna_core::runtime::Value::Str(_))
{
return None;
}
}
if matches!(op, Op::GetField) {
let cx = rop.inst.c() as usize;
if cx >= head_proto.consts.len()
|| !matches!(head_proto.consts[cx], luna_core::runtime::Value::Str(_))
{
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
let ins = rop.inst;
let a = ins.a() as usize;
let b = ins.b() as usize;
let c = ins.c() as usize;
match op {
Op::Call => {
unreachable!("Op::Call only appears at effective_end (truncation guarded above)")
}
Op::ForLoop => {
unreachable!("Op::ForLoop only appears at effective_end (loop-end guarded above)")
}
Op::TForLoop => unreachable!(
"Op::TForLoop only appears at effective_end (close-on-back-edge guarded above)"
),
Op::TForPrep => {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
Op::TForCall => {
if rop.inline_depth > 0 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if a + 6 >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let nvars = ins.c();
if nvars == 0 || nvars > 250 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Concat => {
if rop.inline_depth > 0 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let n_operands = ins.b() as usize;
if n_operands < 2 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
match a.checked_add(n_operands) {
Some(end) if end <= max_stack => {}
_ => return None,
}
}
Op::GetTabUp => {
if a >= max_stack {
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
Op::SetField | Op::GetField => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if matches!(op, Op::SetField) && c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if matches!(op, Op::GetField) && b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Jmp => {
}
Op::Move => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::LoadI => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::LoadF => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::LoadNil => {
match a.checked_add(b) {
Some(end) if end < max_stack => {}
_ => return None,
}
}
Op::Close => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if rop.inline_depth > 0 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Closure => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if rop.inline_depth > 0 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let bx = ins.bx() as usize;
if bx >= head_proto.protos.len() {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let inner = head_proto.protos[bx];
for d in inner.upvals.iter() {
if !d.in_stack {
continue;
}
let src_idx = d.index as usize;
if src_idx >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
}
Op::LoadK => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let bx = ins.bx() as usize;
if bx >= head_proto.consts.len() {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if !matches!(
head_proto.consts[bx],
luna_core::runtime::Value::Int(_) | luna_core::runtime::Value::Float(_)
) {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Pow => {
if a >= max_stack || b >= max_stack || c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::IDiv | Op::Mod | Op::BAnd | Op::BOr | Op::BXor | Op::Shl | Op::Shr => {
if a >= max_stack || b >= max_stack || c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Unm | Op::BNot => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::EqK => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let bx = ins.b() as usize;
if bx >= head_proto.consts.len() {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if !matches!(
head_proto.consts[bx],
luna_core::runtime::Value::Int(_) | luna_core::runtime::Value::Float(_)
) {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if i + 1 >= effective_end {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let next = &record.ops[i + 1];
if !matches!(next.inst.op(), Op::Jmp) || next.pc != rop.pc + 1 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
consumed_by_cmp[i + 1] = true;
}
Op::Test => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if i + 1 >= effective_end {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let next = &record.ops[i + 1];
let took_jmp = matches!(next.inst.op(), Op::Jmp) && next.pc == rop.pc + 1;
let skipped_jmp = next.pc == rop.pc + 2;
if took_jmp {
consumed_by_cmp[i + 1] = true;
cmp_dirs[i] = Some(CmpDir::TookJmp);
} else if skipped_jmp {
let slot = (rop.pc + 1) as usize;
let jmp_inst = head_proto.code.get(slot).copied();
if !jmp_inst.is_some_and(|x| matches!(x.op(), Op::Jmp)) {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
cmp_dirs[i] = Some(CmpDir::SkippedJmp);
} else {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::TestSet => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if i + 1 >= effective_end {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
let next = &record.ops[i + 1];
let took_jmp = matches!(next.inst.op(), Op::Jmp) && next.pc == rop.pc + 1;
let skipped_jmp = next.pc == rop.pc + 2;
if took_jmp {
consumed_by_cmp[i + 1] = true;
cmp_dirs[i] = Some(CmpDir::TookJmp);
} else if skipped_jmp {
let slot = (rop.pc + 1) as usize;
let jmp_inst = head_proto.code.get(slot).copied();
if !jmp_inst.is_some_and(|x| matches!(x.op(), Op::Jmp)) {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
cmp_dirs[i] = Some(CmpDir::SkippedJmp);
} else {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Lt | Op::Le | Op::Eq => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-ab-oob");
return None;
}
}
if i + 1 >= record.ops.len() {
{
checkpoint("bail:cmp-at-record-end");
return None;
}
}
let next = &record.ops[i + 1];
let took_jmp = matches!(next.inst.op(), Op::Jmp) && next.pc == rop.pc + 1;
let skipped_jmp = next.pc == rop.pc + 2;
if took_jmp {
if i + 1 < effective_end {
consumed_by_cmp[i + 1] = true;
}
cmp_dirs[i] = Some(CmpDir::TookJmp);
} else if skipped_jmp {
let slot = (rop.pc + 1) as usize;
let jmp_inst = head_proto.code.get(slot).copied();
if !jmp_inst.is_some_and(|x| matches!(x.op(), Op::Jmp)) {
{
checkpoint("bail:cmp-skipped-but-no-jmp-slot");
return None;
}
}
cmp_dirs[i] = Some(CmpDir::SkippedJmp);
} else {
{
checkpoint("bail:cmp-next-pc-mismatch");
return None;
}
}
}
Op::NewTable => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::GetI => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::GetTable => {
if a >= max_stack || b >= max_stack || c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::SetI => {
if a >= max_stack || c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::SetTable => {
if ins.k() {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if a >= max_stack || b >= max_stack || c >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::SetList => {
if ins.k() || ins.b() == 0 {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if a >= max_stack || a + ins.b() as usize >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::Len => {
if a >= max_stack || b >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
Op::GetUpval => {
if a >= max_stack {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
if b >= head_proto.upvals.len() {
{
checkpoint("bail:cmp-dirs-body-other");
return None;
}
}
}
_ => unreachable!("whitelist gated above"),
}
}
for (i, rop) in record.ops[..effective_end].iter().enumerate() {
if matches!(rop.inst.op(), Op::Jmp) && !consumed_by_cmp[i] && i + 1 != effective_end {
return None;
}
}
if let Some(call_idx) = call_idx_opt {
let rop = &record.ops[call_idx];
debug_assert_eq!(rop.inline_depth, 0, "TraceEnd::Call only at depth 0");
if !std::ptr::eq(rop.proto.as_ptr(), head_proto.as_ptr()) {
return None;
}
let a = rop.inst.a() as usize;
if a >= max_stack {
return None;
}
}
if let Some(return_idx) = return_idx_opt {
let rop = &record.ops[return_idx];
debug_assert_eq!(rop.inline_depth, 0, "TraceEnd::Return only at depth 0");
if !std::ptr::eq(rop.proto.as_ptr(), head_proto.as_ptr()) {
return None;
}
if matches!(rop.inst.op(), Op::Return1) {
let a = rop.inst.a() as usize;
if a >= max_stack {
return None;
}
}
}
if let Some(for_loop_idx) = for_loop_idx_opt {
let rop = &record.ops[for_loop_idx];
debug_assert_eq!(rop.inline_depth, 0, "TraceEnd::ForLoop only at depth 0");
if !std::ptr::eq(rop.proto.as_ptr(), head_proto.as_ptr()) {
return None;
}
let a = rop.inst.a() as usize;
match rop.inst.op() {
Op::ForLoop => {
if opts.pre53 {
return None;
}
if a + 3 >= max_stack {
return None;
}
if a < record.entry_tags.len()
&& record.entry_tags[a] == luna_core::runtime::value::raw::FLOAT
{
return None;
}
}
Op::TForLoop => {
if a + 4 >= max_stack {
return None;
}
}
_ => unreachable!("for_loop_idx_opt only set for Op::ForLoop / Op::TForLoop"),
}
}
let mut new_table_sig = module.make_signature();
new_table_sig.returns.push(AbiParam::new(types::I64));
let new_table_id = module
.declare_function("luna_jit_new_table", Linkage::Import, &new_table_sig)
.ok()?;
let mut set_int_sig = module.make_signature();
set_int_sig.params.push(AbiParam::new(types::I64));
set_int_sig.params.push(AbiParam::new(types::I64));
set_int_sig.params.push(AbiParam::new(types::I64));
let set_int_id = module
.declare_function("luna_jit_table_set_int", Linkage::Import, &set_int_sig)
.ok()?;
let mut set_nil_sig = module.make_signature();
set_nil_sig.params.push(AbiParam::new(types::I64));
set_nil_sig.params.push(AbiParam::new(types::I64));
let set_nil_id = module
.declare_function("luna_jit_table_set_nil", Linkage::Import, &set_nil_sig)
.ok()?;
let mut set_raw_sig = module.make_signature();
set_raw_sig.params.push(AbiParam::new(types::I64));
set_raw_sig.params.push(AbiParam::new(types::I64));
set_raw_sig.params.push(AbiParam::new(types::I64));
set_raw_sig.params.push(AbiParam::new(types::I64));
let set_raw_id = module
.declare_function("luna_jit_table_set_raw", Linkage::Import, &set_raw_sig)
.ok()?;
let mut set_field_sig = module.make_signature();
set_field_sig.params.push(AbiParam::new(types::I64));
set_field_sig.params.push(AbiParam::new(types::I64));
set_field_sig.params.push(AbiParam::new(types::I64));
set_field_sig.params.push(AbiParam::new(types::I64));
let set_field_id = module
.declare_function("luna_jit_table_set_field", Linkage::Import, &set_field_sig)
.ok()?;
let mut get_field_sig = module.make_signature();
get_field_sig.params.push(AbiParam::new(types::I64));
get_field_sig.params.push(AbiParam::new(types::I64));
get_field_sig.returns.push(AbiParam::new(types::I64));
let get_field_id = module
.declare_function("luna_jit_table_get_field", Linkage::Import, &get_field_sig)
.ok()?;
let mut get_tab_up_sig = module.make_signature();
get_tab_up_sig.params.push(AbiParam::new(types::I64));
get_tab_up_sig.params.push(AbiParam::new(types::I64));
get_tab_up_sig.returns.push(AbiParam::new(types::I64));
let get_tab_up_id = module
.declare_function("luna_jit_op_get_tab_up", Linkage::Import, &get_tab_up_sig)
.ok()?;
let mut op_closure_sig = module.make_signature();
op_closure_sig.params.push(AbiParam::new(types::I64));
op_closure_sig.returns.push(AbiParam::new(types::I64));
let op_closure_id = module
.declare_function("luna_jit_op_closure", Linkage::Import, &op_closure_sig)
.ok()?;
let mut spill_sig = module.make_signature();
spill_sig.params.push(AbiParam::new(types::I64));
spill_sig.params.push(AbiParam::new(types::I64));
spill_sig.params.push(AbiParam::new(types::I64));
let spill_id = module
.declare_function("luna_jit_spill_to_stack", Linkage::Import, &spill_sig)
.ok()?;
let mut op_close_sig = module.make_signature();
op_close_sig.params.push(AbiParam::new(types::I64));
op_close_sig.returns.push(AbiParam::new(types::I64));
let op_close_id = module
.declare_function("luna_jit_op_close", Linkage::Import, &op_close_sig)
.ok()?;
let mut op_tforcall_sig = module.make_signature();
op_tforcall_sig.params.push(AbiParam::new(types::I64));
op_tforcall_sig.params.push(AbiParam::new(types::I64));
op_tforcall_sig.params.push(AbiParam::new(types::I64));
op_tforcall_sig.params.push(AbiParam::new(types::I64));
op_tforcall_sig.params.push(AbiParam::new(types::I64));
op_tforcall_sig.returns.push(AbiParam::new(types::I64));
let op_tforcall_id = module
.declare_function("luna_jit_op_tforcall", Linkage::Import, &op_tforcall_sig)
.ok()?;
let mut stack_load_sig = module.make_signature();
stack_load_sig.params.push(AbiParam::new(types::I64));
stack_load_sig.returns.push(AbiParam::new(types::I64));
let stack_load_id = module
.declare_function("luna_jit_stack_load", Linkage::Import, &stack_load_sig)
.ok()?;
let mut stack_tag_sig = module.make_signature();
stack_tag_sig.params.push(AbiParam::new(types::I64));
stack_tag_sig.returns.push(AbiParam::new(types::I64));
let stack_tag_id = module
.declare_function("luna_jit_stack_tag", Linkage::Import, &stack_tag_sig)
.ok()?;
let mut op_concat_sig = module.make_signature();
op_concat_sig.params.push(AbiParam::new(types::I64));
op_concat_sig.params.push(AbiParam::new(types::I64));
op_concat_sig.returns.push(AbiParam::new(types::I64));
let op_concat_id = module
.declare_function("luna_jit_op_concat", Linkage::Import, &op_concat_sig)
.ok()?;
let mut str_buf_acquire_sig = module.make_signature();
str_buf_acquire_sig.returns.push(AbiParam::new(types::I64));
let str_buf_acquire_id = module
.declare_function(
"luna_jit_str_buf_acquire",
Linkage::Import,
&str_buf_acquire_sig,
)
.ok()?;
let mut str_buf_release_sig = module.make_signature();
str_buf_release_sig.params.push(AbiParam::new(types::I64));
let str_buf_release_id = module
.declare_function(
"luna_jit_str_buf_release",
Linkage::Import,
&str_buf_release_sig,
)
.ok()?;
let mut str_buf_extend_sig = module.make_signature();
str_buf_extend_sig.params.push(AbiParam::new(types::I64));
str_buf_extend_sig.params.push(AbiParam::new(types::I64));
str_buf_extend_sig.returns.push(AbiParam::new(types::I64));
let str_buf_extend_id = module
.declare_function(
"luna_jit_str_buf_extend",
Linkage::Import,
&str_buf_extend_sig,
)
.ok()?;
let mut str_buf_intern_sig = module.make_signature();
str_buf_intern_sig.params.push(AbiParam::new(types::I64));
str_buf_intern_sig.returns.push(AbiParam::new(types::I64));
let str_buf_intern_id = module
.declare_function(
"luna_jit_str_buf_intern",
Linkage::Import,
&str_buf_intern_sig,
)
.ok()?;
let _ = (
str_buf_acquire_id,
str_buf_release_id,
str_buf_extend_id,
str_buf_intern_id,
);
let mut update_raw_sig = module.make_signature();
update_raw_sig.params.push(AbiParam::new(types::I64));
update_raw_sig.params.push(AbiParam::new(types::I64));
let update_raw_id = module
.declare_function(
"luna_jit_stack_update_raw",
Linkage::Import,
&update_raw_sig,
)
.ok()?;
let mut get_int_sig = module.make_signature();
get_int_sig.params.push(AbiParam::new(types::I64));
get_int_sig.params.push(AbiParam::new(types::I64));
get_int_sig.returns.push(AbiParam::new(types::I64));
let get_int_id = module
.declare_function("luna_jit_table_get_int", Linkage::Import, &get_int_sig)
.ok()?;
let mut len_sig = module.make_signature();
len_sig.params.push(AbiParam::new(types::I64));
len_sig.returns.push(AbiParam::new(types::I64));
let len_id = module
.declare_function("luna_jit_table_len", Linkage::Import, &len_sig)
.ok()?;
let mut upval_get_sig = module.make_signature();
upval_get_sig.params.push(AbiParam::new(types::I64));
upval_get_sig.returns.push(AbiParam::new(types::I64));
let upval_get_id = module
.declare_function("luna_jit_upval_get", Linkage::Import, &upval_get_sig)
.ok()?;
let mut materialize_sig = module.make_signature();
materialize_sig.params.push(AbiParam::new(types::I64));
materialize_sig.params.push(AbiParam::new(types::I64));
materialize_sig.returns.push(AbiParam::new(types::I64));
let materialize_id = module
.declare_function(
"luna_jit_trace_materialize_frames",
Linkage::Import,
&materialize_sig,
)
.ok()?;
let mut mat_sunk_sig = module.make_signature();
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.params.push(AbiParam::new(types::I64));
mat_sunk_sig.returns.push(AbiParam::new(types::I64));
let mat_sunk_id = module
.declare_function(
"luna_jit_materialize_sunk_table",
Linkage::Import,
&mat_sunk_sig,
)
.ok()?;
let mut sig = module.make_signature();
sig.params.push(AbiParam::new(types::I64));
sig.returns.push(AbiParam::new(types::I64));
let (trace_fn_name, trace_fn_linkage) = match aot_fn_name {
Some(name) => (name, Linkage::Export),
None => ("luna_jit_trace", Linkage::Local),
};
let fn_id = module
.declare_function(trace_fn_name, trace_fn_linkage, &sig)
.ok()?;
let mut ctx = module.make_context();
ctx.func.signature = sig;
ctx.func.name = UserFuncName::user(0, fn_id.as_u32());
let mut fbc = FunctionBuilderContext::new();
let mut bcx = FunctionBuilder::new(&mut ctx.func, &mut fbc);
let entry = bcx.create_block();
bcx.append_block_params_for_function_params(entry);
bcx.switch_to_block(entry);
bcx.seal_block(entry);
let reg_state = bcx.block_params(entry)[0];
let trace_fn_sig_ref: cranelift_codegen::ir::SigRef = {
let mut sig = module.make_signature();
sig.params.push(AbiParam::new(types::I64));
sig.returns.push(AbiParam::new(types::I64));
bcx.func.import_signature(sig)
};
let global_side_trace_box: Box<TCellPtr> = Box::new(TCellPtr::null());
let _global_side_trace_cell_addr = (&*global_side_trace_box) as *const TCellPtr as i64;
let mut regs_full: Vec<Variable> = Vec::with_capacity(window_size_us);
for i in 0..window_size_us {
let v = bcx.declare_var(types::I64);
if i < max_stack {
let offset = (i as i32) * 8;
let v0 = bcx
.ins()
.load(types::I64, MemFlags::new(), reg_state, offset);
bcx.def_var(v, v0);
} else {
let z = bcx.ins().iconst(types::I64, 0);
bcx.def_var(v, z);
}
regs_full.push(v);
}
let tforcall_tag_var = bcx.declare_var(types::I64);
{
let z = bcx.ins().iconst(types::I64, 0);
bcx.def_var(tforcall_tag_var, z);
}
let base_var = bcx.declare_var(types::I64);
{
let z = bcx.ins().iconst(types::I64, 0);
bcx.def_var(base_var, z);
BASE_VAR_SCAFFOLD_DECLARED.with(|c| c.set(c.get().wrapping_add(1)));
}
const MAX_SUNK_CAP: u32 = 8;
let return_a_for_sunk_check: Option<u32> = match end_idx_opt {
Some((idx, TraceEnd::Return)) if idx < record.ops.len() => {
let term = &record.ops[idx];
if matches!(term.inst.op(), Op::Return1) && term.inline_depth == 0 {
Some(term.inst.a())
} else {
None
}
}
_ => None,
};
let mut virt_vars: Vec<Option<Vec<Variable>>> = vec![None; escape.sites.len()];
let mut virt_kinds: Vec<Option<Vec<RegKind>>> = vec![None; escape.sites.len()];
let mut sunk_alloc_seen: u32 = 0;
let mut materialize_emit_count: u32 = 0;
let mut closure_seen: u32 = 0;
for (idx, site) in escape.sites.iter_mut().enumerate() {
if site.state != EscapeState::Sinkable {
continue;
}
let array_cap = site.array_cap as usize;
let n_hash = site.hash_keys.len();
let total_slots = array_cap + n_hash;
if total_slots == 0
|| array_cap > MAX_SUNK_CAP as usize
|| (site.inline_depth == 0 && return_a_for_sunk_check == Some(site.a))
{
site.state = EscapeState::Escaped;
continue;
}
let mut vars = Vec::with_capacity(total_slots);
for _ in 0..total_slots {
let v = bcx.declare_var(types::I64);
let z = bcx.ins().iconst(types::I64, 0);
bcx.def_var(v, z);
vars.push(v);
}
virt_vars[idx] = Some(vars);
virt_kinds[idx] = Some(vec![RegKind::Unset; total_slots]);
sunk_alloc_seen += 1;
}
if let Some(ref ba) = active_accum {
let buf_var = bcx.declare_var(types::I64);
let acquire_ref = module.declare_func_in_func(str_buf_acquire_id, bcx.func);
let intern_ref = module.declare_func_in_func(str_buf_intern_id, bcx.func);
let release_ref = module.declare_func_in_func(str_buf_release_id, bcx.func);
let extend_ref = module.declare_func_in_func(str_buf_extend_id, bcx.func);
let call_inst = bcx.ins().call(acquire_ref, &[]);
let ptr = bcx.inst_results(call_inst)[0];
bcx.def_var(buf_var, ptr);
let accum_raw = bcx.use_var(regs_full[ba.accum_slot as usize]);
let buf_ptr = bcx.use_var(buf_var);
let _ = bcx.ins().call(extend_ref, &[buf_ptr, accum_raw]);
flush_ctx = Some(FlushCtx {
buf_var,
accum_slot: ba.accum_slot,
intern_ref,
release_ref,
});
}
let body_loop = bcx.create_block();
bcx.ins().jump(body_loop, &[]);
bcx.switch_to_block(body_loop);
let mut current_kinds: Vec<RegKind> = (0..window_size_us)
.map(|i| {
if i < max_stack && i < record.entry_tags.len() {
RegKind::from_entry_tag(record.entry_tags[i]).unwrap_or(RegKind::Unset)
} else {
RegKind::Unset
}
})
.collect();
let mut dispatchable: bool = true;
let mut dispatch_off_reason: Option<&'static str> = None;
let mut per_exit_kinds: Vec<(u32, Vec<RegKind>, Box<TCellPtr>)> = Vec::new();
let mut per_exit_inline_vec: Vec<(
u32,
u32,
Vec<RegKind>,
TArc<[FrameMaterializeInfo]>,
Box<TCellPtr>,
)> = Vec::new();
let mut call_chain: Vec<FrameMaterializeInfo> = Vec::new();
let mut upval_cache: std::collections::HashMap<u32, Variable> =
std::collections::HashMap::new();
checkpoint("pre:main-emit-loop");
for (i, rop) in record.ops[..effective_end].iter().enumerate() {
let off = op_offsets[i] as usize;
let regs: &[Variable] = ®s_full[off..off + max_stack];
if let Some(ref ba) = active_accum
&& let Some(ref fctx) = flush_ctx
{
if i == ba.pre1_idx || i == ba.pre2_idx || i == ba.post_idx {
continue;
}
if i == ba.concat_idx {
let piece_raw = bcx.use_var(regs[ba.piece_slot as usize]);
let buf_ptr = bcx.use_var(fctx.buf_var);
let extend_ref = module.declare_func_in_func(str_buf_extend_id, bcx.func);
let call_inst = bcx.ins().call(extend_ref, &[buf_ptr, piece_raw]);
let status = bcx.inst_results(call_inst)[0];
let zero = bcx.ins().iconst(types::I64, 0);
let is_err = bcx.ins().icmp(IntCC::SignedLessThan, status, zero);
let continue_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
bcx.ins().brif(is_err, deopt_blk, &[], continue_blk, &[]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
continue;
}
}
if consumed_by_cmp[i] {
continue;
}
if folded_ops[i] {
let fold = math_folds.iter().find(|f| {
f.start_idx == i
|| (matches!(f.kind, FoldKind::Min2 | FoldKind::Max2)
&& (f.start_idx + 1 == i || f.call_idx == i))
});
if let Some(fold) = fold {
match fold.kind {
FoldKind::Libm1 if fold.start_idx == i => {
let mut libm_sig = module.make_signature();
libm_sig.params.push(AbiParam::new(types::F64));
libm_sig.returns.push(AbiParam::new(types::F64));
let libm_id = module
.declare_function(fold.fn_name, Linkage::Import, &libm_sig)
.ok()?;
let libm_ref = module.declare_func_in_func(libm_id, bcx.func);
let arg_src = fold.arg_src.expect("Libm1 has arg_src");
let FoldArgSrc::Reg { reg: arg_reg } = arg_src;
let arg_kind = k_op(¤t_kinds, off as u32 + arg_reg);
let arg_f64 = if matches!(arg_kind, RegKind::Float) {
use_var_f64(&mut bcx, regs, arg_reg)
} else {
let raw = bcx.use_var(regs[arg_reg as usize]);
bcx.ins().fcvt_from_sint(types::F64, raw)
};
let call = bcx.ins().call(libm_ref, &[arg_f64]);
let r = bcx.inst_results(call)[0];
def_var_f64(&mut bcx, regs[fold.dst_reg as usize], r);
current_kinds[off + fold.dst_reg as usize] = RegKind::Float;
}
FoldKind::Libm1 => {
}
FoldKind::Min2 | FoldKind::Max2 if fold.call_idx == i => {
let k1 = k_op(¤t_kinds, off as u32 + fold.arg1_reg);
let k2 = k_op(¤t_kinds, off as u32 + fold.arg2_reg);
let result_kind = match (k1, k2) {
(RegKind::Float, _) | (_, RegKind::Float) => RegKind::Float,
_ => RegKind::Int,
};
if matches!(result_kind, RegKind::Float) {
let coerce_to_f64 = |bcx: &mut FunctionBuilder<'_>,
regs: &[Variable],
current_kinds: &[RegKind],
off: usize,
reg: u32|
-> Value {
let kind = k_op(current_kinds, off as u32 + reg);
if matches!(kind, RegKind::Float) {
use_var_f64(bcx, regs, reg)
} else {
let raw = bcx.use_var(regs[reg as usize]);
bcx.ins().fcvt_from_sint(types::F64, raw)
}
};
let a1 =
coerce_to_f64(&mut bcx, regs, ¤t_kinds, off, fold.arg1_reg);
let a2 =
coerce_to_f64(&mut bcx, regs, ¤t_kinds, off, fold.arg2_reg);
let r = match fold.kind {
FoldKind::Min2 => bcx.ins().fmin(a1, a2),
FoldKind::Max2 => bcx.ins().fmax(a1, a2),
FoldKind::Libm1 => unreachable!(),
};
def_var_f64(&mut bcx, regs[fold.dst_reg as usize], r);
current_kinds[off + fold.dst_reg as usize] = RegKind::Float;
} else {
let a1 = bcx.use_var(regs[fold.arg1_reg as usize]);
let a2 = bcx.use_var(regs[fold.arg2_reg as usize]);
let r = match fold.kind {
FoldKind::Min2 => bcx.ins().smin(a1, a2),
FoldKind::Max2 => bcx.ins().smax(a1, a2),
FoldKind::Libm1 => unreachable!(),
};
bcx.def_var(regs[fold.dst_reg as usize], r);
current_kinds[off + fold.dst_reg as usize] = RegKind::Int;
}
}
FoldKind::Min2 | FoldKind::Max2 => {
}
}
}
continue;
}
let ins = rop.inst;
let op = ins.op();
match op {
Op::Jmp => {
}
Op::Move => {
let src = bcx.use_var(regs[ins.b() as usize]);
bcx.def_var(regs[ins.a() as usize], src);
current_kinds[off + ins.a() as usize] = k_op(¤t_kinds, off as u32 + ins.b());
}
Op::LoadI => {
let imm = ins.sbx() as i64;
let v = bcx.ins().iconst(types::I64, imm);
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = RegKind::Int;
}
Op::LoadF => {
let f = ins.sbx() as f64;
let v = bcx.ins().f64const(f);
def_var_f64(&mut bcx, regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = RegKind::Float;
}
Op::LoadNil => {
let a_us = ins.a() as usize;
let b_us = ins.b() as usize;
let zero = bcx.ins().iconst(types::I64, 0);
for k in 0..=b_us {
bcx.def_var(regs[a_us + k], zero);
current_kinds[off + a_us + k] = RegKind::Nil;
}
}
Op::LoadK => {
let bx = ins.bx() as usize;
let (v, k) = match head_proto.consts[bx] {
luna_core::runtime::Value::Int(n) => {
(bcx.ins().iconst(types::I64, n), RegKind::Int)
}
luna_core::runtime::Value::Float(f) => {
let fv = bcx.ins().f64const(f);
let bits = bcx.ins().bitcast(types::I64, MemFlags::new(), fv);
(bits, RegKind::Float)
}
_ => unreachable!("pre-emit gates Int / Float consts"),
};
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = k;
}
Op::Add | Op::Sub | Op::Mul | Op::Div | Op::Pow => {
let kb = k_op(¤t_kinds, off as u32 + ins.b());
let kc = k_op(¤t_kinds, off as u32 + ins.c());
if matches!(op, Op::Pow) {
let lhs = match kb {
RegKind::Float => use_var_f64(&mut bcx, regs, ins.b()),
_ => {
let raw = bcx.use_var(regs[ins.b() as usize]);
bcx.ins().fcvt_from_sint(types::F64, raw)
}
};
let rhs = match kc {
RegKind::Float => use_var_f64(&mut bcx, regs, ins.c()),
_ => {
let raw = bcx.use_var(regs[ins.c() as usize]);
bcx.ins().fcvt_from_sint(types::F64, raw)
}
};
let mut pow_sig = module.make_signature();
pow_sig.params.push(AbiParam::new(types::F64));
pow_sig.params.push(AbiParam::new(types::F64));
pow_sig.returns.push(AbiParam::new(types::F64));
let pow_id = module
.declare_function("pow", Linkage::Import, &pow_sig)
.ok()?;
let pow_ref = module.declare_func_in_func(pow_id, bcx.func);
let call = bcx.ins().call(pow_ref, &[lhs, rhs]);
let r = bcx.inst_results(call)[0];
def_var_f64(&mut bcx, regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Float;
continue;
}
let float_path = matches!(kb, RegKind::Float) || matches!(kc, RegKind::Float);
if float_path {
if !matches!(kb, RegKind::Float) || !matches!(kc, RegKind::Float) {
return None;
}
let lhs = use_var_f64(&mut bcx, regs, ins.b());
let rhs = use_var_f64(&mut bcx, regs, ins.c());
let r = match op {
Op::Add => bcx.ins().fadd(lhs, rhs),
Op::Sub => bcx.ins().fsub(lhs, rhs),
Op::Mul => bcx.ins().fmul(lhs, rhs),
Op::Div => bcx.ins().fdiv(lhs, rhs),
_ => unreachable!(),
};
def_var_f64(&mut bcx, regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Float;
} else {
if matches!(op, Op::Div) {
return None;
}
let lhs = bcx.use_var(regs[ins.b() as usize]);
let rhs = bcx.use_var(regs[ins.c() as usize]);
let r = match op {
Op::Add => bcx.ins().iadd(lhs, rhs),
Op::Sub => bcx.ins().isub(lhs, rhs),
Op::Mul => bcx.ins().imul(lhs, rhs),
_ => unreachable!(),
};
bcx.def_var(regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Int;
}
}
Op::IDiv | Op::Mod | Op::BAnd | Op::BOr | Op::BXor | Op::Shl | Op::Shr => {
let kb = k_op(¤t_kinds, off as u32 + ins.b());
let kc = k_op(¤t_kinds, off as u32 + ins.c());
if matches!(kb, RegKind::Float) || matches!(kc, RegKind::Float) {
return None;
}
let lhs = bcx.use_var(regs[ins.b() as usize]);
let rhs = bcx.use_var(regs[ins.c() as usize]);
let r = match op {
Op::IDiv => bcx.ins().sdiv(lhs, rhs),
Op::Mod => bcx.ins().srem(lhs, rhs),
Op::BAnd => bcx.ins().band(lhs, rhs),
Op::BOr => bcx.ins().bor(lhs, rhs),
Op::BXor => bcx.ins().bxor(lhs, rhs),
Op::Shl => bcx.ins().ishl(lhs, rhs),
Op::Shr => bcx.ins().ushr(lhs, rhs),
_ => unreachable!("whitelist gated above"),
};
bcx.def_var(regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Int;
}
Op::Unm | Op::BNot => {
let kb = k_op(¤t_kinds, off as u32 + ins.b());
if matches!(op, Op::BNot) && matches!(kb, RegKind::Float) {
return None;
}
if matches!(op, Op::Unm) && matches!(kb, RegKind::Float) {
let src = use_var_f64(&mut bcx, regs, ins.b());
let r = bcx.ins().fneg(src);
def_var_f64(&mut bcx, regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Float;
} else {
let src = bcx.use_var(regs[ins.b() as usize]);
let r = match op {
Op::Unm => bcx.ins().ineg(src),
Op::BNot => bcx.ins().bnot(src),
_ => unreachable!("whitelist gated above"),
};
bcx.def_var(regs[ins.a() as usize], r);
current_kinds[off + ins.a() as usize] = RegKind::Int;
}
}
Op::EqK => {
let bx = ins.b() as usize;
let ka = k_op(¤t_kinds, off as u32 + ins.a());
let cond = match head_proto.consts[bx] {
luna_core::runtime::Value::Int(n) => {
if matches!(ka, RegKind::Float) {
return None;
}
let lhs = bcx.use_var(regs[ins.a() as usize]);
let rhs = bcx.ins().iconst(types::I64, n);
let int_cc = if ins.k() {
IntCC::Equal
} else {
IntCC::NotEqual
};
bcx.ins().icmp(int_cc, lhs, rhs)
}
luna_core::runtime::Value::Float(f) => {
if !matches!(ka, RegKind::Float) {
return None;
}
let lhs = use_var_f64(&mut bcx, regs, ins.a());
let rhs = bcx.ins().f64const(f);
let float_cc = if ins.k() {
FloatCC::Equal
} else {
FloatCC::NotEqual
};
bcx.ins().fcmp(float_cc, lhs, rhs)
}
_ => unreachable!("pre-emit gates Int / Float const only"),
};
let continue_blk = bcx.create_block();
let side_exit_blk = bcx.create_block();
bcx.ins().brif(cond, continue_blk, &[], side_exit_blk, &[]);
bcx.switch_to_block(side_exit_blk);
bcx.seal_block(side_exit_blk);
let side_exit_pc = rop.pc + 2;
if !call_chain.is_empty() {
let head_resume_pc = call_chain[0].pc;
let mut snapshot: Vec<FrameMaterializeInfo> = call_chain.clone();
if let Some(last) = snapshot.last_mut() {
last.pc = side_exit_pc;
}
let chain_rc: TArc<[FrameMaterializeInfo]> = snapshot.into();
let chain_ptr = TArc::as_ptr(&chain_rc) as *const FrameMaterializeInfo as i64;
let chain_len = chain_rc.len() as i64;
let site_idx = per_exit_inline_vec.len() as u32;
let mut kinds_snapshot: Vec<RegKind> = current_kinds.clone();
let mat_count = emit_materialize_live_sunk(
&mut bcx,
&mut module,
mat_sunk_id,
&escape,
&virt_vars,
&virt_kinds,
®s_full,
&op_offsets,
i,
&mut kinds_snapshot,
head_proto,
opts.aot,
&mut defined_aot_data,
);
materialize_emit_count += mat_count;
let inline_side_box_0: Box<TCellPtr> = Box::new(TCellPtr::null());
let _inline_side_cell_addr_0 = (&*inline_side_box_0) as *const TCellPtr as i64;
let chain_for_helper = chain_rc.clone();
per_exit_inline_vec.push((
side_exit_pc,
head_resume_pc,
kinds_snapshot,
chain_rc,
inline_side_box_0,
));
let n_arg = bcx.ins().iconst(types::I64, chain_len);
let ptr_arg = emit_chain_ptr_arg(
&mut module,
&mut bcx,
&chain_for_helper,
chain_ptr,
opts.aot,
&mut defined_aot_data,
);
let mat_ref = module.declare_func_in_func(materialize_id, bcx.func);
let _ = bcx.ins().call(mat_ref, &[n_arg, ptr_arg]);
emit_store_back_and_return_site(
&mut bcx,
®s_full[..window_size_us],
reg_state,
site_idx,
side_exit_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
);
} else {
let mut snapshot: Vec<RegKind> = current_kinds[..max_stack].to_vec();
let mat_count = emit_materialize_live_sunk(
&mut bcx,
&mut module,
mat_sunk_id,
&escape,
&virt_vars,
&virt_kinds,
®s_full,
&op_offsets,
i,
&mut snapshot,
head_proto,
opts.aot,
&mut defined_aot_data,
);
materialize_emit_count += mat_count;
let tag_side_box_0: Box<TCellPtr> = Box::new(TCellPtr::null());
let _tag_side_cell_addr_0 = (&*tag_side_box_0) as *const TCellPtr as i64;
let tag_side_local_0 = per_exit_kinds.len() as u32;
per_exit_kinds.push((side_exit_pc, snapshot, tag_side_box_0));
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
side_exit_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_TAG, tag_side_local_0),
);
}
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
}
Op::Test => {
let a_kind = k_op(¤t_kinds, off as u32 + ins.a());
let truthy_known: Option<bool> = match a_kind {
RegKind::Int
| RegKind::Float
| RegKind::Table
| RegKind::Closure
| RegKind::Str => Some(true),
RegKind::Nil => Some(false),
RegKind::Unset => None,
};
let k_bit = ins.k();
let recorded_passed = matches!(cmp_dirs[i], Some(CmpDir::SkippedJmp));
if let Some(truthy) = truthy_known {
let test_passed = truthy != k_bit;
if test_passed != recorded_passed {
return None;
}
} else {
let slot_arg = bcx.ins().iconst(types::I64, ins.a() as i64);
let stack_tag_ref = module.declare_func_in_func(stack_tag_id, bcx.func);
let tag_call = bcx.ins().call(stack_tag_ref, &[slot_arg]);
let tag = bcx.inst_results(tag_call)[0];
let one = bcx.ins().iconst(types::I64, 1);
let is_truthy = bcx.ins().icmp(IntCC::UnsignedGreaterThan, tag, one);
let not_truthy = bcx.ins().bxor_imm(is_truthy, 1);
let k_bit_const = bcx.ins().iconst(types::I8, k_bit as i64);
let test_passed_runtime = bcx.ins().icmp(IntCC::Equal, not_truthy, k_bit_const);
let recorded_const = bcx.ins().iconst(types::I8, recorded_passed as i64);
let ok = bcx
.ins()
.icmp(IntCC::Equal, test_passed_runtime, recorded_const);
let cont = bcx.create_block();
let deopt = bcx.create_block();
bcx.ins().brif(ok, cont, &[], deopt, &[]);
bcx.switch_to_block(deopt);
bcx.seal_block(deopt);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(cont);
bcx.seal_block(cont);
}
}
Op::TestSet => {
let b_kind = k_op(¤t_kinds, off as u32 + ins.b());
let truthy_known: Option<bool> = match b_kind {
RegKind::Int
| RegKind::Float
| RegKind::Table
| RegKind::Closure
| RegKind::Str => Some(true),
RegKind::Nil => Some(false),
RegKind::Unset => None,
};
let k_bit = ins.k();
let recorded_passed = matches!(cmp_dirs[i], Some(CmpDir::TookJmp));
if let Some(truthy) = truthy_known {
let test_passed = truthy == k_bit;
if test_passed != recorded_passed {
return None;
}
if test_passed {
let v = bcx.use_var(regs[ins.b() as usize]);
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = b_kind;
}
} else {
let slot_arg = bcx.ins().iconst(types::I64, ins.b() as i64);
let stack_tag_ref = module.declare_func_in_func(stack_tag_id, bcx.func);
let tag_call = bcx.ins().call(stack_tag_ref, &[slot_arg]);
let tag = bcx.inst_results(tag_call)[0];
let one = bcx.ins().iconst(types::I64, 1);
let is_truthy = bcx.ins().icmp(IntCC::UnsignedGreaterThan, tag, one);
let k_bit_const = bcx.ins().iconst(types::I8, k_bit as i64);
let test_passed_runtime = bcx.ins().icmp(IntCC::Equal, is_truthy, k_bit_const);
let recorded_const = bcx.ins().iconst(types::I8, recorded_passed as i64);
let ok = bcx
.ins()
.icmp(IntCC::Equal, test_passed_runtime, recorded_const);
let cont = bcx.create_block();
let deopt = bcx.create_block();
bcx.ins().brif(ok, cont, &[], deopt, &[]);
bcx.switch_to_block(deopt);
bcx.seal_block(deopt);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(cont);
bcx.seal_block(cont);
if recorded_passed {
let v = bcx.use_var(regs[ins.b() as usize]);
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = RegKind::Unset;
}
}
}
Op::Lt | Op::Le | Op::Eq => {
let dir = cmp_dirs[i].expect("cmp dir set in pre-emit");
let invert = matches!(dir, CmpDir::SkippedJmp);
let k_effective = if invert { !ins.k() } else { ins.k() };
let ka = k_op(¤t_kinds, off as u32 + ins.a());
let kb = k_op(¤t_kinds, off as u32 + ins.b());
let float_path = matches!(ka, RegKind::Float) || matches!(kb, RegKind::Float);
let cond = if float_path {
if !matches!(ka, RegKind::Float) || !matches!(kb, RegKind::Float) {
return None;
}
let lhs = use_var_f64(&mut bcx, regs, ins.a());
let rhs = use_var_f64(&mut bcx, regs, ins.b());
let float_cc = match (op, k_effective) {
(Op::Lt, true) => FloatCC::LessThan,
(Op::Lt, false) => FloatCC::GreaterThanOrEqual,
(Op::Le, true) => FloatCC::LessThanOrEqual,
(Op::Le, false) => FloatCC::GreaterThan,
(Op::Eq, true) => FloatCC::Equal,
(Op::Eq, false) => FloatCC::NotEqual,
_ => unreachable!("whitelist gated above"),
};
bcx.ins().fcmp(float_cc, lhs, rhs)
} else {
let lhs = bcx.use_var(regs[ins.a() as usize]);
let rhs = bcx.use_var(regs[ins.b() as usize]);
let int_cc = match (op, k_effective) {
(Op::Lt, true) => IntCC::SignedLessThan,
(Op::Lt, false) => IntCC::SignedGreaterThanOrEqual,
(Op::Le, true) => IntCC::SignedLessThanOrEqual,
(Op::Le, false) => IntCC::SignedGreaterThan,
(Op::Eq, true) => IntCC::Equal,
(Op::Eq, false) => IntCC::NotEqual,
_ => unreachable!("whitelist gated above"),
};
bcx.ins().icmp(int_cc, lhs, rhs)
};
let continue_blk = bcx.create_block();
let side_exit_blk = bcx.create_block();
bcx.ins().brif(cond, continue_blk, &[], side_exit_blk, &[]);
let side_exit_pc: u32 = match dir {
CmpDir::TookJmp => rop.pc + 2,
CmpDir::SkippedJmp => {
let jmp_pc = (rop.pc + 1) as usize;
let jmp_inst = head_proto.code[jmp_pc];
let pc_after_jmp = (rop.pc as i64) + 2;
(pc_after_jmp + jmp_inst.sj() as i64) as u32
}
};
bcx.switch_to_block(side_exit_blk);
bcx.seal_block(side_exit_blk);
if !call_chain.is_empty() {
let head_resume_pc = call_chain[0].pc;
let mut snapshot: Vec<FrameMaterializeInfo> = call_chain.clone();
if let Some(last) = snapshot.last_mut() {
last.pc = side_exit_pc;
}
let chain_rc: TArc<[FrameMaterializeInfo]> = snapshot.into();
let chain_ptr = TArc::as_ptr(&chain_rc) as *const FrameMaterializeInfo as i64;
let chain_len = chain_rc.len() as i64;
let site_idx = per_exit_inline_vec.len() as u32;
let mut kinds_snapshot: Vec<RegKind> = current_kinds.clone();
let mat_count = emit_materialize_live_sunk(
&mut bcx,
&mut module,
mat_sunk_id,
&escape,
&virt_vars,
&virt_kinds,
®s_full,
&op_offsets,
i,
&mut kinds_snapshot,
head_proto,
opts.aot,
&mut defined_aot_data,
);
materialize_emit_count += mat_count;
let inline_side_box_1: Box<TCellPtr> = Box::new(TCellPtr::null());
let _inline_side_cell_addr_1 = (&*inline_side_box_1) as *const TCellPtr as i64;
let chain_for_helper = chain_rc.clone();
per_exit_inline_vec.push((
side_exit_pc,
head_resume_pc,
kinds_snapshot,
chain_rc,
inline_side_box_1,
));
let n_arg = bcx.ins().iconst(types::I64, chain_len);
let ptr_arg = emit_chain_ptr_arg(
&mut module,
&mut bcx,
&chain_for_helper,
chain_ptr,
opts.aot,
&mut defined_aot_data,
);
let mat_ref = module.declare_func_in_func(materialize_id, bcx.func);
let _ = bcx.ins().call(mat_ref, &[n_arg, ptr_arg]);
emit_store_back_and_return_site(
&mut bcx,
®s_full[..window_size_us],
reg_state,
site_idx,
side_exit_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
);
} else {
let mut snapshot: Vec<RegKind> = current_kinds[..max_stack].to_vec();
let mat_count = emit_materialize_live_sunk(
&mut bcx,
&mut module,
mat_sunk_id,
&escape,
&virt_vars,
&virt_kinds,
®s_full,
&op_offsets,
i,
&mut snapshot,
head_proto,
opts.aot,
&mut defined_aot_data,
);
materialize_emit_count += mat_count;
let tag_side_box_1: Box<TCellPtr> = Box::new(TCellPtr::null());
let _tag_side_cell_addr_1 = (&*tag_side_box_1) as *const TCellPtr as i64;
let tag_side_local_1 = per_exit_kinds.len() as u32;
per_exit_kinds.push((side_exit_pc, snapshot, tag_side_box_1));
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
side_exit_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_TAG, tag_side_local_1),
);
}
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
}
Op::NewTable => {
if let Some(OpAction::NewTableSite { site_idx }) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& virt_vars[site_idx as usize].is_some()
{
continue;
}
let func_ref = module.declare_func_in_func(new_table_id, bcx.func);
let call = bcx.ins().call(func_ref, &[]);
let t = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], t);
current_kinds[off + ins.a() as usize] = RegKind::Table;
}
Op::GetI => {
if let Some(OpAction::GetIRead { site_idx, key }) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& let Some(vars) = virt_vars[site_idx as usize].as_ref()
{
let slot = (key as usize) - 1;
let v = bcx.use_var(vars[slot]);
bcx.def_var(regs[ins.a() as usize], v);
let k = virt_kinds[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_kinds")[slot];
current_kinds[off + ins.a() as usize] = k;
continue;
}
let t = bcx.use_var(regs[ins.b() as usize]);
let k_imm = bcx.ins().iconst(types::I64, ins.c() as i64);
let func_ref = module.declare_func_in_func(get_int_id, bcx.func);
let call = bcx.ins().call(func_ref, &[t, k_imm]);
let v = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
let inferred = if i + 1 < effective_end {
infer_getx_exit_lookahead(ins.a(), &record.ops[i + 1..effective_end])
} else {
None
};
match inferred {
Some(ExitTag::Int) => current_kinds[off + ins.a() as usize] = RegKind::Int,
Some(ExitTag::Table) => current_kinds[off + ins.a() as usize] = RegKind::Table,
Some(ExitTag::Float) => current_kinds[off + ins.a() as usize] = RegKind::Float,
_ => {
dispatchable = false;
dispatch_off_reason = dispatch_off_reason.or(Some("GetI:inference-fail"));
}
}
}
Op::GetTable => {
let t = bcx.use_var(regs[ins.b() as usize]);
let key = bcx.use_var(regs[ins.c() as usize]);
let func_ref = module.declare_func_in_func(get_int_id, bcx.func);
let call = bcx.ins().call(func_ref, &[t, key]);
let v = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
let inferred = if i + 1 < effective_end {
infer_getx_exit_lookahead(ins.a(), &record.ops[i + 1..effective_end])
} else {
None
};
match inferred {
Some(ExitTag::Int) => current_kinds[off + ins.a() as usize] = RegKind::Int,
Some(ExitTag::Table) => current_kinds[off + ins.a() as usize] = RegKind::Table,
Some(ExitTag::Float) => current_kinds[off + ins.a() as usize] = RegKind::Float,
_ => {
dispatchable = false;
dispatch_off_reason =
dispatch_off_reason.or(Some("GetTable:inference-fail"));
}
}
}
Op::SetField => {
if let Some(OpAction::SetFieldSunkWrite {
site_idx,
hash_slot,
}) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& virt_vars[site_idx as usize].is_some()
{
let array_cap = escape.sites[site_idx as usize].array_cap as usize;
let slot = array_cap + hash_slot as usize;
let src_kind = current_kinds[off + ins.c() as usize];
let v = bcx.use_var(regs[ins.c() as usize]);
let vars = virt_vars[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_vars");
bcx.def_var(vars[slot], v);
let kinds_vec = virt_kinds[site_idx as usize]
.as_mut()
.expect("Sinkable site has virt_kinds");
kinds_vec[slot] = src_kind;
continue;
}
let t = bcx.use_var(regs[ins.a() as usize]);
let key_v = match head_proto.consts[ins.b() as usize] {
luna_core::runtime::Value::Str(s) => s,
_ => unreachable!("pre-emit gates Str const at K[B]"),
};
let key_arg =
emit_str_key_arg(module, &mut bcx, key_v, opts.aot, &mut defined_aot_data);
let val_kind = k_op(¤t_kinds, off as u32 + ins.c());
let val_tag = match val_kind {
RegKind::Int => luna_core::runtime::value::raw::INT,
RegKind::Float => luna_core::runtime::value::raw::FLOAT,
RegKind::Table => luna_core::runtime::value::raw::TABLE,
RegKind::Closure => luna_core::runtime::value::raw::CLOSURE,
RegKind::Str => luna_core::runtime::value::raw::STR,
RegKind::Nil => luna_core::runtime::value::raw::NIL,
RegKind::Unset => luna_core::runtime::value::raw::INT,
};
let val_raw = bcx.use_var(regs[ins.c() as usize]);
let tag_arg = bcx.ins().iconst(types::I64, val_tag as i64);
let func_ref = module.declare_func_in_func(set_field_id, bcx.func);
bcx.ins().call(func_ref, &[t, key_arg, val_raw, tag_arg]);
}
Op::GetField => {
if let Some(OpAction::GetFieldSunkRead {
site_idx,
hash_slot,
}) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& let Some(vars) = virt_vars[site_idx as usize].as_ref()
{
let array_cap = escape.sites[site_idx as usize].array_cap as usize;
let slot = array_cap + hash_slot as usize;
let v = bcx.use_var(vars[slot]);
bcx.def_var(regs[ins.a() as usize], v);
let k = virt_kinds[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_kinds")[slot];
current_kinds[off + ins.a() as usize] = k;
continue;
}
let t = bcx.use_var(regs[ins.b() as usize]);
let key_v = match head_proto.consts[ins.c() as usize] {
luna_core::runtime::Value::Str(s) => s,
_ => unreachable!("pre-emit gates Str const at K[C]"),
};
let key_arg =
emit_str_key_arg(module, &mut bcx, key_v, opts.aot, &mut defined_aot_data);
let ic_active = luna_core::jit::trace_types::field_ic_enabled()
&& record
.field_ic_snapshot
.as_ref()
.is_some_and(|s| s.op_idx as usize == i);
let v = if ic_active {
let snap = record
.field_ic_snapshot
.as_ref()
.expect("ic_active implies snapshot present");
let mt = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t,
super::TABLE_METATABLE_OFFSET as i32,
);
let zero = bcx.ins().iconst(types::I64, 0);
let mt_ok = bcx.ins().icmp(IntCC::Equal, mt, zero);
let nodes_len = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t,
super::TABLE_NODES_LEN_OFFSET as i32,
);
let nodes_len_imm = bcx.ins().iconst(types::I64, snap.nodes_len as i64);
let len_ok = bcx.ins().icmp(IntCC::Equal, nodes_len, nodes_len_imm);
let guards_12 = bcx.ins().band(mt_ok, len_ok);
let fast_blk = bcx.create_block();
let slow_blk = bcx.create_block();
let merge_blk = bcx.create_block();
bcx.append_block_param(merge_blk, types::I64);
bcx.ins().brif(guards_12, fast_blk, &[], slow_blk, &[]);
bcx.switch_to_block(fast_blk);
bcx.seal_block(fast_blk);
let nodes_ptr = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t,
super::TABLE_NODES_PTR_OFFSET as i32,
);
let node_offset = (snap.slot_idx as usize * super::SIZEOF_NODE) as i64;
let node_addr = bcx.ins().iadd_imm(nodes_ptr, node_offset);
let key_raw = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
node_addr,
super::NODE_KEY_RAW_OFFSET as i32,
);
let key_imm = bcx.ins().iconst(types::I64, snap.key_ptr_bits as i64);
let key_ok = bcx.ins().icmp(IntCC::Equal, key_raw, key_imm);
let val_tag_i8 = bcx.ins().load(
types::I8,
cranelift_codegen::ir::MemFlags::trusted(),
node_addr,
super::NODE_VAL_TAG_OFFSET as i32,
);
let val_tag = bcx.ins().uextend(types::I64, val_tag_i8);
let tag_imm = bcx.ins().iconst(types::I64, snap.cached_val_tag as i64);
let tag_ok = bcx.ins().icmp(IntCC::Equal, val_tag, tag_imm);
let guards_34 = bcx.ins().band(key_ok, tag_ok);
let load_blk = bcx.create_block();
bcx.ins().brif(guards_34, load_blk, &[], slow_blk, &[]);
bcx.switch_to_block(load_blk);
bcx.seal_block(load_blk);
let val_raw = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
node_addr,
super::NODE_VAL_RAW_OFFSET as i32,
);
bcx.ins().jump(merge_blk, &[val_raw.into()]);
bcx.switch_to_block(slow_blk);
bcx.seal_block(slow_blk);
let func_ref = module.declare_func_in_func(get_field_id, bcx.func);
let call = bcx.ins().call(func_ref, &[t, key_arg]);
let v_slow = bcx.inst_results(call)[0];
bcx.ins().jump(merge_blk, &[v_slow.into()]);
bcx.switch_to_block(merge_blk);
bcx.seal_block(merge_blk);
bcx.block_params(merge_blk)[0]
} else {
let func_ref = module.declare_func_in_func(get_field_id, bcx.func);
let call = bcx.ins().call(func_ref, &[t, key_arg]);
bcx.inst_results(call)[0]
};
bcx.def_var(regs[ins.a() as usize], v);
let inferred = if i + 1 < effective_end {
infer_getx_exit_lookahead(ins.a(), &record.ops[i + 1..effective_end])
} else {
None
};
match inferred {
Some(ExitTag::Int) => current_kinds[off + ins.a() as usize] = RegKind::Int,
Some(ExitTag::Table) => current_kinds[off + ins.a() as usize] = RegKind::Table,
Some(ExitTag::Float) => current_kinds[off + ins.a() as usize] = RegKind::Float,
_ => {
dispatchable = false;
dispatch_off_reason =
dispatch_off_reason.or(Some("GetField:inference-fail"));
}
}
}
Op::GetTabUp => {
let upval_idx_arg = bcx.ins().iconst(types::I64, ins.b() as i64);
let key_v = match head_proto.consts[ins.c() as usize] {
luna_core::runtime::Value::Str(s) => s,
_ => unreachable!("pre-emit gates Str const at K[C]"),
};
let key_arg =
emit_str_key_arg(module, &mut bcx, key_v, opts.aot, &mut defined_aot_data);
let func_ref = module.declare_func_in_func(get_tab_up_id, bcx.func);
let call = bcx.ins().call(func_ref, &[upval_idx_arg, key_arg]);
let v = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
let inferred = if i + 1 < effective_end {
infer_getx_exit_lookahead(ins.a(), &record.ops[i + 1..effective_end])
} else {
None
};
match inferred {
Some(ExitTag::Int) => current_kinds[off + ins.a() as usize] = RegKind::Int,
Some(ExitTag::Table) => current_kinds[off + ins.a() as usize] = RegKind::Table,
Some(ExitTag::Float) => current_kinds[off + ins.a() as usize] = RegKind::Float,
_ => {
dispatchable = false;
dispatch_off_reason =
dispatch_off_reason.or(Some("GetTabUp:inference-fail"));
}
}
}
Op::SetI => {
if let Some(OpAction::SetISunkWrite { site_idx, key }) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& virt_vars[site_idx as usize].is_some()
{
let slot = (key as usize) - 1;
let src_kind = current_kinds[off + ins.c() as usize];
let v = bcx.use_var(regs[ins.c() as usize]);
let vars = virt_vars[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_vars");
bcx.def_var(vars[slot], v);
let kinds_vec = virt_kinds[site_idx as usize]
.as_mut()
.expect("Sinkable site has virt_kinds");
kinds_vec[slot] = src_kind;
continue;
}
let t = bcx.use_var(regs[ins.a() as usize]);
let k_imm = bcx.ins().iconst(types::I64, ins.b() as i64);
let val_kind = k_op(¤t_kinds, off as u32 + ins.c());
emit_table_set(
&mut bcx,
&mut module,
set_int_id,
set_nil_id,
set_raw_id,
t,
k_imm,
val_kind,
regs[ins.c() as usize],
);
}
Op::SetTable => {
if let Some(OpAction::SetTableSunkWrite { site_idx, key }) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& virt_vars[site_idx as usize].is_some()
{
let slot = (key as usize) - 1;
let src_kind = current_kinds[off + ins.c() as usize];
let v = bcx.use_var(regs[ins.c() as usize]);
let vars = virt_vars[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_vars");
bcx.def_var(vars[slot], v);
let kinds_vec = virt_kinds[site_idx as usize]
.as_mut()
.expect("Sinkable site has virt_kinds");
kinds_vec[slot] = src_kind;
continue;
}
let t = bcx.use_var(regs[ins.a() as usize]);
let key = bcx.use_var(regs[ins.b() as usize]);
let val_kind = k_op(¤t_kinds, off as u32 + ins.c());
emit_table_set(
&mut bcx,
&mut module,
set_int_id,
set_nil_id,
set_raw_id,
t,
key,
val_kind,
regs[ins.c() as usize],
);
}
Op::SetList => {
let b_bytecode = ins.b() as usize;
let effective_b = if b_bytecode == 0 {
match record.ops[i].var_count {
Some(n) => n as usize,
None => return None,
}
} else {
b_bytecode
};
if let Some(OpAction::SetListWrite { site_idx }) = escape.op_actions[i]
&& escape.sites[site_idx as usize].state == EscapeState::Sinkable
&& virt_vars[site_idx as usize].is_some()
{
let a = ins.a() as usize;
let mut src_vals: Vec<Value> = Vec::with_capacity(effective_b);
let mut src_kinds: Vec<RegKind> = Vec::with_capacity(effective_b);
for vi in 1..=effective_b {
src_vals.push(bcx.use_var(regs[a + vi]));
src_kinds.push(current_kinds[off + a + vi]);
}
let vars = virt_vars[site_idx as usize]
.as_ref()
.expect("Sinkable site has virt_vars");
for (vi, &v) in src_vals.iter().enumerate() {
bcx.def_var(vars[vi], v);
}
let kinds_vec = virt_kinds[site_idx as usize]
.as_mut()
.expect("Sinkable site has virt_kinds");
for (vi, &k) in src_kinds.iter().enumerate() {
kinds_vec[vi] = k;
}
continue;
}
let a = ins.a() as usize;
let c_off = ins.c() as i64;
let t = bcx.use_var(regs[a]);
for ii in 1..=effective_b {
let key = bcx.ins().iconst(types::I64, c_off + ii as i64);
let src_kind = k_op(¤t_kinds, (off + a + ii) as u32);
emit_table_set(
&mut bcx,
&mut module,
set_int_id,
set_nil_id,
set_raw_id,
t,
key,
src_kind,
regs[a + ii],
);
}
}
Op::Len => {
let t = bcx.use_var(regs[ins.b() as usize]);
let func_ref = module.declare_func_in_func(len_id, bcx.func);
let call = bcx.ins().call(func_ref, &[t]);
let v = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = RegKind::Int;
}
Op::Closure => {
let bx = ins.bx() as usize;
let inner = head_proto.protos[bx];
let spill_ref = module.declare_func_in_func(spill_id, bcx.func);
for d in inner.upvals.iter() {
if !d.in_stack {
continue;
}
let src_idx = d.index as usize;
let src_kind = current_kinds[off + src_idx];
let tag_byte = match src_kind {
RegKind::Int => luna_core::runtime::value::raw::INT,
RegKind::Float => luna_core::runtime::value::raw::FLOAT,
RegKind::Table => luna_core::runtime::value::raw::TABLE,
RegKind::Closure => luna_core::runtime::value::raw::CLOSURE,
RegKind::Str => luna_core::runtime::value::raw::STR,
RegKind::Nil => luna_core::runtime::value::raw::NIL,
RegKind::Unset => {
return None;
}
};
let slot_arg = bcx.ins().iconst(types::I64, d.index as i64);
let tag_arg = bcx.ins().iconst(types::I64, tag_byte as i64);
let raw_arg = bcx.use_var(regs[src_idx]);
bcx.ins().call(spill_ref, &[slot_arg, tag_arg, raw_arg]);
}
let bx_arg = bcx.ins().iconst(types::I64, ins.bx() as i64);
let func_ref = module.declare_func_in_func(op_closure_id, bcx.func);
let call = bcx.ins().call(func_ref, &[bx_arg]);
let v = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
current_kinds[off + ins.a() as usize] = RegKind::Closure;
closure_seen += 1;
}
Op::Close => {
let a_us = ins.a() as usize;
let spill_ref = module.declare_func_in_func(spill_id, bcx.func);
for slot in a_us..max_stack {
let k = current_kinds[off + slot];
let tag_byte = match k {
RegKind::Int => luna_core::runtime::value::raw::INT,
RegKind::Float => luna_core::runtime::value::raw::FLOAT,
RegKind::Table => luna_core::runtime::value::raw::TABLE,
RegKind::Closure => luna_core::runtime::value::raw::CLOSURE,
RegKind::Str => luna_core::runtime::value::raw::STR,
RegKind::Nil => luna_core::runtime::value::raw::NIL,
RegKind::Unset => continue,
};
let slot_arg = bcx.ins().iconst(types::I64, slot as i64);
let tag_arg = bcx.ins().iconst(types::I64, tag_byte as i64);
let raw_arg = bcx.use_var(regs[slot]);
bcx.ins().call(spill_ref, &[slot_arg, tag_arg, raw_arg]);
}
let a_arg = bcx.ins().iconst(types::I64, ins.a() as i64);
let func_ref = module.declare_func_in_func(op_close_id, bcx.func);
let call = bcx.ins().call(func_ref, &[a_arg]);
let status = bcx.inst_results(call)[0];
let continue_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
bcx.ins().brif(status, deopt_blk, &[], continue_blk, &[]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
}
Op::GetUpval => {
let idx_b = ins.b();
let v = if let Some(&cached_var) = upval_cache.get(&idx_b) {
bcx.use_var(cached_var)
} else {
let idx_arg = bcx.ins().iconst(types::I64, ins.b() as i64);
let func_ref = module.declare_func_in_func(upval_get_id, bcx.func);
let call = bcx.ins().call(func_ref, &[idx_arg]);
let new_v = bcx.inst_results(call)[0];
let cache_var = bcx.declare_var(types::I64);
bcx.def_var(cache_var, new_v);
upval_cache.insert(idx_b, cache_var);
new_v
};
bcx.def_var(regs[ins.a() as usize], v);
let upper = effective_end.min(record.ops.len() - 1) + 1;
let inferred = if i + 1 < upper {
infer_upval_exit(ins.a(), &record.ops[i + 1..upper])
} else {
None
};
match inferred {
Some(ExitTag::Closure) => {
current_kinds[off + ins.a() as usize] = RegKind::Closure;
}
_ => {
current_kinds[off + ins.a() as usize] = RegKind::Unset;
dispatchable = false;
dispatch_off_reason =
dispatch_off_reason.or(Some("GetUpval:not-Closure-use"));
}
}
}
Op::Call => {
if self_link_idx_opt.is_some() && i + 1 == effective_end {
continue;
}
debug_assert!(
i + 1 < effective_end,
"self-rec Call must be followed by callee op in effective_end"
);
let callee_base = op_offsets[i + 1];
call_chain.push(FrameMaterializeInfo {
base_offset: callee_base,
pc: rop.pc + 1,
nresults: 1,
});
}
Op::Return0 => {
debug_assert!(
!call_chain.is_empty(),
"Return0 at depth>0 has a matching frame"
);
call_chain.pop();
}
Op::Return1 => {
let a_callee = ins.a() as usize;
let call_a = enclosing_call_a[i]
.expect("Return1 at depth>0 has an enclosing Op::Call")
as usize;
let caller_off = off
.checked_sub(call_a + 1)
.expect("op_offsets invariant: callee window > caller window");
let src_var = regs_full[off + a_callee];
let dst_var = regs_full[caller_off + call_a];
let v = bcx.use_var(src_var);
bcx.def_var(dst_var, v);
current_kinds[caller_off + call_a] = current_kinds[off + a_callee];
debug_assert!(
!call_chain.is_empty(),
"Return1 at depth>0 has a matching frame"
);
call_chain.pop();
}
Op::TForCall => {
let a_us = ins.a() as usize;
let nvars = ins.c() as i64;
let ipairs_addr = luna_core::vm::builtins::ipairs_iter
as luna_core::runtime::value::NativeFn
as usize;
let is_ipairs_trace = record.tfor_iter_ptr == Some(ipairs_addr);
let spill_ref = module.declare_func_in_func(spill_id, bcx.func);
let spill_slot = |bcx: &mut FunctionBuilder<'_>, slot: usize| {
let k = current_kinds[off + slot];
let tag_byte = match k {
RegKind::Int => luna_core::runtime::value::raw::INT,
RegKind::Float => luna_core::runtime::value::raw::FLOAT,
RegKind::Table => luna_core::runtime::value::raw::TABLE,
RegKind::Closure => luna_core::runtime::value::raw::CLOSURE,
RegKind::Str => luna_core::runtime::value::raw::STR,
RegKind::Nil => luna_core::runtime::value::raw::NIL,
RegKind::Unset => return,
};
let slot_arg = bcx.ins().iconst(types::I64, slot as i64);
let tag_arg = bcx.ins().iconst(types::I64, tag_byte as i64);
let raw_arg = bcx.use_var(regs[slot]);
bcx.ins().call(spill_ref, &[slot_arg, tag_arg, raw_arg]);
};
if !is_ipairs_trace {
for slot in a_us..=(a_us + 2) {
spill_slot(&mut bcx, slot);
}
}
let emit_helper_call = |bcx: &mut FunctionBuilder<'_>, module: &mut M| -> () {
let out_ss =
bcx.create_sized_stack_slot(cranelift_codegen::ir::StackSlotData::new(
cranelift_codegen::ir::StackSlotKind::ExplicitSlot,
24,
3,
));
let ctrl_addr = bcx.ins().stack_addr(types::I64, out_ss, 0);
let key_addr = bcx.ins().stack_addr(types::I64, out_ss, 8);
let val_addr = bcx.ins().stack_addr(types::I64, out_ss, 16);
let a_arg = bcx.ins().iconst(types::I64, a_us as i64);
let nvars_arg = bcx.ins().iconst(types::I64, nvars);
let func_ref = module.declare_func_in_func(op_tforcall_id, bcx.func);
let call_inst = bcx
.ins()
.call(func_ref, &[a_arg, nvars_arg, ctrl_addr, key_addr, val_addr]);
let status_or_tag = bcx.inst_results(call_inst)[0];
let zero = bcx.ins().iconst(types::I64, 0);
let is_err = bcx.ins().icmp(IntCC::SignedLessThan, status_or_tag, zero);
let cont_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
bcx.ins().brif(is_err, deopt_blk, &[], cont_blk, &[]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(cont_blk);
bcx.seal_block(cont_blk);
bcx.def_var(tforcall_tag_var, status_or_tag);
let ctrl_raw = bcx.ins().stack_load(types::I64, out_ss, 0);
let key_raw = bcx.ins().stack_load(types::I64, out_ss, 8);
let val_raw = bcx.ins().stack_load(types::I64, out_ss, 16);
bcx.def_var(regs[a_us + 2], ctrl_raw);
bcx.def_var(regs[a_us + 4], key_raw);
if (nvars as usize) >= 2 && a_us + 5 < max_stack {
bcx.def_var(regs[a_us + 5], val_raw);
}
};
if is_ipairs_trace {
let ctrl = bcx.use_var(regs[a_us + 2]);
let t_raw = bcx.use_var(regs[a_us + 1]);
let one = bcx.ins().iconst(types::I64, 1);
let next_i = bcx.ins().iadd(ctrl, one);
let key_m1 = ctrl;
let asize = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t_raw,
super::TABLE_ASIZE_OFFSET as i32,
);
let in_range = bcx.ins().icmp(IntCC::UnsignedLessThan, key_m1, asize);
let metatable = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t_raw,
super::TABLE_METATABLE_OFFSET as i32,
);
let zero = bcx.ins().iconst(types::I64, 0);
let no_meta = bcx.ins().icmp(IntCC::Equal, metatable, zero);
let fast_ok = bcx.ins().band(in_range, no_meta);
let fast_blk = bcx.create_block();
let slow_blk = bcx.create_block();
let merge_blk = bcx.create_block();
bcx.ins().brif(fast_ok, fast_blk, &[], slow_blk, &[]);
bcx.switch_to_block(fast_blk);
bcx.seal_block(fast_blk);
let avals_ptr = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
t_raw,
super::TABLE_ARRAY_PTR_OFFSET as i32,
);
let three = bcx.ins().iconst(types::I64, 3);
let val_off = bcx.ins().ishl(key_m1, three);
let val_addr_fast = bcx.ins().iadd(avals_ptr, val_off);
let val_raw_fast = bcx.ins().load(
types::I64,
cranelift_codegen::ir::MemFlags::trusted(),
val_addr_fast,
0,
);
let avals_bytes = bcx.ins().ishl(asize, three);
let tag_base = bcx.ins().iadd(avals_ptr, avals_bytes);
let tag_addr = bcx.ins().iadd(tag_base, key_m1);
let val_tag_i8 = bcx.ins().load(
types::I8,
cranelift_codegen::ir::MemFlags::trusted(),
tag_addr,
0,
);
let val_tag = bcx.ins().uextend(types::I64, val_tag_i8);
let nil_const = bcx
.ins()
.iconst(types::I64, luna_core::runtime::value::raw::NIL as i64);
let int_const = bcx
.ins()
.iconst(types::I64, luna_core::runtime::value::raw::INT as i64);
let is_nil = bcx.ins().icmp(IntCC::Equal, val_tag, nil_const);
if let Some(expected_tag) = record.tfor_val_tag
&& expected_tag != luna_core::runtime::value::raw::NIL
{
let exp_const = bcx.ins().iconst(types::I64, expected_tag as i64);
let is_exp = bcx.ins().icmp(IntCC::Equal, val_tag, exp_const);
let ok = bcx.ins().bor(is_nil, is_exp);
let guard_continue = bcx.create_block();
let guard_deopt = bcx.create_block();
bcx.ins().brif(ok, guard_continue, &[], guard_deopt, &[]);
bcx.switch_to_block(guard_deopt);
bcx.seal_block(guard_deopt);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(guard_continue);
bcx.seal_block(guard_continue);
}
let zero_raw = bcx.ins().iconst(types::I64, 0);
let r4_raw = bcx.ins().select(is_nil, zero_raw, next_i);
let r4_tag = bcx.ins().select(is_nil, nil_const, int_const);
bcx.def_var(regs[a_us + 2], next_i);
bcx.def_var(regs[a_us + 4], r4_raw);
if a_us + 5 < max_stack {
let prev_v5 = bcx.use_var(regs[a_us + 5]);
let chosen_v5 = bcx.ins().select(is_nil, prev_v5, val_raw_fast);
bcx.def_var(regs[a_us + 5], chosen_v5);
}
bcx.def_var(tforcall_tag_var, r4_tag);
bcx.ins().jump(merge_blk, &[]);
bcx.switch_to_block(slow_blk);
bcx.seal_block(slow_blk);
spill_slot(&mut bcx, a_us + 2);
emit_helper_call(&mut bcx, module);
bcx.ins().jump(merge_blk, &[]);
bcx.switch_to_block(merge_blk);
bcx.seal_block(merge_blk);
} else {
emit_helper_call(&mut bcx, module);
}
current_kinds[off + a_us + 2] = RegKind::Unset;
current_kinds[off + a_us + 4] = RegKind::Unset;
if (nvars as usize) >= 2 && a_us + 5 < max_stack {
current_kinds[off + a_us + 5] = RegKind::Unset;
}
}
Op::Concat => {
let a_us = ins.a() as usize;
let n_operands = ins.b() as usize;
let spill_ref = module.declare_func_in_func(spill_id, bcx.func);
let update_raw_ref = module.declare_func_in_func(update_raw_id, bcx.func);
for slot in a_us..(a_us + n_operands) {
let k = current_kinds[off + slot];
let slot_arg = bcx.ins().iconst(types::I64, slot as i64);
let raw_arg = bcx.use_var(regs[slot]);
let tag_byte_opt = match k {
RegKind::Int => Some(luna_core::runtime::value::raw::INT),
RegKind::Float => Some(luna_core::runtime::value::raw::FLOAT),
RegKind::Table => Some(luna_core::runtime::value::raw::TABLE),
RegKind::Closure => Some(luna_core::runtime::value::raw::CLOSURE),
RegKind::Str => Some(luna_core::runtime::value::raw::STR),
RegKind::Nil => Some(luna_core::runtime::value::raw::NIL),
RegKind::Unset => None,
};
if let Some(tag_byte) = tag_byte_opt {
let tag_arg = bcx.ins().iconst(types::I64, tag_byte as i64);
bcx.ins().call(spill_ref, &[slot_arg, tag_arg, raw_arg]);
} else {
bcx.ins().call(update_raw_ref, &[slot_arg, raw_arg]);
}
}
let a_arg = bcx.ins().iconst(types::I64, a_us as i64);
let n_arg = bcx.ins().iconst(types::I64, n_operands as i64);
let func_ref = module.declare_func_in_func(op_concat_id, bcx.func);
let call_inst = bcx.ins().call(func_ref, &[a_arg, n_arg]);
let status = bcx.inst_results(call_inst)[0];
let zero = bcx.ins().iconst(types::I64, 0);
let is_err = bcx.ins().icmp(IntCC::SignedLessThan, status, zero);
let continue_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
bcx.ins().brif(is_err, deopt_blk, &[], continue_blk, &[]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
&mut bcx,
®s_full[..max_stack],
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
let stack_load_ref = module.declare_func_in_func(stack_load_id, bcx.func);
let a_arg_reload = bcx.ins().iconst(types::I64, a_us as i64);
let reload_inst = bcx.ins().call(stack_load_ref, &[a_arg_reload]);
let result_raw = bcx.inst_results(reload_inst)[0];
bcx.def_var(regs[a_us], result_raw);
current_kinds[off + a_us] = RegKind::Unset;
}
Op::TForPrep => unreachable!("Op::TForPrep bailed in pre-emit pass"),
Op::TForLoop => unreachable!("Op::TForLoop only appears at effective_end"),
_ => unreachable!("non-whitelisted op rejected in pre-emit pass"),
}
}
let has_cmp = record.ops[..effective_end]
.iter()
.any(|r| matches!(r.inst.op(), Op::Lt | Op::Le | Op::Eq));
let do_internal_loop = opts.internal_loop
&& (has_cmp || for_loop_idx_opt.is_some() || self_link_idx_opt.is_some())
&& call_idx_opt.is_none()
&& return_idx_opt.is_none()
&& inline_abort_idx_opt.is_none()
&& downrec_idx_opt.is_none();
let caller_regs: &[Variable] = ®s_full[..max_stack];
let mut downrec_link_for_compiled: Option<(u32, u32)> = None;
let mut downrec_multi_way_count_for_compiled: u8 = 0;
if let Some((_dr_idx, dr_return_pc, _target_proto_id, _depth_delta)) = downrec_idx_opt {
debug_assert!(
dr_return_pc != 0,
"DownRec recorder should never trip on a PC=0 Op::Return — Op::Return's PC is past the prologue"
);
let _ = _target_proto_id;
let _ = _depth_delta;
let stitch_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
let saved_pc_offset = (window_size_us as i32) * 8;
let saved_pc = bcx
.ins()
.load(types::I64, MemFlags::trusted(), reg_state, saved_pc_offset);
let mut candidates: Vec<u32> = Vec::with_capacity(DOWNREC_MULTI_WAY_GUARD_MAX);
candidates.push(dr_return_pc);
for retf in &record.retfs {
if candidates.len() >= DOWNREC_MULTI_WAY_GUARD_MAX {
break;
}
if retf.proto.as_ptr() as usize == _target_proto_id
&& !candidates.contains(&retf.caller_pc)
{
candidates.push(retf.caller_pc);
}
}
for (i, candidate_pc) in candidates.iter().enumerate() {
let imm_pc = bcx.ins().iconst(types::I64, *candidate_pc as i64);
let eq = bcx.ins().icmp(IntCC::Equal, saved_pc, imm_pc);
let miss_blk = if i + 1 < candidates.len() {
bcx.create_block()
} else {
deopt_blk
};
bcx.ins().brif(eq, stitch_blk, &[], miss_blk, &[]);
if i + 1 < candidates.len() {
bcx.switch_to_block(miss_blk);
bcx.seal_block(miss_blk);
}
}
let multi_way_candidate_count = candidates.len();
bcx.switch_to_block(stitch_blk);
bcx.seal_block(stitch_blk);
let raw_ret =
(1u64 << 63) | ((SIDE_SENT_DOWNREC_CODE as u64) << 56) | (record.head_pc as u64);
let stitch_ret = bcx.ins().iconst(types::I64, raw_ret as i64);
bcx.ins().return_(&[stitch_ret]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.head_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
downrec_link_for_compiled = Some((0, record.head_pc));
if multi_way_candidate_count >= 2 {
dispatchable = true;
} else {
dispatchable = false;
dispatch_off_reason = dispatch_off_reason.or(Some("downrec-stitch-pending"));
}
downrec_multi_way_count_for_compiled =
multi_way_candidate_count.min(u8::MAX as usize) as u8;
} else if let Some((_self_link_idx, _kind)) = self_link_idx_opt {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.head_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
dispatchable = false;
dispatch_off_reason = dispatch_off_reason.or(Some("self-link-retf-r1"));
} else if let Some(call_idx) = call_idx_opt {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.ops[call_idx].pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
} else if let Some(inline_abort_idx) = inline_abort_idx_opt {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.ops[inline_abort_idx].pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
} else if let Some(return_idx) = return_idx_opt {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.ops[return_idx].pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
} else if let Some(for_loop_idx) = for_loop_idx_opt {
let rop = &record.ops[for_loop_idx];
let a = rop.inst.a() as usize;
match rop.inst.op() {
Op::ForLoop => {
let count = bcx.use_var(regs_full[a + 1]);
let zero = bcx.ins().iconst(types::I64, 0);
let cond = bcx.ins().icmp(IntCC::SignedGreaterThan, count, zero);
let continue_blk = bcx.create_block();
let exit_blk = bcx.create_block();
bcx.ins().brif(cond, continue_blk, &[], exit_blk, &[]);
bcx.switch_to_block(exit_blk);
bcx.seal_block(exit_blk);
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
rop.pc + 1,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
let cur = bcx.use_var(regs_full[a]);
let step = bcx.use_var(regs_full[a + 2]);
let next = bcx.ins().iadd(cur, step);
bcx.def_var(regs_full[a], next);
let one = bcx.ins().iconst(types::I64, 1);
let count_new = bcx.ins().isub(count, one);
bcx.def_var(regs_full[a + 1], count_new);
bcx.def_var(regs_full[a + 3], next);
if do_internal_loop {
bcx.ins().jump(body_loop, &[]);
} else {
let body_pc = ((rop.pc as i32) + 1 - rop.inst.bx() as i32).max(0) as u32;
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
body_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
}
}
Op::TForLoop => {
let tag = bcx.use_var(tforcall_tag_var);
let nil_const = bcx
.ins()
.iconst(types::I64, luna_core::runtime::value::raw::NIL as i64);
let int_const = bcx
.ins()
.iconst(types::I64, luna_core::runtime::value::raw::INT as i64);
let is_nil = bcx.ins().icmp(IntCC::Equal, tag, nil_const);
let nil_exit_blk = bcx.create_block();
let not_nil_blk = bcx.create_block();
bcx.ins().brif(is_nil, nil_exit_blk, &[], not_nil_blk, &[]);
bcx.switch_to_block(nil_exit_blk);
bcx.seal_block(nil_exit_blk);
let mut nil_snapshot: Vec<RegKind> = current_kinds[..max_stack].to_vec();
if a + 4 < nil_snapshot.len() {
nil_snapshot[a + 4] = RegKind::Nil;
}
let tag_side_box_2: Box<TCellPtr> = Box::new(TCellPtr::null());
let _tag_side_cell_addr_2 = (&*tag_side_box_2) as *const TCellPtr as i64;
let tag_side_local_2 = per_exit_kinds.len() as u32;
per_exit_kinds.push((rop.pc + 1, nil_snapshot, tag_side_box_2));
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
rop.pc + 1,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_TAG, tag_side_local_2),
);
bcx.switch_to_block(not_nil_blk);
bcx.seal_block(not_nil_blk);
let is_int = bcx.ins().icmp(IntCC::Equal, tag, int_const);
let continue_blk = bcx.create_block();
let deopt_blk = bcx.create_block();
bcx.ins().brif(is_int, continue_blk, &[], deopt_blk, &[]);
bcx.switch_to_block(deopt_blk);
bcx.seal_block(deopt_blk);
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
rop.pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
bcx.switch_to_block(continue_blk);
bcx.seal_block(continue_blk);
let ctrl = bcx.use_var(regs_full[a + 4]);
bcx.def_var(regs_full[a + 2], ctrl);
if do_internal_loop {
bcx.ins().jump(body_loop, &[]);
} else {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.head_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
}
}
_ => unreachable!("for_loop_idx_opt only set for Op::ForLoop / Op::TForLoop"),
}
} else if do_internal_loop {
bcx.ins().jump(body_loop, &[]);
} else {
emit_store_back_and_return_pc(
&mut bcx,
caller_regs,
reg_state,
record.head_pc,
flush_ctx.as_ref(),
0i64,
trace_fn_sig_ref,
encode_side_sentinel(SIDE_SENT_KIND_GLOBAL, 0),
);
}
bcx.seal_block(body_loop);
bcx.finalize();
if std::env::var("LUNA_TRACE_IR_DUMP")
.map(|v| v == "1")
.unwrap_or(false)
{
eprintln!(
"=== TRACE IR DUMP head_pc={} n_recorded_ops={} ===\n{}\n=== END ===",
record.head_pc,
record.ops.len(),
ctx.func.display()
);
}
let want_asm_dump = std::env::var("LUNA_TRACE_ASM_DUMP")
.map(|v| v == "1")
.unwrap_or(false);
if want_asm_dump {
ctx.set_disasm(true);
}
module.define_function(fn_id, &mut ctx).ok()?;
if want_asm_dump
&& let Some(cc) = ctx.compiled_code()
&& let Some(vcode) = cc.vcode.as_ref()
{
eprintln!(
"=== TRACE ASM DUMP head_pc={} n_recorded_ops={} ===\n{}\n=== END ===",
record.head_pc,
record.ops.len(),
vcode
);
}
module.clear_context(&mut ctx);
if let Some(for_loop_idx) = for_loop_idx_opt {
let rop = &record.ops[for_loop_idx];
let a = rop.inst.a() as usize;
match rop.inst.op() {
Op::ForLoop => {
current_kinds[a] = RegKind::Int;
current_kinds[a + 1] = RegKind::Int;
current_kinds[a + 3] = RegKind::Int;
}
Op::TForLoop => {
current_kinds[a + 2] = RegKind::Int;
}
_ => {}
}
}
const MIN_DISPATCHABLE_TRUNC_BODY_BASE: usize = 20;
const MIN_DISPATCHABLE_TRUNC_BODY_FLOOR: usize = 40;
let max_depth_used = record
.ops
.iter()
.map(|r| r.inline_depth as usize)
.max()
.unwrap_or(0);
let adaptive = MIN_DISPATCHABLE_TRUNC_BODY_BASE.saturating_sub(max_depth_used * 2);
let min_dispatchable_trunc_body = adaptive.max(MIN_DISPATCHABLE_TRUNC_BODY_FLOOR);
if (call_idx_opt.is_some() || return_idx_opt.is_some())
&& effective_end < min_dispatchable_trunc_body
&& per_exit_inline_vec.is_empty()
&& sunk_alloc_seen == 0
{
dispatchable = false;
dispatch_off_reason = dispatch_off_reason.or(Some("length-gate"));
}
if inline_abort_idx_opt.is_some() {
dispatchable = false;
dispatch_off_reason = dispatch_off_reason.or(Some("InlineAbort-gate"));
}
let mut exit_tags_vec = kinds_to_exit_tags(¤t_kinds[..max_stack]);
for site in &escape.sites {
if site.state == EscapeState::Sinkable && site.inline_depth == 0 {
let idx = site.a as usize;
if idx < exit_tags_vec.len() {
exit_tags_vec[idx] = ExitTag::Untouched;
}
}
}
let global_tag_res_kind = classify_exit_tags(&exit_tags_vec);
let exit_tags: TArc<[ExitTag]> = exit_tags_vec.into();
let mut tags_side_boxes: Vec<Box<TCellPtr>> = Vec::with_capacity(per_exit_kinds.len());
let per_exit_tags: TArc<[(u32, TArc<[ExitTag]>)]> = per_exit_kinds
.into_iter()
.map(|(pc, kinds, side_box)| {
let tags: TArc<[ExitTag]> = kinds_to_exit_tags(&kinds).into();
tags_side_boxes.push(side_box);
(pc, tags)
})
.collect::<Vec<_>>()
.into();
let tags_side_trace_ptrs: TArc<[Box<TCellPtr>]> = tags_side_boxes.into();
let per_exit_inline: TArc<[InlineSideExit]> = per_exit_inline_vec
.into_iter()
.map(
|(cont_pc, head_resume_pc, kinds, chain, side_trace_ptr)| InlineSideExit {
cont_pc,
head_resume_pc,
exit_tags: kinds_to_exit_tags(&kinds).into(),
chain,
side_trace_ptr,
},
)
.collect::<Vec<_>>()
.into();
checkpoint("post:emit-pass-done");
let exit_hit_counts: TArc<[TCellU32]> = {
let total = per_exit_inline.len() + per_exit_tags.len() + 1;
let v: Vec<TCellU32> = (0..total).map(|_| TCellU32::new(0)).collect();
v.into()
};
let exit_side_trace_ptrs: TArc<[TCellPtr]> = {
let total = per_exit_inline.len() + per_exit_tags.len() + 1;
let v: Vec<TCellPtr> = (0..total).map(|_| TCellPtr::null()).collect();
v.into()
};
let compiled = CompiledTrace {
head_pc: record.head_pc,
entry: placeholder_trace_fn,
n_ops: record.ops.len() as u32,
dispatchable,
window_size,
exit_tags,
global_tag_res_kind,
is_inline_abort_close: inline_abort_idx_opt.is_some(),
dispatch_off_reason: if dispatchable {
None
} else {
dispatch_off_reason
},
entry_tags: record.entry_tags.clone().into(),
per_exit_tags,
exit_hit_counts,
exit_side_trace_ptrs,
tags_side_trace_ptrs,
global_side_trace_ptr: global_side_trace_box,
side_trace_cache: TRefLock::new(std::collections::HashMap::new()),
has_any_side_wired: TCellBool::new(false),
per_exit_inline,
sinkable_sites_seen: escape.sinkable_count(),
accum_bufferable_seen: escape
.accum_sites
.iter()
.filter(|s| s.state == BufferState::Bufferable)
.count() as u32,
sunk_alloc_seen,
materialize_emit_count,
closure_seen,
body_writes: compute_body_writes(record, &op_offsets).into(),
downrec_link: downrec_link_for_compiled,
downrec_multi_way_count: downrec_multi_way_count_for_compiled,
};
Some((fn_id, compiled))
}
#[cfg(test)]
mod s14b_v0_scaffolding {
use super::{AccumSite, BufferState, EscapeAnalysis};
#[test]
fn buffer_state_variants_exist() {
let b = BufferState::Bufferable;
let nb = BufferState::NonBuffered;
assert_ne!(b, nb);
}
#[test]
fn accum_site_clones_cleanly() {
let site = AccumSite {
op_idx: 0,
pc: 0,
accum_slot: 0,
piece_slot: 1,
inline_depth: 0,
state: BufferState::Bufferable,
};
let cloned = site.clone();
assert_eq!(site.op_idx, cloned.op_idx);
assert_eq!(site.state, cloned.state);
}
#[test]
fn escape_analysis_default_has_empty_accum_fields() {
let ea: EscapeAnalysis = Default::default();
assert!(ea.accum_sites.is_empty());
assert!(ea.accum_live_at_op.is_empty());
}
}
#[cfg(test)]
mod s13a_depth_invariant {
use super::{MAX_INLINE_DEPTH, verify_depth_invariant};
#[test]
fn empty_sequence_is_valid() {
assert!(verify_depth_invariant(&[]));
}
#[test]
fn single_op_at_depth_zero_is_valid() {
assert!(verify_depth_invariant(&[(0, false)]));
assert!(verify_depth_invariant(&[(0, true)]));
}
#[test]
fn single_op_at_nonzero_depth_is_invalid() {
assert!(!verify_depth_invariant(&[(1, false)]));
assert!(!verify_depth_invariant(&[(2, true)]));
}
#[test]
fn linear_ascent_one_step_at_a_time_is_valid() {
assert!(verify_depth_invariant(&[
(0, true),
(1, true),
(2, true),
(3, false),
]));
}
#[test]
fn ascent_without_preceding_call_is_invalid() {
assert!(!verify_depth_invariant(&[(0, false), (1, false)]));
}
#[test]
fn ascent_skipping_a_depth_level_is_invalid() {
assert!(!verify_depth_invariant(&[(0, true), (2, false)]));
}
#[test]
fn arbitrary_descent_is_valid() {
assert!(verify_depth_invariant(&[
(0, true),
(1, true),
(2, true),
(3, false),
(0, false),
]));
}
#[test]
fn re_ascent_after_descent_is_valid() {
assert!(verify_depth_invariant(&[
(0, true),
(1, false),
(0, true),
(1, true),
(2, false),
]));
}
#[test]
fn boundary_at_max_inline_depth_is_valid() {
let mut items: Vec<(u8, bool)> = Vec::new();
for d in 0..=MAX_INLINE_DEPTH {
let is_call = d < MAX_INLINE_DEPTH;
items.push((d, is_call));
}
assert!(verify_depth_invariant(&items));
}
#[test]
fn depth_exceeding_max_inline_depth_is_invalid() {
let mut items: Vec<(u8, bool)> = Vec::new();
for d in 0..=MAX_INLINE_DEPTH {
items.push((d, true));
}
items.push((MAX_INLINE_DEPTH + 1, false));
assert!(!verify_depth_invariant(&items));
}
#[test]
fn descent_then_ascent_without_call_is_invalid() {
assert!(!verify_depth_invariant(&[
(0, true),
(1, false),
(0, false),
(1, false),
]));
}
}
#[cfg(test)]
mod s2b_arith {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
let cl = vm.load(src, b"=t").expect("compile");
cl.proto
}
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn make_record(head_pc: u32, ops: &[Inst], proto: Gc<Proto>) -> TraceRecord {
let mut rec = TraceRecord::start(proto, head_pc, Vec::new(), false);
for (i, inst) in ops.iter().copied().enumerate() {
let pushed = rec.push(RecordedOp {
proto,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
assert!(pushed, "test trace must fit MAX_TRACE_LEN");
}
rec.closed = true;
rec
}
#[test]
fn closed_empty_trace_returns_head_pc_and_passes_regs_through() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = make_record(7, &[], p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("empty closed trace must compile");
assert_eq!(ct.head_pc, 7);
assert_eq!(ct.n_ops, 0);
let mut state: Vec<i64> = vec![100, 200, 300, 400];
state.resize(p.max_stack as usize, 0);
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 7, "clean close returns head_pc");
assert_eq!(state[0], 100);
assert_eq!(state[1], 200);
assert_eq!(state[2], 300);
assert_eq!(state[3], 400);
}
#[test]
fn add_trace_computes_sum_into_dst_reg() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let add = Inst::iabc(Op::Add, 0, 1, 2, false);
let rec = make_record(11, &[add], p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("Add trace must compile");
assert_eq!(ct.head_pc, 11);
assert_eq!(ct.n_ops, 1);
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 10;
state[2] = 3;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 11);
assert_eq!(state[0], 13, "10 + 3");
assert_eq!(state[1], 10, "input untouched");
assert_eq!(state[2], 3, "input untouched");
}
#[test]
fn chained_arith_threads_results_through_regs() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Add, 0, 0, 1, false),
Inst::iabc(Op::Mul, 0, 0, 2, false),
Inst::iabc(Op::Sub, 0, 0, 3, false),
];
let rec = make_record(0, &prog, p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("Add/Mul/Sub chain must compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 5;
state[1] = 3;
state[2] = 4;
state[3] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0, "head_pc 0");
assert_eq!(state[0], 25);
}
#[test]
fn move_then_mul_propagates_via_move() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Move, 0, 3, 0, false),
Inst::iabc(Op::Mul, 0, 0, 1, false),
];
let rec = make_record(0, &prog, p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("Move + Mul must compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 999; state[1] = 6;
state[3] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0);
assert_eq!(state[0], 42, "7 * 6");
}
#[test]
fn trailing_jmp_is_emit_no_op() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Add, 0, 1, 2, false),
Inst::isj(Op::Jmp, -3),
];
let rec = make_record(0, &prog, p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("Add + trailing Jmp must compile");
assert_eq!(ct.n_ops, 2);
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 4;
state[2] = 5;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0);
assert_eq!(state[0], 9);
}
#[test]
fn non_closed_trace_does_not_compile() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = TraceRecord::start(p, 0, Vec::new(), false);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn unsupported_op_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [Inst::iabc(Op::Concat, 0, 0, 0, false)];
let rec = make_record(0, &prog, p);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn inline_depth_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 1, var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn cross_proto_op_bails() {
let mut vm1 = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let mut vm2 = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p1 = load_proto(&mut vm1, WIDE_SRC);
let p2 = load_proto(&mut vm2, WIDE_SRC);
let mut rec = TraceRecord::start(p1, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p2,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm1.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn out_of_bounds_reg_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, b"return 0");
let prog = [Inst::iabc(Op::Add, 0, 1, 2, false)];
let rec = make_record(0, &prog, p);
assert!((p.max_stack as usize) <= 2, "precondition for this test");
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn compiled_trace_is_callable_repeatedly() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [Inst::iabc(Op::Add, 0, 1, 2, false)];
let rec = make_record(0, &prog, p);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
for k in 0..1000 {
state[1] = k;
state[2] = 2 * k;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0);
assert_eq!(state[0], 3 * k);
}
}
}
#[cfg(test)]
mod s2b_cmp {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
let cl = vm.load(src, b"=t").expect("compile");
cl.proto
}
fn cmp_jmp_record(proto: Gc<Proto>, head_pc: u32, cmp_pc: u32, cmp: Inst) -> TraceRecord {
let mut rec = TraceRecord::start(proto, head_pc, Vec::new(), false);
rec.push(RecordedOp {
proto,
pc: cmp_pc,
inst: cmp,
inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto,
pc: cmp_pc + 1,
inst: Inst::isj(Op::Jmp, -1),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
rec
}
#[test]
fn lt_k1_returns_head_pc_when_cmp_matches() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let lt = Inst::iabc(Op::Lt, 1, 2, 0, true);
let rec = cmp_jmp_record(p, 5, 10, lt);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("Lt + trailing Jmp must compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 3; state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 5, "clean close returns head_pc");
}
#[test]
fn lt_k1_side_exits_when_cmp_inverts() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let lt = Inst::iabc(Op::Lt, 1, 2, 0, true); let rec = cmp_jmp_record(p, 5, 10, lt);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 9; state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 12, "side-exit returns failing PC = cmp_pc + 2");
assert_eq!(state[1], 9);
assert_eq!(state[2], 7);
}
#[test]
fn lt_k0_inverts_continue_condition() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let lt = Inst::iabc(Op::Lt, 1, 2, 0, false);
let rec = cmp_jmp_record(p, 3, 8, lt);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 9;
state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 3);
state[1] = 3;
state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 10, "cmp_pc=8 + 2 = 10");
}
#[test]
fn le_emits_signed_less_equal() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let le = Inst::iabc(Op::Le, 1, 2, 0, true);
let rec = cmp_jmp_record(p, 0, 0, le);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 5;
state[2] = 5;
assert_eq!(unsafe { (ct.entry)(state.as_mut_ptr()) }, 0);
state[1] = 5;
state[2] = 4;
assert_eq!(unsafe { (ct.entry)(state.as_mut_ptr()) }, 2);
}
#[test]
fn eq_emits_int_equality() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let eq = Inst::iabc(Op::Eq, 0, 1, 0, true);
let rec = cmp_jmp_record(p, 7, 4, eq);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 42;
state[1] = 42;
assert_eq!(unsafe { (ct.entry)(state.as_mut_ptr()) }, 7);
state[0] = 42;
state[1] = 41;
assert_eq!(unsafe { (ct.entry)(state.as_mut_ptr()) }, 6); }
#[test]
fn arith_then_cmp_side_exit_stores_back_post_arith_value() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Sub, 0, 0, 1, false),
Inst::iabc(Op::Lt, 0, 2, 0, true), Inst::isj(Op::Jmp, -3),
];
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
for (i, inst) in prog.iter().copied().enumerate() {
rec.push(RecordedOp {
proto: p,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
}
rec.closed = true;
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 10;
state[1] = 7;
state[2] = 5;
assert_eq!(unsafe { (ct.entry)(state.as_mut_ptr()) }, 0);
assert_eq!(state[0], 3);
state[0] = 10;
state[1] = 1;
state[2] = 5;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 3);
assert_eq!(state[0], 9, "post-arith value must be in reg_state");
assert_eq!(state[1], 1);
assert_eq!(state[2], 5);
}
#[test]
fn cmp_at_trailing_position_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Lt, 0, 1, 0, true),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn cmp_followed_by_non_jmp_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Lt, 0, 1, 0, true),
Inst::iabc(Op::Add, 2, 0, 1, false), ];
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
for (i, inst) in prog.iter().copied().enumerate() {
rec.push(RecordedOp {
proto: p,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
}
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn cmp_jmp_with_wrong_pc_offset_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Lt, 0, 1, 0, true),
inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 5, inst: Inst::isj(Op::Jmp, -1),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn orphan_jmp_mid_trace_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::isj(Op::Jmp, -1), Inst::iabc(Op::Add, 0, 1, 2, false),
];
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
for (i, inst) in prog.iter().copied().enumerate() {
rec.push(RecordedOp {
proto: p,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
}
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn loop_pattern_alternates_continue_and_exit() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Sub, 0, 0, 1, false),
Inst::iabc(Op::Lt, 0, 2, 0, false), Inst::isj(Op::Jmp, -3),
];
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
for (i, inst) in prog.iter().copied().enumerate() {
rec.push(RecordedOp {
proto: p,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
}
rec.closed = true;
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 20;
state[1] = 3; state[2] = 5;
let mut iters = 0;
loop {
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
iters += 1;
if r != 0 {
assert_eq!(r, 3, "side-exit PC = cmp_pc(1) + 2");
break;
}
assert!(iters < 100, "loop should terminate");
}
assert_eq!(state[0], 2);
assert_eq!(iters, 6);
}
}
#[cfg(test)]
mod s2b_table_ops {
use super::*;
use crate::jit_backend::enter_jit;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
let cl = vm.load(src, b"=t").expect("compile");
cl.proto
}
fn closed_record(proto: Gc<Proto>, head_pc: u32, ops: &[Inst]) -> TraceRecord {
let mut rec = TraceRecord::start(proto, head_pc, Vec::new(), false);
for (i, inst) in ops.iter().copied().enumerate() {
let pushed = rec.push(RecordedOp {
proto,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
assert!(pushed);
}
rec.closed = true;
rec
}
fn run_trace(vm: &mut Vm, ct: &CompiledTrace, state: &mut [i64]) -> i64 {
let _guard = enter_jit(vm, None);
unsafe { (ct.entry)(state.as_mut_ptr()) }
}
#[test]
fn new_table_writes_non_null_table_ptr_into_dst() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::NewTable, 0, 0, 0, false)]);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
let r = run_trace(&mut vm, &ct, &mut state);
assert_eq!(r, 0);
assert!(
state[0] != 0,
"NewTable must return a non-null Gc<Table> ptr"
);
assert!(vm.jit.pending_err.is_none(), "no deopt expected");
}
#[test]
fn set_i_then_get_i_roundtrips_through_a_fresh_table() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(
p,
0,
&[
Inst::iabc(Op::NewTable, 0, 0, 0, false),
Inst::iabc(Op::SetI, 0, 1, 2, false),
Inst::iabc(Op::GetI, 3, 0, 1, false),
],
);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[2] = 42; let r = run_trace(&mut vm, &ct, &mut state);
assert_eq!(r, 0);
assert!(vm.jit.pending_err.is_none(), "no metatable → no deopt");
assert_eq!(state[3], 42, "Get must see the value Set wrote");
}
#[test]
fn len_reports_array_size_after_set_i_sequence() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(
p,
0,
&[
Inst::iabc(Op::NewTable, 0, 0, 0, false),
Inst::iabc(Op::SetI, 0, 1, 1, false),
Inst::iabc(Op::SetI, 0, 2, 1, false),
Inst::iabc(Op::SetI, 0, 3, 1, false),
Inst::iabc(Op::Len, 2, 0, 0, false),
],
);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 99;
let r = run_trace(&mut vm, &ct, &mut state);
assert_eq!(r, 0);
assert_eq!(state[2], 3, "Len must report array length 3");
}
#[test]
fn metatable_on_set_i_parks_pending_err() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let t = vm.heap.new_table();
let mt = vm.heap.new_table();
unsafe { t.as_mut() }.set_metatable(Some(mt));
let rec = closed_record(p, 0, &[Inst::iabc(Op::SetI, 0, 1, 1, false)]);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = t.as_ptr() as i64;
state[1] = 7;
let r = run_trace(&mut vm, &ct, &mut state);
assert_eq!(r, 0, "trace still returns head_pc");
assert!(
vm.jit.pending_err.is_some(),
"metatable-bearing table must park a deopt request"
);
}
#[test]
fn metatable_on_get_i_parks_pending_err() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let t = vm.heap.new_table();
let mt = vm.heap.new_table();
unsafe { t.as_mut() }.set_metatable(Some(mt));
let rec = closed_record(p, 0, &[Inst::iabc(Op::GetI, 1, 0, 1, false)]);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = t.as_ptr() as i64;
run_trace(&mut vm, &ct, &mut state);
assert!(vm.jit.pending_err.is_some(), "GetI deopt on metatable");
}
#[test]
fn metatable_on_len_parks_pending_err() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let t = vm.heap.new_table();
let mt = vm.heap.new_table();
unsafe { t.as_mut() }.set_metatable(Some(mt));
let rec = closed_record(p, 0, &[Inst::iabc(Op::Len, 1, 0, 0, false)]);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = t.as_ptr() as i64;
run_trace(&mut vm, &ct, &mut state);
assert!(vm.jit.pending_err.is_some(), "Len deopt on metatable");
}
#[test]
fn pending_err_short_circuits_downstream_helpers() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let t = vm.heap.new_table();
let mt = vm.heap.new_table();
unsafe { t.as_mut() }.set_metatable(Some(mt));
let rec = closed_record(
p,
0,
&[
Inst::iabc(Op::SetI, 0, 1, 1, false),
Inst::iabc(Op::SetI, 0, 2, 1, false),
Inst::iabc(Op::GetI, 2, 0, 1, false),
],
);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = t.as_ptr() as i64;
state[1] = 11;
let r = run_trace(&mut vm, &ct, &mut state);
assert_eq!(r, 0);
assert!(vm.jit.pending_err.is_some());
assert_eq!(state[2], 0);
}
#[test]
fn new_table_dst_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::NewTable, 200, 0, 0, false)]);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn get_i_table_reg_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::GetI, 0, 200, 1, false)]);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn set_i_value_reg_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::SetI, 0, 1, 200, false)]);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn len_table_reg_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::Len, 0, 200, 0, false)]);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
}
#[cfg(test)]
mod s2b_call_truncation {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
let cl = vm.load(src, b"=t").expect("compile");
cl.proto
}
fn closed_record(proto: Gc<Proto>, head_pc: u32, ops: &[Inst]) -> TraceRecord {
let mut rec = TraceRecord::start(proto, head_pc, Vec::new(), false);
for (i, inst) in ops.iter().copied().enumerate() {
let pushed = rec.push(RecordedOp {
proto,
pc: i as u32,
inst,
inline_depth: 0,
var_count: None,
});
assert!(pushed);
}
rec.closed = true;
rec
}
#[test]
fn single_call_op_side_exits_at_call_pc() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(
p,
5,
&[Inst::iabc(Op::Call, 0, 1, 1, false)], );
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("Op::Call-only trace must compile");
let mut state: Vec<i64> = vec![42, 43, 44, 0, 0];
state.resize(p.max_stack as usize, 0);
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0, "side-exit at call's PC, not head_pc");
assert_eq!(state[0], 42);
assert_eq!(state[1], 43);
}
#[test]
fn arith_then_call_stores_back_post_arith_state() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Add, 0, 0, 1, false),
Inst::iabc(Op::Call, 2, 1, 0, false),
];
let rec = closed_record(p, 0, &prog);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 100;
state[1] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 1);
assert_eq!(state[0], 107, "post-arith state visible to interp");
assert_eq!(state[1], 7);
}
#[test]
fn ops_after_first_call_are_silently_dropped() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Add, 0, 1, 2, false),
Inst::iabc(Op::Call, 3, 1, 0, false),
Inst::iabc(Op::Mul, 0, 200, 200, false), ];
let rec = closed_record(p, 0, &prog);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("compile despite post-truncation OOB");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[1] = 30;
state[2] = 12;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 1, "side-exit at Call.pc = 1");
assert_eq!(state[0], 42);
}
#[test]
fn cmp_immediately_before_call_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Lt, 0, 1, 0, true),
Inst::iabc(Op::Call, 2, 1, 0, false),
];
let rec = closed_record(p, 0, &prog);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn call_a_register_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let rec = closed_record(p, 0, &[Inst::iabc(Op::Call, 200, 1, 0, false)]);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn cmp_then_jmp_then_call_truncation() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Lt, 0, 1, 0, true),
Inst::isj(Op::Jmp, -1),
Inst::iabc(Op::Add, 0, 0, 2, false),
Inst::iabc(Op::Call, 3, 1, 0, false),
];
let rec = closed_record(p, 0, &prog);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 5;
state[1] = 10;
state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 3, "side-exit at Call.pc = 3");
assert_eq!(state[0], 12);
state[0] = 99;
state[1] = 10;
state[2] = 7;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 2, "cmp side-exit takes precedence over Call truncation");
assert_eq!(state[0], 99, "Add never ran on this path");
}
#[test]
fn forloop_continues_internal_loop_until_count_hits_zero() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Add, 0, 0, 3, false), Inst::iabc(Op::ForLoop, 1, 0, 0, false), ];
let rec = closed_record(p, 0, &prog);
let opts = CompileOptions {
internal_loop: true,
pre53: false,
aot: false,
};
let ct =
try_compile_trace_with_options(vm.jit.storage.as_mut(), &rec, opts).expect("compile");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 0;
state[1] = 1;
state[2] = 5; state[3] = 1; state[4] = 0;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 2, "ForLoop count-exhaustion side-exits at pc+1");
assert_eq!(state[0], 6);
assert_eq!(state[2], 0);
}
#[test]
fn forloop_one_shot_returns_body_pc_on_continue() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [Inst::iabc(Op::ForLoop, 0, 0, 0, false)];
let rec = closed_record(p, 9, &prog);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("compile one-shot ForLoop trace");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
state[0] = 10;
state[1] = 3; state[2] = 1; state[3] = 0;
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 1, "one-shot continue returns body_pc=(rop.pc+1)-bx=1");
assert_eq!(state[0], 11);
assert_eq!(state[1], 2); assert_eq!(state[3], 11); }
#[test]
fn forloop_pre53_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [Inst::iabc(Op::ForLoop, 0, 0, 0, false)];
let rec = closed_record(p, 0, &prog);
let opts = CompileOptions {
internal_loop: false,
pre53: true,
aot: false,
};
assert!(try_compile_trace_with_options(vm.jit.storage.as_mut(), &rec, opts).is_none());
}
#[test]
fn forloop_a_plus_3_out_of_bounds_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [Inst::iabc(Op::ForLoop, 3, 0, 0, false)];
let rec = closed_record(p, 0, &prog);
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn op_call_then_unwhitelisted_op_still_compiles() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let prog = [
Inst::iabc(Op::Call, 0, 1, 0, false),
Inst::iabc(Op::Return0, 0, 0, 0, false), ];
let rec = closed_record(p, 0, &prog);
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("compile despite post-truncation Return0");
let mut state: Vec<i64> = vec![0; p.max_stack as usize];
let r = unsafe { (ct.entry)(state.as_mut_ptr()) };
assert_eq!(r, 0);
}
}
#[cfg(test)]
mod s4_step3a_op_offsets {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
vm.load(src, b"=t").expect("compile").proto
}
fn make_record(proto: Gc<Proto>, items: Vec<(Inst, u8)>) -> TraceRecord {
let mut rec = TraceRecord::start(proto, 0, Vec::new(), true);
for (i, (inst, depth)) in items.into_iter().enumerate() {
let pushed = rec.push(RecordedOp {
proto,
pc: i as u32,
inst,
inline_depth: depth,
var_count: None,
});
assert!(pushed, "rec.push should not overflow");
}
rec
}
#[test]
fn depth_zero_only_yields_zero_offsets() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, b"return 0");
let rec = make_record(
p,
vec![
(Inst::iabc(Op::LoadI, 0, 0, 128, false), 0),
(Inst::iabc(Op::Add, 1, 0, 0, false), 0),
(Inst::iabc(Op::Return1, 1, 0, 0, false), 0),
],
);
let (offsets, enclosing) = compute_op_offsets(&rec);
assert_eq!(offsets, vec![0u32, 0, 0]);
assert_eq!(enclosing, vec![None, None, None]);
}
#[test]
fn single_call_bumps_then_drops_offset() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, b"return 0");
let rec = make_record(
p,
vec![
(Inst::iabc(Op::Move, 0, 0, 0, false), 0),
(Inst::iabc(Op::Call, 3, 2, 2, false), 0), (Inst::iabc(Op::LoadI, 0, 0, 128, false), 1), (Inst::iabc(Op::Return1, 0, 0, 0, false), 1),
(Inst::iabc(Op::Move, 4, 3, 0, false), 0), ],
);
let (offsets, enclosing) = compute_op_offsets(&rec);
assert_eq!(offsets, vec![0u32, 0, 4, 4, 0]);
assert_eq!(enclosing, vec![None, None, Some(3), Some(3), None]);
}
#[test]
fn nested_calls_accumulate_offsets() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, b"return 0");
let rec = make_record(
p,
vec![
(Inst::iabc(Op::Call, 2, 1, 0, false), 0),
(Inst::iabc(Op::Call, 4, 1, 0, false), 1),
(Inst::iabc(Op::LoadI, 0, 0, 128, false), 2),
(Inst::iabc(Op::Return0, 0, 0, 0, false), 2),
(Inst::iabc(Op::Return0, 0, 0, 0, false), 1),
(Inst::iabc(Op::Move, 0, 0, 0, false), 0),
],
);
let (offsets, enclosing) = compute_op_offsets(&rec);
assert_eq!(offsets, vec![0u32, 3, 8, 8, 3, 0]);
assert_eq!(
enclosing,
vec![None, Some(2), Some(4), Some(4), Some(2), None]
);
}
}
#[cfg(test)]
mod s4_step3b_inline_emit {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
vm.load(src, b"=t").expect("compile").proto
}
#[test]
fn cmp_at_depth_one_no_longer_aborts_via_inline_abort() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let cl = vm
.load(b"local function f(a,b) return a+b end return f", b"=t")
.expect("compile");
let p = cl.proto.protos[0];
assert!(!p.is_vararg, "fixture must be non-vararg");
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 1,
inst: Inst::iabc(Op::Call, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Lt, 0, 1, 0, true),
inline_depth: 1,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 1,
inst: Inst::isj(Op::Jmp, 0),
inline_depth: 1,
var_count: None,
});
rec.closed = true;
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec)
.expect("cmp@d>0 now compiles via inline-cmp emit");
assert_eq!(ct.per_exit_inline.len(), 1);
assert!(
ct.window_size as usize >= p.max_stack as usize,
"window_size ({}) must be ≥ max_stack ({})",
ct.window_size,
p.max_stack,
);
}
#[test]
fn first_op_at_depth_gt_zero_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 1, var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn first_op_on_cross_proto_bails() {
let mut vm1 = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let mut vm2 = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p1 = load_proto(&mut vm1, WIDE_SRC);
let p2 = load_proto(&mut vm2, WIDE_SRC);
let mut rec = TraceRecord::start(p1, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p2,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm1.jit.storage.as_mut(), &rec).is_none());
}
}
#[cfg(test)]
mod s4_step4b_skeleton {
use super::*;
use luna_core::version::LuaVersion;
use luna_core::vm::Vm;
use luna_core::vm::isa::{Inst, Op};
const WIDE_SRC: &[u8] = b"local a,b,c,d = 0,0,0,0; return a+b+c+d";
fn load_proto(vm: &mut Vm, src: &[u8]) -> Gc<Proto> {
vm.load(src, b"=t").expect("compile").proto
}
#[test]
fn frame_materialize_info_layout_is_stable() {
assert_eq!(std::mem::size_of::<FrameMaterializeInfo>(), 12);
assert_eq!(std::mem::align_of::<FrameMaterializeInfo>(), 4);
}
#[test]
fn compiled_traces_have_empty_per_exit_metas_when_no_inline_cmp() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.closed = true;
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("simple add compiles");
assert!(
ct.per_exit_inline.is_empty(),
"no cmp@d>0 site → per_exit_inline empty"
);
}
#[test]
fn helper_with_no_lua_frame_returns_deopt() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let cl = vm.load(WIDE_SRC, b"=t").expect("compile");
let metas: [FrameMaterializeInfo; 0] = [];
let r = {
let _g = crate::jit_backend::enter_jit(&mut vm, Some(cl));
unsafe { super::super::luna_jit_trace_materialize_frames(0, metas.as_ptr()) }
};
assert_eq!(r, -1, "no live Lua frame → helper returns deopt sentinel");
}
#[test]
fn helper_pushes_one_inlined_frame_with_correct_metadata() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let cl = vm.load(WIDE_SRC, b"=t").expect("compile");
vm.jit_ensure_stack(64);
vm.jit_push_inlined_frame(cl, 1, 7, 1);
let frames_before = {
let _g = crate::jit_backend::enter_jit(&mut vm, Some(cl));
drop(_g);
0
};
let _ = frames_before;
let metas = [FrameMaterializeInfo {
base_offset: 5,
pc: 11,
nresults: 1,
}];
let r = {
let _g = crate::jit_backend::enter_jit(&mut vm, Some(cl));
unsafe { super::super::luna_jit_trace_materialize_frames(1, metas.as_ptr()) }
};
assert_eq!(r, 0, "successful push returns 0");
let pushed = vm.jit_last_lua_frame().expect("frame was pushed");
assert_eq!(pushed.base, 6);
assert_eq!(pushed.pc, 11);
assert_eq!(pushed.func_slot, 5);
assert_eq!(pushed.nresults, 1);
assert_eq!(pushed.n_varargs, 0);
}
#[test]
fn per_exit_metas_populated_for_cmp_at_depth_one() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let cl = vm
.load(b"local function f(a,b) return a+b end return f", b"=t")
.expect("compile");
let p = cl.proto.protos[0];
assert!(!p.is_vararg);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 1,
inst: Inst::iabc(Op::Call, 0, 1, 2, false), inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Lt, 0, 1, 0, true),
inline_depth: 1,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 1,
inst: Inst::isj(Op::Jmp, 0),
inline_depth: 1,
var_count: None,
});
rec.closed = true;
let ct = try_compile_trace(vm.jit.storage.as_mut(), &rec).expect("compiles via inline-cmp");
assert_eq!(
ct.per_exit_inline.len(),
1,
"one cmp@d>0 site → one per-exit-inline entry"
);
let info = &ct.per_exit_inline[0];
assert_eq!(info.cont_pc, 2);
assert_eq!(info.chain.len(), 1, "one inlined frame in chain");
let m = info.chain[0];
assert_eq!(m.base_offset, 1);
assert_eq!(m.pc, 2);
assert_eq!(m.nresults, 1);
}
#[test]
fn self_recursive_call_with_multiple_returns_bails() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let p = load_proto(&mut vm, WIDE_SRC);
let mut rec = TraceRecord::start(p, 0, Vec::new(), false);
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Call, 0, 1, 3, false), inline_depth: 0,
var_count: None,
});
rec.push(RecordedOp {
proto: p,
pc: 0,
inst: Inst::iabc(Op::Add, 0, 1, 2, false),
inline_depth: 1,
var_count: None,
});
rec.closed = true;
assert!(try_compile_trace(vm.jit.storage.as_mut(), &rec).is_none());
}
#[test]
fn helper_pushes_multiple_frames_in_order() {
let mut vm = crate::jit_backend::test_vm_new(LuaVersion::Lua55);
let cl = vm.load(WIDE_SRC, b"=t").expect("compile");
vm.jit_ensure_stack(64);
vm.jit_push_inlined_frame(cl, 1, 0, 1);
let metas = [
FrameMaterializeInfo {
base_offset: 3,
pc: 7,
nresults: 1,
},
FrameMaterializeInfo {
base_offset: 8,
pc: 7,
nresults: 1,
},
FrameMaterializeInfo {
base_offset: 13,
pc: 9,
nresults: 1,
},
];
let r = {
let _g = crate::jit_backend::enter_jit(&mut vm, Some(cl));
unsafe { super::super::luna_jit_trace_materialize_frames(3, metas.as_ptr()) }
};
assert_eq!(r, 0);
let inner = vm.jit_last_lua_frame().expect("inner frame");
assert_eq!(inner.base, 1 + 13);
assert_eq!(inner.pc, 9);
}
}
#[cfg(test)]
mod s6_step_a1 {
use super::*;
#[test]
fn regkind_nil_maps_to_exittag_nil() {
let kinds = vec![
RegKind::Unset,
RegKind::Int,
RegKind::Nil,
RegKind::Float,
RegKind::Nil,
];
let tags = kinds_to_exit_tags(&kinds);
assert_eq!(tags.len(), 5);
assert!(matches!(tags[0], ExitTag::Untouched));
assert!(matches!(tags[1], ExitTag::Int));
assert!(matches!(tags[2], ExitTag::Nil));
assert!(matches!(tags[3], ExitTag::Float));
assert!(matches!(tags[4], ExitTag::Nil));
}
}