jvmrs 0.1.2

A JVM implementation in Rust with Cranelift JIT, AOT compilation, and WebAssembly support
Documentation
//! Compressed ordinary object pointers (oops) for memory-constrained environments.
//!
//! Mirrors the HotSpot compressed oops mechanism: references are stored as
//! 16-bit compressed handles instead of 32-bit handles when the heap address
//! space is small enough, reducing per-reference storage by 50%. This is
//! valuable for embedded/no_std targets and large reference arrays.
//!
//! # Encoding
//!
//! A compressed reference is a `u16` index derived from the raw 32-bit handle.
//! The encoding is a shifted offset from an optional heap base:
//!
//! ```text
//! compressed = (raw_handle - base) >> shift
//! raw_handle = base + (compressed << shift)
//! ```
//!
//! With the default `shift = 0` and `base = 0`, compressed handles support
//! heaps of up to 65,535 objects/arrays. Higher shifts can be used when all
//! allocations are aligned (e.g. `shift = 2` for 4-byte aligned slots).
//!
//! # Example
//!
//! ```rust
//! use jvmrs::compressed_oops::{CompressedOops, OopMode};
//!
//! let oops = CompressedOops::compressed();
//! let handle: u32 = 42;
//! let encoded = oops.encode(handle).unwrap();
//! assert_eq!(encoded, 42);
//! assert_eq!(oops.decode(encoded), handle);
//! assert_eq!(oops.savings_per_slot(), 2);
//! ```

use crate::error::MemoryError;

/// Maximum handle value that can be encoded in compressed mode.
pub const MAX_COMPRESSED_HANDLE: u32 = 0xFFFF;

/// Addressing mode for object references.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OopMode {
    /// 32-bit references (no compression).
    Standard,
    /// 16-bit compressed references.
    Compressed,
}

impl Default for OopMode {
    fn default() -> Self {
        OopMode::Standard
    }
}

/// Compressed oops configuration.
///
/// Controls how raw 32-bit heap handles are encoded into and decoded from
/// 16-bit compressed references. The configuration is immutable after
/// construction; switching modes at runtime requires a new instance.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressedOops {
    /// Addressing mode.
    pub mode: OopMode,
    /// Heap base offset subtracted before compression.
    pub base: u32,
    /// Power-of-two alignment shift applied during compression.
    pub shift: u32,
}

impl Default for CompressedOops {
    fn default() -> Self {
        Self::standard()
    }
}

impl CompressedOops {
    /// Create a standard (32-bit, uncompressed) configuration.
    pub const fn standard() -> Self {
        Self {
            mode: OopMode::Standard,
            base: 0,
            shift: 0,
        }
    }

    /// Create a compressed configuration with default base=0, shift=0.
    pub const fn compressed() -> Self {
        Self {
            mode: OopMode::Compressed,
            base: 0,
            shift: 0,
        }
    }

    /// Create a compressed configuration with a custom base and alignment shift.
    ///
    /// `shift` should be the log2 of the allocation alignment (e.g. 2 for
    /// 4-byte aligned slots), which extends the addressable range.
    pub const fn compressed_with(base: u32, shift: u32) -> Self {
        Self {
            mode: OopMode::Compressed,
            base,
            shift,
        }
    }

    /// Returns true if compressed encoding is active.
    pub fn is_compressed(&self) -> bool {
        matches!(self.mode, OopMode::Compressed)
    }

    /// Largest raw handle representable under this configuration.
    ///
    /// In compressed mode this is the maximum *aligned* handle that round-trips
    /// losslessly through encode/decode.
    pub fn max_handle(&self) -> u32 {
        match self.mode {
            OopMode::Standard => u32::MAX,
            OopMode::Compressed => self.base + (MAX_COMPRESSED_HANDLE << self.shift),
        }
    }

    /// Returns true if `handle` can be encoded under this configuration.
    ///
    /// In compressed mode, the handle must be within the addressable range and
    /// (when `shift > 0`) must be aligned to `1 << shift` bytes; this preserves
    /// the lossless encode/decode property.
    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
            }
        }
    }

    /// Encode a raw 32-bit handle into a compressed 16-bit reference.
    ///
    /// In standard mode, the low 16 bits are returned. In compressed mode, the
    /// handle is validated against the addressable range and required alignment,
    /// guaranteeing a lossless round-trip.
    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)
            }
        }
    }

    /// Decode a compressed reference back into the raw 32-bit handle.
    pub fn decode(&self, compressed: u16) -> u32 {
        match self.mode {
            OopMode::Standard => compressed as u32,
            OopMode::Compressed => self.base + ((compressed as u32) << self.shift),
        }
    }

    /// Bytes saved per reference slot when using compression.
    ///
    /// Standard mode stores 4 bytes per reference; compressed mode stores 2.
    pub fn savings_per_slot(&self) -> usize {
        match self.mode {
            OopMode::Standard => 0,
            OopMode::Compressed => 2,
        }
    }

    /// Estimate total bytes saved for a container holding `n` references.
    pub fn savings(&self, n: usize) -> usize {
        self.savings_per_slot() * n
    }

    /// Encode a slice of raw handles in bulk.
    ///
    /// Returns `Err` on the first handle that does not fit.
    pub fn encode_slice(&self, handles: &[u32]) -> Result<Vec<u16>, MemoryError> {
        handles.iter().map(|&h| self.encode(h)).collect()
    }

    /// Decode a slice of compressed references in bulk.
    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);
        // 4-byte alignment; addressable up to base + (0xFFFF << 2)
        assert_eq!(oops.max_handle(), 0xFFFF * 4);
        assert!(oops.fits(0xFFFF * 4));
        // Just beyond range
        assert!(!oops.fits(0xFFFF * 4 + 4));
        // Unaligned handle within range is rejected to preserve lossless round-trip
        assert!(!oops.fits(0xFFFF * 4 + 1));

        let handle = 0x1000 * 4; // aligned to 4 bytes
        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);
        // Unaligned handles must be rejected so the round-trip is lossless.
        assert!(!oops.fits(5));
        assert!(matches!(
            oops.encode(5),
            Err(MemoryError::CompressedOopsOverflow(5))
        ));
        // The nearest aligned handle is accepted.
        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));
    }
}