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 SIGNATURE: &[u8; 4] = b"\x1bLua";
const LUAC_VERSION_53: u8 = 0x53;
const LUAC_FORMAT: u8 = 0x00;
const LUAC_DATA: &[u8; 6] = b"\x19\x93\r\n\x1a\n";
const LUAC_INT: i64 = 0x5678;
const LUAC_NUM: f64 = 370.5;
const PUC53_SIZE_OP: u32 = 6;
const PUC53_SIZE_A: u32 = 8;
const PUC53_SIZE_B: u32 = 9;
const PUC53_SIZE_C: u32 = 9;
const PUC53_POS_A: u32 = PUC53_SIZE_OP;
const PUC53_POS_C: u32 = PUC53_POS_A + PUC53_SIZE_A;
const PUC53_POS_B: u32 = PUC53_POS_C + PUC53_SIZE_C;
const PUC53_MAX_ARG_BX: u32 = (1 << (PUC53_SIZE_B + PUC53_SIZE_C)) - 1;
const PUC53_MAX_ARG_SBX: i32 = (PUC53_MAX_ARG_BX >> 1) as i32;
const PUC53_BITRK: u32 = 1 << (PUC53_SIZE_B - 1);
const OP_MOVE: u8 = 0;
const OP_LOADK: u8 = 1;
const OP_LOADKX: u8 = 2;
const OP_LOADBOOL: u8 = 3;
const OP_LOADNIL: u8 = 4;
const OP_GETUPVAL: u8 = 5;
const OP_GETTABUP: u8 = 6;
const OP_GETTABLE: u8 = 7;
const OP_SETTABUP: u8 = 8;
const OP_SETUPVAL: u8 = 9;
const OP_SETTABLE: u8 = 10;
const OP_NEWTABLE: u8 = 11;
const OP_SELF: u8 = 12;
const OP_ADD: u8 = 13;
const OP_SUB: u8 = 14;
const OP_MUL: u8 = 15;
const OP_MOD: u8 = 16;
const OP_POW: u8 = 17;
const OP_DIV: u8 = 18;
const OP_IDIV: u8 = 19;
const OP_BAND: u8 = 20;
const OP_BOR: u8 = 21;
const OP_BXOR: u8 = 22;
const OP_SHL: u8 = 23;
const OP_SHR: u8 = 24;
const OP_UNM: u8 = 25;
const OP_BNOT: u8 = 26;
const OP_NOT: u8 = 27;
const OP_LEN: u8 = 28;
const OP_CONCAT: u8 = 29;
const OP_JMP: u8 = 30;
const OP_EQ: u8 = 31;
const OP_LT: u8 = 32;
const OP_LE: u8 = 33;
const OP_TEST: u8 = 34;
const OP_TESTSET: u8 = 35;
const OP_CALL: u8 = 36;
const OP_TAILCALL: u8 = 37;
const OP_RETURN: u8 = 38;
const OP_FORLOOP: u8 = 39;
const OP_FORPREP: u8 = 40;
const OP_TFORCALL: u8 = 41;
const OP_TFORLOOP: u8 = 42;
const OP_SETLIST: u8 = 43;
const OP_CLOSURE: u8 = 44;
const OP_VARARG: u8 = 45;
const OP_EXTRAARG: u8 = 46;
const NUM_PUC53_OPS: u8 = OP_EXTRAARG + 1;
const LUA_TNIL: u8 = 0;
const LUA_TBOOLEAN: u8 = 1;
const LUA_TNUMFLT: u8 = 3; const LUA_TNUMINT: u8 = 3 | (1 << 4); const LUA_TSHRSTR: u8 = 4; const LUA_TLNGSTR: u8 = 4 | (1 << 4);
#[derive(Clone, Copy, Debug)]
struct Puc53Inst {
op: u8,
a: u32,
b: u32,
c: u32,
}
impl Puc53Inst {
fn bx(self) -> u32 {
(self.b << PUC53_SIZE_C) | self.c
}
fn sbx(self) -> i32 {
self.bx() as i32 - PUC53_MAX_ARG_SBX
}
}
fn decode_53(word: u32) -> Puc53Inst {
let op = (word & ((1 << PUC53_SIZE_OP) - 1)) as u8;
let a = (word >> PUC53_POS_A) & ((1 << PUC53_SIZE_A) - 1);
let c = (word >> PUC53_POS_C) & ((1 << PUC53_SIZE_C) - 1);
let b = (word >> PUC53_POS_B) & ((1 << PUC53_SIZE_B) - 1);
Puc53Inst { op, a, b, c }
}
fn r_int(r: &mut Reader) -> Result<i32, String> {
Ok(i32::from_le_bytes(r.take(4)?.try_into().unwrap()))
}
fn r_size(r: &mut Reader) -> Result<u64, String> {
Ok(u64::from_le_bytes(r.take(8)?.try_into().unwrap()))
}
fn r_integer(r: &mut Reader) -> Result<i64, String> {
Ok(i64::from_le_bytes(r.take(8)?.try_into().unwrap()))
}
fn r_number(r: &mut Reader) -> Result<f64, String> {
Ok(f64::from_bits(u64::from_le_bytes(
r.take(8)?.try_into().unwrap(),
)))
}
fn r_string<'a>(r: &mut Reader<'a>) -> Result<Option<&'a [u8]>, String> {
let b = r.u8()?;
let size: u64 = if b == 0xFF { r_size(r)? } else { b as u64 };
if size == 0 {
return Ok(None);
}
let len = size
.checked_sub(1)
.ok_or_else(|| "bad 5.3 string size".to_string())?;
let slice = r.take(len as usize)?;
Ok(Some(slice))
}
fn check_header(r: &mut Reader) -> Result<(), String> {
let sig = r.take(4)?;
if sig != SIGNATURE {
return Err("bad PUC 5.3 signature".to_string());
}
if r.u8()? != LUAC_VERSION_53 {
return Err("PUC 5.3 translator: version mismatch".to_string());
}
if r.u8()? != LUAC_FORMAT {
return Err("PUC 5.3 translator: unsupported format byte (only 0x00)".to_string());
}
let data = r.take(6)?;
if data != LUAC_DATA {
return Err("PUC 5.3 translator: corrupted LUAC_DATA literal".to_string());
}
let sz_int = r.u8()?;
if sz_int != 4 {
return Err(format!(
"PUC 5.3 translator: expected sizeof(int)=4, got {sz_int}"
));
}
let sz_size = r.u8()?;
if sz_size != 8 {
return Err(format!(
"PUC 5.3 translator: expected sizeof(size_t)=8, got {sz_size}"
));
}
let sz_inst = r.u8()?;
if sz_inst != 4 {
return Err(format!(
"PUC 5.3 translator: expected sizeof(Instruction)=4, got {sz_inst}"
));
}
let sz_int_ty = r.u8()?;
if sz_int_ty != 8 {
return Err(format!(
"PUC 5.3 translator: expected sizeof(lua_Integer)=8, got {sz_int_ty}"
));
}
let sz_num = r.u8()?;
if sz_num != 8 {
return Err(format!(
"PUC 5.3 translator: expected sizeof(lua_Number)=8, got {sz_num}"
));
}
let int_check = r_integer(r)?;
if int_check != LUAC_INT {
return Err(format!(
"PUC 5.3 translator: endianness mismatch (LUAC_INT expected 0x5678, got 0x{int_check:x})"
));
}
let num_check = r_number(r)?;
if num_check != LUAC_NUM {
return Err(format!(
"PUC 5.3 translator: float format mismatch (LUAC_NUM expected 370.5, got {num_check})"
));
}
Ok(())
}
struct Translated {
code: Vec<Inst>,
puc_to_luna_pc: Vec<Option<u32>>,
luna_to_puc_pc: Vec<usize>,
max_temp_bump: u8,
}
#[derive(Clone, Copy, Debug)]
enum FixupKind {
Jmp,
ForPrep,
ForLoop,
TForLoop,
}
#[derive(Clone, Copy, Debug)]
struct Fixup {
luna_pc: usize,
target_puc_pc: i64,
kind: FixupKind,
a: u32,
}
fn translate_code(puc_code: &[u32]) -> Result<Translated, String> {
let mut code: Vec<Inst> = Vec::with_capacity(puc_code.len());
let mut puc_to_luna_pc: Vec<Option<u32>> = Vec::with_capacity(puc_code.len());
let mut luna_to_puc_pc: Vec<usize> = Vec::with_capacity(puc_code.len());
let mut max_temp_bump: u8 = 0;
let mut jump_fixups: Vec<Fixup> = Vec::new();
for (puc_pc, &word) in puc_code.iter().enumerate() {
translate_one(
puc_pc,
word,
&mut code,
&mut puc_to_luna_pc,
&mut luna_to_puc_pc,
&mut max_temp_bump,
&mut jump_fixups,
)
.map_err(|e| format!("{e} (pc={puc_pc})"))?;
}
for fx in jump_fixups {
let target_luna_pc = resolve_target(fx.target_puc_pc, &puc_to_luna_pc, code.len())?;
let next_pc = fx.luna_pc as i64 + 1;
let delta = target_luna_pc - next_pc;
let inst = match fx.kind {
FixupKind::Jmp => {
Inst::isj(Op::Jmp, delta as i32)
}
FixupKind::ForPrep => {
if delta < 0 {
return Err(format!(
"PUC 5.3 FORPREP at luna pc {} resolved to backward delta {} \
(FORPREP must jump forward)",
fx.luna_pc, delta
));
}
let bx = delta as u32;
if bx > crate::vm::isa::MAX_BX {
return Err(format!(
"PUC 5.3 FORPREP at luna pc {} forward distance {bx} \
out of luna Bx range",
fx.luna_pc
));
}
Inst::iabx(Op::ForPrep, fx.a, bx)
}
FixupKind::ForLoop => {
if delta > 0 {
return Err(format!(
"PUC 5.3 FORLOOP at luna pc {} resolved to forward delta {} \
(FORLOOP must jump backward)",
fx.luna_pc, delta
));
}
let back = (-delta) as u32;
if back > crate::vm::isa::MAX_BX {
return Err(format!(
"PUC 5.3 FORLOOP at luna pc {} back-distance {back} \
out of luna Bx range",
fx.luna_pc
));
}
Inst::iabx(Op::ForLoop, fx.a, back)
}
FixupKind::TForLoop => {
if delta > 0 {
return Err(format!(
"PUC 5.3 TFORLOOP at luna pc {} resolved to forward delta {} \
(TFORLOOP must jump backward)",
fx.luna_pc, delta
));
}
let back = (-delta) as u32;
if back > crate::vm::isa::MAX_BX {
return Err(format!(
"PUC 5.3 TFORLOOP at luna pc {} back-distance {back} \
out of luna Bx range",
fx.luna_pc
));
}
Inst::iabx(Op::TForLoop, fx.a, back)
}
};
code[fx.luna_pc] = inst;
}
Ok(Translated {
code,
puc_to_luna_pc,
luna_to_puc_pc,
max_temp_bump,
})
}
fn arith_op_for(puc_op: u8) -> Option<Op> {
Some(match puc_op {
OP_ADD => Op::Add,
OP_SUB => Op::Sub,
OP_MUL => Op::Mul,
OP_MOD => Op::Mod,
OP_POW => Op::Pow,
OP_DIV => Op::Div,
OP_IDIV => Op::IDiv,
OP_BAND => Op::BAnd,
OP_BOR => Op::BOr,
OP_BXOR => Op::BXor,
OP_SHL => Op::Shl,
OP_SHR => Op::Shr,
_ => return None,
})
}
fn resolve_target(
target_puc_pc: i64,
puc_to_luna_pc: &[Option<u32>],
code_len: usize,
) -> Result<i64, String> {
if target_puc_pc < 0 {
return Err(format!("PUC 5.3 jump targets negative pc {target_puc_pc}"));
}
if target_puc_pc as usize >= puc_to_luna_pc.len() {
return Ok(code_len as i64);
}
let mut t = target_puc_pc as usize;
loop {
if t >= puc_to_luna_pc.len() {
return Ok(code_len as i64);
}
if let Some(p) = puc_to_luna_pc[t] {
return Ok(p as i64);
}
t += 1;
}
}
#[allow(clippy::too_many_arguments)]
fn translate_one(
puc_pc: usize,
word: u32,
code: &mut Vec<Inst>,
puc_to_luna_pc: &mut Vec<Option<u32>>,
luna_to_puc_pc: &mut Vec<usize>,
max_temp_bump: &mut u8,
jump_fixups: &mut Vec<Fixup>,
) -> Result<(), String> {
let i = decode_53(word);
if i.op >= NUM_PUC53_OPS {
return Err(format!(
"PUC 5.3 translator: unknown opcode {} (max {})",
i.op,
NUM_PUC53_OPS - 1
));
}
let b_is_k = i.b & PUC53_BITRK != 0;
let c_is_k = i.c & PUC53_BITRK != 0;
let b_idx = i.b & (PUC53_BITRK - 1);
let c_idx = i.c & (PUC53_BITRK - 1);
if let Some(luna_op) = arith_op_for(i.op) {
if b_is_k {
let tmp = i.a.max(c_idx) + 1;
let pair =
super::lower_k_via_tmp(luna_op, i.a, b_idx, c_idx, c_is_k, tmp, max_temp_bump)?;
let first_luna_pc = code.len() as u32;
puc_to_luna_pc.push(Some(first_luna_pc));
luna_to_puc_pc.push(puc_pc);
code.push(pair[0]);
luna_to_puc_pc.push(puc_pc);
code.push(pair[1]);
return Ok(());
}
}
match i.op {
OP_JMP => {
if i.a != 0 {
return Err(
"PUC 5.3 translator: OP_JMP with close-upvalues hint (A != 0) not yet \
supported (planned: emit `Close (A-1); Jmp sBx` with PC remap)"
.to_string(),
);
}
let target = puc_pc as i64 + 1 + i.sbx() as i64;
let luna_pc = code.len();
puc_to_luna_pc.push(Some(luna_pc as u32));
luna_to_puc_pc.push(puc_pc);
code.push(Inst::isj(Op::Jmp, 0)); jump_fixups.push(Fixup {
luna_pc,
target_puc_pc: target,
kind: FixupKind::Jmp,
a: 0,
});
return Ok(());
}
OP_FORPREP => {
let sbx = i.sbx();
let target = puc_pc as i64 + 1 + sbx as i64;
let luna_pc = code.len();
puc_to_luna_pc.push(Some(luna_pc as u32));
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabx(Op::ForPrep, i.a, 0)); jump_fixups.push(Fixup {
luna_pc,
target_puc_pc: target,
kind: FixupKind::ForPrep,
a: i.a,
});
return Ok(());
}
OP_FORLOOP => {
let sbx = i.sbx();
let target = puc_pc as i64 + 1 + sbx as i64;
let luna_pc = code.len();
puc_to_luna_pc.push(Some(luna_pc as u32));
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabx(Op::ForLoop, i.a, 0));
jump_fixups.push(Fixup {
luna_pc,
target_puc_pc: target,
kind: FixupKind::ForLoop,
a: i.a,
});
return Ok(());
}
OP_LOADBOOL => {
if b_idx != 0 && c_idx != 0 {
let first_luna_pc = code.len() as u32;
puc_to_luna_pc.push(Some(first_luna_pc));
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabc(Op::LoadTrue, i.a, 0, 0, false));
let jmp_luna_pc = code.len();
luna_to_puc_pc.push(puc_pc);
code.push(Inst::isj(Op::Jmp, 0));
jump_fixups.push(Fixup {
luna_pc: jmp_luna_pc,
target_puc_pc: puc_pc as i64 + 2,
kind: FixupKind::Jmp,
a: 0,
});
return Ok(());
}
}
OP_CONCAT => {
if b_idx != i.a {
let len = c_idx
.checked_sub(b_idx)
.ok_or("PUC 5.3 translator: CONCAT C < B")?
+ 1;
if len > 0xFF {
return Err(format!(
"PUC 5.3 translator: CONCAT len {len} > 255 — out of luna's 8-bit field"
));
}
let last_dest = i.a.saturating_add(len - 1);
let last_src = b_idx.saturating_add(len - 1);
if last_dest > 0xFF || last_src > 0xFF {
return Err(format!(
"PUC 5.3 translator: CONCAT range out of 8-bit register field \
(a={}, b={}, len={})",
i.a, b_idx, len
));
}
let first_luna_pc = code.len() as u32;
puc_to_luna_pc.push(Some(first_luna_pc));
if b_idx > i.a {
for off in 0..len {
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabc(Op::Move, i.a + off, b_idx + off, 0, false));
}
} else {
for off in (0..len).rev() {
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabc(Op::Move, i.a + off, b_idx + off, 0, false));
}
}
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabc(Op::Concat, i.a, len, 0, false));
return Ok(());
}
}
OP_TFORLOOP => {
let sbx = i.sbx();
if sbx > 0 {
return Err(format!(
"PUC 5.3 translator: TFORLOOP sBx {sbx} > 0 (expected backward jump)"
));
}
let iter_base = i.a.checked_sub(2).ok_or_else(|| {
format!(
"PUC 5.3 translator: TFORLOOP.A={} < 2 (cannot convert to luna iter_base)",
i.a
)
})?;
let target = puc_pc as i64 + 1 + sbx as i64;
let luna_pc = code.len();
puc_to_luna_pc.push(Some(luna_pc as u32));
luna_to_puc_pc.push(puc_pc);
code.push(Inst::iabx(Op::TForLoop, iter_base, 0));
jump_fixups.push(Fixup {
luna_pc,
target_puc_pc: target,
kind: FixupKind::TForLoop,
a: iter_base,
});
return Ok(());
}
_ => {}
}
let inst = translate_inst(word)?;
puc_to_luna_pc.push(Some(code.len() as u32));
luna_to_puc_pc.push(puc_pc);
code.push(inst);
Ok(())
}
fn translate_inst(word: u32) -> Result<Inst, String> {
let i = decode_53(word);
if i.op >= NUM_PUC53_OPS {
return Err(format!(
"PUC 5.3 translator: unknown opcode {} (max {})",
i.op,
NUM_PUC53_OPS - 1
));
}
let b_is_k = i.b & PUC53_BITRK != 0;
let c_is_k = i.c & PUC53_BITRK != 0;
let b_idx = i.b & (PUC53_BITRK - 1);
let c_idx = i.c & (PUC53_BITRK - 1);
fn fit_b(v: u32, name: &str) -> Result<u32, String> {
if v > 0xFF {
Err(format!(
"PUC 5.3 translator: {name} operand {v} > 255 — out of luna's 8-bit field"
))
} else {
Ok(v)
}
}
fn reject_b_k(opname: &str, b_is_k: bool) -> Result<(), String> {
if b_is_k {
Err(format!(
"PUC 5.3 translator: {opname} with RK on B not yet supported \
(baseline accepts only RK on C; emit a `LoadK tmp; OP A tmp B` \
sequence is the planned polish)"
))
} else {
Ok(())
}
}
Ok(match i.op {
OP_MOVE => Inst::iabc(Op::Move, i.a, fit_b(b_idx, "MOVE.B")?, 0, false),
OP_LOADNIL => {
Inst::iabc(Op::LoadNil, i.a, fit_b(b_idx, "LOADNIL.B")?, 0, false)
}
OP_GETUPVAL => Inst::iabc(Op::GetUpval, i.a, fit_b(b_idx, "GETUPVAL.B")?, 0, false),
OP_SETUPVAL => Inst::iabc(Op::SetUpval, i.a, fit_b(b_idx, "SETUPVAL.B")?, 0, false),
OP_NEWTABLE => {
Inst::iabc(
Op::NewTable,
i.a,
fit_b(b_idx, "NEWTABLE.B")?,
fit_b(c_idx, "NEWTABLE.C")?,
false,
)
}
OP_NOT => Inst::iabc(Op::Not, i.a, fit_b(b_idx, "NOT.B")?, 0, false),
OP_LEN => Inst::iabc(Op::Len, i.a, fit_b(b_idx, "LEN.B")?, 0, false),
OP_UNM => Inst::iabc(Op::Unm, i.a, fit_b(b_idx, "UNM.B")?, 0, false),
OP_BNOT => Inst::iabc(Op::BNot, i.a, fit_b(b_idx, "BNOT.B")?, 0, false),
OP_LOADK => Inst::iabx(Op::LoadK, i.a, i.bx()),
OP_LOADKX => Inst::iabx(Op::LoadKx, i.a, 0),
OP_LOADBOOL => match (b_idx, c_idx) {
(0, 0) => Inst::iabc(Op::LoadFalse, i.a, 0, 0, false),
(0, _) => Inst::iabc(Op::LFalseSkip, i.a, 0, 0, false),
(_, 0) => Inst::iabc(Op::LoadTrue, i.a, 0, 0, false),
(_, _) => {
return Err("PUC 5.3 translator: LOADBOOL true+skip not yet supported \
(luna has no LTrueSkip; planned polish is to emit \
`LoadTrue; Jmp +1` with PC remap)"
.to_string());
}
},
OP_GETTABUP => {
if !c_is_k {
return Err(
"PUC 5.3 translator: GETTABUP with register key — not yet supported \
(only constant-string keys land in baseline)"
.to_string(),
);
}
Inst::iabc(
Op::GetTabUp,
i.a,
fit_b(b_idx, "GETTABUP.B")?,
fit_b(c_idx, "GETTABUP.C")?,
false,
)
}
OP_GETTABLE => {
if c_is_k {
Inst::iabc(
Op::GetField,
i.a,
fit_b(b_idx, "GETTABLE.B")?,
fit_b(c_idx, "GETTABLE.C")?,
false,
)
} else {
Inst::iabc(
Op::GetTable,
i.a,
fit_b(b_idx, "GETTABLE.B")?,
fit_b(c_idx, "GETTABLE.C")?,
false,
)
}
}
OP_SETTABUP => {
if !b_is_k {
return Err(
"PUC 5.3 translator: SETTABUP with register key — not yet supported"
.to_string(),
);
}
Inst::iabc(
Op::SetTabUp,
i.a,
fit_b(b_idx, "SETTABUP.B")?,
fit_b(c_idx, "SETTABUP.C")?,
c_is_k,
)
}
OP_SETTABLE => {
if b_is_k {
Inst::iabc(
Op::SetField,
i.a,
fit_b(b_idx, "SETTABLE.B")?,
fit_b(c_idx, "SETTABLE.C")?,
c_is_k,
)
} else {
Inst::iabc(
Op::SetTable,
i.a,
fit_b(b_idx, "SETTABLE.B")?,
fit_b(c_idx, "SETTABLE.C")?,
c_is_k,
)
}
}
OP_SELF => {
if !c_is_k {
return Err(
"PUC 5.3 translator: SELF with register key — not yet supported".to_string(),
);
}
Inst::iabc(
Op::SelfOp,
i.a,
fit_b(b_idx, "SELF.B")?,
fit_b(c_idx, "SELF.C")?,
false,
)
}
OP_ADD => arith(
Op::Add,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"ADD",
reject_b_k,
)?,
OP_SUB => arith(
Op::Sub,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"SUB",
reject_b_k,
)?,
OP_MUL => arith(
Op::Mul,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"MUL",
reject_b_k,
)?,
OP_MOD => arith(
Op::Mod,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"MOD",
reject_b_k,
)?,
OP_POW => arith(
Op::Pow,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"POW",
reject_b_k,
)?,
OP_DIV => arith(
Op::Div,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"DIV",
reject_b_k,
)?,
OP_IDIV => arith(
Op::IDiv,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"IDIV",
reject_b_k,
)?,
OP_BAND => arith(
Op::BAnd,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"BAND",
reject_b_k,
)?,
OP_BOR => arith(
Op::BOr,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"BOR",
reject_b_k,
)?,
OP_BXOR => arith(
Op::BXor,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"BXOR",
reject_b_k,
)?,
OP_SHL => arith(
Op::Shl,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"SHL",
reject_b_k,
)?,
OP_SHR => arith(
Op::Shr,
i.a,
b_idx,
b_is_k,
c_idx,
c_is_k,
"SHR",
reject_b_k,
)?,
OP_CONCAT => {
if b_idx != i.a {
return Err("PUC 5.3 translator: CONCAT with B != A not yet supported \
(would require a MOVE pre-pass)"
.to_string());
}
let len = c_idx
.checked_sub(b_idx)
.ok_or("PUC 5.3 translator: CONCAT C < B")?
+ 1;
Inst::iabc(Op::Concat, i.a, fit_b(len, "CONCAT len")?, 0, false)
}
OP_JMP => {
if i.a != 0 {
return Err(
"PUC 5.3 translator: OP_JMP with close-upvalues hint (A != 0) not yet \
supported (planned: emit `Close (A-1); Jmp sBx` with PC remap)"
.to_string(),
);
}
Inst::isj(Op::Jmp, i.sbx())
}
OP_EQ => cmp(Op::Eq, i.a, b_idx, b_is_k, c_idx, c_is_k, "EQ")?,
OP_LT => cmp(Op::Lt, i.a, b_idx, b_is_k, c_idx, c_is_k, "LT")?,
OP_LE => cmp(Op::Le, i.a, b_idx, b_is_k, c_idx, c_is_k, "LE")?,
OP_TEST => {
Inst::iabc(Op::Test, i.a, 0, 0, c_idx != 0)
}
OP_TESTSET => Inst::iabc(Op::TestSet, i.a, fit_b(b_idx, "TESTSET.B")?, 0, c_idx != 0),
OP_CALL => Inst::iabc(
Op::Call,
i.a,
fit_b(b_idx, "CALL.B")?,
fit_b(c_idx, "CALL.C")?,
false,
),
OP_TAILCALL => Inst::iabc(
Op::TailCall,
i.a,
fit_b(b_idx, "TAILCALL.B")?,
fit_b(c_idx, "TAILCALL.C")?,
false,
),
OP_RETURN => Inst::iabc(Op::Return, i.a, fit_b(b_idx, "RETURN.B")?, 0, false),
OP_FORPREP => {
let sbx = i.sbx();
if !(0..=crate::vm::isa::MAX_BX as i32).contains(&sbx) {
return Err(format!(
"PUC 5.3 translator: FORPREP sBx {sbx} out of luna Bx range"
));
}
Inst::iabx(Op::ForPrep, i.a, sbx as u32)
}
OP_FORLOOP => {
let sbx = i.sbx();
if sbx > 0 {
return Err(format!(
"PUC 5.3 translator: FORLOOP sBx {sbx} > 0 (expected backward jump)"
));
}
let back = (-sbx) as u32;
if back > crate::vm::isa::MAX_BX {
return Err(format!(
"PUC 5.3 translator: FORLOOP back-distance {back} out of luna Bx range"
));
}
Inst::iabx(Op::ForLoop, i.a, back)
}
OP_TFORCALL => {
Inst::iabc(Op::TForCall, i.a, 0, fit_b(c_idx, "TFORCALL.C")?, false)
}
OP_TFORLOOP => {
let sbx = i.sbx();
if sbx > 0 {
return Err(format!(
"PUC 5.3 translator: TFORLOOP sBx {sbx} > 0 (expected backward jump)"
));
}
let back = (-sbx) as u32;
if back > crate::vm::isa::MAX_BX {
return Err(format!(
"PUC 5.3 translator: TFORLOOP back-distance {back} out of luna Bx range"
));
}
let iter_base = i.a.checked_sub(2).ok_or_else(|| {
format!(
"PUC 5.3 translator: TFORLOOP.A={} < 2 (cannot convert to luna iter_base)",
i.a
)
})?;
Inst::iabx(Op::TForLoop, iter_base, back)
}
OP_SETLIST => {
if c_idx == 0 {
return Err(
"PUC 5.3 translator: SETLIST with C == 0 (EXTRAARG block-index) \
not yet supported"
.to_string(),
);
}
Inst::iabc(
Op::SetList,
i.a,
fit_b(b_idx, "SETLIST.B")?,
fit_b(c_idx, "SETLIST.C")?,
false,
)
}
OP_CLOSURE => Inst::iabx(Op::Closure, i.a, i.bx()),
OP_VARARG => Inst::iabc(Op::Vararg, i.a, fit_b(b_idx, "VARARG.B")?, 0, false),
OP_EXTRAARG => Inst::iax(Op::ExtraArg, i.bx()),
_ => unreachable!("opcode-range guard above ruled this out"),
})
}
#[allow(clippy::too_many_arguments)]
fn arith(
op: Op,
a: u32,
b: u32,
b_is_k: bool,
c: u32,
c_is_k: bool,
name: &str,
reject_b_k: fn(&str, bool) -> Result<(), String>,
) -> Result<Inst, String> {
reject_b_k(name, b_is_k)?;
if b > 0xFF {
return Err(format!("PUC 5.3 translator: {name}.B operand {b} > 255"));
}
if c > 0xFF {
return Err(format!("PUC 5.3 translator: {name}.C operand {c} > 255"));
}
Ok(Inst::iabc(op, a, b, c, c_is_k))
}
fn cmp(
op: Op,
a: u32,
b: u32,
b_is_k: bool,
c: u32,
c_is_k: bool,
name: &str,
) -> Result<Inst, String> {
if b_is_k || c_is_k {
return Err(format!(
"PUC 5.3 translator: {name} with RK operand not yet supported \
(baseline accepts only register operands)"
));
}
if b > 0xFF || c > 0xFF {
return Err(format!(
"PUC 5.3 translator: {name} B/C operand out of luna 8-bit range"
));
}
let k = a != 0;
Ok(Inst::iabc(op, b, c, 0, k))
}
fn r_const(r: &mut Reader, heap: &mut Heap) -> Result<Value, String> {
Ok(match r.u8()? {
LUA_TNIL => Value::Nil,
LUA_TBOOLEAN => Value::Bool(r.u8()? != 0),
LUA_TNUMFLT => Value::Float(r_number(r)?),
LUA_TNUMINT => Value::Int(r_integer(r)?),
LUA_TSHRSTR | LUA_TLNGSTR => {
match r_string(r)? {
Some(b) => Value::Str(heap.intern(b)),
None => return Err("PUC 5.3 translator: NULL string in const pool".to_string()),
}
}
t => return Err(format!("PUC 5.3 translator: bad constant tag {t}")),
})
}
fn r_proto(
r: &mut Reader,
heap: &mut Heap,
parent_source: Option<Gc<crate::runtime::LuaStr>>,
) -> Result<Gc<Proto>, String> {
let source = match r_string(r)? {
Some(b) => heap.intern(b),
None => parent_source.unwrap_or_else(|| heap.intern(b"")),
};
let line_defined = r_int(r)? as u32;
let last_line_defined = r_int(r)? as u32;
let num_params = r.u8()?;
let is_vararg = r.u8()? != 0;
let max_stack_puc = r.u8()?;
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative code length".to_string());
}
let n = n as usize;
let mut puc_words: Vec<u32> = Vec::with_capacity(n);
for _ in 0..n {
puc_words.push(u32::from_le_bytes(r.take(4)?.try_into().unwrap()));
}
let translated = translate_code(&puc_words)?;
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative const count".to_string());
}
let mut consts = Vec::with_capacity(n as usize);
for _ in 0..n {
consts.push(r_const(r, heap)?);
}
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative upval count".to_string());
}
let mut upvals: Vec<UpvalDesc> = Vec::with_capacity(n as usize);
for _ in 0..n {
let in_stack = r.u8()? != 0;
let index = r.u8()?;
upvals.push(UpvalDesc {
in_stack,
index,
name: Box::from(""),
read_only: false,
});
}
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative proto count".to_string());
}
let mut protos = Vec::with_capacity(n as usize);
for _ in 0..n {
protos.push(r_proto(r, heap, Some(source))?);
}
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative lineinfo count".to_string());
}
let mut puc_lines: Vec<u32> = Vec::with_capacity(n as usize);
for _ in 0..n {
let ln = r_int(r)?;
puc_lines.push(if ln < 0 { 0 } else { ln as u32 });
}
let lines: Vec<u32> = translated
.luna_to_puc_pc
.iter()
.map(|&puc_pc| {
puc_lines
.get(puc_pc)
.copied()
.unwrap_or(0)
})
.collect();
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative locvar count".to_string());
}
let mut locvars = Vec::with_capacity(n as usize);
for _ in 0..n {
let name = match r_string(r)? {
Some(b) => String::from_utf8_lossy(b).into_owned().into(),
None => Box::from(""),
};
let start_pc = r_int(r)? as u32;
let end_pc = r_int(r)? as u32;
locvars.push(LocVar {
name,
reg: u32::MAX,
start_pc,
end_pc,
});
}
let n = r_int(r)?;
if n < 0 {
return Err("PUC 5.3 translator: negative upval-name count".to_string());
}
let n = n as usize;
if n > upvals.len() {
return Err("PUC 5.3 translator: more upval names than upvals".to_string());
}
for i in 0..n {
let name = match r_string(r)? {
Some(b) => String::from_utf8_lossy(b).into_owned().into(),
None => Box::from(""),
};
upvals[i].name = name;
}
let env_upval_idx = upvals
.iter()
.take(u8::MAX as usize)
.position(|u| &*u.name == "_ENV")
.map_or(u8::MAX, |i| i as u8);
let max_stack = max_stack_puc.saturating_add(translated.max_temp_bump);
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: false,
max_stack,
lines: 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()),
}))
}
pub(super) fn undump_puc_53(bytes: &[u8], heap: &mut Heap) -> Result<Gc<Proto>, String> {
let mut r = Reader::at(bytes, 0);
check_header(&mut r)?;
let _main_nupvals = r.u8()?;
let proto = r_proto(&mut r, heap, None)?;
if r.pos() != bytes.len() {
return Err(format!(
"PUC 5.3 translator: {} trailing bytes after main proto",
bytes.len() - r.pos()
));
}
Ok(proto)
}
#[cfg(test)]
#[allow(clippy::identity_op, clippy::erasing_op)]
mod tests {
use super::*;
#[test]
fn decode_53_layout_roundtrip() {
let word = 0u32 | (3 << 6) | (7 << 14) | (5 << 23);
let i = decode_53(word);
assert_eq!(i.op, 0);
assert_eq!(i.a, 3);
assert_eq!(i.b, 5);
assert_eq!(i.c, 7);
}
#[test]
fn decode_53_sbx_sign() {
let bx: u32 = (PUC53_MAX_ARG_SBX - 3) as u32;
let word = 39u32
| 0
| ((bx & ((1 << PUC53_SIZE_C) - 1)) << PUC53_POS_C)
| (((bx >> PUC53_SIZE_C) & ((1 << PUC53_SIZE_B) - 1)) << PUC53_POS_B);
let i = decode_53(word);
assert_eq!(i.op, 39);
assert_eq!(i.sbx(), -3);
}
#[test]
fn translate_move() {
let word = 0u32 | (3 << 6) | (0 << 14) | (5 << 23);
let inst = translate_inst(word).unwrap();
assert_eq!(inst.op(), Op::Move);
assert_eq!(inst.a(), 3);
assert_eq!(inst.b(), 5);
}
#[test]
fn translate_loadbool_false_skip() {
let word = 3u32 | (2 << 6) | (1 << 14) | (0 << 23);
let inst = translate_inst(word).unwrap();
assert_eq!(inst.op(), Op::LFalseSkip);
assert_eq!(inst.a(), 2);
}
#[test]
fn translate_loadbool_true_skip_rejected() {
let word = 3u32 | (2 << 6) | (1 << 14) | (1 << 23);
assert!(translate_inst(word).unwrap_err().contains("LOADBOOL"));
}
#[test]
fn translate_add_rk_c() {
let c_field = PUC53_BITRK | 7;
let word = 13u32 | (4 << 6) | (c_field << 14) | (5 << 23);
let inst = translate_inst(word).unwrap();
assert_eq!(inst.op(), Op::Add);
assert_eq!(inst.a(), 4);
assert_eq!(inst.b(), 5);
assert_eq!(inst.c(), 7);
assert!(inst.k());
}
#[test]
fn translate_add_rk_b_rejected() {
let b_field = PUC53_BITRK | 5;
let word = 13u32 | (4 << 6) | (7 << 14) | (b_field << 23);
assert!(translate_inst(word).unwrap_err().contains("RK on B"));
}
#[test]
fn translate_jmp_close_rejected() {
let bx: u32 = (PUC53_MAX_ARG_SBX + 10) as u32;
let word = 30u32 | (3 << 6) | ((bx & 0x1FF) << 14) | (((bx >> 9) & 0x1FF) << 23);
assert!(translate_inst(word).unwrap_err().contains("close-upvalues"));
}
#[test]
fn translate_tforcall() {
let word = 41u32 | (5 << 6) | (2 << 14) | (0 << 23);
let inst = translate_inst(word).unwrap();
assert_eq!(inst.op(), Op::TForCall);
assert_eq!(inst.a(), 5);
assert_eq!(inst.b(), 0);
assert_eq!(inst.c(), 2);
}
#[test]
fn translate_tforloop_back_jump() {
let bx: u32 = (PUC53_MAX_ARG_SBX - 3) as u32;
let word = 42u32 | (7 << 6) | ((bx & 0x1FF) << 14) | (((bx >> 9) & 0x1FF) << 23);
let inst = translate_inst(word).unwrap();
assert_eq!(inst.op(), Op::TForLoop);
assert_eq!(inst.a(), 5);
assert_eq!(inst.bx(), 3);
}
#[test]
fn translate_tforloop_forward_jump_rejected() {
let bx: u32 = (PUC53_MAX_ARG_SBX + 3) as u32;
let word = 42u32 | (7 << 6) | ((bx & 0x1FF) << 14) | (((bx >> 9) & 0x1FF) << 23);
assert!(
translate_inst(word)
.unwrap_err()
.contains("expected backward jump")
);
}
#[test]
fn translate_tforloop_low_a_rejected() {
let bx: u32 = (PUC53_MAX_ARG_SBX - 1) as u32;
let word = 42u32 | (1 << 6) | ((bx & 0x1FF) << 14) | (((bx >> 9) & 0x1FF) << 23);
assert!(translate_inst(word).unwrap_err().contains("< 2"));
}
fn enc_iabc(op: u8, a: u32, b: u32, c: u32) -> u32 {
(op as u32) | ((a & 0xFF) << 6) | ((c & 0x1FF) << 14) | ((b & 0x1FF) << 23)
}
fn enc_iasbx(op: u8, a: u32, sbx: i32) -> u32 {
let bx = (sbx + PUC53_MAX_ARG_SBX) as u32;
(op as u32) | ((a & 0xFF) << 6) | ((bx & 0x1FF) << 14) | (((bx >> 9) & 0x1FF) << 23)
}
#[test]
fn pc_remap_identity_on_one_to_one_stream() {
let words = vec![
enc_iabc(OP_MOVE, 0, 1, 0), enc_iabc(OP_LOADNIL, 0, 0, 0), enc_iabc(OP_RETURN, 0, 1, 0), ];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 3);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(1), Some(2)]);
assert_eq!(t.luna_to_puc_pc, vec![0, 1, 2]);
assert_eq!(t.max_temp_bump, 0);
}
#[test]
fn jump_remap_through_fixup_loop() {
let words = vec![
enc_iasbx(OP_JMP, 0, 1), enc_iabc(OP_MOVE, 0, 1, 0), enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 3);
assert_eq!(t.code[0].op(), Op::Jmp);
assert_eq!(t.code[0].sj(), 1);
}
fn assert_rk_on_b_lowered(puc_op: u8, luna_op: Op) {
let b_field = PUC53_BITRK | 5; let words = vec![enc_iabc(puc_op, 2, b_field, 3)];
let t = translate_code(&words).expect("RK-on-B arith must lower cleanly");
assert_eq!(t.code.len(), 2, "{puc_op}: expected 2-inst lowering");
assert_eq!(t.code[0].op(), Op::LoadK);
assert_eq!(t.code[0].a(), 4);
assert_eq!(t.code[0].bx(), 5);
assert_eq!(t.code[1].op(), luna_op);
assert_eq!(t.code[1].a(), 2);
assert_eq!(t.code[1].b(), 4);
assert_eq!(t.code[1].c(), 3);
assert!(!t.code[1].k());
assert_eq!(t.puc_to_luna_pc, vec![Some(0)]);
assert_eq!(t.luna_to_puc_pc, vec![0, 0]);
assert_eq!(t.max_temp_bump, 5);
}
#[test]
fn rk_on_b_lowering_add() {
assert_rk_on_b_lowered(OP_ADD, Op::Add);
}
#[test]
fn rk_on_b_lowering_sub() {
assert_rk_on_b_lowered(OP_SUB, Op::Sub);
}
#[test]
fn rk_on_b_lowering_mul() {
assert_rk_on_b_lowered(OP_MUL, Op::Mul);
}
#[test]
fn rk_on_b_lowering_mod() {
assert_rk_on_b_lowered(OP_MOD, Op::Mod);
}
#[test]
fn rk_on_b_lowering_pow() {
assert_rk_on_b_lowered(OP_POW, Op::Pow);
}
#[test]
fn rk_on_b_lowering_div() {
assert_rk_on_b_lowered(OP_DIV, Op::Div);
}
#[test]
fn rk_on_b_lowering_idiv() {
assert_rk_on_b_lowered(OP_IDIV, Op::IDiv);
}
#[test]
fn rk_on_b_lowering_band() {
assert_rk_on_b_lowered(OP_BAND, Op::BAnd);
}
#[test]
fn rk_on_b_lowering_bor() {
assert_rk_on_b_lowered(OP_BOR, Op::BOr);
}
#[test]
fn rk_on_b_lowering_bxor() {
assert_rk_on_b_lowered(OP_BXOR, Op::BXor);
}
#[test]
fn rk_on_b_lowering_shl() {
assert_rk_on_b_lowered(OP_SHL, Op::Shl);
}
#[test]
fn rk_on_b_lowering_shr() {
assert_rk_on_b_lowered(OP_SHR, Op::Shr);
}
#[test]
fn rk_on_b_propagates_c_k_flag() {
let b_field = PUC53_BITRK | 5;
let c_field = PUC53_BITRK | 7;
let words = vec![enc_iabc(OP_ADD, 2, b_field, c_field)];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 2);
assert_eq!(t.code[0].op(), Op::LoadK);
assert_eq!(t.code[0].bx(), 5);
assert_eq!(t.code[1].op(), Op::Add);
assert!(t.code[1].k(), "C side stays RK, k bit must propagate");
assert_eq!(t.code[1].c(), 7);
}
#[test]
fn rk_on_b_shifts_downstream_jump_target() {
let b_field = PUC53_BITRK | 0;
let words = vec![
enc_iabc(OP_ADD, 0, b_field, 1),
enc_iasbx(OP_JMP, 0, 1),
enc_iabc(OP_MOVE, 0, 1, 0),
enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 5);
assert_eq!(t.code[0].op(), Op::LoadK);
assert_eq!(t.code[1].op(), Op::Add);
assert_eq!(t.code[2].op(), Op::Jmp);
assert_eq!(
t.code[2].sj(),
1,
"JMP target must remap to luna pc 4 after lowering shift"
);
assert_eq!(t.code[3].op(), Op::Move);
assert_eq!(t.code[4].op(), Op::Return);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(2), Some(3), Some(4)]);
}
#[test]
fn translate_loadbool_true_skip() {
let words = vec![
enc_iabc(OP_LOADBOOL, 2, 1, 1), enc_iabc(OP_MOVE, 0, 1, 0), enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).expect("LOADBOOL true+skip must lower");
assert_eq!(t.code.len(), 4, "1 punt-lowered pair + 2 1:1 insts");
assert_eq!(t.code[0].op(), Op::LoadTrue);
assert_eq!(t.code[0].a(), 2);
assert_eq!(t.code[1].op(), Op::Jmp);
assert_eq!(
t.code[1].sj(),
1,
"Jmp must skip exactly one luna inst when next PUC op is 1:1"
);
assert_eq!(t.code[2].op(), Op::Move);
assert_eq!(t.code[3].op(), Op::Return);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(2), Some(3)]);
assert_eq!(t.luna_to_puc_pc, vec![0, 0, 1, 2]);
assert_eq!(t.max_temp_bump, 0);
}
#[test]
fn translate_loadbool_true_skip_target_remaps_through_lowered_next() {
let b_field = PUC53_BITRK | 0;
let words = vec![
enc_iabc(OP_LOADBOOL, 2, 1, 1),
enc_iabc(OP_ADD, 0, b_field, 1),
enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 5);
assert_eq!(t.code[0].op(), Op::LoadTrue);
assert_eq!(t.code[1].op(), Op::Jmp);
assert_eq!(
t.code[1].sj(),
2,
"Jmp must skip the full LoadK+Add pair (2 luna insts)"
);
assert_eq!(t.code[2].op(), Op::LoadK);
assert_eq!(t.code[3].op(), Op::Add);
assert_eq!(t.code[4].op(), Op::Return);
}
#[test]
fn translate_loadbool_false_no_skip_still_one_to_one() {
let words = vec![enc_iabc(OP_LOADBOOL, 3, 0, 0), enc_iabc(OP_RETURN, 0, 1, 0)];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 2);
assert_eq!(t.code[0].op(), Op::LoadFalse);
assert_eq!(t.code[0].a(), 3);
assert_eq!(t.code[1].op(), Op::Return);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(1)]);
}
#[test]
fn translate_loadbool_true_no_skip_still_one_to_one() {
let words = vec![enc_iabc(OP_LOADBOOL, 3, 1, 0), enc_iabc(OP_RETURN, 0, 1, 0)];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 2);
assert_eq!(t.code[0].op(), Op::LoadTrue);
assert_eq!(t.code[1].op(), Op::Return);
}
#[test]
fn translate_loadbool_false_skip_still_one_to_one() {
let words = vec![
enc_iabc(OP_LOADBOOL, 3, 0, 1),
enc_iabc(OP_MOVE, 0, 1, 0),
enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 3);
assert_eq!(t.code[0].op(), Op::LFalseSkip);
}
#[test]
fn translate_concat_b_gt_a() {
let words = vec![enc_iabc(OP_CONCAT, 0, 2, 4), enc_iabc(OP_RETURN, 0, 2, 0)];
let t = translate_code(&words).expect("CONCAT B>A must lower");
assert_eq!(t.code.len(), 5);
assert_eq!(t.code[0].op(), Op::Move);
assert_eq!(t.code[0].a(), 0);
assert_eq!(t.code[0].b(), 2);
assert_eq!(t.code[1].op(), Op::Move);
assert_eq!(t.code[1].a(), 1);
assert_eq!(t.code[1].b(), 3);
assert_eq!(t.code[2].op(), Op::Move);
assert_eq!(t.code[2].a(), 2);
assert_eq!(t.code[2].b(), 4);
assert_eq!(t.code[3].op(), Op::Concat);
assert_eq!(t.code[3].a(), 0);
assert_eq!(t.code[3].b(), 3, "Concat len = C - B + 1 = 4 - 2 + 1");
assert_eq!(t.code[4].op(), Op::Return);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(4)]);
assert_eq!(t.luna_to_puc_pc, vec![0, 0, 0, 0, 1]);
}
#[test]
fn translate_concat_b_lt_a() {
let words = vec![enc_iabc(OP_CONCAT, 3, 0, 2), enc_iabc(OP_RETURN, 0, 2, 0)];
let t = translate_code(&words).expect("CONCAT B<A must lower");
assert_eq!(t.code.len(), 5);
assert_eq!(t.code[0].op(), Op::Move);
assert_eq!(t.code[0].a(), 5);
assert_eq!(t.code[0].b(), 2);
assert_eq!(t.code[1].op(), Op::Move);
assert_eq!(t.code[1].a(), 4);
assert_eq!(t.code[1].b(), 1);
assert_eq!(t.code[2].op(), Op::Move);
assert_eq!(t.code[2].a(), 3);
assert_eq!(t.code[2].b(), 0);
assert_eq!(t.code[3].op(), Op::Concat);
assert_eq!(t.code[3].a(), 3);
assert_eq!(t.code[3].b(), 3);
assert_eq!(t.code[4].op(), Op::Return);
}
#[test]
fn translate_concat_b_eq_a_still_one_to_one() {
let words = vec![enc_iabc(OP_CONCAT, 2, 2, 4), enc_iabc(OP_RETURN, 0, 2, 0)];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 2);
assert_eq!(t.code[0].op(), Op::Concat);
assert_eq!(t.code[0].a(), 2);
assert_eq!(t.code[0].b(), 3);
assert_eq!(t.code[1].op(), Op::Return);
}
#[test]
fn translate_concat_b_ne_a_shifts_downstream_jump() {
let words = vec![
enc_iabc(OP_CONCAT, 0, 2, 4),
enc_iasbx(OP_JMP, 0, 1),
enc_iabc(OP_MOVE, 0, 1, 0),
enc_iabc(OP_RETURN, 0, 1, 0),
];
let t = translate_code(&words).unwrap();
assert_eq!(t.code.len(), 7);
assert_eq!(t.code[3].op(), Op::Concat);
assert_eq!(t.code[4].op(), Op::Jmp);
assert_eq!(
t.code[4].sj(),
1,
"JMP target must remap past the CONCAT lowering"
);
assert_eq!(t.code[6].op(), Op::Return);
assert_eq!(t.puc_to_luna_pc, vec![Some(0), Some(4), Some(5), Some(6)]);
}
#[test]
fn translate_concat_c_lt_b_rejected() {
let words = vec![enc_iabc(OP_CONCAT, 0, 4, 2)];
let err = match translate_code(&words) {
Ok(_) => panic!("CONCAT C<B must reject"),
Err(e) => e,
};
assert!(err.contains("CONCAT C < B"), "got: {err}");
}
}