#[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,
},
Allocation {
requested: usize,
source: String,
},
}
#[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::new();
pack_dispatch_table_into(entries, &mut out)?;
Ok(out)
}
pub fn pack_dispatch_table_into(
entries: &[OpcodeHandlerEntry],
out: &mut Vec<u32>,
) -> Result<(), PackError> {
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 len = packed_dispatch_table_len(entries.len());
if len > out.capacity() {
out.try_reserve_exact(len - out.capacity())
.map_err(|source| PackError::Allocation {
requested: len,
source: source.to_string(),
})?;
}
out.clear();
out.extend(entries.iter().map(|entry| {
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;
}
packed
}));
Ok(())
}
#[must_use]
pub fn pack_entry(entry: OpcodeHandlerEntry) -> u32 {
let mut packed = 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;
}
packed
}
#[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("Fix: pack must succeed");
assert_eq!(packed.len(), 1);
let recovered = unpack_entry(packed[0]);
assert_eq!(recovered, entry, "round-trip must preserve every field");
assert_eq!(unpack_entry(pack_entry(entry)), entry);
}
#[test]
fn pack_into_reuses_output_and_is_transactional_on_invalid_entry() {
let entries = [
OpcodeHandlerEntry {
handler_offset: 1,
handler_arity: 2,
side_effecting: false,
control_flow: false,
},
OpcodeHandlerEntry {
handler_offset: 3,
handler_arity: 4,
side_effecting: true,
control_flow: true,
},
];
let mut out = Vec::with_capacity(8);
out.extend_from_slice(&[u32::MAX; 8]);
let ptr = out.as_ptr();
pack_dispatch_table_into(&entries, &mut out).unwrap();
assert_eq!(
out,
entries.iter().copied().map(pack_entry).collect::<Vec<_>>()
);
assert_eq!(out.as_ptr(), ptr);
let before = out.clone();
let bad = [OpcodeHandlerEntry {
handler_offset: 1 << 24,
handler_arity: 0,
side_effecting: false,
control_flow: false,
}];
assert!(matches!(
pack_dispatch_table_into(&bad, &mut out),
Err(PackError::OffsetTooLarge { .. })
));
assert_eq!(out, before);
}
#[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("Fix: 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("Fix: 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("Fix: 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");
}
}