Skip to main content

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}