hopper_runtime/segment.rs
1//! Runtime-local segment primitive.
2//!
3//! `Segment` stores an absolute byte offset and size in two `u32` fields. It is
4//! `Copy`, const-constructable, and carries no field name. The corresponding
5//! `hopper_core::segment_map::StaticSegment` adds a name for schema and tooling
6//! use.
7//!
8//! # Design
9//!
10//! Generated accessors can embed a `Segment` constant next to their layout
11//! metadata:
12//!
13//! - macros emit `const BALANCE: Segment = Segment::body(0, 8);`
14//! - call sites read `account.segment_mut_const::<u64>(&mut b, BALANCE)?`
15//! - the compiler can propagate the constant through bounds checks and pointer
16//! arithmetic.
17//!
18//! `Segment` never appears in an on-chain layout, it is a compile-time
19//! description only. Use `hopper_core::account::SegmentDescriptor` for
20//! bytes that travel on the wire.
21
22use crate::layout::HopperHeader;
23
24/// Compile-time descriptor of a typed byte range inside an account.
25///
26/// The `u32` fields match Hopper's segment metadata encoding and keep the
27/// descriptor eight bytes wide.
28#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
29#[repr(C)]
30pub struct Segment {
31 /// Absolute byte offset from the start of account data (includes
32 /// the 16-byte Hopper header). This is what the access primitives
33 /// want, so storing it absolute avoids a runtime addition.
34 pub offset: u32,
35 /// Byte size of the segment.
36 pub size: u32,
37}
38
39impl Segment {
40 /// Construct a segment from an absolute offset (measured from the
41 /// start of account data, including the Hopper header).
42 #[inline(always)]
43 pub const fn new(offset: u32, size: u32) -> Self {
44 Self { offset, size }
45 }
46
47 /// Construct a segment from a body-relative offset (offset measured
48 /// past the 16-byte Hopper header). This is the form that macros
49 /// most often emit: `#[hopper::state]` computes field offsets
50 /// relative to the struct body, and body-relative is what
51 /// `SegmentMap::SEGMENTS` stores.
52 #[inline(always)]
53 pub const fn body(body_offset: u32, size: u32) -> Self {
54 Self {
55 offset: HopperHeader::SIZE as u32 + body_offset,
56 size,
57 }
58 }
59
60 /// One-past-the-end byte offset, widened to `u64` so `offset + size`
61 /// cannot wrap before containment and overlap comparisons.
62 #[inline(always)]
63 pub const fn end(&self) -> u64 {
64 self.offset as u64 + self.size as u64
65 }
66
67 /// Whether two segments share any byte.
68 #[inline(always)]
69 pub const fn overlaps(&self, other: &Segment) -> bool {
70 (self.offset as u64) < other.end() && (other.offset as u64) < self.end()
71 }
72
73 /// Whether this segment is contained fully within `container`.
74 #[inline(always)]
75 pub const fn contained_in(&self, container: &Segment) -> bool {
76 self.offset >= container.offset && self.end() <= container.end()
77 }
78}
79
80// ══════════════════════════════════════════════════════════════════════
81// TypedSegment<T, const OFFSET: u32>
82// ══════════════════════════════════════════════════════════════════════
83//
84// Where `Segment` carries `(offset, size)` at runtime, `TypedSegment`
85// folds **both** values into the type system: `T` determines the size
86// via `size_of::<T>()`, and `OFFSET` is a const generic. The struct
87// itself is a ZST, no memory at all. This is the runtime's
88// const-generic segment implementation: at every
89// call site the compiler substitutes the literal offset and literal
90// size into the bounds check + pointer add, leaving pure
91// `ptr + constant` arithmetic in the emitted BPF.
92//
93// Use `TypedSegment` when you know the layout at compile time (i.e.
94// every `#[hopper::state]` field). Fall back to `Segment` when the
95// offset is data-dependent (e.g. a user-provided index into a fixed
96// array).
97
98/// Compile-time typed segment descriptor: `T` is the overlay type,
99/// `OFFSET` is the absolute byte offset from the start of account
100/// data. Zero-sized.
101///
102/// ```ignore
103/// // Matches Vault.balance at body offset 0, past the 16-byte header:
104/// const VAULT_BALANCE: TypedSegment<WireU64, { HopperHeader::SIZE as u32 }>
105/// = TypedSegment::new();
106///
107/// let bal = account.segment_ref_typed(&mut borrows, VAULT_BALANCE)?;
108/// ```
109#[derive(Copy, Clone, Debug, Default)]
110pub struct TypedSegment<T: crate::Pod, const OFFSET: u32> {
111 _marker: core::marker::PhantomData<fn() -> T>,
112}
113
114impl<T: crate::Pod, const OFFSET: u32> TypedSegment<T, OFFSET> {
115 /// Construct the marker. Runs entirely at compile time.
116 #[inline(always)]
117 pub const fn new() -> Self {
118 Self {
119 _marker: core::marker::PhantomData,
120 }
121 }
122
123 /// The absolute byte offset of this segment (`OFFSET` const-generic).
124 #[inline(always)]
125 pub const fn offset() -> u32 {
126 OFFSET
127 }
128
129 /// The byte size of this segment (`size_of::<T>()`, folded at compile time).
130 #[inline(always)]
131 pub const fn size() -> u32 {
132 core::mem::size_of::<T>() as u32
133 }
134
135 /// One-past-the-end byte offset, widened like [`Segment::end`].
136 #[inline(always)]
137 pub const fn end() -> u64 {
138 OFFSET as u64 + core::mem::size_of::<T>() as u64
139 }
140
141 /// Lower to a runtime [`Segment`] when a heterogeneous collection
142 /// of segments is needed (e.g. a validation pass that iterates).
143 #[inline(always)]
144 pub const fn as_segment() -> Segment {
145 Segment::new(OFFSET, core::mem::size_of::<T>() as u32)
146 }
147}
148
149// SAFETY: Proof that `TypedSegment` really is zero-sized.
150const _: () = {
151 assert!(
152 core::mem::size_of::<TypedSegment<[u8; 8], 0>>() == 0,
153 "TypedSegment must be zero-sized so it costs nothing to pass around",
154 );
155};
156
157/// Generic field role for account field capability descriptors.
158pub const FIELD_ROLE_DATA: u8 = 0;
159/// Field carries authority or signer identity semantics.
160pub const FIELD_ROLE_AUTHORITY: u8 = 1;
161/// Field carries accounting or balance semantics.
162pub const FIELD_ROLE_BALANCE: u8 = 2;
163/// Field carries migration/versioning semantics.
164pub const FIELD_ROLE_VERSION: u8 = 3;
165/// Field is intended to become immutable after initialization.
166pub const FIELD_POLICY_IMMUTABLE_AFTER_INIT: u8 = 1 << 0;
167/// Field mutations should use checked arithmetic.
168pub const FIELD_POLICY_CHECKED_MATH: u8 = 1 << 1;
169/// Field mutations should be gated by an admin or authority proof.
170pub const FIELD_POLICY_AUTHORITY_GATED: u8 = 1 << 2;
171
172/// Zero-sized field capability: type, byte offset, semantic role, and policy.
173///
174/// This is the runtime half of proof-carrying field access. Macros and tooling
175/// can emit these ZSTs for each account field, then require a matching
176/// capability in higher-level mutation helpers without storing extra metadata
177/// in the account body.
178#[derive(Copy, Clone, Debug, Default)]
179pub struct FieldCapability<T: crate::Pod, const OFFSET: u32, const ROLE: u8, const POLICY: u8> {
180 _marker: core::marker::PhantomData<fn() -> T>,
181}
182
183impl<T: crate::Pod, const OFFSET: u32, const ROLE: u8, const POLICY: u8>
184 FieldCapability<T, OFFSET, ROLE, POLICY>
185{
186 #[inline(always)]
187 pub const fn new() -> Self {
188 Self {
189 _marker: core::marker::PhantomData,
190 }
191 }
192
193 #[inline(always)]
194 pub const fn role() -> u8 {
195 ROLE
196 }
197
198 #[inline(always)]
199 pub const fn policy() -> u8 {
200 POLICY
201 }
202
203 #[inline(always)]
204 pub const fn typed_segment() -> TypedSegment<T, OFFSET> {
205 TypedSegment::new()
206 }
207
208 #[inline(always)]
209 pub const fn as_segment() -> Segment {
210 TypedSegment::<T, OFFSET>::as_segment()
211 }
212
213 #[inline(always)]
214 pub const fn has_policy(flag: u8) -> bool {
215 POLICY & flag != 0
216 }
217}
218
219// SAFETY: Field capabilities must also remain ZSTs.
220const _: () = {
221 assert!(
222 core::mem::size_of::<FieldCapability<[u8; 8], 0, FIELD_ROLE_DATA, 0>>() == 0,
223 "FieldCapability must be zero-sized",
224 );
225};
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn typed_segment_is_zero_sized() {
233 assert_eq!(core::mem::size_of::<TypedSegment<[u8; 8], 16>>(), 0);
234 }
235
236 #[test]
237 fn typed_segment_offset_and_size_fold() {
238 const S: TypedSegment<[u8; 8], 16> = TypedSegment::new();
239 // The values come from the type system directly.
240 assert_eq!(TypedSegment::<[u8; 8], 16>::offset(), 16);
241 assert_eq!(TypedSegment::<[u8; 8], 16>::size(), 8);
242 assert_eq!(TypedSegment::<[u8; 8], 16>::end(), 24);
243 let _ = S; // ensure const ctor works
244 }
245
246 #[test]
247 fn typed_segment_lowers_to_runtime_segment() {
248 const S: Segment = TypedSegment::<[u8; 8], 16>::as_segment();
249 assert_eq!(S.offset, 16);
250 assert_eq!(S.size, 8);
251 }
252
253 #[test]
254 fn body_adds_header() {
255 let s = Segment::body(0, 8);
256 assert_eq!(s.offset, HopperHeader::SIZE as u32);
257 assert_eq!(s.size, 8);
258 assert_eq!(s.end(), HopperHeader::SIZE as u64 + 8);
259 }
260
261 #[test]
262 fn overlaps_detects_shared_bytes() {
263 let a = Segment::new(0, 16);
264 let b = Segment::new(8, 16);
265 let c = Segment::new(16, 16);
266 assert!(a.overlaps(&b));
267 assert!(!a.overlaps(&c)); // adjacent, no shared bytes
268 assert!(b.overlaps(&c));
269 }
270
271 #[test]
272 fn contained_in_reports_proper_nesting() {
273 let outer = Segment::new(0, 32);
274 let inner = Segment::new(8, 8);
275 let equal = Segment::new(0, 32);
276 let escape = Segment::new(24, 16);
277 assert!(inner.contained_in(&outer));
278 assert!(equal.contained_in(&outer));
279 assert!(!escape.contained_in(&outer));
280 }
281
282 #[test]
283 fn end_arithmetic_does_not_wrap_at_u32_max() {
284 // Pre-fix, `end()` wrapped: offset u32::MAX + size 2 → end 1,
285 // making this segment falsely "contained" in a small container
286 // and falsely non-overlapping with its own bytes.
287 let huge = Segment::new(u32::MAX, 2);
288 let container = Segment::new(0, 32);
289 assert_eq!(huge.end(), u32::MAX as u64 + 2);
290 assert!(!huge.contained_in(&container));
291 assert!(!huge.overlaps(&container));
292 // And it does overlap a range that genuinely reaches it.
293 let touching = Segment::new(u32::MAX, 1);
294 assert!(huge.overlaps(&touching));
295 }
296}