lindera_dictionary/dictionary/connection_cost_matrix.rs
1use crate::{LinderaResult, error::LinderaErrorKind, util::Data};
2
3use byteorder::{ByteOrder, LittleEndian};
4
5/// Byte length of the transposed format header:
6/// `[i16 -1][i16 forward_size][i16 backward_size]`.
7const NEW_FORMAT_HEADER_LEN: usize = 6;
8
9/// Byte length of the legacy format header: `[i16 forward_size][i16 backward_size]`.
10const OLD_FORMAT_HEADER_LEN: usize = 4;
11
12/// A `*const i16` that is safe to share across threads.
13///
14/// A bare raw pointer is `!Send + !Sync`, which would strip both auto traits
15/// from [`ConnectionCostMatrix`] and, through `Arc<ConnectionCostMatrix>`,
16/// from `Dictionary` and every binding wrapper (`lindera-binding-core` has a
17/// compile-time `assert_send_sync` for exactly this). Confining the assertion
18/// to the pointer instead of blanket-asserting it for the whole struct keeps
19/// a future non-`Send` field catchable by the compiler.
20#[derive(Clone, Copy)]
21struct CostsPtr(*const i16);
22
23// SAFETY: the pointee is a `[i16]` that stays immutable for the lifetime of
24// the owning `ConnectionCostMatrix` (its `storage` field keeps the allocation
25// alive and no `&mut` path to it exists), and `i16` is `Send`. Sending the
26// pointer therefore exposes no more than sending a `&'static [i16]` would.
27unsafe impl Send for CostsPtr {}
28
29// SAFETY: same as `Send`. The pointee is never mutated, so concurrent shared
30// reads through this pointer cannot race, and `i16` is `Sync`.
31unsafe impl Sync for CostsPtr {}
32
33/// Owner of the cost values behind [`ConnectionCostMatrix`]'s cached pointer.
34///
35/// After [`ConnectionCostMatrix::load`] returns there is exactly one live
36/// variant and it never changes. The enum exists only so the borrowed and
37/// owned cases can share a single pointer-derivation site
38/// ([`ConnectionCostMatrix::from_storage`]); the hot path never matches on it.
39enum CostStorage {
40 /// Zero-copy: the payload is read in place from `matrix.mtx`'s bytes.
41 ///
42 /// Only constructed after [`ConnectionCostMatrix::is_borrowable`] has
43 /// confirmed a little-endian host and an `i16`-aligned payload start,
44 /// which together mean the on-disk bytes already *are* the in-memory
45 /// values.
46 Borrowed(Data),
47 /// Fallback: values decoded into an owned buffer, used when the payload
48 /// cannot be viewed in place (unaligned base address, big-endian host, or
49 /// the legacy non-transposed format, which needs a transpose anyway).
50 /// `Vec<i16>` is inherently `i16`-aligned, so no explicit alignment work
51 /// is needed here.
52 Owned(Vec<i16>),
53}
54
55impl CostStorage {
56 /// Returns the cost values this storage holds.
57 ///
58 /// Called only from [`ConnectionCostMatrix::from_storage`] at construction
59 /// time; the hot path reads the cached pointer instead, so this `match` is
60 /// never executed per lookup.
61 ///
62 /// # Returns
63 ///
64 /// The flat, transposed cost table.
65 fn costs(&self) -> &[i16] {
66 match self {
67 // SAFETY: `ConnectionCostMatrix::is_borrowable` verified a
68 // little-endian host and an `i16`-aligned payload start, and
69 // `load` verified the buffer is at least `NEW_FORMAT_HEADER_LEN`
70 // bytes long, so the subslice below cannot panic. `len() / 2`
71 // rounds down, so every element lies fully inside the backing
72 // allocation, whose size is by construction at most `isize::MAX`
73 // bytes. The bytes are never mutated (`Data` is reached through
74 // `&self` and no `&mut` path exists), a slice's `as_ptr()` is
75 // never null, and `i16` has no invalid bit patterns.
76 Self::Borrowed(data) => unsafe {
77 let payload = &data[NEW_FORMAT_HEADER_LEN..];
78 core::slice::from_raw_parts(payload.as_ptr().cast::<i16>(), payload.len() / 2)
79 },
80 Self::Owned(costs) => costs,
81 }
82 }
83}
84
85impl Clone for CostStorage {
86 /// Clones the storage, re-establishing the borrow invariant rather than
87 /// duplicating the variant blindly.
88 ///
89 /// `Data::Static` and `Data::Map` clone to the very same bytes (a copied
90 /// `'static` reference, and an `Arc` sharing one mapping), so a borrow
91 /// survives. `Data::Vec` does not: cloning it allocates a fresh buffer,
92 /// and `Vec<u8>` guarantees only 1-byte alignment, so the copy can land
93 /// at an address where the payload is no longer `i16`-aligned. Borrowing
94 /// from such a buffer would be undefined behaviour, so this decodes into
95 /// an owned buffer instead -- correct, just no longer zero-copy.
96 ///
97 /// # Returns
98 ///
99 /// Storage holding the same cost values, borrowed when that stays sound.
100 fn clone(&self) -> Self {
101 match self {
102 Self::Borrowed(data) => {
103 let cloned = data.clone();
104 if ConnectionCostMatrix::is_borrowable(&cloned) {
105 Self::Borrowed(cloned)
106 } else {
107 Self::Owned(self.costs().to_vec())
108 }
109 }
110 Self::Owned(costs) => Self::Owned(costs.clone()),
111 }
112 }
113}
114
115/// The connection cost matrix, in transposed layout
116/// (`costs[forward_id + backward_id * forward_size]`).
117///
118/// The values are borrowed from their backing bytes whenever possible (see
119/// [`ConnectionCostMatrix::is_zero_copy`]); for UniDic that avoids a 71 MB
120/// copy and 71 MB of anonymous RSS on every load.
121///
122/// `#[repr(C)]` is load-bearing: it pins the four fields the Viterbi inner
123/// loop touches into the first 24 bytes, i.e. a single cache line, and keeps
124/// the much larger `storage` -- which is read exactly once, at construction --
125/// behind them. Letting the default layout interleave them measured as a
126/// ~4.7% tokenize regression on IPADIC.
127#[repr(C)]
128pub struct ConnectionCostMatrix {
129 /// Pointer to the first cost value, derived once in
130 /// [`ConnectionCostMatrix::from_storage`].
131 ///
132 /// The address is stable across moves of `self`: moving `CostStorage`
133 /// moves only the `Data`/`Vec` headers, while the pointee lives in
134 /// `.rodata` (`Data::Static`), a stable heap allocation (`Data::Vec` and
135 /// `CostStorage::Owned`), or an `Arc`-owned mapping (`Data::Map`).
136 costs_ptr: CostsPtr,
137 /// Number of `i16` cost values reachable from `costs_ptr`.
138 costs_len: usize,
139 /// Number of forward (left-context) ids, i.e. the length of one row.
140 pub forward_size: u32,
141 /// Number of backward (right-context) ids, i.e. the number of rows.
142 pub backward_size: u32,
143 /// Keeps the cost values alive. Never mutated after construction and
144 /// never handed out by reference. Cold: nothing on the hot path reads it.
145 storage: CostStorage,
146}
147
148impl ConnectionCostMatrix {
149 /// Load a `ConnectionCostMatrix` from raw binary data.
150 ///
151 /// Supports the transposed format (header marker `-1`) and the legacy
152 /// format. The transposed format is borrowed in place when the host is
153 /// little-endian and the payload is `i16`-aligned; otherwise, and always
154 /// for the legacy format, the values are decoded into an owned buffer.
155 ///
156 /// # Arguments
157 ///
158 /// * `conn_data` - Raw binary data for the connection cost matrix.
159 ///
160 /// # Returns
161 ///
162 /// A `ConnectionCostMatrix`, or an error if the data is too short, the
163 /// header is malformed, or the axis sizes do not fit the payload.
164 pub fn load(conn_data: impl Into<Data>) -> LinderaResult<ConnectionCostMatrix> {
165 let conn_data = conn_data.into();
166 if conn_data.len() < OLD_FORMAT_HEADER_LEN {
167 return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
168 "Connection cost matrix data too short: {} bytes",
169 conn_data.len()
170 )));
171 }
172
173 let first_v = LittleEndian::read_i16(&conn_data[0..2]);
174
175 if first_v == -1 {
176 // New format (transposed)
177 if conn_data.len() < NEW_FORMAT_HEADER_LEN {
178 return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
179 "Connection cost matrix header too short for new format: {} bytes",
180 conn_data.len()
181 )));
182 }
183 let forward_size = LittleEndian::read_i16(&conn_data[2..4]) as u32;
184 let backward_size = LittleEndian::read_i16(&conn_data[4..6]) as u32;
185 // Round down: a trailing odd byte is not a whole cost value.
186 let costs_len = (conn_data.len() - NEW_FORMAT_HEADER_LEN) / 2;
187 Self::validate_axes(forward_size, backward_size, costs_len)?;
188
189 let storage = if Self::is_borrowable(&conn_data) {
190 CostStorage::Borrowed(conn_data)
191 } else {
192 let mut costs_data = vec![0i16; costs_len];
193 // Slice exactly `costs_len * 2` bytes: `read_i16_into` panics
194 // unless `src.len() == 2 * dst.len()`, which `&conn_data[6..]`
195 // violates for an odd-length buffer.
196 let end = NEW_FORMAT_HEADER_LEN + costs_len * 2;
197 LittleEndian::read_i16_into(
198 &conn_data[NEW_FORMAT_HEADER_LEN..end],
199 &mut costs_data,
200 );
201 CostStorage::Owned(costs_data)
202 };
203
204 Ok(Self::from_storage(storage, forward_size, backward_size))
205 } else {
206 // Old format: laid out as `[backward_id + forward_id *
207 // backward_size]`, so it must be transposed and can never be
208 // viewed in place.
209 let forward_size = first_v as u32;
210 let backward_size = LittleEndian::read_i16(&conn_data[2..4]) as u32;
211 let costs_len = (conn_data.len() - OLD_FORMAT_HEADER_LEN) / 2;
212 Self::validate_axes(forward_size, backward_size, costs_len)?;
213
214 let mut old_costs_data = vec![0i16; costs_len];
215 let end = OLD_FORMAT_HEADER_LEN + costs_len * 2;
216 LittleEndian::read_i16_into(
217 &conn_data[OLD_FORMAT_HEADER_LEN..end],
218 &mut old_costs_data,
219 );
220
221 // Transpose to new layout in memory
222 let mut costs_data = vec![0i16; costs_len];
223 for f in 0..forward_size {
224 for b in 0..backward_size {
225 let old_id = (b + f * backward_size) as usize;
226 let new_id = (f + b * forward_size) as usize;
227 costs_data[new_id] = old_costs_data[old_id];
228 }
229 }
230
231 Ok(Self::from_storage(
232 CostStorage::Owned(costs_data),
233 forward_size,
234 backward_size,
235 ))
236 }
237 }
238
239 /// Builds a matrix from already-validated storage, deriving the cached
240 /// costs pointer from it exactly once.
241 ///
242 /// This is the only site that computes `costs_ptr`. [`Clone`] routes
243 /// through it as well, so a clone can never keep a pointer into the
244 /// source's buffer.
245 ///
246 /// # Arguments
247 ///
248 /// * `storage` - Validated backing storage for the cost values.
249 /// * `forward_size` - Number of forward context ids.
250 /// * `backward_size` - Number of backward context ids.
251 ///
252 /// # Returns
253 ///
254 /// The constructed `ConnectionCostMatrix`.
255 fn from_storage(storage: CostStorage, forward_size: u32, backward_size: u32) -> Self {
256 let costs = storage.costs();
257 // The borrow ends here; moving `storage` into the struct below moves
258 // only the `Data`/`Vec` header, never the pointee, so this address
259 // stays valid.
260 let costs_ptr = CostsPtr(costs.as_ptr());
261 let costs_len = costs.len();
262 Self {
263 storage,
264 costs_ptr,
265 costs_len,
266 backward_size,
267 forward_size,
268 }
269 }
270
271 /// Whether the transposed payload inside `conn_data` can be viewed as
272 /// `[i16]` in place.
273 ///
274 /// # Arguments
275 ///
276 /// * `conn_data` - The whole matrix buffer, header included. Must be at
277 /// least [`NEW_FORMAT_HEADER_LEN`] bytes long.
278 ///
279 /// # Returns
280 ///
281 /// `true` when the host is little-endian and the payload start is
282 /// `i16`-aligned. The payload stores little-endian `i16` values, so on
283 /// such a host the on-disk bytes already are the in-memory values.
284 fn is_borrowable(conn_data: &Data) -> bool {
285 // On a big-endian host every value would need a byte swap, which only
286 // the owning path can do.
287 if !cfg!(target_endian = "little") {
288 return false;
289 }
290 // Pure address arithmetic; nothing is dereferenced here. `mmap` bases
291 // are page-aligned and embedded data is aligned by
292 // `include_bytes_aligned!`, so this holds on every shipped path. A
293 // `Vec<u8>` from `read_file` satisfies it in practice too, but that is
294 // not guaranteed, hence the runtime check.
295 conn_data[NEW_FORMAT_HEADER_LEN..]
296 .as_ptr()
297 .cast::<i16>()
298 .is_aligned()
299 }
300
301 /// Rejects a header whose axis sizes do not fit the payload.
302 ///
303 /// [`Self::row`] and [`Self::cost`] index a `costs_len`-long slice, so an
304 /// oversized header could otherwise only surface as a panic at tokenize
305 /// time. This also catches a `forward_size` above `i16::MAX`, which the
306 /// `as u32` cast in the header parse wraps to roughly four billion.
307 ///
308 /// # Arguments
309 ///
310 /// * `forward_size` - Number of forward context ids from the header.
311 /// * `backward_size` - Number of backward context ids from the header.
312 /// * `costs_len` - Number of whole `i16` values in the payload.
313 ///
314 /// # Returns
315 ///
316 /// `Ok(())` when the payload holds at least `forward_size *
317 /// backward_size` values, otherwise an error.
318 fn validate_axes(forward_size: u32, backward_size: u32, costs_len: usize) -> LinderaResult<()> {
319 let required = (forward_size as usize)
320 .checked_mul(backward_size as usize)
321 .ok_or_else(|| {
322 LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
323 "Connection cost matrix axes overflow: forward_size={forward_size}, backward_size={backward_size}"
324 ))
325 })?;
326 if costs_len < required {
327 return Err(LinderaErrorKind::Deserialize.with_error(anyhow::anyhow!(
328 "Connection cost matrix payload holds {costs_len} values but the header requires {required} (forward_size={forward_size}, backward_size={backward_size})"
329 )));
330 }
331 Ok(())
332 }
333
334 /// Returns the whole cost table as a flat slice in transposed layout.
335 ///
336 /// Replaces the former `costs_data` field, which forced every load to
337 /// materialize an owned `Vec<i16>`.
338 ///
339 /// # Returns
340 ///
341 /// The cost values, indexed by `forward_id + backward_id * forward_size`.
342 #[inline(always)]
343 pub fn costs(&self) -> &[i16] {
344 // SAFETY: `costs_ptr`/`costs_len` were derived in `from_storage` from
345 // `self.storage`, which this `&self` borrow keeps alive. `storage`,
346 // `costs_ptr` and `costs_len` are private and no method takes
347 // `&mut self`, so the pointee is never mutated or reallocated. Moving
348 // `self` does not move the pointee (`Data::Static` lives in `.rodata`,
349 // `Data::Vec` and `CostStorage::Owned` in a stable heap allocation,
350 // `Data::Map` in an `Arc`-owned mapping), and `Clone` re-derives the
351 // pointer through `from_storage` rather than copying it. The
352 // remaining preconditions were established by `CostStorage::costs`.
353 unsafe { core::slice::from_raw_parts(self.costs_ptr.0, self.costs_len) }
354 }
355
356 /// Whether the cost values are read in place from the backing bytes,
357 /// i.e. no copy was made at load time.
358 ///
359 /// Exposed so tests and diagnostics can assert that the mmap and embedded
360 /// paths actually take the zero-copy branch.
361 ///
362 /// # Returns
363 ///
364 /// `true` when the matrix borrows its values, `false` when it owns a
365 /// decoded copy.
366 pub fn is_zero_copy(&self) -> bool {
367 matches!(self.storage, CostStorage::Borrowed(_))
368 }
369
370 /// Returns the contiguous cost row for a fixed backward (right-context)
371 /// id, so callers relaxing many forward ids against the same backward id
372 /// pay the offset computation and bounds check once.
373 ///
374 /// # Arguments
375 ///
376 /// * `backward_id` - The backward context id selecting the row.
377 ///
378 /// # Returns
379 ///
380 /// A `forward_size`-long slice indexed directly by forward context id.
381 #[inline]
382 pub fn row(&self, backward_id: u32) -> &[i16] {
383 let start = (backward_id * self.forward_size) as usize;
384 &self.costs()[start..start + self.forward_size as usize]
385 }
386
387 #[inline]
388 pub fn cost(&self, forward_id: u32, backward_id: u32) -> i32 {
389 // Context-id access profiling (feature `ctxfreq`); compiled out by default.
390 #[cfg(feature = "ctxfreq")]
391 crate::builder::context_id_remap::record_access(forward_id, backward_id);
392
393 let cost_id = (forward_id + backward_id * self.forward_size) as usize;
394 self.costs()[cost_id] as i32
395 }
396}
397
398impl Clone for ConnectionCostMatrix {
399 /// Clones the matrix, re-deriving the cached costs pointer from the cloned
400 /// storage.
401 ///
402 /// A derived `Clone` would copy `costs_ptr` verbatim. For
403 /// `CostStorage::Borrowed(Data::Static)` and `Data::Map` that would happen
404 /// to be sound (the pointee is `'static` or `Arc`-shared), but for
405 /// `Data::Vec` and `CostStorage::Owned` the clone allocates a fresh buffer
406 /// while the copied pointer still refers to the source's -- a
407 /// use-after-free as soon as the source is dropped. Deriving `Clone` on
408 /// this type is therefore forbidden.
409 ///
410 /// Alignment is handled one level down, in `CostStorage`'s own `Clone`:
411 /// a reallocated `Data::Vec` may no longer be `i16`-aligned, so the
412 /// borrow is re-validated there and falls back to an owned buffer.
413 ///
414 /// # Returns
415 ///
416 /// An independent `ConnectionCostMatrix` holding the same costs.
417 fn clone(&self) -> Self {
418 Self::from_storage(self.storage.clone(), self.forward_size, self.backward_size)
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425 use byteorder::{LittleEndian, WriteBytesExt};
426
427 /// Backing buffer whose `body` starts at a 16-byte boundary, so the cost
428 /// payload at offset 6 is `i16`-aligned and the borrowed path is taken.
429 #[repr(C, align(16))]
430 struct AlignedBuf<const N: usize> {
431 body: [u8; N],
432 }
433
434 /// Backing buffer whose `body` starts one byte past a 2-byte boundary, so
435 /// the cost payload at offset 6 lands on an odd address and the owning
436 /// fallback is taken. This is deterministic, unlike relying on whatever
437 /// alignment the allocator happens to give a `Vec<u8>`.
438 #[repr(C, align(2))]
439 struct MisalignedBuf<const N: usize> {
440 _pad: u8,
441 body: [u8; N],
442 }
443
444 /// `[-1, 2, 3]` header followed by six costs in transposed layout.
445 ///
446 /// Spelled out as a `const` rather than built at runtime so it can back
447 /// the `static`s below; leaking a `Box` instead would trip Miri's leak
448 /// checker. `transposed_bytes_match_the_const` guards the hand-written
449 /// encoding.
450 const TRANSPOSED: [u8; 18] = [
451 0xff, 0xff, // -1: transposed-layout marker
452 0x02, 0x00, // forward_size = 2
453 0x03, 0x00, // backward_size = 3
454 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00,
455 ];
456
457 /// [`TRANSPOSED`] with a trailing byte, so the payload holds a partial
458 /// `i16` at the end.
459 const TRANSPOSED_ODD: [u8; 19] = [
460 0xff, 0xff, 0x02, 0x00, 0x03, 0x00, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e,
461 0x00, 0x0f, 0x00, 0x00,
462 ];
463
464 /// `[-1, 1, 1]` plus a single cost of 7, mirroring `lindera-cc-cedict`'s
465 /// 8-byte `matrix.mtx`.
466 const DEGENERATE: [u8; 8] = [0xff, 0xff, 0x01, 0x00, 0x01, 0x00, 0x07, 0x00];
467
468 static ALIGNED_TRANSPOSED: AlignedBuf<18> = AlignedBuf { body: TRANSPOSED };
469 static MISALIGNED_TRANSPOSED: MisalignedBuf<18> = MisalignedBuf {
470 _pad: 0,
471 body: TRANSPOSED,
472 };
473 static MISALIGNED_TRANSPOSED_ODD: MisalignedBuf<19> = MisalignedBuf {
474 _pad: 0,
475 body: TRANSPOSED_ODD,
476 };
477 static ALIGNED_DEGENERATE: AlignedBuf<8> = AlignedBuf { body: DEGENERATE };
478
479 fn assert_sample_costs(matrix: &ConnectionCostMatrix) {
480 assert_eq!(matrix.forward_size, 2);
481 assert_eq!(matrix.backward_size, 3);
482 assert_eq!(matrix.cost(0, 0), 10);
483 assert_eq!(matrix.cost(1, 0), 11);
484 assert_eq!(matrix.cost(0, 1), 12);
485 assert_eq!(matrix.cost(1, 1), 13);
486 assert_eq!(matrix.cost(0, 2), 14);
487 assert_eq!(matrix.cost(1, 2), 15);
488 assert_eq!(matrix.row(0), &[10, 11]);
489 assert_eq!(matrix.row(1), &[12, 13]);
490 assert_eq!(matrix.row(2), &[14, 15]);
491 }
492
493 #[test]
494 fn test_load_transposed() {
495 let matrix = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
496 assert_sample_costs(&matrix);
497 }
498
499 #[test]
500 fn test_load_old_format() {
501 let mut data = Vec::new();
502 data.write_i16::<LittleEndian>(2).unwrap(); // forward_size
503 data.write_i16::<LittleEndian>(3).unwrap(); // backward_size
504 // Old layout: [backward_id + forward_id * backward_size]
505 // [0][0], [1][0], [2][0], [0][1], [1][1], [2][1]
506 for v in [10i16, 12, 14, 11, 13, 15] {
507 data.write_i16::<LittleEndian>(v).unwrap();
508 }
509
510 let matrix = ConnectionCostMatrix::load(data).unwrap();
511 assert_sample_costs(&matrix);
512 // A transpose is required, so the legacy format can never be borrowed.
513 assert!(!matrix.is_zero_copy());
514 }
515
516 #[test]
517 fn test_load_data_too_short() {
518 let data: Vec<u8> = vec![0x01, 0x02];
519 let result = ConnectionCostMatrix::load(data);
520 assert!(result.is_err());
521 }
522
523 #[test]
524 fn transposed_bytes_match_the_const() {
525 let mut expected = Vec::new();
526 expected.write_i16::<LittleEndian>(-1).unwrap();
527 expected.write_i16::<LittleEndian>(2).unwrap();
528 expected.write_i16::<LittleEndian>(3).unwrap();
529 for v in [10i16, 11, 12, 13, 14, 15] {
530 expected.write_i16::<LittleEndian>(v).unwrap();
531 }
532 assert_eq!(TRANSPOSED.as_slice(), expected.as_slice());
533
534 assert_eq!(&TRANSPOSED_ODD[..18], TRANSPOSED.as_slice());
535 assert_eq!(TRANSPOSED_ODD[18], 0);
536 }
537
538 #[test]
539 fn borrowed_and_owned_produce_identical_costs() {
540 let borrowed = ConnectionCostMatrix::load(&ALIGNED_TRANSPOSED.body[..]).unwrap();
541 let owned = ConnectionCostMatrix::load(&MISALIGNED_TRANSPOSED.body[..]).unwrap();
542
543 assert!(borrowed.is_zero_copy(), "aligned payload must be borrowed");
544 assert!(
545 !owned.is_zero_copy(),
546 "misaligned payload must fall back to an owned copy"
547 );
548
549 assert_sample_costs(&borrowed);
550 assert_sample_costs(&owned);
551 assert_eq!(borrowed.costs(), owned.costs());
552 }
553
554 #[test]
555 fn clone_does_not_alias_the_source_buffer() {
556 // `Data::Vec` is the variant a derived `Clone` would get wrong twice
557 // over: the clone allocates a fresh buffer, so a bitwise-copied
558 // pointer would dangle once the source is dropped, and the fresh
559 // buffer is only guaranteed 1-byte aligned, so keeping the borrow
560 // could produce an unaligned `&[i16]`. Miri catches both.
561 let source = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
562 let cloned = source.clone();
563 drop(source);
564 assert_sample_costs(&cloned);
565 }
566
567 #[test]
568 fn cloning_a_static_backed_matrix_stays_zero_copy() {
569 // `Data::Static` clones to the very same bytes, so the borrow -- and
570 // with it the alignment that `load` validated -- survives.
571 let source = ConnectionCostMatrix::load(&ALIGNED_TRANSPOSED.body[..]).unwrap();
572 assert!(source.is_zero_copy());
573
574 let cloned = source.clone();
575 assert!(cloned.is_zero_copy());
576 assert_eq!(cloned.costs().as_ptr(), source.costs().as_ptr());
577 assert_sample_costs(&cloned);
578 }
579
580 #[test]
581 fn matrix_survives_move() {
582 let matrix = ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap();
583 let boxed = Box::new(matrix);
584 assert_sample_costs(&boxed);
585
586 // Force the holding `Vec` to reallocate, moving the struct itself.
587 let mut holder = Vec::with_capacity(1);
588 holder.push(*boxed);
589 for _ in 0..64 {
590 holder.push(ConnectionCostMatrix::load(TRANSPOSED.to_vec()).unwrap());
591 }
592 for matrix in &holder {
593 assert_sample_costs(matrix);
594 }
595 }
596
597 #[test]
598 fn connection_cost_matrix_is_send_and_sync() {
599 fn assert_send_sync<T: Send + Sync>() {}
600 assert_send_sync::<ConnectionCostMatrix>();
601 }
602
603 #[test]
604 fn rejects_axes_larger_than_payload() {
605 let mut data = Vec::new();
606 data.write_i16::<LittleEndian>(-1).unwrap();
607 data.write_i16::<LittleEndian>(100).unwrap();
608 data.write_i16::<LittleEndian>(100).unwrap();
609 data.write_i16::<LittleEndian>(0).unwrap();
610
611 let result = ConnectionCostMatrix::load(data);
612 assert!(result.is_err());
613 }
614
615 #[test]
616 fn odd_length_payload_does_not_panic() {
617 // `read_i16_into` panics unless `src.len() == 2 * dst.len()`, so the
618 // owning path must slice off the trailing odd byte.
619 let matrix = ConnectionCostMatrix::load(&MISALIGNED_TRANSPOSED_ODD.body[..]).unwrap();
620 assert!(!matrix.is_zero_copy());
621 assert_sample_costs(&matrix);
622 }
623
624 #[test]
625 fn degenerate_single_cell_matrix_is_borrowable() {
626 // `lindera-cc-cedict` ships an 8-byte `matrix.mtx` (`[-1, 1, 1]` plus
627 // a single cost).
628 let matrix = ConnectionCostMatrix::load(&ALIGNED_DEGENERATE.body[..]).unwrap();
629 assert!(matrix.is_zero_copy());
630 assert_eq!(matrix.cost(0, 0), 7);
631 }
632}