Skip to main content

celox_state_layout/
lib.rs

1//! Shared physical simulation-state layout contracts.
2//!
3//! These types and offsets form the ABI between layout construction, generated
4//! code, and runtime state access. They contain no frontend or backend IR.
5
6use celox_design::{
7    AbsoluteAddrBase, RegionedAbsoluteAddrBase, RuntimeEventSite, SPARSE_WORKING_REGION,
8    STABLE_REGION,
9};
10use fxhash::{FxHashMap as HashMap, FxHashSet as HashSet};
11use serde::{Deserialize, Serialize};
12use std::hash::Hash;
13
14mod trace;
15pub use trace::{TRACE_GROUP_BYTES, TraceLayout};
16
17pub const RUNTIME_EVENT_CAPACITY: usize = 1024;
18pub const RUNTIME_EVENT_WRITING: u64 = u64::MAX;
19pub const STATE_HEADER_SIZE: usize = 32;
20pub const STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET: usize = 0;
21/// Remaining iterations for an in-function native tick loop.
22#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
23pub const STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET: usize = 8;
24#[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
25pub const STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET: usize = 24;
26#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
27pub const STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET: usize = 16;
28/// Runtime-event write sequence observed when a native tick batch starts.
29pub const RUNTIME_EVENT_HEADER_SIZE: usize = 8;
30pub const RUNTIME_EVENT_SLOT_SEQ_OFFSET: usize = 0;
31pub const RUNTIME_EVENT_SLOT_SITE_OFFSET: usize = 8;
32pub const RUNTIME_EVENT_SLOT_ARG_COUNT_OFFSET: usize = 16;
33pub const RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET: usize = 24;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct RuntimeEventArgLayout {
37    pub value_word_offset: usize,
38    pub mask_word_offset: usize,
39    pub word_count: usize,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct RuntimeEventSiteLayout {
44    pub args: Vec<RuntimeEventArgLayout>,
45    pub payload_words: usize,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SparseWorkingLayout {
50    pub active_index: usize,
51    pub chunk_count: usize,
52    pub dirty_words_offset: usize,
53    pub dirty_word_count: usize,
54    pub summary_words_offset: usize,
55    pub summary_word_count: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
59pub enum MemoryLayoutMode {
60    Packed,
61    ElementStrided,
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
65pub struct UnpackedArrayLayout {
66    pub element_width: usize,
67    pub element_count: usize,
68    pub element_stride: usize,
69    pub plane_size: usize,
70}
71
72/// One semantic state object that requires stable storage.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct StateObjectLayout<A> {
75    pub address: A,
76    pub width: usize,
77    pub is_4state: bool,
78}
79
80/// Semantic constraints produced by optimization and consumed when physical
81/// state layout is finalized.
82///
83/// This deliberately contains no physical offsets.  Alias pairs state that
84/// two semantic objects may share one stable-state home; layout construction
85/// still validates representation compatibility before applying the pair.
86#[derive(Debug, Clone)]
87pub struct LayoutRequirements<A> {
88    state_aliases: HashMap<A, A>,
89}
90
91impl<A> Default for LayoutRequirements<A> {
92    fn default() -> Self {
93        Self {
94            state_aliases: HashMap::default(),
95        }
96    }
97}
98
99impl<A> LayoutRequirements<A> {
100    pub fn state_aliases(&self) -> &HashMap<A, A> {
101        &self.state_aliases
102    }
103
104    pub fn state_aliases_mut(&mut self) -> &mut HashMap<A, A> {
105        &mut self.state_aliases
106    }
107
108    pub fn is_empty(&self) -> bool {
109        self.state_aliases.is_empty()
110    }
111
112    pub fn clear(&mut self) {
113        self.state_aliases.clear();
114    }
115}
116
117/// Complete, backend-independent input to physical layout construction.
118///
119/// The compiler facade adapts its phase artifacts into this value. Layout
120/// construction therefore never needs to inspect frontend IR, optimizer plans,
121/// or a mixed compiler `Program`.
122#[derive(Debug, Clone)]
123pub struct LayoutInput<A> {
124    pub state_objects: Vec<StateObjectLayout<A>>,
125    pub working_addresses: Vec<A>,
126    pub sparse_addresses: Vec<A>,
127    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
128    pub requirements: LayoutRequirements<A>,
129    pub ff_referenced_addresses: HashSet<A>,
130    pub num_events: usize,
131    pub runtime_event_sites: Vec<RuntimeEventSite>,
132}
133
134/// Adapter implemented by the phase artifact that precedes physical layout.
135pub trait LayoutSource<A> {
136    fn layout_input(&self, mode: MemoryLayoutMode) -> LayoutInput<A>;
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
140#[serde(bound(
141    serialize = "A: Serialize + Eq + Hash",
142    deserialize = "A: Deserialize<'de> + Eq + Hash"
143))]
144pub struct MemoryLayout<A> {
145    /// Present only in builds that record waveforms.
146    pub trace: Option<TraceLayout>,
147    pub four_state: bool,
148    pub mode: MemoryLayoutMode,
149    /// Stable region offsets. Includes all declared state objects.
150    pub offsets: HashMap<A, usize>,
151    pub widths: HashMap<A, usize>,
152    /// Whether each state object has a four-state source type.
153    pub is_4states: HashMap<A, bool>,
154    pub unpacked_arrays: HashMap<A, UnpackedArrayLayout>,
155    /// Stable region size in bytes.
156    pub total_size: usize,
157
158    /// Working region offsets. Includes only actually-used state objects.
159    pub working_offsets: HashMap<A, usize>,
160    pub working_base_offset: usize,
161    /// Copy-on-write next-state data for dynamically addressed FF targets.
162    pub sparse_offsets: HashMap<A, usize>,
163    pub sparse_base_offset: usize,
164    pub sparse_layouts: HashMap<A, SparseWorkingLayout>,
165    pub sparse_active_bits_offset: usize,
166    pub sparse_active_capacity: usize,
167    pub merged_total_size: usize,
168
169    pub triggered_bits_offset: usize,
170    pub triggered_bits_total_size: usize,
171
172    pub scratch_base_offset: usize,
173    pub scratch_size: usize,
174
175    pub runtime_event_capacity: usize,
176    pub runtime_event_slot_size: usize,
177    pub runtime_event_buffer_size: usize,
178    pub runtime_event_site_layouts: Vec<RuntimeEventSiteLayout>,
179}
180
181type PhysicalLayoutObject<A> = (A, usize, bool, usize, usize);
182
183fn sort_layout_objects<A: Copy + Ord>(objects: &mut [PhysicalLayoutObject<A>]) {
184    // Packing by decreasing alignment avoids padding. Equal-alignment objects
185    // use semantic-address order so randomized input maps cannot perturb every
186    // physical offset and the generated machine code that embeds it.
187    objects.sort_unstable_by_key(|(address, _, _, _, alignment)| {
188        (std::cmp::Reverse(*alignment), *address)
189    });
190}
191
192impl<A> MemoryLayout<A>
193where
194    A: Copy + Eq + Hash + Ord,
195{
196    pub fn build<S>(source: &S, four_state: bool, mode: MemoryLayoutMode) -> Self
197    where
198        S: LayoutSource<A>,
199    {
200        let input = source.layout_input(mode);
201        let LayoutInput {
202            state_objects,
203            working_addresses,
204            sparse_addresses,
205            unpacked_arrays,
206            requirements,
207            ff_referenced_addresses,
208            num_events,
209            runtime_event_sites,
210        } = input;
211
212        let mut stable_objects = state_objects
213            .into_iter()
214            .map(|object| {
215                let size = unpacked_arrays
216                    .get(&object.address)
217                    .map(|layout| layout.plane_size)
218                    .unwrap_or_else(|| get_byte_size(object.width));
219                let alignment = unpacked_arrays
220                    .get(&object.address)
221                    .map(|layout| layout.element_stride.min(8))
222                    .unwrap_or_else(|| get_alignment(object.width));
223                (
224                    object.address,
225                    object.width,
226                    object.is_4state,
227                    size,
228                    alignment,
229                )
230            })
231            .collect::<Vec<_>>();
232        sort_layout_objects(&mut stable_objects);
233
234        let mut offsets = HashMap::default();
235        let mut widths = HashMap::default();
236        let mut is_4states = HashMap::default();
237        let runtime_event_site_layouts = build_runtime_event_site_layouts(&runtime_event_sites);
238        let runtime_event_slot_size = RUNTIME_EVENT_SLOT_PAYLOAD_OFFSET
239            + runtime_event_site_layouts
240                .iter()
241                .map(|site| site.payload_words)
242                .max()
243                .unwrap_or(0)
244                * 8;
245
246        let mut current_offset = STATE_HEADER_SIZE;
247        for (address, width, is_4state, size, alignment) in stable_objects {
248            current_offset = align_up(current_offset, alignment);
249            offsets.insert(address, current_offset);
250            widths.insert(address, width);
251            is_4states.insert(address, is_4state);
252            current_offset += size;
253            if four_state {
254                current_offset += size;
255            }
256        }
257
258        let mut working_objects = working_addresses
259            .iter()
260            .map(|address| {
261                let width = widths[address];
262                let size = unpacked_arrays
263                    .get(address)
264                    .map(|layout| layout.plane_size)
265                    .unwrap_or_else(|| get_byte_size(width));
266                let alignment = unpacked_arrays
267                    .get(address)
268                    .map(|layout| layout.element_stride.min(8))
269                    .unwrap_or_else(|| get_alignment(width));
270                (*address, width, is_4states[address], size, alignment)
271            })
272            .collect::<Vec<_>>();
273        sort_layout_objects(&mut working_objects);
274
275        let mut working_offsets = HashMap::default();
276        let mut working_size = 0;
277        for (address, _, _, size, alignment) in working_objects {
278            working_size = align_up(working_size, alignment);
279            working_offsets.insert(address, working_size);
280            working_size += size;
281            if four_state {
282                working_size += size;
283            }
284        }
285
286        let mut sparse_objects = sparse_addresses
287            .iter()
288            .map(|address| {
289                let width = widths[address];
290                let size = unpacked_arrays
291                    .get(address)
292                    .map(|layout| layout.plane_size)
293                    .unwrap_or_else(|| get_byte_size(width));
294                let alignment = unpacked_arrays
295                    .get(address)
296                    .map(|layout| layout.element_stride.min(8))
297                    .unwrap_or_else(|| get_alignment(width));
298                (*address, width, is_4states[address], size, alignment)
299            })
300            .collect::<Vec<_>>();
301        sort_layout_objects(&mut sparse_objects);
302
303        let mut sparse_offsets = HashMap::default();
304        let mut sparse_size = 0usize;
305        for (address, _, _, size, alignment) in sparse_objects {
306            sparse_size = align_up(sparse_size, alignment);
307            sparse_offsets.insert(address, sparse_size);
308            let plane_count = if four_state { 2 } else { 1 };
309            let final_chunk_size = align_up(size, 8);
310            let physical_extent = (plane_count - 1) * size + final_chunk_size;
311            sparse_size += align_up(physical_extent, 8);
312        }
313
314        let working_base_offset = align_up(current_offset, 8);
315        let sparse_base_offset = align_up(working_base_offset + working_size, 8);
316        let mut sparse_metadata_offset = align_up(sparse_base_offset + sparse_size, 8);
317        let mut sparse_layouts = HashMap::default();
318        let mut sparse_order = sparse_addresses;
319        sparse_order.sort_unstable();
320        let sparse_active_capacity = sparse_order.len();
321        for (active_index, address) in sparse_order.into_iter().enumerate() {
322            let chunk_count = unpacked_arrays
323                .get(&address)
324                .map(|layout| layout.plane_size.div_ceil(8))
325                .unwrap_or_else(|| widths[&address].div_ceil(64));
326            let dirty_word_count = chunk_count.div_ceil(64);
327            let summary_word_count = dirty_word_count.div_ceil(64);
328            let dirty_words_offset = sparse_metadata_offset;
329            sparse_metadata_offset += dirty_word_count * 8;
330            let summary_words_offset = sparse_metadata_offset;
331            sparse_metadata_offset += summary_word_count * 8;
332            sparse_layouts.insert(
333                address,
334                SparseWorkingLayout {
335                    active_index,
336                    chunk_count,
337                    dirty_words_offset,
338                    dirty_word_count,
339                    summary_words_offset,
340                    summary_word_count,
341                },
342            );
343        }
344
345        let sparse_active_bits_offset = align_up(sparse_metadata_offset, 8);
346        sparse_metadata_offset =
347            sparse_active_bits_offset + sparse_active_capacity.div_ceil(64) * 8;
348        let triggered_bits_offset = align_up(sparse_metadata_offset, 8);
349        let triggered_bits_total_size = num_events.div_ceil(8);
350        let scratch_base_offset = align_up(triggered_bits_offset + triggered_bits_total_size, 8);
351        let runtime_event_buffer_size =
352            RUNTIME_EVENT_HEADER_SIZE + RUNTIME_EVENT_CAPACITY * runtime_event_slot_size;
353        let merged_total_size = scratch_base_offset;
354
355        let mut address_aliases = requirements.state_aliases.into_iter().collect::<Vec<_>>();
356        address_aliases.sort_unstable();
357        for (alias, canonical) in address_aliases {
358            let fourstate_ok = !four_state
359                || (is_4states.get(&alias) == Some(&false)
360                    && is_4states.get(&canonical) == Some(&false));
361            let alias_fits = widths
362                .get(&alias)
363                .zip(widths.get(&canonical))
364                .is_some_and(|(&alias_width, &canonical_width)| alias_width <= canonical_width);
365            if fourstate_ok
366                && alias_fits
367                && !ff_referenced_addresses.contains(&alias)
368                && let Some(&canonical_offset) = offsets.get(&canonical)
369            {
370                offsets.insert(alias, canonical_offset);
371            }
372        }
373
374        Self {
375            trace: None,
376            four_state,
377            mode,
378            offsets,
379            widths,
380            is_4states,
381            unpacked_arrays,
382            total_size: current_offset,
383            working_offsets,
384            working_base_offset,
385            sparse_offsets,
386            sparse_base_offset,
387            sparse_layouts,
388            sparse_active_bits_offset,
389            sparse_active_capacity,
390            merged_total_size,
391            triggered_bits_offset,
392            triggered_bits_total_size,
393            scratch_base_offset,
394            scratch_size: 0,
395            runtime_event_capacity: RUNTIME_EVENT_CAPACITY,
396            runtime_event_slot_size,
397            runtime_event_buffer_size,
398            runtime_event_site_layouts,
399        }
400    }
401
402    /// Append backend-private scratch storage without changing any semantic
403    /// state offset. Backend planning happens after the backend-neutral state
404    /// layout has been finalized, so scratch is always the final region.
405    pub fn with_backend_scratch(mut self, scratch_size: usize) -> Self {
406        self.scratch_size = scratch_size;
407        self.merged_total_size = align_up(self.scratch_base_offset + scratch_size, 8);
408        self
409    }
410
411    /// Reserve observer activity after scheduler metadata and before backend
412    /// scratch. Stable/working addresses and clock-trigger offsets stay fixed.
413    pub fn enable_trace(&mut self) {
414        if self.trace.is_some() {
415            return;
416        }
417        assert_eq!(
418            self.scratch_size, 0,
419            "trace must be enabled before backend planning"
420        );
421        let homes = self
422            .offsets
423            .iter()
424            .map(|(address, &offset)| {
425                let planes = if self.four_state { 2 } else { 1 };
426                (offset, offset + self.plane_size(address) * planes)
427            })
428            .collect();
429        let trace = TraceLayout::new(self.scratch_base_offset, self.total_size, homes);
430        self.scratch_base_offset = align_up(trace.end_offset(), 8);
431        self.merged_total_size = self.scratch_base_offset;
432        self.trace = Some(trace);
433    }
434
435    pub fn trace_notification_offsets<R: RegionedAddress<A>>(
436        &self,
437        address: &R,
438    ) -> Option<[usize; 2]> {
439        if address.region() != STABLE_REGION {
440            return None;
441        }
442        let trace = self.trace.as_ref()?;
443        Some(trace.notification_offsets(self.offsets[&address.absolute_address()]))
444    }
445
446    pub fn plane_size(&self, address: &A) -> usize {
447        self.unpacked_arrays
448            .get(address)
449            .map(|layout| layout.plane_size)
450            .unwrap_or_else(|| get_byte_size(self.widths[address]))
451    }
452
453    pub fn region_base_offset<R>(&self, address: &R) -> usize
454    where
455        R: RegionedAddress<A>,
456    {
457        let absolute = address.absolute_address();
458        match address.region() {
459            STABLE_REGION => self.offsets[&absolute],
460            SPARSE_WORKING_REGION => self.sparse_base_offset + self.sparse_offsets[&absolute],
461            _ => self.working_base_offset + self.working_offsets[&absolute],
462        }
463    }
464
465    pub fn map_static_bit_offset(&self, address: &A, bit_offset: usize) -> (usize, usize) {
466        let Some(array) = self.unpacked_arrays.get(address) else {
467            return (bit_offset / 8, bit_offset % 8);
468        };
469        let element = bit_offset / array.element_width;
470        let intra_element = bit_offset % array.element_width;
471        (
472            element * array.element_stride + intra_element / 8,
473            intra_element % 8,
474        )
475    }
476
477    pub fn regioned_static_byte_and_intra<R>(
478        &self,
479        address: &R,
480        bit_offset: usize,
481    ) -> Option<(i32, usize)>
482    where
483        R: RegionedAddress<A>,
484    {
485        let absolute = address.absolute_address();
486        let base = match address.region() {
487            STABLE_REGION => *self.offsets.get(&absolute).unwrap_or(&0),
488            SPARSE_WORKING_REGION => {
489                self.sparse_base_offset + *self.sparse_offsets.get(&absolute).unwrap_or(&0)
490            }
491            _ => self.working_base_offset + *self.working_offsets.get(&absolute).unwrap_or(&0),
492        };
493        let (byte, intra) = self.map_static_bit_offset(&absolute, bit_offset);
494        Some((i32::try_from(base.checked_add(byte)?).ok()?, intra))
495    }
496}
497
498pub trait RegionedAddress<A> {
499    fn region(&self) -> u32;
500    fn absolute_address(&self) -> A;
501}
502
503impl<V: Copy> RegionedAddress<AbsoluteAddrBase<V>> for RegionedAbsoluteAddrBase<V> {
504    fn region(&self) -> u32 {
505        self.region
506    }
507
508    fn absolute_address(&self) -> AbsoluteAddrBase<V> {
509        self.absolute_addr()
510    }
511}
512
513fn build_runtime_event_site_layouts(sites: &[RuntimeEventSite]) -> Vec<RuntimeEventSiteLayout> {
514    sites
515        .iter()
516        .map(|site| {
517            let mut payload_words = 0;
518            let args = site
519                .arg_widths
520                .iter()
521                .map(|width| {
522                    let word_count = (*width).div_ceil(64).max(1);
523                    let value_word_offset = payload_words;
524                    payload_words += word_count;
525                    let mask_word_offset = payload_words;
526                    payload_words += word_count;
527                    RuntimeEventArgLayout {
528                        value_word_offset,
529                        mask_word_offset,
530                        word_count,
531                    }
532                })
533                .collect();
534            RuntimeEventSiteLayout {
535                args,
536                payload_words,
537            }
538        })
539        .collect()
540}
541
542const fn align_up(offset: usize, alignment: usize) -> usize {
543    (offset + alignment - 1) & !(alignment - 1)
544}
545
546fn get_alignment(width: usize) -> usize {
547    let size = get_byte_size(width);
548    if size == 0 {
549        1
550    } else if size <= 8 {
551        size.next_power_of_two()
552    } else {
553        8
554    }
555}
556
557pub const fn get_byte_size(width: usize) -> usize {
558    width.div_ceil(8)
559}
560
561#[cfg(test)]
562mod tests {
563    use super::*;
564
565    #[test]
566    fn byte_size_rounds_up_partial_bytes() {
567        assert_eq!(get_byte_size(0), 0);
568        assert_eq!(get_byte_size(1), 1);
569        assert_eq!(get_byte_size(8), 1);
570        assert_eq!(get_byte_size(9), 2);
571    }
572
573    #[test]
574    fn layout_order_is_independent_of_input_iteration_order() {
575        let high_address = (3u32, 64, false, 8, 8);
576        let low_address = (1u32, 64, false, 8, 8);
577        let less_aligned = (0u32, 32, false, 4, 4);
578        let mut forward = vec![high_address, less_aligned, low_address];
579        let mut reverse = forward.iter().copied().rev().collect::<Vec<_>>();
580
581        sort_layout_objects(&mut forward);
582        sort_layout_objects(&mut reverse);
583
584        assert_eq!(forward, reverse);
585        assert_eq!(forward, vec![low_address, high_address, less_aligned]);
586    }
587
588    #[test]
589    fn layout_requirements_own_semantic_aliases_until_layout() {
590        let mut requirements = LayoutRequirements::default();
591        requirements.state_aliases_mut().insert(2u32, 1u32);
592
593        assert_eq!(requirements.state_aliases().get(&2), Some(&1));
594        assert!(!requirements.is_empty());
595
596        requirements.clear();
597        assert!(requirements.is_empty());
598    }
599
600    #[test]
601    fn layout_applies_aliases_from_requirements() {
602        struct AliasLayoutSource;
603
604        impl LayoutSource<u32> for AliasLayoutSource {
605            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
606                let mut requirements = LayoutRequirements::default();
607                requirements.state_aliases_mut().insert(2, 1);
608                LayoutInput {
609                    state_objects: vec![
610                        StateObjectLayout {
611                            address: 1,
612                            width: 8,
613                            is_4state: false,
614                        },
615                        StateObjectLayout {
616                            address: 2,
617                            width: 8,
618                            is_4state: false,
619                        },
620                    ],
621                    working_addresses: Vec::new(),
622                    sparse_addresses: Vec::new(),
623                    unpacked_arrays: HashMap::default(),
624                    requirements,
625                    ff_referenced_addresses: HashSet::default(),
626                    num_events: 0,
627                    runtime_event_sites: Vec::new(),
628                }
629            }
630        }
631
632        let layout = MemoryLayout::build(&AliasLayoutSource, false, MemoryLayoutMode::Packed);
633        assert_eq!(layout.offsets[&1], layout.offsets[&2]);
634    }
635
636    #[test]
637    fn backend_scratch_only_extends_the_final_layout_region() {
638        struct EmptyLayoutSource;
639
640        impl LayoutSource<u32> for EmptyLayoutSource {
641            fn layout_input(&self, _mode: MemoryLayoutMode) -> LayoutInput<u32> {
642                LayoutInput {
643                    state_objects: Vec::new(),
644                    working_addresses: Vec::new(),
645                    sparse_addresses: Vec::new(),
646                    unpacked_arrays: HashMap::default(),
647                    requirements: LayoutRequirements::default(),
648                    ff_referenced_addresses: HashSet::default(),
649                    num_events: 3,
650                    runtime_event_sites: Vec::new(),
651                }
652            }
653        }
654
655        let base = MemoryLayout::build(&EmptyLayoutSource, false, MemoryLayoutMode::Packed);
656        let expanded = base.clone().with_backend_scratch(13);
657
658        assert_eq!(base.scratch_size, 0);
659        assert_eq!(expanded.scratch_base_offset, base.scratch_base_offset);
660        assert_eq!(expanded.scratch_size, 13);
661        assert_eq!(expanded.merged_total_size, base.scratch_base_offset + 16);
662        assert_eq!(expanded.offsets, base.offsets);
663        assert_eq!(expanded.working_offsets, base.working_offsets);
664        assert_eq!(expanded.triggered_bits_offset, base.triggered_bits_offset);
665    }
666
667    #[test]
668    #[cfg(any(target_arch = "x86_64", target_arch = "aarch64"))]
669    fn state_header_fields_do_not_overlap() {
670        const {
671            assert!(
672                STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET + 8
673                    <= STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET
674            );
675            assert!(
676                STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET + 8
677                    <= STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET
678            );
679            assert!(
680                STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET + 8
681                    <= STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET
682            );
683            assert!(STATE_HEADER_NATIVE_LOOP_EVENT_SEQ_OFFSET + 8 <= STATE_HEADER_SIZE);
684        }
685    }
686}