#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OpcodeHandlerEntry {
pub handler_offset: u32,
pub handler_arity: u8,
pub side_effecting: bool,
pub control_flow: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum PackError {
OffsetTooLarge {
opcode: usize,
offset: u32,
},
ArityTooLarge {
opcode: usize,
arity: u8,
},
}
#[must_use]
#[inline]
pub const fn packed_dispatch_table_len(entries_len: usize) -> usize {
entries_len
}
pub fn pack_dispatch_table(entries: &[OpcodeHandlerEntry]) -> Result<Vec<u32>, PackError> {
let mut out = Vec::with_capacity(packed_dispatch_table_len(entries.len()));
pack_dispatch_table_into(entries, &mut out)?;
Ok(out)
}
pub fn pack_dispatch_table_into(
entries: &[OpcodeHandlerEntry],
out: &mut Vec<u32>,
) -> Result<(), PackError> {
out.clear();
out.reserve(entries.len());
for (idx, entry) in entries.iter().enumerate() {
if entry.handler_offset >= (1u32 << 24) {
return Err(PackError::OffsetTooLarge {
opcode: idx,
offset: entry.handler_offset,
});
}
if entry.handler_arity > 15 {
return Err(PackError::ArityTooLarge {
opcode: idx,
arity: entry.handler_arity,
});
}
let mut packed: u32 = entry.handler_offset & 0x00FF_FFFF;
packed |= (u32::from(entry.handler_arity) & 0xF) << 24;
if entry.side_effecting {
packed |= 1 << 28;
}
if entry.control_flow {
packed |= 1 << 29;
}
out.push(packed);
}
Ok(())
}
#[must_use]
pub fn unpack_entry(packed: u32) -> OpcodeHandlerEntry {
OpcodeHandlerEntry {
handler_offset: packed & 0x00FF_FFFF,
handler_arity: ((packed >> 24) & 0xF) as u8,
side_effecting: (packed >> 28) & 0x1 == 1,
control_flow: (packed >> 29) & 0x1 == 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_trip_preserves_entry_fields() {
let entry = OpcodeHandlerEntry {
handler_offset: 0x123456,
handler_arity: 7,
side_effecting: true,
control_flow: false,
};
let packed = pack_dispatch_table(&[entry]).expect("pack must succeed");
assert_eq!(packed.len(), 1);
let recovered = unpack_entry(packed[0]);
assert_eq!(recovered, entry, "round-trip must preserve every field");
}
#[test]
fn round_trip_handles_all_flag_combinations() {
for side_effecting in [false, true] {
for control_flow in [false, true] {
let entry = OpcodeHandlerEntry {
handler_offset: 42,
handler_arity: 3,
side_effecting,
control_flow,
};
let packed = pack_dispatch_table(&[entry]).unwrap();
assert_eq!(unpack_entry(packed[0]), entry);
}
}
}
#[test]
fn pack_rejects_offset_at_field_boundary() {
let entry = OpcodeHandlerEntry {
handler_offset: 1u32 << 24, handler_arity: 0,
side_effecting: false,
control_flow: false,
};
match pack_dispatch_table(&[entry]) {
Err(PackError::OffsetTooLarge { opcode: 0, offset }) => {
assert_eq!(offset, 1u32 << 24);
}
other => panic!("expected OffsetTooLarge at the 24-bit boundary; got {other:?}"),
}
}
#[test]
fn pack_rejects_arity_at_field_boundary() {
let entry = OpcodeHandlerEntry {
handler_offset: 0,
handler_arity: 16, side_effecting: false,
control_flow: false,
};
match pack_dispatch_table(&[entry]) {
Err(PackError::ArityTooLarge { opcode: 0, arity }) => {
assert_eq!(arity, 16);
}
other => panic!("expected ArityTooLarge at the 4-bit boundary; got {other:?}"),
}
}
#[test]
fn pack_preserves_per_entry_index_in_error() {
let mut entries = vec![
OpcodeHandlerEntry {
handler_offset: 0,
handler_arity: 0,
side_effecting: false,
control_flow: false,
};
10
];
entries[7].handler_offset = 1u32 << 25; match pack_dispatch_table(&entries) {
Err(PackError::OffsetTooLarge { opcode: 7, .. }) => {}
other => panic!("expected error at opcode 7; got {other:?}"),
}
}
#[test]
fn pack_empty_table_returns_empty_vec() {
let packed = pack_dispatch_table(&[]).expect("empty pack must succeed");
assert!(packed.is_empty());
}
#[test]
fn pack_into_reuses_existing_capacity() {
let entries = [
OpcodeHandlerEntry {
handler_offset: 8,
handler_arity: 2,
side_effecting: false,
control_flow: true,
},
OpcodeHandlerEntry {
handler_offset: 16,
handler_arity: 3,
side_effecting: true,
control_flow: false,
},
];
let mut out = Vec::with_capacity(64);
let before = out.capacity();
pack_dispatch_table_into(&entries, &mut out).expect("pack_into must succeed");
assert_eq!(out.len(), entries.len());
assert_eq!(
out.capacity(),
before,
"pack_into must reuse caller-owned capacity"
);
assert_eq!(unpack_entry(out[0]), entries[0]);
assert_eq!(unpack_entry(out[1]), entries[1]);
}
#[test]
fn required_len_matches_entry_count() {
assert_eq!(packed_dispatch_table_len(0), 0);
assert_eq!(packed_dispatch_table_len(256), 256);
}
#[test]
fn pack_full_256_opcode_table_succeeds() {
let entries: Vec<_> = (0..256u32)
.map(|opcode| OpcodeHandlerEntry {
handler_offset: opcode * 16,
handler_arity: (opcode % 4) as u8,
side_effecting: opcode % 2 == 0,
control_flow: opcode % 8 == 0,
})
.collect();
let packed = pack_dispatch_table(&entries).expect("full 256-table must pack");
assert_eq!(packed.len(), 256);
assert_eq!(unpack_entry(packed[0]), entries[0]);
assert_eq!(unpack_entry(packed[127]), entries[127]);
assert_eq!(unpack_entry(packed[255]), entries[255]);
}
#[test]
fn handler_arity_zero_packs_cleanly() {
let entry = OpcodeHandlerEntry {
handler_offset: 100,
handler_arity: 0,
side_effecting: false,
control_flow: false,
};
let packed = pack_dispatch_table(&[entry]).unwrap();
assert_eq!(packed[0], 100, "arity=0 + flags=0 packs as just the offset");
}
}