use super::super::reader::Reader;
use crate::runtime::Value;
use crate::runtime::function::{LocVar, Proto, UpvalDesc};
use crate::runtime::heap::{Gc, GcHeader, Heap, ObjTag};
use crate::vm::isa::{Inst, Op};
const HEADER_LEN: usize = 12;
#[derive(Clone, Copy, Debug)]
struct Pre51Inst {
op: u8,
a: u32,
b: u32,
c: u32,
bx: u32,
sbx: i32,
}
const PRE51_BITRK: u32 = 1 << 8;
const PRE51_MAXARG_BX: u32 = (1 << 18) - 1;
const PRE51_MAXARG_SBX: i32 = (PRE51_MAXARG_BX >> 1) as i32;
fn decode_inst_51(raw: u32) -> Pre51Inst {
let op = (raw & 0x3F) as u8;
let a = (raw >> 6) & 0xFF;
let c = (raw >> 14) & 0x1FF;
let b = (raw >> 23) & 0x1FF;
let bx = (raw >> 14) & PRE51_MAXARG_BX;
let sbx = bx as i32 - PRE51_MAXARG_SBX;
Pre51Inst {
op,
a,
b,
c,
bx,
sbx,
}
}
const OP_MOVE: u8 = 0;
const OP_LOADK: u8 = 1;
const OP_LOADBOOL: u8 = 2;
const OP_LOADNIL: u8 = 3;
const OP_GETUPVAL: u8 = 4;
const OP_GETGLOBAL: u8 = 5;
const OP_GETTABLE: u8 = 6;
const OP_SETGLOBAL: u8 = 7;
const OP_SETUPVAL: u8 = 8;
const OP_SETTABLE: u8 = 9;
const OP_NEWTABLE: u8 = 10;
const OP_SELF: u8 = 11;
const OP_ADD: u8 = 12;
const OP_SUB: u8 = 13;
const OP_MUL: u8 = 14;
const OP_DIV: u8 = 15;
const OP_MOD: u8 = 16;
const OP_POW: u8 = 17;
const OP_UNM: u8 = 18;
const OP_NOT: u8 = 19;
const OP_LEN: u8 = 20;
const OP_CONCAT: u8 = 21;
const OP_JMP: u8 = 22;
const OP_EQ: u8 = 23;
const OP_LT: u8 = 24;
const OP_LE: u8 = 25;
const OP_TEST: u8 = 26;
const OP_TESTSET: u8 = 27;
const OP_CALL: u8 = 28;
const OP_TAILCALL: u8 = 29;
const OP_RETURN: u8 = 30;
const OP_FORLOOP: u8 = 31;
const OP_FORPREP: u8 = 32;
const OP_TFORLOOP: u8 = 33;
const OP_SETLIST: u8 = 34;
const OP_CLOSE: u8 = 35;
const OP_CLOSURE: u8 = 36;
const OP_VARARG: u8 = 37;
pub(in crate::vm::dump) fn undump(bytes: &[u8], heap: &mut Heap) -> Result<Gc<Proto>, String> {
validate_header(bytes)?;
let mut r = Reader::at(bytes, HEADER_LEN);
let proto = r_proto(&mut r, heap, None)?;
if r.pos() != bytes.len() {
return Err(format!(
"trailing bytes in PUC 5.1 chunk (consumed {}, total {})",
r.pos(),
bytes.len()
));
}
Ok(proto)
}
fn validate_header(bytes: &[u8]) -> Result<(), String> {
if bytes.len() < HEADER_LEN {
return Err("truncated PUC 5.1 binary chunk header".to_string());
}
if &bytes[0..4] != b"\x1bLua" {
return Err("bad PUC 5.1 signature".to_string());
}
if bytes[4] != 0x51 {
return Err(format!(
"expected PUC 5.1 version byte 0x51, got 0x{:02x}",
bytes[4]
));
}
if bytes[5] != 0x00 {
return Err(format!(
"unsupported PUC 5.1 format byte 0x{:02x}",
bytes[5]
));
}
if bytes[6] != 0x01 {
return Err("luna only supports little-endian PUC 5.1 chunks".to_string());
}
if bytes[7] != 4 {
return Err(format!("PUC 5.1 sizeof(int) must be 4, got {}", bytes[7]));
}
if bytes[8] != 8 {
return Err(format!(
"PUC 5.1 sizeof(size_t) must be 8, got {}",
bytes[8]
));
}
if bytes[9] != 4 {
return Err(format!(
"PUC 5.1 sizeof(Instruction) must be 4, got {}",
bytes[9]
));
}
if bytes[10] != 8 {
return Err(format!(
"PUC 5.1 sizeof(lua_Number) must be 8, got {}",
bytes[10]
));
}
if bytes[11] != 0 {
return Err(
"luna only supports floating-point PUC 5.1 chunks (integral build rejected)"
.to_string(),
);
}
Ok(())
}
fn r_string_51<'a>(r: &mut Reader<'a>) -> Result<&'a [u8], String> {
let n = u64::from_le_bytes(r.take(8)?.try_into().unwrap()) as usize;
if n == 0 {
return Ok(&[]);
}
let bytes = r.take(n)?;
if let Some((b'\0', rest)) = bytes.split_last() {
Ok(rest)
} else {
Ok(bytes)
}
}
fn r_int_51(r: &mut Reader) -> Result<i32, String> {
Ok(i32::from_le_bytes(r.take(4)?.try_into().unwrap()))
}
fn r_number_51(r: &mut Reader) -> Result<f64, String> {
Ok(f64::from_bits(u64::from_le_bytes(
r.take(8)?.try_into().unwrap(),
)))
}
fn r_const_51(r: &mut Reader, heap: &mut Heap) -> Result<Value, String> {
Ok(match r.u8()? {
0 => Value::Nil,
1 => Value::Bool(r.u8()? != 0),
3 => Value::Float(r_number_51(r)?),
4 => {
let s = r_string_51(r)?;
Value::Str(heap.intern(s))
}
t => return Err(format!("bad PUC 5.1 constant tag {t}")),
})
}
fn r_proto(
r: &mut Reader,
heap: &mut Heap,
parent_env_idx: Option<u8>,
) -> Result<Gc<Proto>, String> {
let source_raw = r_string_51(r)?;
let source = heap.intern(source_raw);
let line_defined = r_int_51(r)?.max(0) as u32;
let last_line_defined = r_int_51(r)?.max(0) as u32;
let nups = r.u8()? as usize;
let num_params = r.u8()?;
let vararg_byte = r.u8()?;
let is_vararg = (vararg_byte & 0x02) != 0;
let has_compat_vararg_arg = (vararg_byte & 0x04) != 0; let max_stack = r.u8()?;
let n_code = r_int_51(r)?.max(0) as usize;
let mut raw_code = Vec::with_capacity(n_code);
for _ in 0..n_code {
raw_code.push(decode_inst_51(r.u32()?));
}
let n_consts = r_int_51(r)?.max(0) as usize;
let mut consts: Vec<Value> = Vec::with_capacity(n_consts);
for _ in 0..n_consts {
consts.push(r_const_51(r, heap)?);
}
let n_protos = r_int_51(r)?.max(0) as usize;
let needs_env = raw_code
.iter()
.any(|i| matches!(i.op, OP_GETGLOBAL | OP_SETGLOBAL));
let synth_env = needs_env || parent_env_idx.is_none();
let env_shift: u8 = if synth_env { 1 } else { 0 };
let mut upvals: Vec<UpvalDesc> = Vec::with_capacity(nups + env_shift as usize);
if synth_env {
let (in_stack, index) = match parent_env_idx {
None => (false, 0), Some(p_env) => (false, p_env), };
upvals.push(UpvalDesc {
in_stack,
index,
name: "_ENV".to_string().into_boxed_str(),
read_only: false,
});
}
for _ in 0..nups {
upvals.push(UpvalDesc {
in_stack: false,
index: 0,
name: "".to_string().into_boxed_str(),
read_only: false,
});
}
let our_env_idx_for_children: Option<u8> = if synth_env { Some(0) } else { parent_env_idx };
let mut protos: Vec<Gc<Proto>> = Vec::with_capacity(n_protos);
let mut child_nups: Vec<usize> = Vec::with_capacity(n_protos);
for _ in 0..n_protos {
let (child, puc_nups) = r_proto_with_puc_nups(r, heap, our_env_idx_for_children)?;
protos.push(child);
child_nups.push(puc_nups);
}
let n_lines = r_int_51(r)?.max(0) as usize;
let mut raw_lines: Vec<u32> = Vec::with_capacity(n_lines);
for _ in 0..n_lines {
raw_lines.push(r_int_51(r)?.max(0) as u32);
}
let n_loc = r_int_51(r)?.max(0) as usize;
let mut locvars: Vec<LocVar> = Vec::with_capacity(n_loc);
for _ in 0..n_loc {
let name = String::from_utf8_lossy(r_string_51(r)?)
.into_owned()
.into_boxed_str();
let start_pc = r_int_51(r)?.max(0) as u32;
let end_pc = r_int_51(r)?.max(0) as u32;
locvars.push(LocVar {
name,
reg: 0,
start_pc,
end_pc,
});
}
let n_upnames = r_int_51(r)?.max(0) as usize;
if n_upnames != nups && n_upnames != 0 {
return Err(format!(
"PUC 5.1 upvalue-name count {n_upnames} ≠ nups {nups}"
));
}
for i in 0..n_upnames {
let name = String::from_utf8_lossy(r_string_51(r)?)
.into_owned()
.into_boxed_str();
upvals[env_shift as usize + i].name = name;
}
let translated = translate_code(
&raw_code,
&raw_lines,
&child_nups,
env_shift,
&mut upvals,
&protos,
consts.len(),
)?;
let max_stack = max_stack.saturating_add(translated.max_temp_bump);
let env_upval_idx = upvals
.iter()
.position(|u| &*u.name == "_ENV")
.map_or(u8::MAX, |i| i as u8);
Ok(heap.adopt_proto(Proto {
hdr: GcHeader::new(ObjTag::Proto),
code: translated.code.into_boxed_slice(),
consts: consts.into_boxed_slice(),
protos: protos.into_boxed_slice(),
upvals: upvals.into_boxed_slice(),
num_params,
is_vararg,
has_vararg_table_pseudo: false,
has_compat_vararg_arg,
max_stack,
lines: translated.lines.into_boxed_slice(),
source,
line_defined,
last_line_defined,
locvars: locvars.into_boxed_slice(),
cache: std::cell::Cell::new(None),
jit: std::cell::Cell::new(crate::runtime::function::JitProtoState::Untried),
env_upval_idx,
trace_hot_count: std::cell::Cell::new(0),
call_hot_count: std::cell::Cell::new(0),
trace_discard_count: std::cell::Cell::new(0),
trace_gave_up: std::cell::Cell::new(false),
traces: crate::jit::send_compat::TRefLock::new(Vec::new()),
}))
}
fn r_proto_with_puc_nups(
r: &mut Reader,
heap: &mut Heap,
parent_env_idx: Option<u8>,
) -> Result<(Gc<Proto>, usize), String> {
let saved = r.pos();
let _src = r_string_51(r)?;
let _ld = r_int_51(r)?;
let _lld = r_int_51(r)?;
let nups = r.u8()? as usize;
let rewind_bytes = r.peek_underlying_slice();
let mut rewound = Reader::at(rewind_bytes, saved);
let proto = r_proto(&mut rewound, heap, parent_env_idx)?;
let new_pos = rewound.pos();
r.skip_to(new_pos)?;
Ok((proto, nups))
}
struct Translated {
code: Vec<Inst>,
lines: Vec<u32>,
#[allow(dead_code)] puc_to_luna_pc: Vec<Option<u32>>,
max_temp_bump: u8,
}
fn translate_code(
raw_code: &[Pre51Inst],
raw_lines: &[u32],
child_nups: &[usize],
env_shift: u8,
upvals: &mut [UpvalDesc],
protos: &[Gc<Proto>],
n_consts: usize,
) -> Result<Translated, String> {
let mut out: Vec<Inst> = Vec::with_capacity(raw_code.len());
let mut out_lines: Vec<u32> = Vec::with_capacity(raw_code.len());
let mut puc_to_luna_pc: Vec<Option<u32>> = vec![None; raw_code.len()];
let mut jump_fixups: Vec<(usize, i64, JumpKind)> = Vec::new();
let mut max_temp_bump: u8 = 0;
let mut closure_idx2 = 0usize;
let mut i = 0usize;
while i < raw_code.len() {
let inst = raw_code[i];
let line = raw_lines.get(i).copied().unwrap_or(0);
let pre_emit_len = out.len();
let up = |raw_idx: u32| -> Result<u32, String> {
let shifted = raw_idx + env_shift as u32;
if shifted > 0xFF {
return Err(format!("upvalue index {shifted} > 255 after _ENV synth"));
}
Ok(shifted)
};
let rk = |raw_field: u32| -> Result<(u32, bool), String> {
if raw_field & PRE51_BITRK != 0 {
let k_idx = raw_field & 0xFF;
if k_idx as usize >= n_consts {
return Err(format!("RK const index {k_idx} out of range"));
}
Ok((k_idx, true))
} else {
if raw_field > 0xFF {
return Err(format!("register index {raw_field} > 255"));
}
Ok((raw_field, false))
}
};
match inst.op {
OP_MOVE => {
out.push(Inst::iabc(Op::Move, inst.a, inst.b, 0, false));
}
OP_LOADK => {
if inst.bx > crate::vm::isa::MAX_BX {
return Err(format!("LOADK Bx {} exceeds luna MAX_BX", inst.bx));
}
out.push(Inst::iabx(Op::LoadK, inst.a, inst.bx));
}
OP_LOADBOOL => {
match (inst.b != 0, inst.c != 0) {
(false, false) => {
out.push(Inst::iabc(Op::LoadFalse, inst.a, 0, 0, false));
}
(false, true) => {
out.push(Inst::iabc(Op::LFalseSkip, inst.a, 0, 0, false));
}
(true, false) => {
out.push(Inst::iabc(Op::LoadTrue, inst.a, 0, 0, false));
}
(true, true) => {
out.push(Inst::iabc(Op::LoadTrue, inst.a, 0, 0, false));
out.push(Inst::isj(Op::Jmp, 1));
}
}
}
OP_LOADNIL => {
if inst.b < inst.a {
return Err(format!(
"LOADNIL A={} > B={} (illegal 5.1 range)",
inst.a, inst.b
));
}
let count_minus_1 = inst.b - inst.a;
out.push(Inst::iabc(Op::LoadNil, inst.a, count_minus_1, 0, false));
}
OP_GETUPVAL => {
let b = up(inst.b)?;
out.push(Inst::iabc(Op::GetUpval, inst.a, b, 0, false));
}
OP_SETUPVAL => {
let b = up(inst.b)?;
out.push(Inst::iabc(Op::SetUpval, inst.a, b, 0, false));
}
OP_GETGLOBAL => {
let env_idx = 0u32; if inst.bx > 0xFF {
return Err(format!(
"GETGLOBAL Bx {} > 255 (ExtraArg unsupported)",
inst.bx
));
}
out.push(Inst::iabc(Op::GetTabUp, inst.a, env_idx, inst.bx, false));
}
OP_SETGLOBAL => {
let env_idx = 0u32;
if inst.bx > 0xFF {
return Err(format!(
"SETGLOBAL Bx {} > 255 (ExtraArg unsupported)",
inst.bx
));
}
out.push(Inst::iabc(Op::SetTabUp, env_idx, inst.bx, inst.a, false));
}
OP_GETTABLE => {
let (c_val, c_is_k) = rk(inst.c)?;
let op = if c_is_k { Op::GetField } else { Op::GetTable };
out.push(Inst::iabc(op, inst.a, inst.b, c_val, c_is_k));
}
OP_SETTABLE => {
let (b_val, b_is_k) = rk(inst.b)?;
let (c_val, c_is_k) = rk(inst.c)?;
let op = if b_is_k { Op::SetField } else { Op::SetTable };
out.push(Inst::iabc(op, inst.a, b_val, c_val, c_is_k));
}
OP_NEWTABLE => {
let b = fb2int_saturating(inst.b);
let c = fb2int_saturating(inst.c);
out.push(Inst::iabc(Op::NewTable, inst.a, b, c, false));
}
OP_SELF => {
let (c_val, c_is_k) = rk(inst.c)?;
out.push(Inst::iabc(Op::SelfOp, inst.a, inst.b, c_val, c_is_k));
}
OP_ADD => arith(&mut out, Op::Add, inst, &rk, &mut max_temp_bump)?,
OP_SUB => arith(&mut out, Op::Sub, inst, &rk, &mut max_temp_bump)?,
OP_MUL => arith(&mut out, Op::Mul, inst, &rk, &mut max_temp_bump)?,
OP_DIV => arith(&mut out, Op::Div, inst, &rk, &mut max_temp_bump)?,
OP_MOD => arith(&mut out, Op::Mod, inst, &rk, &mut max_temp_bump)?,
OP_POW => arith(&mut out, Op::Pow, inst, &rk, &mut max_temp_bump)?,
OP_UNM => {
out.push(Inst::iabc(Op::Unm, inst.a, inst.b, 0, false));
}
OP_NOT => {
out.push(Inst::iabc(Op::Not, inst.a, inst.b, 0, false));
}
OP_LEN => {
out.push(Inst::iabc(Op::Len, inst.a, inst.b, 0, false));
}
OP_CONCAT => {
if inst.b != inst.a {
return Err(format!(
"OP_CONCAT B={} ≠ A={} (5.1→luna concat requires B==A)",
inst.b, inst.a
));
}
if inst.c < inst.b {
return Err(format!("OP_CONCAT C={} < B={} (illegal)", inst.c, inst.b));
}
let count = inst.c - inst.b + 1;
out.push(Inst::iabc(Op::Concat, inst.a, count, 0, false));
}
OP_JMP => {
let target_old = (i as i64) + 1 + inst.sbx as i64;
jump_fixups.push((out.len(), target_old, JumpKind::Jmp));
out.push(Inst::isj(Op::Jmp, 0));
}
OP_EQ => compare(&mut out, Op::Eq, inst, &rk, i, &mut max_temp_bump)?,
OP_LT => compare(&mut out, Op::Lt, inst, &rk, i, &mut max_temp_bump)?,
OP_LE => compare(&mut out, Op::Le, inst, &rk, i, &mut max_temp_bump)?,
OP_TEST => {
let k = inst.c == 0;
out.push(Inst::iabc(Op::Test, inst.a, 0, 0, k));
}
OP_TESTSET => {
let k = inst.c == 0;
out.push(Inst::iabc(Op::TestSet, inst.a, inst.b, 0, k));
}
OP_CALL => {
out.push(Inst::iabc(Op::Call, inst.a, inst.b, inst.c, false));
}
OP_TAILCALL => {
out.push(Inst::iabc(Op::TailCall, inst.a, inst.b, inst.c, false));
}
OP_RETURN => {
out.push(Inst::iabc(Op::Return, inst.a, inst.b, 0, false));
}
OP_FORLOOP => {
let target_old = (i as i64) + 1 + inst.sbx as i64;
jump_fixups.push((out.len(), target_old, JumpKind::ForLoop(inst.a)));
out.push(Inst::iasbx(Op::ForLoop, inst.a, 0));
}
OP_FORPREP => {
let target_old = (i as i64) + 1 + inst.sbx as i64;
jump_fixups.push((out.len(), target_old, JumpKind::ForPrep(inst.a)));
out.push(Inst::iasbx(Op::ForPrep, inst.a, 0));
}
OP_TFORLOOP => {
if inst.c > 0xFF {
return Err(format!("OP_TFORLOOP C={} > 255", inst.c));
}
if i + 1 >= raw_code.len() {
return Err("OP_TFORLOOP at end of code (missing trailing JMP)".into());
}
let jmp = raw_code[i + 1];
if jmp.op != OP_JMP {
return Err(format!(
"OP_TFORLOOP at pc {i} not followed by JMP (got op {})",
jmp.op
));
}
let target_old = (i as i64) + 2 + jmp.sbx as i64;
out.push(Inst::iabc(Op::TForCall, inst.a, 0, inst.c, false));
puc_to_luna_pc[i] = Some(pre_emit_len as u32);
out_lines.push(line);
let tforloop_luna_pc = out.len();
let jmp_line = raw_lines.get(i + 1).copied().unwrap_or(line);
out.push(Inst::iabx(Op::TForLoop, inst.a, 0));
out_lines.push(jmp_line);
puc_to_luna_pc[i + 1] = Some(tforloop_luna_pc as u32);
jump_fixups.push((tforloop_luna_pc, target_old, JumpKind::TForLoop(inst.a)));
i += 2;
continue;
}
OP_SETLIST => {
if inst.c > 0xFF {
return Err(format!("OP_SETLIST C={} > 255", inst.c));
}
if inst.c == 0 {
if i + 1 >= raw_code.len() {
return Err("OP_SETLIST C=0 at end of code (missing C payload)".into());
}
let payload_inst = raw_code[i + 1];
let payload = reconstruct_raw_u32(payload_inst);
if payload > crate::vm::isa::MAX_AX {
return Err(format!("OP_SETLIST payload {payload} > luna MAX_AX"));
}
out.push(Inst::iabc(Op::SetList, inst.a, inst.b, 0, true));
out.push(Inst::iax(Op::ExtraArg, payload));
puc_to_luna_pc[i] = Some(pre_emit_len as u32);
out_lines.push(line);
out_lines.push(line);
puc_to_luna_pc[i + 1] = None;
i += 2;
continue;
} else {
out.push(Inst::iabc(Op::SetList, inst.a, inst.b, inst.c, false));
}
}
OP_CLOSE => {
out.push(Inst::iabc(Op::Close, inst.a, 0, 0, false));
}
OP_CLOSURE => {
if inst.bx as usize >= protos.len() {
return Err(format!(
"OP_CLOSURE proto index {} out of range (have {})",
inst.bx,
protos.len()
));
}
if closure_idx2 >= child_nups.len() {
return Err("CLOSURE/pseudo count mismatch (pass 2)".into());
}
let n = child_nups[closure_idx2];
closure_idx2 += 1;
let child = protos[inst.bx as usize];
let child_upvals = unsafe { &mut child.as_ptr().as_mut().unwrap().upvals };
let child_env_shift: u8 =
if !child_upvals.is_empty() && &*child_upvals[0].name == "_ENV" {
1
} else {
0
};
if child_upvals.len() < child_env_shift as usize + n {
return Err(format!(
"child upval slots {} < env_shift {} + pseudo {}",
child_upvals.len(),
child_env_shift,
n
));
}
for j in 0..n {
let pseudo = raw_code[i + 1 + j];
let (in_stack, src_idx) = match pseudo.op {
OP_MOVE => (true, pseudo.b),
OP_GETUPVAL => {
(false, pseudo.b + env_shift as u32)
}
other => {
return Err(format!(
"OP_CLOSURE pseudo-instruction must be MOVE/GETUPVAL, got op {other}"
));
}
};
if src_idx > 0xFF {
return Err(format!("pseudo upval index {src_idx} > 255"));
}
let slot = child_env_shift as usize + j;
let existing_name = std::mem::take(&mut child_upvals[slot].name);
child_upvals[slot] = UpvalDesc {
in_stack,
index: src_idx as u8,
name: existing_name,
read_only: false,
};
}
out.push(Inst::iabx(Op::Closure, inst.a, inst.bx));
for j in 1..=n {
puc_to_luna_pc[i + j] = None;
}
i += n;
}
OP_VARARG => {
out.push(Inst::iabc(Op::Vararg, inst.a, inst.b, 0, false));
}
other => {
return Err(format!("unsupported PUC 5.1 op {other}"));
}
}
if out.len() > pre_emit_len {
puc_to_luna_pc[i] = Some(pre_emit_len as u32);
for _ in pre_emit_len..out.len() {
out_lines.push(line);
}
}
i += 1;
}
debug_assert_eq!(
out.len(),
out_lines.len(),
"line count must match emit count"
);
let end_of_code = out.len();
for (luna_pc, target_old, kind) in jump_fixups {
let target_new = resolve_jump_target(&puc_to_luna_pc, target_old, end_of_code)?;
let delta = target_new - (luna_pc as i64 + 1);
match kind {
JumpKind::Jmp => {
if !(-crate::vm::isa::MAX_SJ as i64..=crate::vm::isa::MAX_SJ as i64)
.contains(&delta)
{
return Err(format!("JMP delta {delta} exceeds luna sJ range"));
}
out[luna_pc] = Inst::isj(Op::Jmp, delta as i32);
}
JumpKind::ForLoop(a) => {
if !((-crate::vm::isa::MAX_SBX as i64)..=(crate::vm::isa::MAX_SBX as i64))
.contains(&delta)
{
return Err(format!("FORLOOP delta {delta} exceeds luna sBx range"));
}
out[luna_pc] = Inst::iasbx(Op::ForLoop, a, delta as i32);
}
JumpKind::ForPrep(a) => {
if !((-crate::vm::isa::MAX_SBX as i64)..=(crate::vm::isa::MAX_SBX as i64))
.contains(&delta)
{
return Err(format!("FORPREP delta {delta} exceeds luna sBx range"));
}
out[luna_pc] = Inst::iasbx(Op::ForPrep, a, delta as i32);
}
JumpKind::TForLoop(a) => {
if delta > 0 {
return Err(format!(
"TFORLOOP forward delta {delta} (expected backward jump)"
));
}
let back = -delta;
if back < 0 || (back as u32) > crate::vm::isa::MAX_BX {
return Err(format!(
"TFORLOOP back-distance {back} exceeds luna Bx range"
));
}
out[luna_pc] = Inst::iabx(Op::TForLoop, a, back as u32);
}
}
}
let _ = upvals;
Ok(Translated {
code: out,
lines: out_lines,
puc_to_luna_pc,
max_temp_bump,
})
}
#[derive(Clone, Copy, Debug)]
enum JumpKind {
Jmp,
ForLoop(u32),
ForPrep(u32),
TForLoop(u32),
}
fn arith<F>(
out: &mut Vec<Inst>,
op: Op,
inst: Pre51Inst,
rk: &F,
max_temp_bump: &mut u8,
) -> Result<(), String>
where
F: Fn(u32) -> Result<(u32, bool), String>,
{
let (b_val, b_is_k) = rk(inst.b)?;
let (c_val, c_is_k) = rk(inst.c)?;
if b_is_k {
let tmp = inst.a.max(c_val) + 1;
let pair = super::lower_k_via_tmp(op, inst.a, b_val, c_val, c_is_k, tmp, max_temp_bump)?;
out.extend_from_slice(&pair);
} else {
out.push(Inst::iabc(op, inst.a, b_val, c_val, c_is_k));
}
Ok(())
}
fn compare<F>(
out: &mut Vec<Inst>,
op: Op,
inst: Pre51Inst,
rk: &F,
_src_pc: usize,
max_temp_bump: &mut u8,
) -> Result<(), String>
where
F: Fn(u32) -> Result<(u32, bool), String>,
{
let (b_val, b_is_k) = rk(inst.b)?;
let (c_val, c_is_k) = rk(inst.c)?;
let k_flag = inst.a != 0;
let needs_tmp_b = b_is_k;
let needs_tmp_c = c_is_k;
if needs_tmp_b && needs_tmp_c {
let base = b_val.max(c_val) + 1; let _ = base;
let tmp_b = (*max_temp_bump) as u32;
let tmp_c = tmp_b + 1;
if tmp_c > 0xFF {
return Err(format!(
"5.1 compare RK lowering: tmp register {tmp_c} exceeds 255"
));
}
out.push(Inst::iabx(Op::LoadK, tmp_b, b_val));
out.push(Inst::iabx(Op::LoadK, tmp_c, c_val));
*max_temp_bump = (*max_temp_bump).max(tmp_c as u8 + 1);
out.push(Inst::iabc(op, tmp_b, tmp_c, 0, k_flag));
} else if needs_tmp_b {
let tmp = (*max_temp_bump) as u32;
if tmp > 0xFF {
return Err(format!(
"5.1 compare RK lowering: tmp register {tmp} exceeds 255"
));
}
out.push(Inst::iabx(Op::LoadK, tmp, b_val));
*max_temp_bump = (*max_temp_bump).max(tmp as u8 + 1);
out.push(Inst::iabc(op, tmp, c_val, 0, k_flag));
} else if needs_tmp_c {
let tmp = (*max_temp_bump) as u32;
if tmp > 0xFF {
return Err(format!(
"5.1 compare RK lowering: tmp register {tmp} exceeds 255"
));
}
out.push(Inst::iabx(Op::LoadK, tmp, c_val));
*max_temp_bump = (*max_temp_bump).max(tmp as u8 + 1);
out.push(Inst::iabc(op, b_val, tmp, 0, k_flag));
} else {
out.push(Inst::iabc(op, b_val, c_val, 0, k_flag));
}
Ok(())
}
fn resolve_jump_target(
puc_to_luna_pc: &[Option<u32>],
target_old: i64,
end_of_code: usize,
) -> Result<i64, String> {
if target_old < 0 {
return Err(format!("jump target {target_old} out of range"));
}
let t = target_old as usize;
if t == puc_to_luna_pc.len() {
return Ok(end_of_code as i64);
}
if t > puc_to_luna_pc.len() {
return Err(format!("jump target {target_old} out of range"));
}
match puc_to_luna_pc[t] {
Some(p) => Ok(p as i64),
None => Err(format!(
"jump target {target_old} lands on stripped pseudo-instruction"
)),
}
}
fn reconstruct_raw_u32(p: Pre51Inst) -> u32 {
(p.op as u32 & 0x3F) | ((p.a & 0xFF) << 6) | ((p.c & 0x1FF) << 14) | ((p.b & 0x1FF) << 23)
}
fn fb2int_saturating(fb: u32) -> u32 {
let e = (fb >> 3) & 0x1F;
let x = fb & 0x07;
let v = if e == 0 { x } else { (x | 0x08) << (e - 1) };
v.min(0xFF)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fb2int_basic() {
assert_eq!(fb2int_saturating(0), 0);
assert_eq!(fb2int_saturating(7), 7);
assert_eq!(fb2int_saturating(8), 8); assert_eq!(fb2int_saturating(0b0000_1111), 15); }
#[test]
fn fb2int_saturates_at_u8_max() {
let fb = 4u32 << 3;
assert_eq!(fb2int_saturating(fb), 64);
let fb = 5u32 << 3;
assert_eq!(fb2int_saturating(fb), 128);
let fb = (5u32 << 3) | 7;
assert_eq!(fb2int_saturating(fb), 240);
let fb = 6u32 << 3;
assert_eq!(fb2int_saturating(fb), 0xFF);
let fb = (31u32 << 3) | 7;
assert_eq!(fb2int_saturating(fb), 0xFF);
}
#[test]
fn translate_newtable_fb_hint() {
let code = xlate(&[p51(OP_NEWTABLE, 4, 0x08, 0x0F)]);
assert_eq!(code.len(), 1);
assert_eq!(code[0].op(), Op::NewTable);
assert_eq!(code[0].a(), 4);
assert_eq!(code[0].b(), 8);
assert_eq!(code[0].c(), 15);
}
#[test]
fn translate_newtable_fb_hint_saturates() {
let large_fb = 6u32 << 3; let code = xlate(&[p51(OP_NEWTABLE, 0, 0x08, large_fb)]);
assert_eq!(code.len(), 1);
assert_eq!(code[0].op(), Op::NewTable);
assert_eq!(code[0].b(), 8);
assert_eq!(code[0].c(), 0xFF);
}
#[test]
fn decode_inst_51_fields() {
let raw: u32 = (3u32 << 6) | (5u32 << 23);
let i = decode_inst_51(raw);
assert_eq!(i.op, 0);
assert_eq!(i.a, 3);
assert_eq!(i.b, 5);
assert_eq!(i.c, 0);
}
#[test]
fn reconstruct_raw_u32_round_trips_through_decode() {
let original: u32 = 10u32 | (200u32 << 6) | (0x055u32 << 14) | (0x1ABu32 << 23);
let p = decode_inst_51(original);
let rebuilt = reconstruct_raw_u32(p);
assert_eq!(rebuilt, original, "decode → reconstruct must be lossless");
}
#[test]
fn reconstruct_raw_u32_handles_setlist_payload_int() {
let payload: u32 = 12345;
let p = decode_inst_51(payload);
assert_eq!(reconstruct_raw_u32(p), payload);
}
#[test]
fn resolve_jump_target_accepts_one_past_end() {
let map = vec![Some(0), Some(1), Some(2)];
assert_eq!(resolve_jump_target(&map, 3, 5).unwrap(), 5);
assert_eq!(resolve_jump_target(&map, 1, 5).unwrap(), 1);
}
#[test]
fn resolve_jump_target_rejects_stripped_pseudo() {
let map = vec![Some(0), None, Some(1)];
let err = resolve_jump_target(&map, 1, 2).unwrap_err();
assert!(err.contains("stripped pseudo-instruction"), "got: {err}");
}
#[test]
fn resolve_jump_target_rejects_out_of_range() {
let map = vec![Some(0), Some(1)];
let err = resolve_jump_target(&map, 100, 2).unwrap_err();
assert!(err.contains("out of range"), "got: {err}");
let err = resolve_jump_target(&map, -1, 2).unwrap_err();
assert!(err.contains("out of range"), "got: {err}");
}
fn p51(op: u8, a: u32, b: u32, c: u32) -> Pre51Inst {
let bx = (c << 9) | b;
Pre51Inst {
op,
a,
b,
c,
bx,
sbx: bx as i32 - 131071,
}
}
fn xlate(raw: &[Pre51Inst]) -> Vec<Inst> {
let lines = vec![0u32; raw.len()];
let mut upvals: Vec<UpvalDesc> = Vec::new();
let protos: Vec<Gc<Proto>> = Vec::new();
let t = translate_code(raw, &lines, &[], 0, &mut upvals, &protos, 256)
.expect("translate_code must succeed for these fixtures");
t.code
}
#[test]
fn translate_loadbool_false_noskip() {
let code = xlate(&[p51(OP_LOADBOOL, 0, 0, 0)]);
assert_eq!(code.len(), 1);
assert_eq!(code[0].op(), Op::LoadFalse);
assert_eq!(code[0].a(), 0);
}
#[test]
fn translate_loadbool_true_noskip() {
let code = xlate(&[p51(OP_LOADBOOL, 3, 1, 0)]);
assert_eq!(code.len(), 1);
assert_eq!(code[0].op(), Op::LoadTrue);
assert_eq!(code[0].a(), 3);
}
#[test]
fn translate_loadbool_false_skip() {
let code = xlate(&[p51(OP_LOADBOOL, 2, 0, 1)]);
assert_eq!(code.len(), 1);
assert_eq!(code[0].op(), Op::LFalseSkip);
assert_eq!(code[0].a(), 2);
}
#[test]
fn translate_loadbool_true_skip() {
let code = xlate(&[p51(OP_LOADBOOL, 5, 1, 1), p51(OP_MOVE, 0, 0, 0)]);
assert_eq!(code.len(), 3, "true+skip lowers to 2 insts then MOVE");
assert_eq!(code[0].op(), Op::LoadTrue);
assert_eq!(code[0].a(), 5);
assert_eq!(code[1].op(), Op::Jmp);
assert_eq!(code[1].sj(), 1);
assert_eq!(code[2].op(), Op::Move);
}
}