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 (code, lines) = translate_code(
&raw_code,
&raw_lines,
&child_nups,
env_shift,
&mut upvals,
&protos,
consts.len(),
)?;
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: 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: 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: std::cell::RefCell::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))
}
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<(Vec<Inst>, Vec<u32>), String> {
let mut new_pc_for: Vec<i64> = Vec::with_capacity(raw_code.len());
let mut closure_idx = 0usize;
let mut new_pc = 0i64;
let mut i = 0usize;
while i < raw_code.len() {
let inst = raw_code[i];
if inst.op == OP_CLOSURE {
new_pc_for.push(new_pc);
new_pc += 1;
if closure_idx >= child_nups.len() {
return Err(format!(
"OP_CLOSURE #{} has no matching nested proto",
closure_idx
));
}
let n = child_nups[closure_idx];
closure_idx += 1;
for j in 1..=n {
if i + j >= raw_code.len() {
return Err("OP_CLOSURE pseudo-instructions truncate the code stream".into());
}
new_pc_for.push(-1); }
i += 1 + n;
} else {
new_pc_for.push(new_pc);
new_pc += 1;
i += 1;
}
}
let mut out: Vec<Inst> = Vec::with_capacity(new_pc as usize);
let mut out_lines: Vec<u32> = Vec::with_capacity(new_pc as usize);
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 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 => {
if inst.c != 0 {
return Err("OP_LOADBOOL skip form (C != 0) not yet supported".into());
}
let op = if inst.b != 0 {
Op::LoadTrue
} else {
Op::LoadFalse
};
out.push(Inst::iabc(op, inst.a, 0, 0, false));
}
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)?,
OP_SUB => arith(&mut out, Op::Sub, inst, &rk)?,
OP_MUL => arith(&mut out, Op::Mul, inst, &rk)?,
OP_DIV => arith(&mut out, Op::Div, inst, &rk)?,
OP_MOD => arith(&mut out, Op::Mod, inst, &rk)?,
OP_POW => arith(&mut out, Op::Pow, inst, &rk)?,
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;
let target_new = resolve_jump_target(&new_pc_for, target_old)?;
let delta = target_new - (out.len() as i64 + 1);
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.push(Inst::isj(Op::Jmp, delta as i32));
}
OP_EQ => compare(&mut out, Op::Eq, inst, &rk)?,
OP_LT => compare(&mut out, Op::Lt, inst, &rk)?,
OP_LE => compare(&mut out, Op::Le, inst, &rk)?,
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;
let target_new = resolve_jump_target(&new_pc_for, target_old)?;
let delta = target_new - (out.len() as i64 + 1);
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.push(Inst::iasbx(Op::ForLoop, inst.a, delta as i32));
}
OP_FORPREP => {
let target_old = (i as i64) + 1 + inst.sbx as i64;
let target_new = resolve_jump_target(&new_pc_for, target_old)?;
let delta = target_new - (out.len() as i64 + 1);
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.push(Inst::iasbx(Op::ForPrep, inst.a, delta as i32));
}
OP_TFORLOOP => {
return Err(
"OP_TFORLOOP translation not yet implemented (punt-A — see module docs)".into(),
);
}
OP_SETLIST => {
if inst.c == 0 {
return Err("OP_SETLIST C=0 (next-inst block index) not yet supported".into());
}
if inst.c > 0xFF {
return Err(format!("OP_SETLIST C={} > 255", inst.c));
}
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));
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}"));
}
}
out_lines.push(line);
i += 1;
}
debug_assert_eq!(out.len() as i64, new_pc, "pass-1 / pass-2 length disagree");
let _ = upvals;
Ok((out, out_lines))
}
fn arith<F>(out: &mut Vec<Inst>, op: Op, inst: Pre51Inst, rk: &F) -> 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 {
return Err(format!(
"arith op with K on B-side (5.1 RK(B)) not yet supported \
(op={op:?}, A={}, K[{b_val}], …)",
inst.a
));
}
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) -> 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 || c_is_k {
return Err(
"5.1 EQ/LT/LE with RK on operand not yet supported (only register form)".into(),
);
}
let k = inst.a != 0;
out.push(Inst::iabc(op, b_val, c_val, 0, k));
Ok(())
}
fn resolve_jump_target(new_pc_for: &[i64], target_old: i64) -> Result<i64, String> {
if target_old < 0 || target_old as usize >= new_pc_for.len() {
if target_old >= 0 && target_old as usize == new_pc_for.len() {
return Ok(*new_pc_for.last().unwrap_or(&0) + 1);
}
return Err(format!("jump target {target_old} out of range"));
}
let v = new_pc_for[target_old as usize];
if v < 0 {
return Err(format!(
"jump target {target_old} lands on stripped pseudo-instruction"
));
}
Ok(v)
}
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 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);
}
}