use crate::error::MemoryError;
pub const MAX_COMPRESSED_HANDLE: u32 = 0xFFFF;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OopMode {
Standard,
Compressed,
}
impl Default for OopMode {
fn default() -> Self {
OopMode::Standard
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressedOops {
pub mode: OopMode,
pub base: u32,
pub shift: u32,
}
impl Default for CompressedOops {
fn default() -> Self {
Self::standard()
}
}
impl CompressedOops {
pub const fn standard() -> Self {
Self {
mode: OopMode::Standard,
base: 0,
shift: 0,
}
}
pub const fn compressed() -> Self {
Self {
mode: OopMode::Compressed,
base: 0,
shift: 0,
}
}
pub const fn compressed_with(base: u32, shift: u32) -> Self {
Self {
mode: OopMode::Compressed,
base,
shift,
}
}
pub fn is_compressed(&self) -> bool {
matches!(self.mode, OopMode::Compressed)
}
pub fn max_handle(&self) -> u32 {
match self.mode {
OopMode::Standard => u32::MAX,
OopMode::Compressed => self.base + (MAX_COMPRESSED_HANDLE << self.shift),
}
}
pub fn fits(&self, handle: u32) -> bool {
match self.mode {
OopMode::Standard => true,
OopMode::Compressed => {
if handle < self.base || handle > self.max_handle() {
return false;
}
let offset = handle - self.base;
let mask = (1u32 << self.shift) - 1;
offset & mask == 0
}
}
}
pub fn encode(&self, handle: u32) -> Result<u16, MemoryError> {
match self.mode {
OopMode::Standard => Ok((handle & 0xFFFF) as u16),
OopMode::Compressed => {
if !self.fits(handle) {
return Err(MemoryError::CompressedOopsOverflow(handle));
}
Ok(((handle - self.base) >> self.shift) as u16)
}
}
}
pub fn decode(&self, compressed: u16) -> u32 {
match self.mode {
OopMode::Standard => compressed as u32,
OopMode::Compressed => self.base + ((compressed as u32) << self.shift),
}
}
pub fn savings_per_slot(&self) -> usize {
match self.mode {
OopMode::Standard => 0,
OopMode::Compressed => 2,
}
}
pub fn savings(&self, n: usize) -> usize {
self.savings_per_slot() * n
}
pub fn encode_slice(&self, handles: &[u32]) -> Result<Vec<u16>, MemoryError> {
handles.iter().map(|&h| self.encode(h)).collect()
}
pub fn decode_slice(&self, compressed: &[u16]) -> Vec<u32> {
compressed.iter().map(|&c| self.decode(c)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn standard_mode_is_default() {
let oops = CompressedOops::default();
assert_eq!(oops.mode, OopMode::Standard);
assert!(!oops.is_compressed());
assert_eq!(oops.savings_per_slot(), 0);
}
#[test]
fn compressed_mode_round_trip() {
let oops = CompressedOops::compressed();
for handle in [0u32, 1, 100, 65535] {
let encoded = oops.encode(handle).expect("fits");
assert_eq!(oops.decode(encoded), handle);
}
}
#[test]
fn compressed_mode_overflow() {
let oops = CompressedOops::compressed();
assert!(matches!(
oops.encode(65536),
Err(MemoryError::CompressedOopsOverflow(65536))
));
}
#[test]
fn compressed_mode_rejects_below_base() {
let oops = CompressedOops::compressed_with(100, 0);
assert!(matches!(
oops.encode(99),
Err(MemoryError::CompressedOopsOverflow(99))
));
assert_eq!(oops.encode(100).unwrap(), 0);
assert_eq!(oops.decode(0), 100);
}
#[test]
fn shift_extends_addressable_range() {
let oops = CompressedOops::compressed_with(0, 2);
assert_eq!(oops.max_handle(), 0xFFFF * 4);
assert!(oops.fits(0xFFFF * 4));
assert!(!oops.fits(0xFFFF * 4 + 4));
assert!(!oops.fits(0xFFFF * 4 + 1));
let handle = 0x1000 * 4; let encoded = oops.encode(handle).unwrap();
assert_eq!(encoded, 0x1000);
assert_eq!(oops.decode(encoded), handle);
}
#[test]
fn unaligned_handle_with_shift_rejected() {
let oops = CompressedOops::compressed_with(0, 2);
assert!(!oops.fits(5));
assert!(matches!(
oops.encode(5),
Err(MemoryError::CompressedOopsOverflow(5))
));
assert!(oops.fits(4));
assert_eq!(oops.encode(4).unwrap(), 1);
assert_eq!(oops.decode(1), 4);
}
#[test]
fn bulk_round_trip() {
let oops = CompressedOops::compressed();
let handles = vec![0, 1, 2, 3, 100, 65535];
let encoded = oops.encode_slice(&handles).expect("all fit");
assert_eq!(encoded.len(), handles.len());
assert_eq!(oops.decode_slice(&encoded), handles);
}
#[test]
fn bulk_encode_short_circuits_on_overflow() {
let oops = CompressedOops::compressed();
let handles = vec![1, 2, 65536, 3];
assert!(oops.encode_slice(&handles).is_err());
}
#[test]
fn savings_scale_with_slot_count() {
let oops = CompressedOops::compressed();
assert_eq!(oops.savings(0), 0);
assert_eq!(oops.savings(1), 2);
assert_eq!(oops.savings(1000), 2000);
}
#[test]
fn standard_mode_preserves_low_bits() {
let oops = CompressedOops::standard();
assert_eq!(oops.encode(0x1234_5678).unwrap(), 0x5678);
assert_eq!(oops.decode(0x5678), 0x5678);
assert_eq!(oops.max_handle(), u32::MAX);
}
#[test]
fn fits_predicate_matches_encode() {
let oops = CompressedOops::compressed();
assert!(oops.fits(0));
assert!(oops.fits(65535));
assert!(!oops.fits(65536));
}
}