Skip to main content

hopper_core/
segment_map.rs

1//! Compile-time segment mapping for zero-copy account layouts.
2//!
3//! `SegmentMap` provides a static description of the byte regions within a
4//! struct layout. This is separate from
5//! [`SegmentRegistry`](crate::account::SegmentRegistry) (which manages
6//! on-chain *dynamic* segment metadata, written into the account body
7//! at runtime). `SegmentMap` is compile-time knowledge that proc macros
8//! (or hand-written impls) generate from struct definitions.
9//!
10//! ## When to use which
11//!
12//! - **`SegmentMap` + [`StaticSegment`]** - fixed-layout structs annotated
13//!   with `#[hopper::state]`. The segment list is known at compile time;
14//!   lookups fold to const loads.
15//! - **[`SegmentRegistry`](crate::account::SegmentRegistry) +
16//!   [`SegmentDescriptor`](crate::account::SegmentDescriptor)** - accounts
17//!   whose segment count grows on-chain (extension-heavy patterns,
18//!   Token-2022-style mints, dynamic plug-ins). The segment table lives
19//!   inside the account body and is walked at runtime.
20//!
21//! Most Hopper programs use the compile-time path. The runtime registry
22//! exists for genuinely dynamic layouts.
23//!
24//! ## Design Philosophy
25//!
26//! Segment offsets are evaluated at compile time and stored as constants.
27//! This means segment lookups compile down to a const load, no string
28//! matching, no branching, no heap. The generated code is the same shape
29//! as raw Pinocchio pointer arithmetic.
30//!
31//! ## Example
32//!
33//! ```ignore
34//! // Generated by #[hopper::state] or hand-written:
35//! impl SegmentMap for Vault {
36//!     const SEGMENTS: &'static [StaticSegment] = &[
37//!         StaticSegment::new("authority", 0, 32),
38//!         StaticSegment::new("balance", 32, 8),
39//!         StaticSegment::new("bump", 40, 1),
40//!     ];
41//! }
42//!
43//! // Look up a segment:
44//! let seg = Vault::segment("balance").unwrap();
45//! assert_eq!(seg.offset, 32);
46//! assert_eq!(seg.size, 8);
47//! ```
48
49/// A compile-time constant segment descriptor.
50///
51/// Describes a named byte region within an account's data area.
52/// All fields are const-evaluable.
53#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct StaticSegment {
55    /// Field name (compile-time string).
56    pub name: &'static str,
57    /// Byte offset from the start of the layout body (after header).
58    pub offset: u32,
59    /// Byte size of this segment.
60    pub size: u32,
61}
62
63impl StaticSegment {
64    /// Construct a new static segment descriptor.
65    #[inline(always)]
66    pub const fn new(name: &'static str, offset: u32, size: u32) -> Self {
67        Self { name, offset, size }
68    }
69
70    /// Byte offset past the end of this segment.
71    #[inline(always)]
72    pub const fn end(&self) -> u32 {
73        self.offset + self.size
74    }
75}
76
77/// Compile-time segment layout for a zero-copy struct.
78///
79/// Types implementing this trait declare their byte-level field map
80/// as a constant array. This enables segment-level access with
81/// zero runtime overhead, the compiler can resolve segment offsets
82/// to immediate constants in the generated code.
83///
84/// ## Runtime vs Compile-Time
85///
86/// - `SegmentMap` = compile-time, generated from struct definitions
87/// - `SegmentRegistry` = runtime, stored on-chain for dynamic accounts
88///
89/// Both can coexist: a struct might implement `SegmentMap` for its fixed
90/// fields while also using `SegmentRegistry` for its dynamic segments.
91pub trait SegmentMap {
92    /// All segments in this layout, ordered by offset.
93    const SEGMENTS: &'static [StaticSegment];
94
95    /// Look up a segment by field name.
96    ///
97    /// Linear scan over const array. For layouts with ≤16 fields (the norm),
98    /// this compiles to a small branchless sequence.
99    #[inline]
100    fn segment(name: &str) -> Option<StaticSegment> {
101        let mut i = 0;
102        while i < Self::SEGMENTS.len() {
103            let seg = Self::SEGMENTS[i];
104            if const_str_eq(seg.name, name) {
105                return Some(seg);
106            }
107            i += 1;
108        }
109        None
110    }
111
112    /// Total size of the layout body (sum of all segments, or max(end)).
113    #[inline]
114    fn body_size() -> u32 {
115        let segments = Self::SEGMENTS;
116        if segments.is_empty() {
117            return 0;
118        }
119        // Last segment end offset (assuming sorted by offset).
120        let last = segments[segments.len() - 1];
121        last.offset + last.size
122    }
123
124    /// Number of segments.
125    #[inline(always)]
126    fn segment_count() -> usize {
127        Self::SEGMENTS.len()
128    }
129
130    /// Look up a segment by index (zero-overhead const access).
131    ///
132    /// Prefer this over `segment()` in generated accessor code where the
133    /// index is a compile-time constant. The compiler will resolve this to
134    /// an immediate constant load, no branching, no string comparison.
135    #[inline(always)]
136    fn segment_by_index(index: usize) -> StaticSegment {
137        Self::SEGMENTS[index]
138    }
139}
140
141/// Constant string equality for segment name matching.
142///
143/// Works in const contexts. Used by the default `SegmentMap::segment()`.
144#[inline(always)]
145const fn const_str_eq(a: &str, b: &str) -> bool {
146    let a = a.as_bytes();
147    let b = b.as_bytes();
148    if a.len() != b.len() {
149        return false;
150    }
151    let mut i = 0;
152    while i < a.len() {
153        if a[i] != b[i] {
154            return false;
155        }
156        i += 1;
157    }
158    true
159}
160
161// ══════════════════════════════════════════════════════════════════════
162//  FieldMap ↔ SegmentMap alignment assertion
163// ══════════════════════════════════════════════════════════════════════
164
165/// Compile-time assertion that a type's `SegmentMap` and `FieldMap` are
166/// isomorphic: same count, same names, same offsets, same sizes.
167///
168/// Generated macros should produce both from a single source; this
169/// catches any accidental divergence.
170///
171/// Usage:
172/// ```ignore
173/// const _: () = assert_segment_field_alignment::<MyLayout>(HEADER_LEN);
174/// ```
175///
176/// `header_offset` is subtracted from `FieldInfo.offset` before comparing
177/// with `StaticSegment.offset`, since `FieldInfo` offsets are absolute
178/// (including the Hopper header) while `StaticSegment` offsets are relative
179/// to the body start.
180pub const fn assert_segment_field_alignment<T: SegmentMap + hopper_runtime::field_map::FieldMap>(
181    header_offset: usize,
182) {
183    let segments = T::SEGMENTS;
184    let fields = T::FIELDS;
185    assert!(
186        segments.len() == fields.len(),
187        "SegmentMap and FieldMap have different field counts"
188    );
189    let mut i = 0;
190    while i < segments.len() {
191        let seg = &segments[i];
192        let field = &fields[i];
193        assert!(
194            seg.offset as usize == field.offset.wrapping_sub(header_offset),
195            "SegmentMap/FieldMap offset mismatch"
196        );
197        assert!(
198            seg.size as usize == field.size,
199            "SegmentMap/FieldMap size mismatch"
200        );
201        // Name equality
202        assert!(
203            const_str_eq(seg.name, field.name),
204            "SegmentMap/FieldMap name mismatch"
205        );
206        i += 1;
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    struct TestLayout;
215
216    impl SegmentMap for TestLayout {
217        const SEGMENTS: &'static [StaticSegment] = &[
218            StaticSegment::new("authority", 0, 32),
219            StaticSegment::new("balance", 32, 8),
220            StaticSegment::new("bump", 40, 1),
221        ];
222    }
223
224    #[test]
225    fn lookup_by_name() {
226        let seg = TestLayout::segment("balance").unwrap();
227        assert_eq!(seg.offset, 32);
228        assert_eq!(seg.size, 8);
229    }
230
231    #[test]
232    fn lookup_missing() {
233        assert!(TestLayout::segment("nonexistent").is_none());
234    }
235
236    #[test]
237    fn body_size_computed() {
238        assert_eq!(TestLayout::body_size(), 41);
239    }
240
241    #[test]
242    fn segment_count() {
243        assert_eq!(TestLayout::segment_count(), 3);
244    }
245
246    #[test]
247    fn segment_end() {
248        let seg = TestLayout::segment("authority").unwrap();
249        assert_eq!(seg.end(), 32);
250    }
251
252    #[test]
253    fn segment_by_index_constant_access() {
254        let seg = TestLayout::segment_by_index(1);
255        assert_eq!(seg.name, "balance");
256        assert_eq!(seg.offset, 32);
257        assert_eq!(seg.size, 8);
258    }
259
260    // Verify alignment assertion compiles for matching SegmentMap + FieldMap.
261    impl hopper_runtime::field_map::FieldMap for TestLayout {
262        const FIELDS: &'static [hopper_runtime::field_map::FieldInfo] = &[
263            hopper_runtime::field_map::FieldInfo::new("authority", 16, 32),
264            hopper_runtime::field_map::FieldInfo::new("balance", 48, 8),
265            hopper_runtime::field_map::FieldInfo::new("bump", 56, 1),
266        ];
267    }
268
269    // Compiles = SegmentMap and FieldMap are isomorphic.
270    const _: () = assert_segment_field_alignment::<TestLayout>(16);
271}