use crate::{LinderaResult, error::LinderaErrorKind, util::Data};
use byteorder::{ByteOrder, LittleEndian};
const NEW_FORMAT_HEADER_LEN: usize = 6;
const OLD_FORMAT_HEADER_LEN: usize = 4;
#[derive(Clone, Copy)]
struct CostsPtr(*const i16);
unsafe impl Send for CostsPtr {}
unsafe impl Sync for CostsPtr {}
enum CostStorage {
Borrowed(Data),
Owned(Vec<i16>),
}
impl CostStorage {
fn costs(&self) -> &[i16] {
match self {
Self::Borrowed(data) => unsafe {
let payload = &data[NEW_FORMAT_HEADER_LEN..];
core::slice::from_raw_parts(payload.as_ptr().cast::<i16>(), payload.len() / 2)
},
Self::Owned(costs) => costs,
}
}
}
impl Clone for CostStorage {
fn clone(&self) -> Self {
match self {
Self::Borrowed(data) => {
let cloned = data.clone();
if ConnectionCostMatrix::is_borrowable(&cloned) {
Self::Borrowed(cloned)
} else {
Self::Owned(self.costs().to_vec())
}
}
Self::Owned(costs) => Self::Owned(costs.clone()),
}
}
}
#[repr(C)]
pub struct ConnectionCostMatrix {
costs_ptr: CostsPtr,
costs_len: usize,
pub forward_size: u32,
pub backward_size: u32,
storage: CostStorage,
}
impl ConnectionCostMatrix {
pub fn load(conn_data: impl Into<Data>) -> LinderaResult<ConnectionCostMatrix> {
let conn_data = conn_data.into();
if conn_data.len() < OLD_FORMAT_HEADER_LEN {
return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
"Connection cost matrix data too short: {} bytes",
conn_data.len()
)));
}
let first_v = LittleEndian::read_i16(&conn_data[0..2]);
if first_v == -1 {
if conn_data.len() < NEW_FORMAT_HEADER_LEN {
return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
"Connection cost matrix header too short for new format: {} bytes",
conn_data.len()
)));
}
let forward_size = LittleEndian::read_i16(&conn_data[2..4]) as u32;
let backward_size = LittleEndian::read_i16(&conn_data[4..6]) as u32;
let costs_len = (conn_data.len() - NEW_FORMAT_HEADER_LEN) / 2;
Self::validate_axes(forward_size, backward_size, costs_len)?;
let storage = if Self::is_borrowable(&conn_data) {
CostStorage::Borrowed(conn_data)
} else {
let mut costs_data = vec![0i16; costs_len];
let end = NEW_FORMAT_HEADER_LEN + costs_len * 2;
LittleEndian::read_i16_into(
&conn_data[NEW_FORMAT_HEADER_LEN..end],
&mut costs_data,
);
CostStorage::Owned(costs_data)
};
Ok(Self::from_storage(storage, forward_size, backward_size))
} else {
let forward_size = first_v as u32;
let backward_size = LittleEndian::read_i16(&conn_data[2..4]) as u32;
let costs_len = (conn_data.len() - OLD_FORMAT_HEADER_LEN) / 2;
Self::validate_axes(forward_size, backward_size, costs_len)?;
let mut old_costs_data = vec![0i16; costs_len];
let end = OLD_FORMAT_HEADER_LEN + costs_len * 2;
LittleEndian::read_i16_into(
&conn_data[OLD_FORMAT_HEADER_LEN..end],
&mut old_costs_data,
);
let mut costs_data = vec![0i16; costs_len];
for f in 0..forward_size {
for b in 0..backward_size {
let old_id = (b + f * backward_size) as usize;
let new_id = (f + b * forward_size) as usize;
costs_data[new_id] = old_costs_data[old_id];
}
}
Ok(Self::from_storage(
CostStorage::Owned(costs_data),
forward_size,
backward_size,
))
}
}
fn from_storage(storage: CostStorage, forward_size: u32, backward_size: u32) -> Self {
let costs = storage.costs();
let costs_ptr = CostsPtr(costs.as_ptr());
let costs_len = costs.len();
Self {
storage,
costs_ptr,
costs_len,
backward_size,
forward_size,
}
}
fn is_borrowable(conn_data: &Data) -> bool {
if !cfg!(target_endian = "little") {
return false;
}
conn_data[NEW_FORMAT_HEADER_LEN..]
.as_ptr()
.cast::<i16>()
.is_aligned()
}
fn validate_axes(forward_size: u32, backward_size: u32, costs_len: usize) -> LinderaResult<()> {
let required = (forward_size as usize)
.checked_mul(backward_size as usize)
.ok_or_else(|| {
LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
"Connection cost matrix axes overflow: forward_size={forward_size}, backward_size={backward_size}"
))
})?;
if costs_len < required {
return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
"Connection cost matrix payload holds {costs_len} values but the header requires {required} (forward_size={forward_size}, backward_size={backward_size})"
)));
}
Ok(())
}
#[inline(always)]
pub fn costs(&self) -> &[i16] {
unsafe { core::slice::from_raw_parts(self.costs_ptr.0, self.costs_len) }
}
pub fn is_zero_copy(&self) -> bool {
matches!(self.storage, CostStorage::Borrowed(_))
}
#[inline]
pub fn row(&self, backward_id: u32) -> &[i16] {
let start = (backward_id * self.forward_size) as usize;
&self.costs()[start..start + self.forward_size as usize]
}
#[inline]
pub fn cost(&self, forward_id: u32, backward_id: u32) -> i32 {
#[cfg(feature = "ctxfreq")]
crate::builder::context_id_remap::record_access(forward_id, backward_id);
let cost_id = (forward_id + backward_id * self.forward_size) as usize;
self.costs()[cost_id] as i32
}
}
impl Clone for ConnectionCostMatrix {
fn clone(&self) -> Self {
Self::from_storage(self.storage.clone(), self.forward_size, self.backward_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
use byteorder::{LittleEndian, WriteBytesExt};
#[repr(C, align(16))]
struct AlignedBuf<const N: usize> {
body: [u8; N],
}
#[repr(C, align(2))]
struct MisalignedBuf<const N: usize> {
_pad: u8,
body: [u8; N],
}
const TRANSPOSED: [u8; 18] = [
0xff, 0xff, 0x02, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00,
];
const TRANSPOSED_ODD: [u8; 19] = [
0xff, 0xff, 0x02, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e,
0x00, 0x0f, 0x00, 0x00,
];
const DEGENERATE: [u8; 8] = [0xff, 0xff, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00];
static ALIGNED_TRANSPOSED: AlignedBuf<18> = AlignedBuf { body: TRANSPOSED };
static MISALIGNED_TRANSPOSED: MisalignedBuf<18> = MisalignedBuf {
_pad: 0,
body: TRANSPOSED,
};
static MISALIGNED_TRANSPOSED_ODD: MisalignedBuf<19> = MisalignedBuf {
_pad: 0,
body: TRANSPOSED_ODD,
};
static ALIGNED_DEGENERATE: AlignedBuf<8> = AlignedBuf { body: DEGENERATE };
fn assert_sample_costs(matrix: &ConnectionCostMatrix) {
assert_eq!(matrix.forward_size, 2);
assert_eq!(matrix.backward_size, 3);
assert_eq!(matrix.cost(0, 0), 10);
assert_eq!(matrix.cost(1, 0), 11);
assert_eq!(matrix.cost(0, 1), 12);
assert_eq!(matrix.cost(1, 1), 13);
assert_eq!(matrix.cost(0, 2), 14);
assert_eq!(matrix.cost(1, 2), 15);
assert_eq!(matrix.row(0), &[10, 11]);
assert_eq!(matrix.row(1), &[12, 13]);
assert_eq!(matrix.row(2), &[14, 15]);
}
#[test]
fn test_load_transposed() {
let matrix = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
assert_sample_costs(&matrix);
}
#[test]
fn test_load_old_format() {
let mut data = Vec::new();
data.write_i16::<LittleEndian>(2).unwrap(); data.write_i16::<LittleEndian>(3).unwrap(); for v in [10i16, 12, 14, 11, 13, 15] {
data.write_i16::<LittleEndian>(v).unwrap();
}
let matrix = ConnectionCostMatrix::load(data).unwrap();
assert_sample_costs(&matrix);
assert!(!matrix.is_zero_copy());
}
#[test]
fn test_load_data_too_short() {
let data: Vec<u8> = vec![0x01, 0x02];
let result = ConnectionCostMatrix::load(data);
assert!(result.is_err());
}
#[test]
fn transposed_bytes_match_the_const() {
let mut expected = Vec::new();
expected.write_i16::<LittleEndian>(-1).unwrap();
expected.write_i16::<LittleEndian>(2).unwrap();
expected.write_i16::<LittleEndian>(3).unwrap();
for v in [10i16, 11, 12, 13, 14, 15] {
expected.write_i16::<LittleEndian>(v).unwrap();
}
assert_eq!(TRANSPOSED.as_slice(), expected.as_slice());
assert_eq!(&TRANSPOSED_ODD[..18], TRANSPOSED.as_slice());
assert_eq!(TRANSPOSED_ODD[18], 0);
}
#[test]
fn borrowed_and_owned_produce_identical_costs() {
let borrowed = ConnectionCostMatrix::load(&ALIGNED_TRANSPOSED.body[..]).unwrap();
let owned = ConnectionCostMatrix::load(&MISALIGNED_TRANSPOSED.body[..]).unwrap();
assert!(borrowed.is_zero_copy(), "aligned payload must be borrowed");
assert!(
!owned.is_zero_copy(),
"misaligned payload must fall back to an owned copy"
);
assert_sample_costs(&borrowed);
assert_sample_costs(&owned);
assert_eq!(borrowed.costs(), owned.costs());
}
#[test]
fn clone_does_not_alias_the_source_buffer() {
let source = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
let cloned = source.clone();
drop(source);
assert_sample_costs(&cloned);
}
#[test]
fn cloning_a_static_backed_matrix_stays_zero_copy() {
let source = ConnectionCostMatrix::load(&ALIGNED_TRANSPOSED.body[..]).unwrap();
assert!(source.is_zero_copy());
let cloned = source.clone();
assert!(cloned.is_zero_copy());
assert_eq!(cloned.costs().as_ptr(), source.costs().as_ptr());
assert_sample_costs(&cloned);
}
#[test]
fn matrix_survives_move() {
let matrix = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
let boxed = Box::new(matrix);
assert_sample_costs(&boxed);
let mut holder = Vec::with_capacity(1);
holder.push(*boxed);
for _ in 0..64 {
holder.push(ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap());
}
for matrix in &holder {
assert_sample_costs(matrix);
}
}
#[test]
fn connection_cost_matrix_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ConnectionCostMatrix>();
}
#[test]
fn rejects_axes_larger_than_payload() {
let mut data = Vec::new();
data.write_i16::<LittleEndian>(-1).unwrap();
data.write_i16::<LittleEndian>(100).unwrap();
data.write_i16::<LittleEndian>(100).unwrap();
data.write_i16::<LittleEndian>(0).unwrap();
let result = ConnectionCostMatrix::load(data);
assert!(result.is_err());
}
#[test]
fn odd_length_payload_does_not_panic() {
let matrix = ConnectionCostMatrix::load(&MISALIGNED_TRANSPOSED_ODD.body[..]).unwrap();
assert!(!matrix.is_zero_copy());
assert_sample_costs(&matrix);
}
#[test]
fn degenerate_single_cell_matrix_is_borrowable() {
let matrix = ConnectionCostMatrix::load(&ALIGNED_DEGENERATE.body[..]).unwrap();
assert!(matrix.is_zero_copy());
assert_eq!(matrix.cost(0, 0), 7);
}
}