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}