Skip to main content

asupersync/types/
symbol.rs

1//! Symbol types for the RaptorQ-based distributed layer.
2//!
3//! This module provides the core symbol primitives used for erasure coding
4//! in Asupersync's distributed structured concurrency layer. RaptorQ (RFC 6330)
5//! is a fountain code that enables reliable data transmission with loss tolerance.
6//!
7//! # Core Types
8//!
9//! - [`ObjectId`]: Unique identifier for an object being encoded/decoded
10//! - [`SymbolId`]: Identifies a specific symbol within an object (SBN + ESI)
11//! - [`Symbol`]: The actual encoded data with its identity and metadata
12//!
13//! # RaptorQ Concepts
14//!
15//! - **Source symbols**: Original data split into fixed-size chunks
16//! - **Repair symbols**: Generated symbols for redundancy (fountain property)
17//! - **Source Block Number (SBN)**: For objects split into multiple blocks
18//! - **Encoding Symbol ID (ESI)**: Index of symbol within a source block
19//!
20//! # Example
21//!
22//! ```ignore
23//! // Create an object ID for data to encode
24//! let object_id = ObjectId::new_random(&mut rng);
25//!
26//! // Symbol IDs identify specific symbols within the object
27//! let symbol_id = SymbolId::new(object_id, 0, 0); // SBN=0, ESI=0
28//!
29//! // Symbols contain the actual encoded data
30//! let symbol = Symbol::new(symbol_id, data, SymbolKind::Source);
31//! ```
32
33use core::fmt;
34
35/// Maximum symbol payload size in bytes (default: 1280 bytes per RFC 6330 common usage).
36pub const DEFAULT_SYMBOL_SIZE: usize = 1280;
37
38/// A unique identifier for an object being encoded/decoded.
39///
40/// Objects are the high-level data units that get split into symbols
41/// for erasure-coded transmission. Each object has a unique 128-bit ID.
42#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43#[cfg_attr(feature = "test-internals", derive(serde::Serialize))]
44pub struct ObjectId {
45    /// High 64 bits of the object ID.
46    high: u64,
47    /// Low 64 bits of the object ID.
48    low: u64,
49}
50
51impl ObjectId {
52    /// Creates a new object ID from two 64-bit values.
53    #[inline]
54    #[must_use]
55    pub const fn new(high: u64, low: u64) -> Self {
56        Self { high, low }
57    }
58
59    /// Creates an object ID from a 128-bit value.
60    #[inline]
61    #[must_use]
62    pub const fn from_u128(value: u128) -> Self {
63        Self {
64            high: (value >> 64) as u64,
65            low: value as u64,
66        }
67    }
68
69    /// Converts the object ID to a 128-bit value.
70    #[inline]
71    #[must_use]
72    pub const fn as_u128(self) -> u128 {
73        ((self.high as u128) << 64) | (self.low as u128)
74    }
75
76    /// Returns the high 64 bits.
77    #[inline]
78    #[must_use]
79    pub const fn high(self) -> u64 {
80        self.high
81    }
82
83    /// Returns the low 64 bits.
84    #[inline]
85    #[must_use]
86    pub const fn low(self) -> u64 {
87        self.low
88    }
89
90    /// Creates a random object ID using a deterministic RNG.
91    ///
92    /// This is the primary way to create object IDs in production code.
93    #[must_use]
94    pub fn new_random(rng: &mut crate::util::DetRng) -> Self {
95        Self {
96            high: rng.next_u64(),
97            low: rng.next_u64(),
98        }
99    }
100
101    /// Creates an object ID for testing purposes.
102    #[doc(hidden)]
103    #[inline]
104    #[must_use]
105    pub const fn new_for_test(value: u64) -> Self {
106        Self {
107            high: 0,
108            low: value,
109        }
110    }
111
112    /// The nil (zero) object ID.
113    pub const NIL: Self = Self { high: 0, low: 0 };
114}
115
116impl fmt::Debug for ObjectId {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "ObjectId({:016x}{:016x})", self.high, self.low)
119    }
120}
121
122impl fmt::Display for ObjectId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        // Display abbreviated form (first 8 hex chars)
125        write!(f, "Obj-{:08x}", (self.high >> 32) as u32)
126    }
127}
128
129/// Identifies a specific symbol within an object.
130///
131/// A symbol ID consists of:
132/// - The parent object ID
133/// - Source Block Number (SBN): For objects split into multiple blocks
134/// - Encoding Symbol ID (ESI): Index of symbol within the source block
135///
136/// For RaptorQ:
137/// - ESI < K: source symbols (original data)
138/// - ESI >= K: repair symbols (generated for redundancy)
139#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
140pub struct SymbolId {
141    /// The object this symbol belongs to.
142    object_id: ObjectId,
143    /// Source Block Number (which block within a large object).
144    sbn: u8,
145    /// Encoding Symbol ID (which symbol within the block).
146    esi: u32,
147}
148
149impl SymbolId {
150    /// Creates a new symbol ID.
151    #[inline]
152    #[must_use]
153    pub const fn new(object_id: ObjectId, sbn: u8, esi: u32) -> Self {
154        Self {
155            object_id,
156            sbn,
157            esi,
158        }
159    }
160
161    /// Returns the parent object ID.
162    #[inline]
163    #[must_use]
164    pub const fn object_id(self) -> ObjectId {
165        self.object_id
166    }
167
168    /// Returns the Source Block Number.
169    #[inline]
170    #[must_use]
171    pub const fn sbn(self) -> u8 {
172        self.sbn
173    }
174
175    /// Returns the Encoding Symbol ID.
176    #[inline]
177    #[must_use]
178    pub const fn esi(self) -> u32 {
179        self.esi
180    }
181
182    /// Returns true if this is a source symbol (ESI < source_count).
183    #[inline]
184    #[must_use]
185    pub const fn is_source(self, source_count: u32) -> bool {
186        self.esi < source_count
187    }
188
189    /// Returns true if this is a repair symbol (ESI >= source_count).
190    #[inline]
191    #[must_use]
192    pub const fn is_repair(self, source_count: u32) -> bool {
193        self.esi >= source_count
194    }
195
196    /// Creates a symbol ID for testing purposes.
197    #[doc(hidden)]
198    #[inline]
199    #[must_use]
200    pub const fn new_for_test(object_value: u64, sbn: u8, esi: u32) -> Self {
201        Self {
202            object_id: ObjectId::new_for_test(object_value),
203            sbn,
204            esi,
205        }
206    }
207}
208
209impl fmt::Debug for SymbolId {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        write!(
212            f,
213            "SymbolId({}, sbn={}, esi={})",
214            self.object_id, self.sbn, self.esi
215        )
216    }
217}
218
219impl fmt::Display for SymbolId {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        write!(f, "{}:{}:{}", self.object_id, self.sbn, self.esi)
222    }
223}
224
225/// The kind of symbol (source or repair).
226#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
227pub enum SymbolKind {
228    /// A source symbol containing original data.
229    Source,
230    /// A repair symbol generated for redundancy.
231    Repair,
232}
233
234impl SymbolKind {
235    /// Returns true if this is a source symbol.
236    #[inline]
237    #[must_use]
238    pub const fn is_source(self) -> bool {
239        matches!(self, Self::Source)
240    }
241
242    /// Returns true if this is a repair symbol.
243    #[inline]
244    #[must_use]
245    pub const fn is_repair(self) -> bool {
246        matches!(self, Self::Repair)
247    }
248}
249
250impl fmt::Display for SymbolKind {
251    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
252        match self {
253            Self::Source => write!(f, "source"),
254            Self::Repair => write!(f, "repair"),
255        }
256    }
257}
258
259/// An encoded symbol with its data payload.
260///
261/// Symbols are the fundamental unit of erasure-coded data. Each symbol
262/// contains a fixed-size payload and metadata identifying it within its
263/// parent object.
264///
265/// # Memory Layout
266///
267/// The symbol stores its data inline for cache efficiency. For larger
268/// payloads or streaming scenarios, consider using `SymbolRef` (future).
269#[derive(Clone, PartialEq, Eq, Hash)]
270pub struct Symbol {
271    /// Unique identifier for this symbol.
272    id: SymbolId,
273    /// The kind of symbol (source or repair).
274    kind: SymbolKind,
275    /// The symbol payload data.
276    data: Vec<u8>,
277}
278
279impl Symbol {
280    /// Creates a new symbol with the given data.
281    ///
282    /// # Arguments
283    ///
284    /// * `id` - The unique identifier for this symbol
285    /// * `data` - The payload data (will be cloned)
286    /// * `kind` - Whether this is a source or repair symbol
287    #[inline]
288    #[must_use]
289    pub fn new(id: SymbolId, data: Vec<u8>, kind: SymbolKind) -> Self {
290        Self { id, kind, data }
291    }
292
293    /// Creates a symbol from a byte slice (copies the data).
294    #[inline]
295    #[must_use]
296    pub fn from_slice(id: SymbolId, data: &[u8], kind: SymbolKind) -> Self {
297        Self {
298            id,
299            kind,
300            data: data.to_vec(),
301        }
302    }
303
304    /// Creates an empty symbol with the specified size.
305    #[inline]
306    #[must_use]
307    pub fn empty(id: SymbolId, size: usize, kind: SymbolKind) -> Self {
308        Self {
309            id,
310            kind,
311            data: vec![0u8; size],
312        }
313    }
314
315    /// Returns the symbol's unique identifier.
316    #[inline]
317    #[must_use]
318    pub const fn id(&self) -> SymbolId {
319        self.id
320    }
321
322    /// Returns the symbol's kind.
323    #[inline]
324    #[must_use]
325    pub const fn kind(&self) -> SymbolKind {
326        self.kind
327    }
328
329    /// Returns the symbol's data payload.
330    #[must_use]
331    #[inline]
332    pub fn data(&self) -> &[u8] {
333        &self.data
334    }
335
336    /// Returns a mutable reference to the symbol's data payload.
337    #[inline]
338    #[must_use]
339    pub fn data_mut(&mut self) -> &mut [u8] {
340        &mut self.data
341    }
342
343    /// Consumes the symbol and returns its data.
344    #[inline]
345    #[must_use]
346    pub fn into_data(self) -> Vec<u8> {
347        self.data
348    }
349
350    /// Returns the size of the data payload in bytes.
351    #[must_use]
352    #[inline]
353    pub fn len(&self) -> usize {
354        self.data.len()
355    }
356
357    /// Returns true if the data payload is empty.
358    #[must_use]
359    #[inline]
360    pub fn is_empty(&self) -> bool {
361        self.data.is_empty()
362    }
363
364    /// Returns the object ID this symbol belongs to.
365    #[must_use]
366    #[inline]
367    pub const fn object_id(&self) -> ObjectId {
368        self.id.object_id()
369    }
370
371    /// Returns the Source Block Number.
372    #[inline]
373    #[must_use]
374    pub const fn sbn(&self) -> u8 {
375        self.id.sbn()
376    }
377
378    /// Returns the Encoding Symbol ID.
379    #[inline]
380    #[must_use]
381    pub const fn esi(&self) -> u32 {
382        self.id.esi()
383    }
384
385    /// Creates a source symbol for testing purposes.
386    ///
387    /// This default matches the common test case of constructing ordered source
388    /// sequences. When repair-kind semantics matter, tests must opt into
389    /// [`Self::new_repair_for_test`] explicitly because source-vs-repair depends
390    /// on block `K`, not on `esi == 0`.
391    #[doc(hidden)]
392    #[inline]
393    #[must_use]
394    pub fn new_for_test(object_value: u64, sbn: u8, esi: u32, data: &[u8]) -> Self {
395        Self::new_source_for_test(object_value, sbn, esi, data)
396    }
397
398    /// Creates an explicit source symbol for testing purposes.
399    #[doc(hidden)]
400    #[inline]
401    #[must_use]
402    pub fn new_source_for_test(object_value: u64, sbn: u8, esi: u32, data: &[u8]) -> Self {
403        Self {
404            id: SymbolId::new_for_test(object_value, sbn, esi),
405            kind: SymbolKind::Source,
406            data: data.to_vec(),
407        }
408    }
409
410    /// Creates an explicit repair symbol for testing purposes.
411    #[doc(hidden)]
412    #[must_use]
413    #[inline]
414    pub fn new_repair_for_test(object_value: u64, sbn: u8, esi: u32, data: &[u8]) -> Self {
415        Self {
416            id: SymbolId::new_for_test(object_value, sbn, esi),
417            kind: SymbolKind::Repair,
418            data: data.to_vec(),
419        }
420    }
421}
422
423impl fmt::Debug for Symbol {
424    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425        f.debug_struct("Symbol")
426            .field("id", &self.id)
427            .field("kind", &self.kind)
428            .field("data_len", &self.data.len())
429            .finish()
430    }
431}
432
433impl fmt::Display for Symbol {
434    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
435        write!(
436            f,
437            "Symbol({}, {}, {} bytes)",
438            self.id,
439            self.kind,
440            self.data.len()
441        )
442    }
443}
444
445/// Metadata about an object for encoding/decoding.
446///
447/// This contains the parameters needed to encode or decode an object
448/// using RaptorQ.
449#[derive(Clone, Copy, Debug, PartialEq, Eq)]
450pub struct ObjectParams {
451    /// The object ID.
452    pub object_id: ObjectId,
453    /// Total size of the original object in bytes.
454    pub object_size: u64,
455    /// Size of each symbol in bytes.
456    pub symbol_size: u16,
457    /// Number of source blocks the object is divided into.
458    pub source_blocks: u16,
459    /// Number of source symbols per block (K).
460    pub symbols_per_block: u16,
461}
462
463impl ObjectParams {
464    /// Creates new object parameters.
465    #[must_use]
466    #[inline]
467    pub const fn new(
468        object_id: ObjectId,
469        object_size: u64,
470        symbol_size: u16,
471        source_blocks: u16,
472        symbols_per_block: u16,
473    ) -> Self {
474        Self {
475            object_id,
476            object_size,
477            symbol_size,
478            source_blocks,
479            symbols_per_block,
480        }
481    }
482
483    /// Calculates the minimum number of symbols needed for decoding.
484    ///
485    /// `ObjectParams` describes the entire encoded object, so the minimum
486    /// decode threshold is the total source-symbol count across all source
487    /// blocks, not the per-block `K`.
488    #[must_use]
489    #[inline]
490    pub const fn min_symbols_for_decode(&self) -> u32 {
491        self.total_source_symbols()
492    }
493
494    /// Calculates the total number of source symbols across all blocks.
495    #[must_use]
496    pub const fn total_source_symbols(&self) -> u32 {
497        if self.symbol_size == 0 || self.object_size == 0 {
498            return 0;
499        }
500
501        let sym_size = self.symbol_size as u64;
502        let total = self.object_size.div_ceil(sym_size);
503        if total > u32::MAX as u64 {
504            u32::MAX
505        } else {
506            total as u32
507        }
508    }
509
510    /// Creates object parameters for testing.
511    #[doc(hidden)]
512    #[must_use]
513    #[inline]
514    pub const fn new_for_test(object_value: u64, size: u64) -> Self {
515        let symbol_size = DEFAULT_SYMBOL_SIZE as u64;
516        let symbols_per_block = if size == 0 {
517            0
518        } else {
519            (size - 1) / symbol_size + 1
520        };
521        Self {
522            object_id: ObjectId::new_for_test(object_value),
523            object_size: size,
524            symbol_size: DEFAULT_SYMBOL_SIZE as u16,
525            source_blocks: 1,
526            symbols_per_block: symbols_per_block as u16,
527        }
528    }
529}
530
531impl fmt::Display for ObjectParams {
532    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533        write!(
534            f,
535            "ObjectParams({}, {} bytes, {} symbols/block)",
536            self.object_id, self.object_size, self.symbols_per_block
537        )
538    }
539}
540
541#[cfg(test)]
542mod tests {
543    #![allow(
544        clippy::pedantic,
545        clippy::nursery,
546        clippy::expect_fun_call,
547        clippy::map_unwrap_or,
548        clippy::cast_possible_wrap,
549        clippy::future_not_send
550    )]
551    use super::*;
552
553    #[test]
554    fn object_id_conversions() {
555        let id = ObjectId::new(0x1234_5678_9abc_def0, 0xfed_cba9_8765_4321);
556        assert_eq!(id.high(), 0x1234_5678_9abc_def0);
557        assert_eq!(id.low(), 0xfed_cba9_8765_4321);
558
559        let from_u128 = ObjectId::from_u128(id.as_u128());
560        assert_eq!(id, from_u128);
561    }
562
563    #[test]
564    fn object_id_nil() {
565        let nil = ObjectId::NIL;
566        assert_eq!(nil.high(), 0);
567        assert_eq!(nil.low(), 0);
568        assert_eq!(nil.as_u128(), 0);
569    }
570
571    #[test]
572    fn object_id_test_constructor() {
573        let id = ObjectId::new_for_test(42);
574        assert_eq!(id.high(), 0);
575        assert_eq!(id.low(), 42);
576    }
577
578    #[test]
579    fn symbol_id_creation() {
580        let object_id = ObjectId::new_for_test(1);
581        let symbol_id = SymbolId::new(object_id, 0, 5);
582
583        assert_eq!(symbol_id.object_id(), object_id);
584        assert_eq!(symbol_id.sbn(), 0);
585        assert_eq!(symbol_id.esi(), 5);
586    }
587
588    #[test]
589    fn symbol_id_source_vs_repair() {
590        let symbol_id = SymbolId::new_for_test(1, 0, 5);
591
592        // With 10 source symbols, ESI 5 is a source symbol
593        assert!(symbol_id.is_source(10));
594        assert!(!symbol_id.is_repair(10));
595
596        // With 5 source symbols, ESI 5 is a repair symbol
597        assert!(!symbol_id.is_source(5));
598        assert!(symbol_id.is_repair(5));
599    }
600
601    #[test]
602    fn symbol_creation_and_data() {
603        let id = SymbolId::new_for_test(1, 0, 0);
604        let data = vec![1, 2, 3, 4, 5];
605        let symbol = Symbol::new(id, data.clone(), SymbolKind::Source);
606
607        assert_eq!(symbol.id(), id);
608        assert_eq!(symbol.kind(), SymbolKind::Source);
609        assert_eq!(symbol.data(), &data[..]);
610        assert_eq!(symbol.len(), 5);
611        assert!(!symbol.is_empty());
612    }
613
614    #[test]
615    fn symbol_from_slice() {
616        let id = SymbolId::new_for_test(1, 0, 0);
617        let data = [10, 20, 30];
618        let symbol = Symbol::from_slice(id, &data, SymbolKind::Repair);
619
620        assert_eq!(symbol.data(), &data[..]);
621        assert_eq!(symbol.kind(), SymbolKind::Repair);
622    }
623
624    #[test]
625    fn symbol_empty() {
626        let id = SymbolId::new_for_test(1, 0, 0);
627        let symbol = Symbol::empty(id, 100, SymbolKind::Source);
628
629        assert_eq!(symbol.len(), 100);
630        assert!(symbol.data().iter().all(|&b| b == 0));
631    }
632
633    #[test]
634    fn symbol_into_data() {
635        let id = SymbolId::new_for_test(1, 0, 0);
636        let original_data = vec![1, 2, 3];
637        let symbol = Symbol::new(id, original_data.clone(), SymbolKind::Source);
638
639        let recovered = symbol.into_data();
640        assert_eq!(recovered, original_data);
641    }
642
643    #[test]
644    fn symbol_kind_checks() {
645        assert!(SymbolKind::Source.is_source());
646        assert!(!SymbolKind::Source.is_repair());
647        assert!(!SymbolKind::Repair.is_source());
648        assert!(SymbolKind::Repair.is_repair());
649    }
650
651    #[test]
652    fn object_params_calculations() {
653        let params = ObjectParams::new(
654            ObjectId::new_for_test(1),
655            10000, // 10KB object
656            1280,  // symbol size
657            1,     // 1 source block
658            8,     // 8 symbols per block
659        );
660
661        assert_eq!(params.min_symbols_for_decode(), 8);
662        assert_eq!(params.total_source_symbols(), 8);
663    }
664
665    #[test]
666    fn object_params_multi_block() {
667        let params = ObjectParams::new(
668            ObjectId::new_for_test(1),
669            327_680, // 4 full blocks * 64 symbols/block * 1280 bytes
670            1280,
671            4,  // 4 source blocks
672            64, // 64 symbols per block
673        );
674
675        assert_eq!(params.min_symbols_for_decode(), 256);
676        assert_eq!(params.total_source_symbols(), 256);
677    }
678
679    #[test]
680    fn object_params_can_represent_full_256_block_contract() {
681        let params = ObjectParams::new(
682            ObjectId::new_for_test(1),
683            327_680, // 256 blocks * 1 symbol/block * 1280 bytes
684            1280,
685            256,
686            1,
687        );
688
689        assert_eq!(params.source_blocks, 256);
690        assert_eq!(params.min_symbols_for_decode(), 256);
691        assert_eq!(params.total_source_symbols(), 256);
692    }
693
694    #[test]
695    fn object_params_partial_last_block_does_not_overcount_total_symbols() {
696        let params = ObjectParams::new(
697            ObjectId::new_for_test(1),
698            326_400, // 255 symbols worth of payload at 1280 bytes each
699            1280,
700            4,
701            64,
702        );
703
704        assert_eq!(params.min_symbols_for_decode(), 255);
705        assert_eq!(params.total_source_symbols(), 255);
706    }
707
708    #[test]
709    fn display_formatting() {
710        let object_id = ObjectId::new(0x1234_5678_0000_0000, 0);
711        assert!(format!("{object_id}").contains("Obj-"));
712
713        let symbol_id = SymbolId::new(object_id, 1, 42);
714        let display = format!("{symbol_id}");
715        assert!(display.contains(":1:42"));
716
717        let symbol = Symbol::new_for_test(1, 0, 0, &[1, 2, 3]);
718        let display = format!("{symbol}");
719        assert!(display.contains("3 bytes"));
720    }
721
722    // =========================================================================
723    // Wave 31: Data-type trait coverage
724    // =========================================================================
725
726    #[test]
727    fn object_id_ord() {
728        let a = ObjectId::new(0, 1);
729        let b = ObjectId::new(0, 2);
730        let c = ObjectId::new(1, 0);
731        assert!(a < b);
732        assert!(b < c);
733    }
734
735    #[test]
736    fn object_id_hash() {
737        use std::collections::HashSet;
738        let mut set = HashSet::new();
739        set.insert(ObjectId::new_for_test(1));
740        set.insert(ObjectId::new_for_test(2));
741        set.insert(ObjectId::new_for_test(1));
742        assert_eq!(set.len(), 2);
743    }
744
745    #[test]
746    fn symbol_id_ord_hash() {
747        use std::collections::HashSet;
748        let a = SymbolId::new_for_test(1, 0, 0);
749        let b = SymbolId::new_for_test(1, 0, 1);
750        assert!(a < b);
751        let mut set = HashSet::new();
752        set.insert(a);
753        set.insert(b);
754        set.insert(a);
755        assert_eq!(set.len(), 2);
756    }
757
758    #[test]
759    fn symbol_kind_clone_copy_hash_display() {
760        use std::collections::HashSet;
761        let src = SymbolKind::Source;
762        let rep = SymbolKind::Repair;
763        let cloned = src; // Copy
764        assert_eq!(cloned, src);
765        assert_eq!(format!("{src}"), "source");
766        assert_eq!(format!("{rep}"), "repair");
767        let mut set = HashSet::new();
768        set.insert(src);
769        set.insert(rep);
770        assert_eq!(set.len(), 2);
771    }
772
773    #[test]
774    fn symbol_clone_hash() {
775        use std::collections::HashSet;
776        let sym = Symbol::new_for_test(1, 0, 0, &[1, 2, 3]);
777        let cloned = sym.clone();
778        assert_eq!(sym, cloned);
779        let mut set = HashSet::new();
780        set.insert(sym);
781        set.insert(cloned);
782        assert_eq!(set.len(), 1);
783    }
784
785    #[test]
786    fn symbol_data_mut() {
787        let id = SymbolId::new_for_test(1, 0, 0);
788        let mut symbol = Symbol::new(id, vec![1, 2, 3], SymbolKind::Source);
789        symbol.data_mut()[0] = 99;
790        assert_eq!(symbol.data()[0], 99);
791    }
792
793    #[test]
794    fn symbol_empty_is_empty() {
795        let id = SymbolId::new_for_test(1, 0, 0);
796        let symbol = Symbol::empty(id, 0, SymbolKind::Source);
797        assert!(symbol.is_empty());
798        assert_eq!(symbol.len(), 0);
799    }
800
801    #[test]
802    fn symbol_convenience_accessors() {
803        let sym = Symbol::new_for_test(42, 3, 7, &[10, 20]);
804        assert_eq!(sym.object_id(), ObjectId::new_for_test(42));
805        assert_eq!(sym.sbn(), 3);
806        assert_eq!(sym.esi(), 7);
807    }
808
809    #[test]
810    fn symbol_test_constructor_defaults_to_source() {
811        let sym = Symbol::new_for_test(42, 3, 7, &[10, 20]);
812        assert_eq!(sym.kind(), SymbolKind::Source);
813    }
814
815    #[test]
816    fn symbol_repair_test_constructor_preserves_repair_kind() {
817        let sym = Symbol::new_repair_for_test(42, 3, 7, &[10, 20]);
818        assert_eq!(sym.kind(), SymbolKind::Repair);
819    }
820
821    #[test]
822    fn object_params_clone_copy_display() {
823        let params = ObjectParams::new_for_test(1, 5000);
824        let cloned = params;
825        let copied = params; // Copy
826        assert_eq!(cloned, copied);
827        let display = format!("{params}");
828        assert!(display.contains("ObjectParams"));
829        assert!(display.contains("5000"));
830    }
831
832    #[test]
833    fn debug_formatting() {
834        let object_id = ObjectId::new_for_test(42);
835        let debug = format!("{object_id:?}");
836        assert!(debug.contains("ObjectId"));
837
838        let symbol_id = SymbolId::new_for_test(1, 2, 3);
839        let debug = format!("{symbol_id:?}");
840        assert!(debug.contains("SymbolId"));
841        assert!(debug.contains("sbn=2"));
842        assert!(debug.contains("esi=3"));
843    }
844}