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::{self, Inst, Op};
const HEADER_52: &[u8] = &[
0x1b, b'L', b'u', b'a', 0x52, 0x00, 0x01, 0x04, 0x08, 0x04, 0x08, 0x00, 0x19, 0x93, b'\r', b'\n', 0x1a, b'\n',
];
const P_MOVE: u8 = 0;
const P_LOADK: u8 = 1;
const P_LOADKX: u8 = 2;
const P_LOADBOOL: u8 = 3;
const P_LOADNIL: u8 = 4;
const P_GETUPVAL: u8 = 5;
const P_GETTABUP: u8 = 6;
const P_GETTABLE: u8 = 7;
const P_SETTABUP: u8 = 8;
const P_SETUPVAL: u8 = 9;
const P_SETTABLE: u8 = 10;
const P_NEWTABLE: u8 = 11;
const P_SELF: u8 = 12;
const P_ADD: u8 = 13;
const P_SUB: u8 = 14;
const P_MUL: u8 = 15;
const P_DIV: u8 = 16;
const P_MOD: u8 = 17;
const P_POW: u8 = 18;
const P_UNM: u8 = 19;
const P_NOT: u8 = 20;
const P_LEN: u8 = 21;
const P_CONCAT: u8 = 22;
const P_JMP: u8 = 23;
const P_EQ: u8 = 24;
const P_LT: u8 = 25;
const P_LE: u8 = 26;
const P_TEST: u8 = 27;
const P_TESTSET: u8 = 28;
const P_CALL: u8 = 29;
const P_TAILCALL: u8 = 30;
const P_RETURN: u8 = 31;
const P_FORLOOP: u8 = 32;
const P_FORPREP: u8 = 33;
const P_TFORCALL: u8 = 34;
const P_TFORLOOP: u8 = 35;
const P_SETLIST: u8 = 36;
const P_CLOSURE: u8 = 37;
const P_VARARG: u8 = 38;
const P_EXTRAARG: u8 = 39;
const RK_BIT: u32 = 1 << 8;
#[derive(Clone, Copy, Debug)]
struct PucInst {
op: u8,
a: u32,
b: u32, c: u32, }
impl PucInst {
fn decode(raw: u32) -> PucInst {
let op = (raw & 0x3F) as u8;
let a = (raw >> 6) & 0xFF;
let c = (raw >> 14) & 0x1FF;
let b = (raw >> 23) & 0x1FF;
PucInst { op, a, b, c }
}
fn bx(self) -> u32 {
(self.b << 9) | self.c
}
fn sbx(self) -> i32 {
self.bx() as i32 - 131071
}
fn ax(self) -> u32 {
(self.b << 17) | (self.c << 8) | self.a
}
}
fn decode_rk(field: u32) -> Result<(u8, bool), String> {
let is_k = (field & RK_BIT) != 0;
let idx = field & 0xFF;
if (field & !RK_BIT) > 0xFF {
return Err(format!("PUC 5.2 RK index out of range: {field}"));
}
Ok((idx as u8, is_k))
}
fn fb2int(x: u32) -> u32 {
let e = (x >> 3) & 0x1F;
if e == 0 {
x
} else {
((x & 7) + 8) << (e - 1)
}
}
fn fb_to_hint_u8(x: u32) -> u32 {
let n = fb2int(x);
n.min(0xFF)
}
pub(super) fn undump(bytes: &[u8], heap: &mut Heap) -> Result<Gc<Proto>, String> {
if bytes.len() < HEADER_52.len() {
return Err("truncated PUC 5.2 binary chunk (header)".to_string());
}
if &bytes[..HEADER_52.len()] != HEADER_52 {
if &bytes[..4] != b"\x1bLua" {
return Err("not a PUC binary chunk (bad signature)".to_string());
}
if bytes[4] != 0x52 {
return Err(format!(
"PUC 5.2 loader: version byte 0x{:02x} != 0x52",
bytes[4]
));
}
if bytes[6] != 0x01 {
return Err("PUC 5.2 loader: only little-endian chunks supported".to_string());
}
if bytes[7] != 0x04 {
return Err(format!(
"PUC 5.2 loader: expected sizeof(int)=4, got {}",
bytes[7]
));
}
if bytes[8] != 0x08 {
return Err(format!(
"PUC 5.2 loader: expected sizeof(size_t)=8, got {}",
bytes[8]
));
}
if bytes[9] != 0x04 {
return Err(format!(
"PUC 5.2 loader: expected sizeof(Instruction)=4, got {}",
bytes[9]
));
}
if bytes[10] != 0x08 {
return Err(format!(
"PUC 5.2 loader: expected sizeof(lua_Number)=8, got {}",
bytes[10]
));
}
if bytes[11] != 0x00 {
return Err(
"PUC 5.2 loader: integral lua_Number not supported (expected float)".to_string(),
);
}
return Err("PUC 5.2 loader: header tail mismatch".to_string());
}
let mut r = Reader::at(bytes, HEADER_52.len());
let proto = read_function(&mut r, heap, None)?;
if r.pos() != bytes.len() {
return Err(format!(
"PUC 5.2 loader: trailing bytes (read {} of {})",
r.pos(),
bytes.len()
));
}
Ok(proto)
}
fn load_size(r: &mut Reader) -> Result<u64, String> {
Ok(u64::from_le_bytes(r.take(8)?.try_into().unwrap()))
}
fn load_int(r: &mut Reader) -> Result<i32, String> {
let v = i32::from_le_bytes(r.take(4)?.try_into().unwrap());
if v < 0 {
return Err("PUC 5.2 loader: corrupt negative int".to_string());
}
Ok(v)
}
fn load_byte(r: &mut Reader) -> Result<u8, String> {
r.u8()
}
fn load_number(r: &mut Reader) -> Result<f64, String> {
Ok(f64::from_bits(u64::from_le_bytes(
r.take(8)?.try_into().unwrap(),
)))
}
fn load_string<'a>(r: &mut Reader<'a>) -> Result<Option<&'a [u8]>, String> {
let n = load_size(r)? as usize;
if n == 0 {
return Ok(None);
}
let raw = r.take(n)?;
if raw.last() != Some(&0) {
return Err("PUC 5.2 loader: string missing trailing NUL".to_string());
}
Ok(Some(&raw[..n - 1]))
}
fn load_constants(r: &mut Reader, heap: &mut Heap) -> Result<Box<[Value]>, String> {
let n = load_int(r)? as usize;
let mut consts = Vec::with_capacity(n);
for _ in 0..n {
let tag = load_byte(r)?;
let v = match tag {
0 => Value::Nil,
1 => Value::Bool(load_byte(r)? != 0),
3 => Value::Float(load_number(r)?),
4 => {
let s = load_string(r)?.unwrap_or(b"");
Value::Str(heap.intern(s))
}
t => return Err(format!("PUC 5.2 loader: bad constant tag {t}")),
};
consts.push(v);
}
Ok(consts.into_boxed_slice())
}
fn load_upvalues(r: &mut Reader) -> Result<Vec<UpvalDesc>, String> {
let n = load_int(r)? as usize;
let mut upvals = Vec::with_capacity(n);
for _ in 0..n {
let in_stack = load_byte(r)? != 0;
let index = load_byte(r)?;
upvals.push(UpvalDesc {
in_stack,
index,
name: String::new().into(),
read_only: false,
});
}
Ok(upvals)
}
fn load_debug(
r: &mut Reader,
heap: &mut Heap,
upvals: &mut [UpvalDesc],
) -> Result<
(
Gc<crate::runtime::string::LuaStr>,
Box<[u32]>,
Box<[LocVar]>,
),
String,
> {
let source_bytes = load_string(r)?.unwrap_or(b"");
let source = heap.intern(source_bytes);
let n = load_int(r)? as usize;
let mut lines = Vec::with_capacity(n);
for _ in 0..n {
let raw = i32::from_le_bytes(r.take(4)?.try_into().unwrap());
lines.push(raw.max(0) as u32);
}
let n = load_int(r)? as usize;
let mut locvars = Vec::with_capacity(n);
for _ in 0..n {
let name = load_string(r)?.unwrap_or(b"");
let start_pc = load_int(r)? as u32;
let end_pc = load_int(r)? as u32;
locvars.push(LocVar {
name: String::from_utf8_lossy(name).into_owned().into(),
reg: 0,
start_pc,
end_pc,
});
}
let n = load_int(r)? as usize;
if n != upvals.len() && n != 0 {
return Err(format!(
"PUC 5.2 loader: upvalue-name count {n} != upvalue count {}",
upvals.len()
));
}
for i in 0..n {
let name = load_string(r)?.unwrap_or(b"");
upvals[i].name = String::from_utf8_lossy(name).into_owned().into();
}
Ok((source, lines.into_boxed_slice(), locvars.into_boxed_slice()))
}
fn read_function(
r: &mut Reader,
heap: &mut Heap,
parent_source: Option<Gc<crate::runtime::string::LuaStr>>,
) -> Result<Gc<Proto>, String> {
let line_defined = load_int(r)? as u32;
let last_line_defined = load_int(r)? as u32;
let num_params = load_byte(r)?;
let is_vararg = load_byte(r)? != 0;
let max_stack = load_byte(r)?;
let n = load_int(r)? as usize;
let mut raw_code = Vec::with_capacity(n);
for _ in 0..n {
raw_code.push(u32::from_le_bytes(r.take(4)?.try_into().unwrap()));
}
let consts = load_constants(r, heap)?;
let n = load_int(r)? as usize;
let mut protos = Vec::with_capacity(n);
for _ in 0..n {
protos.push(read_function(r, heap, parent_source)?);
}
let mut upvals = load_upvalues(r)?;
let (mut source, lines, locvars) = load_debug(r, heap, &mut upvals)?;
if source.as_bytes().is_empty()
&& let Some(p) = parent_source
{
source = p;
}
let (code, translated_lines, max_temp_bump) = translate_code(&raw_code, &lines, &consts)?;
let max_stack = max_stack.saturating_add(max_temp_bump);
let env_upval_idx = upvals
.iter()
.take(u8::MAX as usize)
.position(|u| &*u.name == "_ENV")
.map_or(u8::MAX, |i| i as u8);
Ok(heap.adopt_proto(Proto {
hdr: GcHeader::new(ObjTag::Proto),
code: code.into_boxed_slice(),
consts,
protos: protos.into_boxed_slice(),
upvals: upvals.into_boxed_slice(),
num_params,
is_vararg,
has_vararg_table_pseudo: false,
has_compat_vararg_arg: false,
max_stack,
lines: translated_lines.into_boxed_slice(),
source,
line_defined,
last_line_defined,
locvars,
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 translate_code(
raw_code: &[u32],
lines: &[u32],
_consts: &[Value],
) -> Result<(Vec<Inst>, Vec<u32>, u8), String> {
let n_src = raw_code.len();
let mut src_to_dst = Vec::with_capacity(n_src + 1);
let mut sizes: Vec<u32> = vec![0; n_src];
let mut is_data_payload: Vec<bool> = vec![false; n_src];
let mut dst_pc: u32 = 0;
let mut src_pc = 0;
while src_pc < n_src {
src_to_dst.push(dst_pc);
let p = PucInst::decode(raw_code[src_pc]);
let size = src_size(p)?;
sizes[src_pc] = size;
dst_pc = dst_pc
.checked_add(size)
.ok_or("translated code too large")?;
if p.op == P_SETLIST && p.c == 0 {
src_pc += 1;
if src_pc >= n_src {
return Err(
"PUC 5.2 translator: SETLIST C=0 at end of code (missing C payload)"
.to_string(),
);
}
src_to_dst.push(dst_pc);
is_data_payload[src_pc] = true;
sizes[src_pc] = 0;
}
src_pc += 1;
}
src_to_dst.push(dst_pc);
let mut out: Vec<Inst> = Vec::with_capacity(dst_pc as usize);
let mut out_lines: Vec<u32> = Vec::with_capacity(dst_pc as usize);
let mut max_temp_bump: u8 = 0;
let mut src_pc = 0;
while src_pc < n_src {
if is_data_payload[src_pc] {
src_pc += 1;
continue;
}
let raw = raw_code[src_pc];
let p = PucInst::decode(raw);
let line = lines.get(src_pc).copied().unwrap_or(0);
let pre_emit = out.len();
let payload = if p.op == P_SETLIST && p.c == 0 {
Some(raw_code[src_pc + 1])
} else {
None
};
translate_one(
&mut out,
src_pc,
p,
&src_to_dst,
payload,
&mut max_temp_bump,
)?;
let emitted = out.len() - pre_emit;
if emitted as u32 != sizes[src_pc] {
return Err(format!(
"PUC 5.2 translator: src_pc {src_pc} expected {} emits, got {}",
sizes[src_pc], emitted
));
}
for _ in 0..emitted {
out_lines.push(line);
}
src_pc += 1;
}
Ok((out, out_lines, max_temp_bump))
}
fn src_size(p: PucInst) -> Result<u32, String> {
match p.op {
P_JMP if p.a != 0 => Ok(2), P_SETLIST if p.c == 0 => Ok(2), P_ADD | P_SUB | P_MUL | P_DIV | P_MOD | P_POW => {
let b_isk = (p.b & RK_BIT) != 0;
let c_isk = (p.c & RK_BIT) != 0;
match (b_isk, c_isk) {
(false, _) => Ok(1), (true, false) => Ok(2), (true, true) => Ok(3), }
}
P_EQ | P_LT | P_LE => {
let b_isk = (p.b & RK_BIT) != 0;
let c_isk = (p.c & RK_BIT) != 0;
Ok(1 + (b_isk as u32) + (c_isk as u32))
}
P_GETTABUP => {
let c_isk = (p.c & RK_BIT) != 0;
Ok(if c_isk { 1 } else { 2 })
}
_ => Ok(1),
}
}
fn remap_jump(src_pc: usize, sbx: i32, src_to_dst: &[u32]) -> Result<i32, String> {
let target_src = (src_pc as i32) + 1 + sbx;
if target_src < 0 || target_src as usize >= src_to_dst.len() {
return Err(format!(
"PUC 5.2 translator: jump target {target_src} out of range"
));
}
let target_dst = src_to_dst[target_src as usize] as i32;
let here_dst = src_to_dst[src_pc] as i32;
Ok(target_dst - (here_dst + 1))
}
fn translate_one(
out: &mut Vec<Inst>,
src_pc: usize,
p: PucInst,
src_to_dst: &[u32],
setlist_payload: Option<u32>,
max_temp_bump: &mut u8,
) -> Result<(), String> {
let a = p.a;
match p.op {
P_MOVE => out.push(Inst::iabc(Op::Move, a, p.b, 0, false)),
P_LOADK => out.push(Inst::iabx(Op::LoadK, a, p.bx())),
P_LOADKX => out.push(Inst::iabc(Op::LoadKx, a, 0, 0, false)),
P_LOADBOOL => {
if p.b == 0 && p.c == 0 {
out.push(Inst::iabc(Op::LoadFalse, a, 0, 0, false));
} else if p.b == 0 && p.c != 0 {
out.push(Inst::iabc(Op::LFalseSkip, a, 0, 0, false));
} else if p.b != 0 && p.c == 0 {
out.push(Inst::iabc(Op::LoadTrue, a, 0, 0, false));
} else {
return Err("PUC 5.2 translator: LOADBOOL A 1 1 unsupported \
(no LTrueSkip in luna)"
.to_string());
}
}
P_LOADNIL => out.push(Inst::iabc(Op::LoadNil, a, p.b, 0, false)),
P_GETUPVAL => out.push(Inst::iabc(Op::GetUpval, a, p.b, 0, false)),
P_GETTABUP => {
let (c_idx, c_isk) = decode_rk(p.c)?;
if c_isk {
out.push(Inst::iabc(Op::GetTabUp, a, p.b, c_idx as u32, false));
} else {
let tmp = a.max(c_idx as u32) + 1;
if tmp > 0xFF {
return Err(format!(
"PUC 5.2 translator: GETTABUP register-key lowering: \
temp register {tmp} exceeds 255 (src_pc {src_pc})"
));
}
*max_temp_bump = (*max_temp_bump).max(tmp as u8 + 1);
out.push(Inst::iabc(Op::GetUpval, tmp, p.b, 0, false));
out.push(Inst::iabc(Op::GetTable, a, tmp, c_idx as u32, false));
}
}
P_GETTABLE => {
let (c_idx, c_isk) = decode_rk(p.c)?;
out.push(Inst::iabc(Op::GetTable, a, p.b, c_idx as u32, c_isk));
}
P_SETTABUP => {
let (b_idx, b_isk) = decode_rk(p.b)?;
let (c_idx, c_isk) = decode_rk(p.c)?;
if !b_isk {
return Err(
"PUC 5.2 translator: SETTABUP with register name not supported".to_string(),
);
}
out.push(Inst::iabc(
Op::SetTabUp,
a,
b_idx as u32,
c_idx as u32,
c_isk,
));
}
P_SETUPVAL => out.push(Inst::iabc(Op::SetUpval, a, p.b, 0, false)),
P_SETTABLE => {
let (b_idx, b_isk) = decode_rk(p.b)?;
let (c_idx, c_isk) = decode_rk(p.c)?;
if b_isk {
out.push(Inst::iabc(
Op::SetField,
a,
b_idx as u32,
c_idx as u32,
c_isk,
));
} else {
out.push(Inst::iabc(
Op::SetTable,
a,
b_idx as u32,
c_idx as u32,
c_isk,
));
}
}
P_NEWTABLE => {
let arr = fb_to_hint_u8(p.b);
let hsh = fb_to_hint_u8(p.c);
out.push(Inst::iabc(Op::NewTable, a, arr, hsh, false));
}
P_SELF => {
let (c_idx, c_isk) = decode_rk(p.c)?;
out.push(Inst::iabc(Op::SelfOp, a, p.b, c_idx as u32, c_isk));
}
op @ (P_ADD | P_SUB | P_MUL | P_DIV | P_MOD | P_POW) => {
let (b_idx, b_isk) = decode_rk(p.b)?;
let (c_idx, c_isk) = decode_rk(p.c)?;
let luna_op = match op {
P_ADD => Op::Add,
P_SUB => Op::Sub,
P_MUL => Op::Mul,
P_DIV => Op::Div,
P_MOD => Op::Mod,
P_POW => Op::Pow,
_ => unreachable!(),
};
match (b_isk, c_isk) {
(false, _) => {
out.push(Inst::iabc(luna_op, a, b_idx as u32, c_idx as u32, c_isk));
}
(true, false) => {
let tmp = a.max(c_idx as u32) + 1;
let pair = super::lower_k_via_tmp(
luna_op,
a,
b_idx as u32,
c_idx as u32,
false,
tmp,
max_temp_bump,
)?;
out.push(pair[0]);
out.push(pair[1]);
}
(true, true) => {
let tmp_b = a + 1;
let tmp_c = a + 2;
if tmp_c > 0xFF {
return Err(format!(
"PUC 5.2 translator: arith K-on-both lowering: \
temp register {tmp_c} exceeds 255 (src_pc {src_pc})"
));
}
if (b_idx as u32) > 0x1FFFF || (c_idx as u32) > 0x1FFFF {
return Err(format!(
"PUC 5.2 translator: arith K-on-both lowering: \
K-pool index exceeds 17-bit Bx (src_pc {src_pc})"
));
}
*max_temp_bump = (*max_temp_bump).max(tmp_c as u8 + 1);
out.push(Inst::iabx(Op::LoadK, tmp_b, b_idx as u32));
out.push(Inst::iabx(Op::LoadK, tmp_c, c_idx as u32));
out.push(Inst::iabc(luna_op, a, tmp_b, tmp_c, false));
}
}
}
P_UNM => out.push(Inst::iabc(Op::Unm, a, p.b, 0, false)),
P_NOT => out.push(Inst::iabc(Op::Not, a, p.b, 0, false)),
P_LEN => out.push(Inst::iabc(Op::Len, a, p.b, 0, false)),
P_CONCAT => {
if p.c < p.b {
return Err("PUC 5.2 translator: CONCAT with C < B".to_string());
}
let count = p.c - p.b + 1;
if p.b != p.a {
return Err("PUC 5.2 translator: CONCAT with A != B not supported".to_string());
}
out.push(Inst::iabc(Op::Concat, a, count, 0, false));
}
P_JMP => {
if a != 0 {
out.push(Inst::iabc(Op::Close, a - 1, 0, 0, false));
}
let sj = remap_jump_for_jmp(src_pc, p.sbx(), src_to_dst, a != 0)?;
out.push(Inst::isj(Op::Jmp, sj));
}
op @ (P_EQ | P_LT | P_LE) => {
let (b_idx, b_isk) = decode_rk(p.b)?;
let (c_idx, c_isk) = decode_rk(p.c)?;
let luna_op = match op {
P_EQ => Op::Eq,
P_LT => Op::Lt,
P_LE => Op::Le,
_ => unreachable!(),
};
let k = a != 0;
let live_max = match (b_isk, c_isk) {
(false, false) => 0, (true, false) => c_idx as u32,
(false, true) => b_idx as u32,
(true, true) => 0,
};
let mut next_tmp = live_max + 1;
let mut alloc_tmp = || -> Result<u8, String> {
let t = next_tmp;
if t > 0xFF {
return Err(format!(
"PUC 5.2 translator: comparison K lowering: \
temp register {t} exceeds 255 (src_pc {src_pc})"
));
}
next_tmp += 1;
*max_temp_bump = (*max_temp_bump).max(t as u8 + 1);
Ok(t as u8)
};
let b_reg = if b_isk {
let t = alloc_tmp()?;
if (b_idx as u32) > 0x1FFFF {
return Err(format!(
"PUC 5.2 translator: comparison K lowering: \
B K-pool index {b_idx} exceeds 17-bit Bx (src_pc {src_pc})"
));
}
out.push(Inst::iabx(Op::LoadK, t as u32, b_idx as u32));
t as u32
} else {
b_idx as u32
};
let c_reg = if c_isk {
let t = alloc_tmp()?;
if (c_idx as u32) > 0x1FFFF {
return Err(format!(
"PUC 5.2 translator: comparison K lowering: \
C K-pool index {c_idx} exceeds 17-bit Bx (src_pc {src_pc})"
));
}
out.push(Inst::iabx(Op::LoadK, t as u32, c_idx as u32));
t as u32
} else {
c_idx as u32
};
out.push(Inst::iabc(luna_op, b_reg, c_reg, 0, k));
}
P_TEST => out.push(Inst::iabc(Op::Test, a, 0, 0, p.c != 0)),
P_TESTSET => out.push(Inst::iabc(Op::TestSet, a, p.b, 0, p.c != 0)),
P_CALL => out.push(Inst::iabc(Op::Call, a, p.b, p.c, false)),
P_TAILCALL => out.push(Inst::iabc(Op::TailCall, a, p.b, p.c, false)),
P_RETURN => out.push(Inst::iabc(Op::Return, a, p.b, 0, false)),
P_FORPREP => {
let sj = remap_jump(src_pc, p.sbx(), src_to_dst)?;
out.push(Inst::iasbx(Op::ForPrep, a, sj));
}
P_FORLOOP => {
let sj = remap_jump(src_pc, p.sbx(), src_to_dst)?;
out.push(Inst::iasbx(Op::ForLoop, a, sj));
}
P_TFORCALL => out.push(Inst::iabc(Op::TForCall, a, 0, p.c, false)),
P_TFORLOOP => {
if a < 2 {
return Err(
"PUC 5.2 translator: TFORLOOP A < 2 (impossible per PUC convention)"
.to_string(),
);
}
let a_luna = a - 2;
let sj = remap_jump(src_pc, p.sbx(), src_to_dst)?;
let bx_val = -sj;
if bx_val < 0 {
return Err(format!(
"PUC 5.2 translator: TFORLOOP forward jump (sj={sj}) not supported"
));
}
let bx = bx_val as u32;
if bx > isa::MAX_BX {
return Err(format!(
"PUC 5.2 translator: TFORLOOP back-edge {bx} > MAX_BX"
));
}
out.push(Inst::iabx(Op::TForLoop, a_luna, bx));
}
P_SETLIST => {
let b = p.b;
if p.c == 0 {
let payload = setlist_payload.ok_or_else(|| {
"PUC 5.2 translator: SETLIST C=0 missing payload (internal bug)".to_string()
})?;
if payload > isa::MAX_AX {
return Err(format!(
"PUC 5.2 translator: SETLIST payload {payload} > luna MAX_AX"
));
}
out.push(Inst::iabc(Op::SetList, a, b, 0, true));
out.push(Inst::iax(Op::ExtraArg, payload));
} else {
out.push(Inst::iabc(Op::SetList, a, b, p.c, false));
}
}
P_CLOSURE => out.push(Inst::iabx(Op::Closure, a, p.bx())),
P_VARARG => out.push(Inst::iabc(Op::Vararg, a, p.b, 0, false)),
P_EXTRAARG => {
if p.ax() > isa::MAX_AX {
return Err(format!(
"PUC 5.2 translator: EXTRAARG ax={} > luna MAX_AX",
p.ax()
));
}
out.push(Inst::iax(Op::ExtraArg, p.ax()));
}
op => return Err(format!("PUC 5.2 translator: unhandled opcode {op}")),
}
Ok(())
}
fn remap_jump_for_jmp(
src_pc: usize,
sbx: i32,
src_to_dst: &[u32],
had_close: bool,
) -> Result<i32, String> {
let target_src = (src_pc as i32) + 1 + sbx;
if target_src < 0 || target_src as usize >= src_to_dst.len() {
return Err(format!(
"PUC 5.2 translator: jump target {target_src} out of range"
));
}
let target_dst = src_to_dst[target_src as usize] as i32;
let jmp_dst = src_to_dst[src_pc] as i32 + if had_close { 1 } else { 0 };
Ok(target_dst - (jmp_dst + 1))
}
#[cfg(test)]
#[allow(clippy::identity_op, clippy::erasing_op)]
mod tests {
use super::*;
#[test]
fn fb2int_known_values() {
assert_eq!(fb2int(0), 0);
assert_eq!(fb2int(7), 7);
assert_eq!(fb2int(8), 8);
assert_eq!(fb2int(16), 16);
assert_eq!(fb2int(18), 20);
}
#[test]
fn decode_inst_layout() {
let raw: u32 = (P_MOVE as u32) | (3u32 << 6) | (0u32 << 14) | (5u32 << 23);
let p = PucInst::decode(raw);
assert_eq!(p.op, P_MOVE);
assert_eq!(p.a, 3);
assert_eq!(p.b, 5);
assert_eq!(p.c, 0);
}
#[test]
fn loadbool_lowering() {
let mut out = Vec::new();
translate_one(
&mut out,
0,
PucInst {
op: P_LOADBOOL,
a: 2,
b: 0,
c: 0,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out[0].op(), Op::LoadFalse);
assert_eq!(out[0].a(), 2);
out.clear();
translate_one(
&mut out,
0,
PucInst {
op: P_LOADBOOL,
a: 3,
b: 0,
c: 1,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out[0].op(), Op::LFalseSkip);
out.clear();
translate_one(
&mut out,
0,
PucInst {
op: P_LOADBOOL,
a: 4,
b: 1,
c: 0,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out[0].op(), Op::LoadTrue);
}
#[test]
fn arith_k_on_rhs_emits_one_inst_with_k_flag() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_ADD,
a: 2,
b: 3,
c: 5 | RK_BIT,
},
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].op(), Op::Add);
assert_eq!(out[0].a(), 2);
assert_eq!(out[0].b(), 3);
assert_eq!(out[0].c(), 5);
assert!(out[0].k());
assert_eq!(bump, 0, "no temp needed when only C is K");
}
#[test]
fn arith_k_on_lhs_lowers_via_loadk_tmp() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_SUB,
a: 1,
b: 7 | RK_BIT,
c: 2,
},
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].op(), Op::LoadK);
let tmp = out[0].a();
assert!(tmp > 2, "tmp ({tmp}) must clear both a=1 and c=2");
assert_eq!(out[0].bx(), 7);
assert_eq!(out[1].op(), Op::Sub);
assert_eq!(out[1].a(), 1);
assert_eq!(out[1].b(), tmp);
assert_eq!(out[1].c(), 2);
assert!(!out[1].k(), "k flag stays clear when lowering K-on-LHS");
assert_eq!(bump, tmp as u8 + 1);
}
#[test]
fn arith_k_on_both_lowers_via_two_loadks() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_MUL,
a: 0,
b: 3 | RK_BIT,
c: 4 | RK_BIT,
},
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 3);
assert_eq!(out[0].op(), Op::LoadK);
assert_eq!(out[0].bx(), 3);
assert_eq!(out[1].op(), Op::LoadK);
assert_eq!(out[1].bx(), 4);
assert_eq!(out[2].op(), Op::Mul);
assert_eq!(out[2].a(), 0);
assert_eq!(out[2].b(), out[0].a());
assert_eq!(out[2].c(), out[1].a());
assert_ne!(out[0].a(), out[1].a(), "tmpB and tmpC must differ");
assert!(bump >= 3, "frame must grow to hold two new temps");
}
#[test]
fn arith_all_six_ops_round_trip_k_on_lhs() {
for (puc_op, luna_op) in [
(P_ADD, Op::Add),
(P_SUB, Op::Sub),
(P_MUL, Op::Mul),
(P_DIV, Op::Div),
(P_MOD, Op::Mod),
(P_POW, Op::Pow),
] {
let mut out = Vec::new();
translate_one(
&mut out,
0,
PucInst {
op: puc_op,
a: 0,
b: 1 | RK_BIT,
c: 2,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out[0].op(), Op::LoadK);
assert_eq!(out[1].op(), luna_op);
}
}
#[test]
fn eq_with_k_on_rhs_lowers_via_loadk_tmp() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_EQ,
a: 1, b: 3,
c: 1 | RK_BIT,
},
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].op(), Op::LoadK);
let tmp = out[0].a();
assert!(tmp > 3, "tmp must clear live reg b=3");
assert_eq!(out[0].bx(), 1);
assert_eq!(out[1].op(), Op::Eq);
assert_eq!(out[1].a(), 3);
assert_eq!(out[1].b(), tmp);
assert!(out[1].k(), "PUC A=1 maps to luna k=true");
assert_eq!(bump, tmp as u8 + 1);
}
#[test]
fn lt_with_k_on_lhs_lowers_via_loadk_tmp() {
let mut out = Vec::new();
translate_one(
&mut out,
0,
PucInst {
op: P_LT,
a: 0,
b: 2 | RK_BIT,
c: 5,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].op(), Op::LoadK);
let tmp = out[0].a();
assert!(tmp > 5, "tmp must clear live reg c=5");
assert_eq!(out[1].op(), Op::Lt);
assert_eq!(out[1].a(), tmp);
assert_eq!(out[1].b(), 5);
assert!(!out[1].k());
}
#[test]
fn le_with_k_on_both_lowers_via_two_loadks() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_LE,
a: 1,
b: 0 | RK_BIT,
c: 2 | RK_BIT,
},
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 3);
assert_eq!(out[0].op(), Op::LoadK);
assert_eq!(out[0].bx(), 0);
assert_eq!(out[1].op(), Op::LoadK);
assert_eq!(out[1].bx(), 2);
assert_eq!(out[2].op(), Op::Le);
assert_eq!(out[2].a(), out[0].a());
assert_eq!(out[2].b(), out[1].a());
assert!(out[2].k());
assert_ne!(out[0].a(), out[1].a());
assert!(bump >= 2);
}
#[test]
fn gettabup_register_key_lowers_to_getupval_gettable() {
let mut out = Vec::new();
let mut bump: u8 = 0;
translate_one(
&mut out,
0,
PucInst {
op: P_GETTABUP,
a: 1,
b: 0,
c: 2, },
&[0, 1],
None,
&mut bump,
)
.unwrap();
assert_eq!(out.len(), 2);
assert_eq!(out[0].op(), Op::GetUpval);
let tmp = out[0].a();
assert!(tmp > 2, "tmp must clear a=1 and c=2");
assert_eq!(out[0].b(), 0, "GetUpval reads upval slot from B");
assert_eq!(out[1].op(), Op::GetTable);
assert_eq!(out[1].a(), 1);
assert_eq!(out[1].b(), tmp);
assert_eq!(out[1].c(), 2);
assert_eq!(bump, tmp as u8 + 1);
}
#[test]
fn gettabup_constant_key_stays_one_inst() {
let mut out = Vec::new();
translate_one(
&mut out,
0,
PucInst {
op: P_GETTABUP,
a: 1,
b: 0,
c: 4 | RK_BIT,
},
&[0, 1],
None,
&mut 0,
)
.unwrap();
assert_eq!(out.len(), 1);
assert_eq!(out[0].op(), Op::GetTabUp);
}
#[test]
fn src_size_matches_emits_for_wave2_punts() {
let cases = [
(P_ADD, 1, 2, 1, "R + R"),
(P_ADD, 1, 2 | RK_BIT, 1, "R + K (fits luna k bit)"),
(P_SUB, 1 | RK_BIT, 2, 2, "K - R"),
(P_MUL, 1 | RK_BIT, 2 | RK_BIT, 3, "K * K"),
(P_EQ, 1, 2, 1, "R == R"),
(P_LT, 1, 2 | RK_BIT, 2, "R < K"),
(P_LE, 1 | RK_BIT, 2, 2, "K <= R"),
(P_EQ, 1 | RK_BIT, 2 | RK_BIT, 3, "K == K"),
(P_GETTABUP, 0, 4 | RK_BIT, 1, "GETTABUP K key"),
(P_GETTABUP, 0, 2, 2, "GETTABUP R key"),
];
for (op, b, c, expected, label) in cases {
let p = PucInst { op, a: 0, b, c };
let size = src_size(p).unwrap();
assert_eq!(size, expected, "src_size mismatch: {label}");
}
}
}