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::{Linkage, Module};
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)]
struct TraceMathFold {
start_idx: usize,
fn_name: &'static str,
arg_reg: u32,
dst_reg: u32,
}
fn try_match_trace_math_fold(
record: &TraceRecord,
i: usize,
head_proto: Gc<Proto>,
) -> Option<TraceMathFold> {
if i + 3 >= record.ops.len() {
return None;
}
for k in 0..=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 i0 = record.ops[i].inst;
let i1 = record.ops[i + 1].inst;
let i2 = record.ops[i + 2].inst;
let i3 = record.ops[i + 3].inst;
if !matches!(i0.op(), Op::GetTabUp) {
return None;
}
if !matches!(i1.op(), Op::GetField) {
return None;
}
if !matches!(i2.op(), Op::Move) {
return None;
}
if !matches!(i3.op(), Op::Call) {
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 fn_name = MATH_LIBM_FNS
.iter()
.find_map(|&(needle, name)| (needle == fname.as_bytes()).then_some(name))?;
if i2.a() != a + 1 {
return None;
}
let arg_reg = i2.b();
if i3.a() != a || i3.b() != 2 || i3.c() != 2 {
return None;
}
Some(TraceMathFold {
start_idx: i,
fn_name,
arg_reg: arg_reg as u32,
dst_reg: a as u32,
})
}
fn infer_getx_exit(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 => {
if nb == getx_a {
Some(ExitTag::Table)
} else {
None
}
}
Op::SetI | Op::SetTable | Op::SetList => {
if na == getx_a {
Some(ExitTag::Table)
} else {
None
}
}
Op::Len => {
if nb == getx_a {
Some(ExitTag::Table)
} else {
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(
bcx: &mut FunctionBuilder<'_>,
module: &mut JITModule,
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>,
) -> 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 =
bcx.ins().iconst(types::I64, key_str.as_ptr() as i64);
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 {
match record.ops[i].var_count {
Some(n) => n,
None => 0, }
} 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 as u32 {
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);
}
}
}
}
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: JITModule,
_entry_raw: *const u8,
}
unsafe impl Send for TraceHandle {}
thread_local! {
static TRACE_JIT_HANDLES: std::cell::RefCell<Vec<TraceHandle>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[derive(Clone, Copy, Debug)]
enum TraceEnd {
Call,
ForLoop,
InlineAbort,
Return,
SelfLink(SelfRecKind),
}
#[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(
bcx: &mut FunctionBuilder<'_>,
module: &mut JITModule,
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,
}
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(record: &TraceRecord) -> Option<CompiledTrace> {
try_compile_trace_with_options(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) };
}
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 try_compile_trace_with_options(
record: &TraceRecord,
opts: CompileOptions,
) -> Option<CompiledTrace> {
checkpoint("enter");
if !record.closed {
checkpoint("bail:not-closed");
return None;
}
checkpoint("post:closed-check");
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() as u32 + 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 record.side_trace_parent.is_some() {
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() as i32) < 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 (parent_proto, parent_head_pc, _) =
record.side_trace_parent.unwrap();
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 + 3 < n {
if let Some(fold) = try_match_trace_math_fold(record, i, head_proto) {
folded_ops[i] = true;
folded_ops[i + 1] = true;
folded_ops[i + 2] = true;
folded_ops[i + 3] = true;
math_folds.push(fold);
i += 4;
} else {
i += 1;
}
}
}
let end_idx_opt: Option<(usize, TraceEnd)> = 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 + 1 <= 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 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) {
checkpoint("bail:cmp-dirs-GetTabUp");
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 => unreachable!(
"GetTabUp only admitted inside math fold"
),
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.map_or(false, |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.map_or(false, |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.map_or(false, |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 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_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,
);
let mut module = JITModule::new(builder);
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 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 fn_id = module
.declare_function("luna_jit_trace", Linkage::Local, &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<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _global_side_trace_cell_addr =
(&*global_side_trace_box) as *const std::cell::Cell<*const u8>
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);
}
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<std::cell::Cell<*const u8>>,
)> = Vec::new();
let mut per_exit_inline_vec: Vec<(
u32,
u32,
Vec<RegKind>,
std::rc::Rc<[FrameMaterializeInfo]>,
Box<std::cell::Cell<*const u8>>,
)> = 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] {
if let Some(fold) = math_folds.iter().find(|f| f.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_kind = k_op(¤t_kinds, off as u32 + fold.arg_reg);
let arg_f64 = if matches!(arg_kind, RegKind::Float) {
use_var_f64(&mut bcx, ®s, fold.arg_reg)
} else {
let raw = bcx.use_var(regs[fold.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[fold.dst_reg as usize] = RegKind::Float;
}
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, ®s, 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, ®s, 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, ®s, ins.b());
let rhs = use_var_f64(&mut bcx, ®s, 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, ®s, 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, ®s, 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: std::rc::Rc<[FrameMaterializeInfo]> =
snapshot.into();
let chain_ptr = std::rc::Rc::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,
);
materialize_emit_count += mat_count;
let inline_side_box_0: Box<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _inline_side_cell_addr_0 =
(&*inline_side_box_0) as *const std::cell::Cell<*const u8> as i64;
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 = bcx.ins().iconst(types::I64, chain_ptr);
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,
);
materialize_emit_count += mat_count;
let tag_side_box_0: Box<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _tag_side_cell_addr_0 =
(&*tag_side_box_0) as *const std::cell::Cell<*const u8> 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, ®s, ins.a());
let rhs = use_var_f64(&mut bcx, ®s, 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: std::rc::Rc<[FrameMaterializeInfo]> =
snapshot.into();
let chain_ptr = std::rc::Rc::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,
);
materialize_emit_count += mat_count;
let inline_side_box_1: Box<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _inline_side_cell_addr_1 =
(&*inline_side_box_1) as *const std::cell::Cell<*const u8> as i64;
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 = bcx.ins().iconst(types::I64, chain_ptr);
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,
);
materialize_emit_count += mat_count;
let tag_side_box_1: Box<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _tag_side_cell_addr_1 =
(&*tag_side_box_1) as *const std::cell::Cell<*const u8> 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(ins.a(), record.ops[i + 1].inst)
} 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(ins.a(), record.ops[i + 1].inst)
} 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_ptr = key_v.as_ptr() as i64;
let key_arg = bcx.ins().iconst(types::I64, key_ptr);
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_ptr = key_v.as_ptr() as i64;
let key_arg = bcx.ins().iconst(types::I64, key_ptr);
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 = bcx.inst_results(call)[0];
bcx.def_var(regs[ins.a() as usize], v);
let inferred = if i + 1 < effective_end {
infer_getx_exit(ins.a(), record.ops[i + 1].inst)
} 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::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() as u32;
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 JITModule|
-> () {
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, &mut module);
bcx.ins().jump(merge_blk, &[]);
bcx.switch_to_block(merge_blk);
bcx.seal_block(merge_blk);
} else {
emit_helper_call(&mut bcx, &mut 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();
let caller_regs: &[Variable] = ®s_full[..max_stack];
if let Some((_self_link_idx, _kind)) = self_link_idx_opt {
let bump_off: u32 = {
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);
}
}
match last_call_idx {
Some(idx) => {
let caller_offset = op_offsets[idx];
let caller_a = record.ops[idx].inst.a();
caller_offset + caller_a + 1
}
None => 0,
}
};
let bump_off_us = bump_off as usize;
if bump_off_us + max_stack > window_size_us {
checkpoint("bail:self-link-bump-oob");
return None;
}
for i in 0..max_stack {
if bump_off_us > 0 {
let src_val = bcx.use_var(regs_full[bump_off_us + i]);
bcx.def_var(regs_full[i], src_val);
}
}
bcx.ins().jump(body_loop, &[]);
} 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<std::cell::Cell<*const u8>> =
Box::new(std::cell::Cell::new(std::ptr::null()));
let _tag_side_cell_addr_2 =
(&*tag_side_box_2) as *const std::cell::Cell<*const u8> 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()
);
}
module.define_function(fn_id, &mut ctx).ok()?;
module.clear_context(&mut ctx);
module.finalize_definitions().ok()?;
let ptr = module.get_finalized_function(fn_id);
let entry_fn: TraceFn = unsafe { std::mem::transmute::<*const u8, TraceFn>(ptr) };
TRACE_JIT_HANDLES.with(|cell| {
cell.borrow_mut().push(TraceHandle {
_module: module,
_entry_raw: ptr,
});
});
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: std::rc::Rc<[ExitTag]> = exit_tags_vec.into();
let mut tags_side_boxes: Vec<Box<std::cell::Cell<*const u8>>> =
Vec::with_capacity(per_exit_kinds.len());
let per_exit_tags: std::rc::Rc<[(u32, std::rc::Rc<[ExitTag]>)]> = per_exit_kinds
.into_iter()
.map(|(pc, kinds, side_box)| {
let tags: std::rc::Rc<[ExitTag]> = kinds_to_exit_tags(&kinds).into();
tags_side_boxes.push(side_box);
(pc, tags)
})
.collect::<Vec<_>>()
.into();
let tags_side_trace_ptrs: std::rc::Rc<[Box<std::cell::Cell<*const u8>>]> =
tags_side_boxes.into();
let per_exit_inline: std::rc::Rc<[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: std::rc::Rc<[std::cell::Cell<u32>]> = {
let total =
per_exit_inline.len() + per_exit_tags.len() + 1;
let v: Vec<std::cell::Cell<u32>> =
(0..total).map(|_| std::cell::Cell::new(0)).collect();
v.into()
};
let exit_side_trace_ptrs: std::rc::Rc<[std::cell::Cell<*const u8>]> = {
let total =
per_exit_inline.len() + per_exit_tags.len() + 1;
let v: Vec<std::cell::Cell<*const u8>> = (0..total)
.map(|_| std::cell::Cell::new(std::ptr::null()))
.collect();
v.into()
};
Some(CompiledTrace {
head_pc: record.head_pc,
entry: entry_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: std::cell::RefCell::new(
std::collections::HashMap::new(),
),
has_any_side_wired: std::cell::Cell::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(),
})
}
#[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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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(&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,
};
let ct = try_compile_trace_with_options(&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(&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,
};
assert!(try_compile_trace_with_options(&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(&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(&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(&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(&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(&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(&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(&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(&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));
}
}