Skip to main content

celox/backend/native/
backend.rs

1//! NativeBackend: SimBackend implementation using a custom host backend.
2//!
3//! Mirrors the structure of JitBackend but compiles through
4//! ISel → scalar MIR → regalloc → host emission instead of Cranelift.
5
6use std::sync::Arc;
7use std::sync::atomic::{AtomicUsize, Ordering};
8use std::time::{Duration, Instant};
9
10use bit_set::BitSet;
11use celox_design::{
12    InitialStateData, InitialStateValue, InitialStateWriteRun, RuntimeCombObserver,
13    RuntimeErrorInfo, RuntimeEventSite, RuntimeSchema,
14};
15use celox_runtime::DesignReflection;
16use celox_runtime::backend::SimBackend;
17use celox_testbench::TestbenchProgram;
18use num_bigint::BigUint;
19use serde::{Deserialize, Serialize};
20
21use crate::ir::{
22    AbsoluteAddr, BlockId, ExecutionUnit, LaidOutProgram, RegionedAbsoluteAddr, RegisterId,
23    SIRInstruction, SIROffset, SIRTerminator, SignalArrayLayout, SignalRef,
24};
25use crate::{CodegenError, HashMap, HashSet, SimulatorError, SimulatorOptions};
26
27use super::super::RuntimeEventBuffer;
28use super::super::traits::SimulatorErrorCode;
29use super::super::{MemoryLayout, get_byte_size};
30#[cfg(any(
31    feature = "x86_64-codegen",
32    all(target_arch = "x86_64", not(feature = "arm64-codegen"))
33))]
34use super::regalloc;
35use super::{emit, jit_mem};
36
37const NATIVE_FEATURE_BMI2: u8 = 1 << 0;
38const NATIVE_FEATURE_AVX: u8 = 1 << 1;
39const NATIVE_FEATURE_FS_STATE_BASE: u8 = 1 << 2;
40const NATIVE_FEATURE_GS_STATE_BASE: u8 = 1 << 3;
41const NATIVE_FEATURE_POPCNT: u8 = 1 << 4;
42const KNOWN_NATIVE_FEATURES: u8 = NATIVE_FEATURE_BMI2
43    | NATIVE_FEATURE_AVX
44    | NATIVE_FEATURE_FS_STATE_BASE
45    | NATIVE_FEATURE_GS_STATE_BASE
46    | NATIVE_FEATURE_POPCNT;
47
48fn current_native_feature_bits() -> u8 {
49    #[cfg(any(
50        feature = "x86_64-codegen",
51        all(target_arch = "x86_64", not(feature = "arm64-codegen"))
52    ))]
53    {
54        celox_backend_x86::native::features::detected_image_feature_bits()
55    }
56    #[cfg(any(
57        feature = "arm64-codegen",
58        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
59    ))]
60    {
61        0
62    }
63}
64
65fn format_native_feature_bits(bits: u8) -> String {
66    let mut names = Vec::new();
67    if bits & NATIVE_FEATURE_BMI2 != 0 {
68        names.push("BMI2");
69    }
70    if bits & NATIVE_FEATURE_AVX != 0 {
71        names.push("AVX");
72    }
73    if bits & NATIVE_FEATURE_POPCNT != 0 {
74        names.push("POPCNT");
75    }
76    if bits & NATIVE_FEATURE_FS_STATE_BASE != 0 {
77        names.push("FS state base");
78    }
79    if bits & NATIVE_FEATURE_GS_STATE_BASE != 0 {
80        names.push("GS state base");
81    }
82    names.join(", ")
83}
84
85// ────────────────────────────────────────────────────────────────
86// Event handle
87// ────────────────────────────────────────────────────────────────
88
89/// JIT function type: `fn(state: *mut u8) -> i64`
90#[cfg(all(target_arch = "x86_64", not(feature = "arm64-codegen")))]
91pub type NativeSimFunc = unsafe extern "sysv64" fn(*mut u8) -> i64;
92#[cfg(any(
93    feature = "arm64-codegen",
94    all(target_arch = "aarch64", not(feature = "x86_64-codegen")),
95    all(feature = "x86_64-codegen", not(target_arch = "x86_64"))
96))]
97pub type NativeSimFunc = unsafe extern "C" fn(*mut u8) -> i64;
98
99/// Time spent inside generated native simulator functions.
100///
101/// Timing is opt-in so normal simulation does not pay for host clock reads.
102/// A call may execute many ticks when the native tick loop is enabled.
103#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
104pub struct NativeExecutionTiming {
105    elapsed: Duration,
106    calls: u64,
107}
108
109impl NativeExecutionTiming {
110    pub fn elapsed(self) -> Duration {
111        self.elapsed
112    }
113
114    pub fn calls(self) -> u64 {
115        self.calls
116    }
117}
118
119/// Compiled event handle for native backend.
120/// Holds the function pointer directly — no indirection at call time.
121#[derive(Clone, Copy)]
122pub struct NativeEventRef {
123    pub func: NativeSimFunc,
124    pub comb_apply_func: NativeSimFunc,
125    pub addr: AbsoluteAddr,
126    pub id: usize,
127}
128
129impl std::fmt::Debug for NativeEventRef {
130    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131        f.debug_struct("NativeEventRef")
132            .field("func", &(self.func as usize))
133            .field("comb_apply_func", &(self.comb_apply_func as usize))
134            .field("addr", &self.addr)
135            .field("id", &self.id)
136            .finish()
137    }
138}
139
140impl super::super::EventHandle for NativeEventRef {
141    fn id(&self) -> usize {
142        self.id
143    }
144    fn addr(&self) -> AbsoluteAddr {
145        self.addr
146    }
147}
148
149// ────────────────────────────────────────────────────────────────
150// Shared compiled code
151// ────────────────────────────────────────────────────────────────
152
153/// Shared compiled code for the native backend.
154/// Can be cloned (via Arc) to create multiple simulator instances
155/// that share the same compiled machine code.
156pub struct SharedNativeCode {
157    comb_func: NativeSimFunc,
158    comb_unit_funcs: Vec<NativeSimFunc>,
159    /// Keep the combined executable image alive so every entry pointer remains
160    /// valid. The image contains all native functions and their trailing
161    /// constant/literal data.
162    _jit_image: jit_mem::JitCode,
163    program_image: NativeProgramImage,
164
165    event_map: HashMap<AbsoluteAddr, NativeEventRef>,
166    eval_only_event_map: HashMap<AbsoluteAddr, NativeEventRef>,
167    apply_event_map: HashMap<AbsoluteAddr, NativeEventRef>,
168    id_to_addr: Vec<AbsoluteAddr>,
169    id_to_event: Vec<NativeEventRef>,
170    layout: MemoryLayout,
171    /// Simulation-state bytes plus the largest native spill/scratch arena
172    /// required by any compiled function.
173    native_memory_size: usize,
174    options: NativeRuntimeOptions,
175    /// (offset, byte_size) pairs for 4-state variables that need X initialization.
176    four_state_inits: Vec<(usize, usize)>,
177}
178
179// Safety: JitCode contains Mmap which is Send+Sync after creation.
180unsafe impl Send for SharedNativeCode {}
181unsafe impl Sync for SharedNativeCode {}
182
183impl SharedNativeCode {
184    /// Attach a compiler-produced image to the precompiled host runtime.
185    ///
186    /// # Safety
187    ///
188    /// The image's machine code must come from a trusted source. Structural
189    /// validation and the container checksum detect corruption, but do not
190    /// authenticate code before it is mapped executable and invoked.
191    pub unsafe fn from_image(program_image: NativeProgramImage) -> Result<Self, SimulatorError> {
192        program_image.validate().map_err(|message| {
193            codegen_message(format!("invalid native program image: {message}"))
194        })?;
195        let unavailable = program_image.required_native_features & !current_native_feature_bits();
196        if unavailable != 0 {
197            return Err(codegen_message(format!(
198                "native program image requires unavailable host features: {}",
199                format_native_feature_bits(unavailable)
200            )));
201        }
202        let symbols = program_image
203            .symbols
204            .iter()
205            .map(|symbol| jit_mem::JitSymbol {
206                offset: symbol.offset,
207                size: symbol.size,
208                name: symbol.name.clone(),
209            })
210            .collect::<Vec<_>>();
211        let jit_image = jit_mem::JitCode::new_named_with_symbols_profiled(
212            program_image.code_image(),
213            "celox_native_image",
214            &symbols,
215            program_image.options.perf_map,
216        )
217        .map_err(|source| codegen_err(CodegenError::NativeMemory { source }))?;
218        let materialize = |event: NativeEventImageRef| -> Result<NativeEventRef, SimulatorError> {
219            Ok(NativeEventRef {
220                func: native_function_at(&jit_image, event.func_offset)?,
221                comb_apply_func: native_function_at(&jit_image, event.comb_apply_offset)?,
222                addr: event.addr,
223                id: event.id,
224            })
225        };
226        let materialize_map = |source: &HashMap<AbsoluteAddr, NativeEventImageRef>| {
227            source
228                .iter()
229                .map(|(&addr, &event)| Ok((addr, materialize(event)?)))
230                .collect::<Result<HashMap<_, _>, SimulatorError>>()
231        };
232        let comb_func = native_function_at(&jit_image, program_image.comb_offset)?;
233        let comb_unit_funcs = program_image
234            .comb_unit_offsets
235            .iter()
236            .copied()
237            .map(|offset| native_function_at(&jit_image, offset))
238            .collect::<Result<Vec<_>, _>>()?;
239        let event_map = materialize_map(&program_image.event_map)?;
240        let eval_only_event_map = materialize_map(&program_image.eval_only_event_map)?;
241        let apply_event_map = materialize_map(&program_image.apply_event_map)?;
242        let id_to_event = program_image
243            .id_to_event
244            .iter()
245            .copied()
246            .map(materialize)
247            .collect::<Result<Vec<_>, _>>()?;
248
249        Ok(Self {
250            comb_func,
251            comb_unit_funcs,
252            _jit_image: jit_image,
253            event_map,
254            eval_only_event_map,
255            apply_event_map,
256            id_to_addr: program_image.id_to_addr.clone(),
257            id_to_event,
258            layout: program_image.layout.clone(),
259            native_memory_size: program_image.native_memory_size,
260            options: program_image.options,
261            four_state_inits: program_image.four_state_inits.clone(),
262            program_image,
263        })
264    }
265
266    /// Returns a reference to the memory layout.
267    pub fn layout(&self) -> &MemoryLayout {
268        &self.layout
269    }
270
271    /// Exact relocatable native image used by this compiled design.
272    ///
273    /// Entry addresses are intentionally not serialized: consumers copy this
274    /// image and resolve [`Self::code_entries`] relative to the new base.
275    pub fn code_image(&self) -> &[u8] {
276        self.program_image.code_image()
277    }
278
279    /// Named function entries inside [`Self::code_image`].
280    pub fn code_entries(&self) -> &[NativeCodeEntry] {
281        self.program_image.code_entries()
282    }
283
284    /// Pointer-free compiler artifact from which this runtime image was loaded.
285    pub fn program_image(&self) -> &NativeProgramImage {
286        &self.program_image
287    }
288
289    pub(crate) fn supports_forces(&self) -> bool {
290        self.options.native_force_support
291    }
292}
293
294/// One callable function in a packed native code image.
295#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
296pub struct NativeCodeEntry {
297    /// Stable diagnostic name for the emitted function.
298    pub name: String,
299    /// Byte offset from the start of the native image.
300    pub offset: usize,
301    /// Size of this function blob, including its private literal data.
302    pub size: usize,
303}
304
305#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
306struct NativeEventImageRef {
307    func_offset: usize,
308    comb_apply_offset: usize,
309    addr: AbsoluteAddr,
310    id: usize,
311}
312
313#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
314struct NativeCodeSymbol {
315    offset: usize,
316    size: usize,
317    name: String,
318}
319
320#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
321struct NativeRuntimeOptions {
322    four_state: bool,
323    native_tick_loop: bool,
324    native_force_support: bool,
325    perf_map: bool,
326}
327
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub(crate) struct NativeRuntimeSchema {
330    pub(crate) runtime_errors: HashMap<i64, RuntimeErrorInfo<AbsoluteAddr>>,
331    pub(crate) runtime_event_sites: Vec<RuntimeEventSite>,
332    pub(crate) comb_observers: Vec<RuntimeCombObserver<AbsoluteAddr>>,
333    pub(crate) testbench_read_roots: HashSet<AbsoluteAddr>,
334    pub(crate) rtl_writes: HashSet<celox_design::VarAtomBase<AbsoluteAddr>>,
335}
336
337/// Pointer-free native compiler artifact which can be attached to the
338/// precompiled Celox runtime.
339#[derive(Clone, Serialize, Deserialize)]
340pub struct NativeProgramImage {
341    code: Vec<u8>,
342    code_entries: Vec<NativeCodeEntry>,
343    symbols: Vec<NativeCodeSymbol>,
344    comb_offset: usize,
345    comb_unit_offsets: Vec<usize>,
346    required_native_features: u8,
347    event_map: HashMap<AbsoluteAddr, NativeEventImageRef>,
348    eval_only_event_map: HashMap<AbsoluteAddr, NativeEventImageRef>,
349    apply_event_map: HashMap<AbsoluteAddr, NativeEventImageRef>,
350    id_to_addr: Vec<AbsoluteAddr>,
351    id_to_event: Vec<NativeEventImageRef>,
352    reflection: DesignReflection,
353    design: crate::ir::RuntimeDesign,
354    testbench: Option<TestbenchProgram<AbsoluteAddr>>,
355    runtime_schema: NativeRuntimeSchema,
356    layout: MemoryLayout,
357    native_memory_size: usize,
358    options: NativeRuntimeOptions,
359    four_state_inits: Vec<(usize, usize)>,
360}
361
362impl NativeProgramImage {
363    /// Complete relocatable machine-code image.
364    pub fn code_image(&self) -> &[u8] {
365        &self.code
366    }
367
368    /// Named entry offsets in [`Self::code_image`].
369    pub fn code_entries(&self) -> &[NativeCodeEntry] {
370        &self.code_entries
371    }
372
373    /// Final state layout consumed by every generated entry.
374    pub fn layout(&self) -> &MemoryLayout {
375        &self.layout
376    }
377
378    /// Source-independent instance hierarchy and signal metadata.
379    pub fn reflection(&self) -> &DesignReflection {
380        &self.reflection
381    }
382
383    /// Source-independent runtime diagnostics and combinational observers.
384    pub(crate) fn runtime_schema(&self) -> &NativeRuntimeSchema {
385        &self.runtime_schema
386    }
387
388    /// Whether the image's generated code and state layout use four-state data.
389    pub(crate) fn four_state(&self) -> bool {
390        self.options.four_state
391    }
392
393    /// Canonical event-domain topology used by the runtime scheduler.
394    pub(crate) fn event_topology(&self) -> &celox_design::EventTopology<AbsoluteAddr> {
395        &self.design.events
396    }
397
398    /// Reconstruct the source-independent runtime metadata retained by this
399    /// image. No frontend parsing or SIR/layout work is needed on the
400    /// execution side of a host-codegen workflow.
401    pub(crate) fn runtime_program(&self) -> crate::ir::RuntimeProgram {
402        crate::ir::RuntimeProgram {
403            design: self.design.clone(),
404            runtime_schema: RuntimeSchema {
405                runtime_errors: self.runtime_schema.runtime_errors.clone(),
406                runtime_event_sites: self.runtime_schema.runtime_event_sites.clone(),
407                comb_observers: self.runtime_schema.comb_observers.clone(),
408                testbench_read_roots: self.runtime_schema.testbench_read_roots.clone(),
409                rtl_writes: self.runtime_schema.rtl_writes.clone(),
410            },
411            testbench: self.testbench.clone(),
412        }
413    }
414
415    pub(super) fn validate(&self) -> Result<(), String> {
416        self.design
417            .validate()
418            .map_err(|error| format!("invalid runtime design: {error}"))?;
419        self.reflection
420            .validate()
421            .map_err(|error| format!("invalid design reflection: {error}"))?;
422        if self.code.is_empty() {
423            return Err("code image is empty".into());
424        }
425        let mut entry_offsets = HashSet::default();
426        let mut previous_end = 0usize;
427        for entry in &self.code_entries {
428            if !entry.offset.is_multiple_of(NATIVE_CODE_ENTRY_ALIGNMENT) {
429                return Err(format!("entry `{}` is not aligned", entry.name));
430            }
431            if entry.size == 0 {
432                return Err(format!("entry `{}` is empty", entry.name));
433            }
434            let end = entry
435                .offset
436                .checked_add(entry.size)
437                .ok_or_else(|| format!("entry `{}` range overflows", entry.name))?;
438            if entry.offset < previous_end || end > self.code.len() {
439                return Err(format!("entry `{}` is outside the code image", entry.name));
440            }
441            if !entry_offsets.insert(entry.offset) {
442                return Err(format!("entry `{}` duplicates an offset", entry.name));
443            }
444            previous_end = end;
445        }
446        if !entry_offsets.contains(&self.comb_offset) {
447            return Err("eval_comb offset does not name an image entry".into());
448        }
449        if self
450            .comb_unit_offsets
451            .iter()
452            .any(|offset| !entry_offsets.contains(offset))
453        {
454            return Err("a combinational unit offset does not name an image entry".into());
455        }
456        if self.required_native_features & !KNOWN_NATIVE_FEATURES != 0 {
457            return Err("native image contains unknown feature requirements".into());
458        }
459        for symbol in &self.symbols {
460            let end = symbol
461                .offset
462                .checked_add(symbol.size)
463                .ok_or_else(|| format!("symbol `{}` range overflows", symbol.name))?;
464            if symbol.size == 0 || end > self.code.len() {
465                return Err(format!(
466                    "symbol `{}` is outside the code image",
467                    symbol.name
468                ));
469            }
470        }
471        for event in self
472            .event_map
473            .values()
474            .chain(self.eval_only_event_map.values())
475            .chain(self.apply_event_map.values())
476            .chain(self.id_to_event.iter())
477        {
478            if !entry_offsets.contains(&event.func_offset)
479                || !entry_offsets.contains(&event.comb_apply_offset)
480            {
481                return Err(format!(
482                    "event {} references a missing image entry",
483                    event.id
484                ));
485            }
486        }
487        let semantic_size = self
488            .layout
489            .merged_total_size
490            .checked_add(self.layout.triggered_bits_total_size)
491            .ok_or_else(|| "semantic memory size overflows".to_string())?;
492        if self.native_memory_size < semantic_size {
493            return Err("native memory is smaller than the semantic state".into());
494        }
495        for &(offset, size) in &self.four_state_inits {
496            let end = size
497                .checked_mul(2)
498                .and_then(|size| offset.checked_add(size))
499                .ok_or_else(|| "four-state initialization range overflows".to_string())?;
500            if end > self.native_memory_size {
501                return Err("four-state initialization exceeds native memory".into());
502            }
503        }
504        Ok(())
505    }
506}
507
508// ────────────────────────────────────────────────────────────────
509// Compilation
510// ────────────────────────────────────────────────────────────────
511
512fn codegen_err(error: CodegenError) -> SimulatorError {
513    error.into()
514}
515
516fn codegen_message(message: impl Into<String>) -> SimulatorError {
517    codegen_err(CodegenError::message(message))
518}
519
520struct CompiledNativeFunction {
521    code: Vec<u8>,
522    symbols: Vec<jit_mem::JitSymbol>,
523    trace: Option<emit::NativeFunctionTrace>,
524    required_state_size: usize,
525    required_native_features: u8,
526}
527
528#[cfg_attr(
529    any(
530        all(feature = "arm64-codegen", not(target_arch = "aarch64")),
531        all(feature = "x86_64-codegen", not(target_arch = "x86_64"))
532    ),
533    allow(dead_code)
534)]
535pub(crate) struct NativeCodegenTrace {
536    pub optimized_sir: String,
537    pub mir: String,
538    pub reactive_graph: String,
539    pub state_layout: String,
540}
541
542fn prepare_merged_sir(
543    units: &[&crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>],
544    layout: &MemoryLayout,
545    four_state: bool,
546    label: &str,
547    first_ff_unit: Option<usize>,
548    diagnostics: &crate::optimizer::SirDiagnostics,
549) -> Result<crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>, SimulatorError> {
550    let verify_enabled = cfg!(debug_assertions) || diagnostics.verify_boundaries;
551    if verify_enabled {
552        for (unit_index, unit) in units.iter().enumerate() {
553            if let Err(error) = unit.verify_result() {
554                return Err(codegen_err(CodegenError::SirVerification {
555                    phase: format!(
556                        "invalid SIR before x86 source-unit merge: {label} source unit {unit_index}"
557                    ),
558                    source: error,
559                }));
560            }
561        }
562    }
563
564    let (mut sir_eu, merge_provenance) = celox_sir::merge_sir_eu_refs_with_provenance(units);
565    let boundaries = merge_provenance.unit_entries[1..].to_vec();
566    let verify = |eu: &crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>,
567                  phase: &'static str| {
568        if verify_enabled {
569            eu.verify_result().map_err(|source| {
570                codegen_err(CodegenError::SirVerification {
571                    phase: phase.to_string(),
572                    source,
573                })
574            })
575        } else {
576            Ok(())
577        }
578    };
579
580    verify(&sir_eu, "before x86 merged-SIR optimization")?;
581    if let Some(first_ff_unit) = first_ff_unit {
582        let removed = crate::optimizer::sir::eliminate_unobserved_comb_state_stores(
583            &mut sir_eu,
584            &merge_provenance,
585            first_ff_unit,
586        )
587        .map_err(|source| {
588            codegen_err(CodegenError::Optimization {
589                context: "comb/FF state-publication DSE",
590                source,
591            })
592        })?;
593        if removed != 0 {
594            crate::optimizer::sir::remove_dead_sir_definitions(&mut sir_eu);
595            verify(&sir_eu, "after comb/FF state-publication DSE")?;
596        }
597    }
598    if label == "eval_comb_apply_ff"
599        && crate::optimizer::sir::promote_fused_comb_static_slots(&mut sir_eu).map_err(
600            |source| {
601                codegen_err(CodegenError::Optimization {
602                    context: "final fused comb StateSSA promotion",
603                    source,
604                })
605            },
606        )?
607    {
608        crate::optimizer::sir::remove_dead_sir_definitions(&mut sir_eu);
609        verify(&sir_eu, "after final fused comb StateSSA promotion")?;
610    }
611    crate::optimizer::sir::pass_eliminate_working_round_trip::eliminate_working_round_trip(
612        &mut sir_eu,
613        &boundaries,
614    );
615    verify(&sir_eu, "after x86 direct working rewrite")?;
616    let promoted_working =
617        crate::optimizer::sir::promote_eval_apply_working_round_trips(&mut sir_eu);
618    if promoted_working {
619        verify(&sir_eu, "after x86 working StateSSA")?;
620        crate::optimizer::sir::remove_dead_sir_definitions(&mut sir_eu);
621        verify(&sir_eu, "after x86 working StateSSA DCE")?;
622    }
623    crate::optimizer::sir::optimize_native_merged_chain(
624        &mut sir_eu,
625        layout,
626        four_state,
627        label == "eval_comb_apply_ff",
628        diagnostics,
629    )
630    .map_err(|source| {
631        codegen_err(CodegenError::Optimization {
632            context: "native merged-chain optimization",
633            source,
634        })
635    })?;
636    verify(&sir_eu, "after x86 merged-chain cleanup")?;
637    Ok(sir_eu)
638}
639
640fn compile_units(
641    units: &[crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>],
642    layout: &MemoryLayout,
643    four_state: bool,
644    label: &str,
645    x86_options: &crate::backend::X86BackendOptions,
646    capture_trace: bool,
647    diagnostics: &crate::optimizer::SirDiagnostics,
648) -> Result<CompiledNativeFunction, SimulatorError> {
649    let units = units.iter().collect::<Vec<_>>();
650    compile_unit_refs(
651        &units,
652        layout,
653        four_state,
654        label,
655        None,
656        x86_options,
657        capture_trace,
658        diagnostics,
659    )
660}
661
662fn compile_unit_refs(
663    units: &[&crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>],
664    layout: &MemoryLayout,
665    four_state: bool,
666    label: &str,
667    first_ff_unit: Option<usize>,
668    x86_options: &crate::backend::X86BackendOptions,
669    capture_trace: bool,
670    diagnostics: &crate::optimizer::SirDiagnostics,
671) -> Result<CompiledNativeFunction, SimulatorError> {
672    let timing = x86_options.diagnostics.phase_timing;
673    if units.is_empty() {
674        // Empty function: just return 0
675        #[cfg(any(
676            feature = "x86_64-codegen",
677            all(target_arch = "x86_64", not(feature = "arm64-codegen"))
678        ))]
679        let (empty_result, empty_mir) = {
680            let mut empty_func =
681                super::mir::MFunction::new(super::mir::VRegAllocator::new(), vec![]);
682            let mut block = super::mir::MBlock::new(super::mir::BlockId(0));
683            block.push(super::mir::MInst::Return);
684            empty_func.push_block(block);
685            let empty_result = emit::emit(&empty_func, &regalloc::AssignmentMap::default(), 0)
686                .map_err(|source| codegen_err(CodegenError::NativeEmission { source }))?;
687            let empty_mir = empty_func.to_string();
688            (empty_result, empty_mir)
689        };
690        #[cfg(any(
691            feature = "arm64-codegen",
692            all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
693        ))]
694        let (empty_result, empty_mir) = {
695            let state_size = layout
696                .merged_total_size
697                .checked_add(layout.triggered_bits_total_size)
698                .ok_or_else(|| {
699                    codegen_err(CodegenError::NativeEmission {
700                        source: emit::EmitError::Range("AArch64 simulation-state size overflow"),
701                    })
702                })?;
703            let result = emit::emit_empty(state_size)
704                .map_err(|source| codegen_err(CodegenError::NativeEmission { source }))?;
705            (result, "bb0:\n  Return\n".to_string())
706        };
707        let trace = capture_trace.then(|| emit::NativeFunctionTrace {
708            optimized_sir: "<empty native function>\n".into(),
709            reactive_graph: String::new(),
710            state_layout: String::new(),
711            mir_before_regalloc: empty_mir.clone(),
712            mir_after_late_memory_folds: empty_mir.clone(),
713            mir_after_scheduling: empty_mir.clone(),
714            mir_after_regalloc: empty_mir,
715            register_assignment: String::new(),
716            spill_frame_size: 0,
717            disassembly: emit::disassemble(&empty_result.code[..empty_result.text_size], 0),
718        });
719        let symbols = perf_symbols_for_emit_result(label, &empty_result);
720        return Ok(CompiledNativeFunction {
721            code: empty_result.code,
722            symbols,
723            trace,
724            required_state_size: empty_result.required_state_size as usize,
725            #[cfg(any(
726                feature = "x86_64-codegen",
727                all(target_arch = "x86_64", not(feature = "arm64-codegen"))
728            ))]
729            required_native_features: empty_result.required_image_features,
730            #[cfg(any(
731                feature = "arm64-codegen",
732                all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
733            ))]
734            required_native_features: 0,
735        });
736    }
737
738    // Merge all EUs and compile the exact SIR/MIR function used at runtime.
739    if timing {
740        tracing::debug!(
741            "[native-timing] compile_units start label={label} eus={}",
742            units.len()
743        );
744    }
745    let start = timing.then(crate::timing::now);
746    let sir_eu = prepare_merged_sir(units, layout, four_state, label, first_ff_unit, diagnostics)?;
747    let mut trace = capture_trace.then(emit::NativeFunctionTrace::default);
748    #[cfg(any(
749        feature = "x86_64-codegen",
750        all(target_arch = "x86_64", not(feature = "arm64-codegen"))
751    ))]
752    let emit_result = emit::emit_prepared_eu(
753        &sir_eu,
754        layout,
755        four_state,
756        label,
757        x86_options,
758        trace.as_mut(),
759    )
760    .map_err(|source| codegen_err(CodegenError::NativePipeline { source }))?;
761    #[cfg(any(
762        feature = "arm64-codegen",
763        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
764    ))]
765    let emit_result = emit::emit_prepared_eu(
766        &sir_eu,
767        layout,
768        four_state,
769        label,
770        x86_options.native_tick_loop,
771        trace.as_mut(),
772    )
773    .map_err(|source| codegen_err(CodegenError::NativePipeline { source }))?;
774    if let Some(start) = start {
775        tracing::debug!(
776            "[native-timing] compile_units done label={label} bytes={} elapsed={:?}",
777            emit_result.code.len(),
778            start.elapsed()
779        );
780    }
781    let symbols = perf_symbols_for_emit_result(label, &emit_result);
782    let required_state_size = emit_result.required_state_size as usize;
783    Ok(CompiledNativeFunction {
784        code: emit_result.code,
785        symbols,
786        trace,
787        required_state_size,
788        #[cfg(any(
789            feature = "x86_64-codegen",
790            all(target_arch = "x86_64", not(feature = "arm64-codegen"))
791        ))]
792        required_native_features: emit_result.required_image_features,
793        #[cfg(any(
794            feature = "arm64-codegen",
795            all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
796        ))]
797        required_native_features: 0,
798    })
799}
800
801fn perf_symbols_for_emit_result(label: &str, result: &emit::EmitResult) -> Vec<jit_mem::JitSymbol> {
802    let code_len = result.text_size;
803    if result.block_offsets.is_empty() {
804        return Vec::new();
805    }
806
807    let mut blocks = result.block_offsets.clone();
808    blocks.sort_by_key(|(_, offset)| *offset);
809
810    let mut symbols = Vec::with_capacity(blocks.len() + 2);
811    let first_offset = blocks[0].1 as usize;
812    if first_offset > 0 {
813        symbols.push(jit_mem::JitSymbol {
814            offset: 0,
815            size: first_offset,
816            name: format!("{label}.prologue"),
817        });
818    }
819
820    for (idx, (block_id, offset)) in blocks.iter().enumerate() {
821        let start = *offset as usize;
822        let end = blocks
823            .get(idx + 1)
824            .map(|(_, next)| *next as usize)
825            .unwrap_or(code_len);
826        if end > start {
827            symbols.push(jit_mem::JitSymbol {
828                offset: start,
829                size: end - start,
830                name: format!("{label}.bb{}", block_id.0),
831            });
832        }
833    }
834
835    symbols
836}
837
838const NATIVE_CODE_ENTRY_ALIGNMENT: usize = 16;
839
840fn append_native_code(
841    image: &mut Vec<u8>,
842    entries: &mut Vec<NativeCodeEntry>,
843    image_symbols: &mut Vec<NativeCodeSymbol>,
844    name: String,
845    compiled: &CompiledNativeFunction,
846) -> Result<usize, SimulatorError> {
847    let offset = image
848        .len()
849        .checked_add(NATIVE_CODE_ENTRY_ALIGNMENT - 1)
850        .map(|value| value & !(NATIVE_CODE_ENTRY_ALIGNMENT - 1))
851        .ok_or_else(|| codegen_message("packed native code image alignment overflow"))?;
852    image.resize(offset, 0);
853    let end = offset
854        .checked_add(compiled.code.len())
855        .ok_or_else(|| codegen_message("packed native code image size overflow"))?;
856    image.extend_from_slice(&compiled.code);
857
858    if compiled.symbols.is_empty() {
859        image_symbols.push(NativeCodeSymbol {
860            offset,
861            size: compiled.code.len(),
862            name: name.clone(),
863        });
864    } else {
865        for symbol in &compiled.symbols {
866            let symbol_end = symbol
867                .offset
868                .checked_add(symbol.size)
869                .ok_or_else(|| codegen_message("native function symbol range overflow"))?;
870            if symbol_end > compiled.code.len() {
871                return Err(codegen_message(format!(
872                    "native function symbol `{}` exceeds its emitted code",
873                    symbol.name
874                )));
875            }
876            image_symbols.push(NativeCodeSymbol {
877                offset: offset + symbol.offset,
878                size: symbol.size,
879                name: format!("{name}.{}", symbol.name),
880            });
881        }
882    }
883
884    entries.push(NativeCodeEntry {
885        name,
886        offset,
887        size: end - offset,
888    });
889    Ok(offset)
890}
891
892fn native_function_at(
893    image: &jit_mem::JitCode,
894    offset: usize,
895) -> Result<NativeSimFunc, SimulatorError> {
896    let ptr = image
897        .entry_ptr(offset)
898        .ok_or_else(|| codegen_message("native function entry exceeds packed code image"))?;
899    // Safety: `offset` was returned by `append_native_code` for a complete
900    // function emitted with `NativeSimFunc`'s target ABI. `image` owns the
901    // executable allocation for at least as long as the returned pointer.
902    Ok(unsafe { std::mem::transmute::<*const u8, NativeSimFunc>(ptr) })
903}
904
905struct NativeCompileTask<'a> {
906    units: Vec<&'a crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>>,
907    label: &'static str,
908    first_ff_unit: Option<usize>,
909    bindings: Vec<String>,
910}
911
912type NativeTaskBindings = HashMap<(&'static str, AbsoluteAddr), usize>;
913
914fn collect_ff_compile_tasks(
915    sir: &LaidOutProgram,
916) -> (Vec<NativeCompileTask<'_>>, NativeTaskBindings) {
917    let mut tasks = Vec::new();
918    let mut task_bindings = HashMap::default();
919    collect_ff_compile_tasks_from(
920        sir,
921        &sir.sir.eval_apply_ffs,
922        "eval_apply_ff",
923        &mut tasks,
924        &mut task_bindings,
925    );
926    collect_ff_compile_tasks_from(
927        sir,
928        &sir.sir.eval_only_ffs,
929        "eval_only_ff",
930        &mut tasks,
931        &mut task_bindings,
932    );
933    collect_ff_compile_tasks_from(
934        sir,
935        &sir.sir.apply_ffs,
936        "apply_ff",
937        &mut tasks,
938        &mut task_bindings,
939    );
940    collect_comb_apply_compile_tasks(sir, &mut tasks, &mut task_bindings);
941    (tasks, task_bindings)
942}
943
944fn collect_comb_apply_compile_tasks<'a>(
945    sir: &'a LaidOutProgram,
946    tasks: &mut Vec<NativeCompileTask<'a>>,
947    task_bindings: &mut NativeTaskBindings,
948) {
949    const LABEL: &str = "eval_comb_apply_ff";
950    for (addr, ff_units) in &sir.sir.eval_apply_ffs {
951        let fused_units = sir.sir.eval_comb_apply_ffs.get(addr);
952        let (unit_refs, first_ff_unit) = if let Some(fused_units) = fused_units {
953            (fused_units.iter().collect::<Vec<_>>(), None)
954        } else {
955            let mut unit_refs = sir.sir.eval_comb.iter().collect::<Vec<_>>();
956            let first_ff_unit =
957                (!unit_refs.is_empty() && !ff_units.is_empty()).then_some(unit_refs.len());
958            unit_refs.extend(ff_units);
959            (unit_refs, first_ff_unit)
960        };
961        let binding = format!("{LABEL} trigger={}", sir.get_path(addr));
962        let index = if let Some(index) = tasks.iter().position(|task| {
963            task.label == LABEL && task.first_ff_unit == first_ff_unit && task.units == unit_refs
964        }) {
965            tasks[index].bindings.push(binding);
966            index
967        } else {
968            let index = tasks.len();
969            tasks.push(NativeCompileTask {
970                units: unit_refs,
971                label: LABEL,
972                first_ff_unit,
973                bindings: vec![binding],
974            });
975            index
976        };
977        task_bindings.insert((LABEL, *addr), index);
978    }
979}
980
981fn collect_ff_compile_tasks_from<'a>(
982    sir: &LaidOutProgram,
983    ff_map: &'a HashMap<
984        AbsoluteAddr,
985        Vec<crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>>,
986    >,
987    label: &'static str,
988    tasks: &mut Vec<NativeCompileTask<'a>>,
989    task_bindings: &mut NativeTaskBindings,
990) {
991    for (addr, units) in ff_map {
992        let unit_refs = units.iter().collect::<Vec<_>>();
993        let binding = format!("{label} trigger={}", sir.get_path(addr));
994        let index = if let Some(index) = tasks.iter().position(|task| task.units == unit_refs) {
995            tasks[index].bindings.push(binding);
996            index
997        } else {
998            let index = tasks.len();
999            tasks.push(NativeCompileTask {
1000                units: unit_refs,
1001                label,
1002                first_ff_unit: None,
1003                bindings: vec![binding],
1004            });
1005            index
1006        };
1007        task_bindings.insert((label, *addr), index);
1008    }
1009}
1010
1011fn append_native_function_trace(
1012    optimized_sir: &mut String,
1013    mir: &mut String,
1014    reactive_graph: &mut String,
1015    state_layout: &mut String,
1016    name: &str,
1017    bindings: &[String],
1018    trace: &emit::NativeFunctionTrace,
1019) {
1020    let mut bindings = bindings.to_vec();
1021    bindings.sort();
1022    bindings.dedup();
1023
1024    optimized_sir.push_str(&format!("=== Native function {name} ===\n"));
1025    if !bindings.is_empty() {
1026        optimized_sir.push_str("Bindings:\n");
1027        for binding in &bindings {
1028            optimized_sir.push_str(&format!("  {binding}\n"));
1029        }
1030    }
1031    optimized_sir.push_str(&trace.optimized_sir);
1032    if !trace.optimized_sir.ends_with('\n') {
1033        optimized_sir.push('\n');
1034    }
1035    optimized_sir.push('\n');
1036
1037    mir.push_str(&format!("=== Native function {name} ===\n"));
1038    if !bindings.is_empty() {
1039        mir.push_str("Bindings:\n");
1040        for binding in &bindings {
1041            mir.push_str(&format!("  {binding}\n"));
1042        }
1043    }
1044    mir.push_str("--- MIR after main optimization, before regalloc-owned late folds ---\n");
1045    mir.push_str(&trace.mir_before_regalloc);
1046    if !trace.mir_before_regalloc.ends_with('\n') {
1047        mir.push('\n');
1048    }
1049    mir.push_str("--- MIR after late memory folds, before allocation-owned scheduling ---\n");
1050    mir.push_str(&trace.mir_after_late_memory_folds);
1051    if !trace.mir_after_late_memory_folds.ends_with('\n') {
1052        mir.push('\n');
1053    }
1054    mir.push_str("--- MIR after allocation-owned scheduling, before spill reconstruction ---\n");
1055    mir.push_str(&trace.mir_after_scheduling);
1056    if !trace.mir_after_scheduling.ends_with('\n') {
1057        mir.push('\n');
1058    }
1059    mir.push_str("--- MIR after register allocation and post-RA peepholes ---\n");
1060    mir.push_str(&trace.mir_after_regalloc);
1061    if !trace.mir_after_regalloc.ends_with('\n') {
1062        mir.push('\n');
1063    }
1064    mir.push_str(&format!("Spill frame: {} bytes\n", trace.spill_frame_size));
1065    mir.push_str("Register assignment:\n");
1066    mir.push_str(&trace.register_assignment);
1067    #[cfg(any(
1068        feature = "x86_64-codegen",
1069        all(target_arch = "x86_64", not(feature = "arm64-codegen"))
1070    ))]
1071    mir.push_str("x86-64 disassembly of emitted function:\n");
1072    #[cfg(any(
1073        feature = "arm64-codegen",
1074        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
1075    ))]
1076    mir.push_str("AArch64 disassembly of emitted function:\n");
1077    mir.push_str(&trace.disassembly);
1078    if !trace.disassembly.ends_with('\n') {
1079        mir.push('\n');
1080    }
1081    mir.push('\n');
1082
1083    if !trace.reactive_graph.is_empty() {
1084        reactive_graph.push_str(&format!("=== Native function {name} ===\n"));
1085        if !bindings.is_empty() {
1086            reactive_graph.push_str("Bindings:\n");
1087            for binding in &bindings {
1088                reactive_graph.push_str(&format!("  {binding}\n"));
1089            }
1090        }
1091        reactive_graph.push_str(&trace.reactive_graph);
1092        if !trace.reactive_graph.ends_with('\n') {
1093            reactive_graph.push('\n');
1094        }
1095        reactive_graph.push('\n');
1096    }
1097
1098    if !trace.state_layout.is_empty() {
1099        state_layout.push_str(&format!("=== Native function {name} ===\n"));
1100        if !bindings.is_empty() {
1101            state_layout.push_str("Bindings:\n");
1102            for binding in &bindings {
1103                state_layout.push_str(&format!("  {binding}\n"));
1104            }
1105        }
1106        state_layout.push_str(&trace.state_layout);
1107        if !trace.state_layout.ends_with('\n') {
1108            state_layout.push('\n');
1109        }
1110        state_layout.push('\n');
1111    }
1112}
1113
1114fn format_native_codegen_trace(
1115    comb: &CompiledNativeFunction,
1116    ff_codes: &HashMap<usize, CompiledNativeFunction>,
1117    tasks: &[NativeCompileTask<'_>],
1118) -> NativeCodegenTrace {
1119    let mut optimized_sir = String::from("=== Optimized SIR used by native emission ===\n");
1120    let mut mir = String::from("=== MIR used by native emission ===\n");
1121    let mut reactive_graph = String::from("=== Reactive clock-event projection oracle ===\n");
1122    let mut state_layout =
1123        String::from("=== Profile-selected native state-layout feasibility ===\n");
1124    append_native_function_trace(
1125        &mut optimized_sir,
1126        &mut mir,
1127        &mut reactive_graph,
1128        &mut state_layout,
1129        "eval_comb",
1130        &[],
1131        comb.trace
1132            .as_ref()
1133            .expect("explicit native trace must capture eval_comb"),
1134    );
1135
1136    let mut ff_entries = ff_codes
1137        .keys()
1138        .map(|&task_id| {
1139            let task = &tasks[task_id];
1140            let mut sort_key = task.bindings.clone();
1141            sort_key.sort();
1142            (sort_key, task_id, task)
1143        })
1144        .collect::<Vec<_>>();
1145    ff_entries.sort_by(|left, right| left.0.cmp(&right.0));
1146    let mut label_indices = HashMap::<&str, usize>::default();
1147    for (_, task_id, task) in ff_entries {
1148        let index = label_indices.entry(task.label).or_default();
1149        let name = format!("{}[{index}]", task.label);
1150        *index += 1;
1151        append_native_function_trace(
1152            &mut optimized_sir,
1153            &mut mir,
1154            &mut reactive_graph,
1155            &mut state_layout,
1156            &name,
1157            &task.bindings,
1158            ff_codes[&task_id]
1159                .trace
1160                .as_ref()
1161                .expect("explicit native trace must capture every FF function"),
1162        );
1163    }
1164    NativeCodegenTrace {
1165        optimized_sir,
1166        mir,
1167        reactive_graph,
1168        state_layout,
1169    }
1170}
1171
1172fn offset_registers(offset: &SIROffset, registers: &mut Vec<RegisterId>) {
1173    match offset {
1174        SIROffset::Dynamic(register) => registers.push(*register),
1175        SIROffset::Element {
1176            index,
1177            dynamic_bit_offset,
1178            ..
1179        } => {
1180            registers.push(*index);
1181            registers.extend(dynamic_bit_offset);
1182        }
1183        SIROffset::Static(_) | SIROffset::PackedElements { .. } => {}
1184    }
1185}
1186
1187fn instruction_registers<A>(instruction: &SIRInstruction<A>) -> Vec<RegisterId> {
1188    let mut registers = Vec::new();
1189    match instruction {
1190        SIRInstruction::Imm(..) => {}
1191        SIRInstruction::Binary(_, lhs, _, rhs) => registers.extend([*lhs, *rhs]),
1192        SIRInstruction::Unary(_, _, source) | SIRInstruction::Slice(_, source, _, _) => {
1193            registers.push(*source);
1194        }
1195        SIRInstruction::Load(_, _, offset, _) => offset_registers(offset, &mut registers),
1196        SIRInstruction::Store(_, offset, _, source, _, _) => {
1197            registers.push(*source);
1198            offset_registers(offset, &mut registers);
1199        }
1200        SIRInstruction::Commit(..) => {}
1201        SIRInstruction::Concat(_, sources) => registers.extend(sources),
1202        SIRInstruction::Mux(_, condition, then_value, else_value) => {
1203            registers.extend([*condition, *then_value, *else_value]);
1204        }
1205        SIRInstruction::RuntimeEvent { args, .. }
1206        | SIRInstruction::CombCaptureEvent { args, .. } => registers.extend(args),
1207        SIRInstruction::CombCaptureEnableIfChanged { old, new, .. } => {
1208            registers.extend([*old, *new]);
1209        }
1210    }
1211    registers
1212}
1213
1214fn comb_block_execution_order<A>(unit: &ExecutionUnit<A>) -> Vec<BlockId> {
1215    fn visit<A>(
1216        unit: &ExecutionUnit<A>,
1217        block_id: BlockId,
1218        visited: &mut HashSet<BlockId>,
1219        postorder: &mut Vec<BlockId>,
1220    ) {
1221        if !visited.insert(block_id) {
1222            return;
1223        }
1224        for successor in celox_sir::cfg::terminator_successors(&unit.blocks[&block_id].terminator) {
1225            visit(unit, successor, visited, postorder);
1226        }
1227        postorder.push(block_id);
1228    }
1229
1230    let mut visited = HashSet::default();
1231    let mut postorder = Vec::with_capacity(unit.blocks.len());
1232    visit(unit, unit.entry_block_id, &mut visited, &mut postorder);
1233    postorder.reverse();
1234    postorder
1235}
1236
1237fn is_comb_runtime_effect(instruction: &SIRInstruction<RegionedAbsoluteAddr>) -> bool {
1238    matches!(
1239        instruction,
1240        SIRInstruction::RuntimeEvent { .. }
1241            | SIRInstruction::CombCaptureEvent { .. }
1242            | SIRInstruction::CombCaptureEnableIfChanged { .. }
1243    )
1244}
1245
1246fn interleave_comb_runtime_effects(
1247    unit: &ExecutionUnit<RegionedAbsoluteAddr>,
1248    ordered_stores: &[(BlockId, usize)],
1249    store_units: Vec<ExecutionUnit<RegionedAbsoluteAddr>>,
1250) -> Vec<ExecutionUnit<RegionedAbsoluteAddr>> {
1251    let ordered_sites = comb_block_execution_order(unit)
1252        .into_iter()
1253        .flat_map(|block_id| {
1254            (0..unit.blocks[&block_id].instructions.len()).map(move |index| (block_id, index))
1255        })
1256        .collect::<Vec<_>>();
1257    let positions = ordered_sites
1258        .iter()
1259        .enumerate()
1260        .map(|(position, &site)| (site, position))
1261        .collect::<HashMap<_, _>>();
1262    let mut effect_groups = vec![Vec::new(); ordered_stores.len() + 1];
1263    for site in ordered_sites {
1264        if !is_comb_runtime_effect(&unit.blocks[&site.0].instructions[site.1]) {
1265            continue;
1266        }
1267        let boundary = ordered_stores
1268            .iter()
1269            .filter(|store| positions[store] < positions[&site])
1270            .count();
1271        effect_groups[boundary].push(site);
1272    }
1273    if effect_groups.iter().all(Vec::is_empty) {
1274        return store_units;
1275    }
1276
1277    let mut result = Vec::with_capacity(store_units.len() + effect_groups.len());
1278    let mut stores = store_units.into_iter();
1279    for (boundary, group) in effect_groups.into_iter().enumerate() {
1280        if !group.is_empty() {
1281            let group = group.into_iter().collect::<HashSet<_>>();
1282            let mut events = unit.clone();
1283            for (block_id, block) in &mut events.blocks {
1284                block.instructions = std::mem::take(&mut block.instructions)
1285                    .into_iter()
1286                    .enumerate()
1287                    .filter_map(|(index, instruction)| {
1288                        let site = (*block_id, index);
1289                        if matches!(
1290                            instruction,
1291                            SIRInstruction::Store(..) | SIRInstruction::Commit(..)
1292                        ) {
1293                            None
1294                        } else if is_comb_runtime_effect(&instruction) {
1295                            group.contains(&site).then_some(instruction)
1296                        } else {
1297                            Some(instruction)
1298                        }
1299                    })
1300                    .collect();
1301            }
1302            result.push(events);
1303        }
1304        if boundary < ordered_stores.len() {
1305            result.push(stores.next().unwrap());
1306        }
1307    }
1308    result
1309}
1310
1311fn split_comb_execution_unit(
1312    unit: &ExecutionUnit<RegionedAbsoluteAddr>,
1313) -> Vec<ExecutionUnit<RegionedAbsoluteAddr>> {
1314    if unit.blocks.len() != 1 {
1315        let definitions = unit
1316            .blocks
1317            .iter()
1318            .flat_map(|(block_id, block)| {
1319                block
1320                    .instructions
1321                    .iter()
1322                    .enumerate()
1323                    .filter_map(|(index, instruction)| {
1324                        instruction
1325                            .defined_register()
1326                            .map(|register| (register, (*block_id, index)))
1327                    })
1328            })
1329            .collect::<HashMap<_, _>>();
1330        let store_sites = comb_block_execution_order(unit)
1331            .into_iter()
1332            .flat_map(|block_id| {
1333                let block = &unit.blocks[&block_id];
1334                block
1335                    .instructions
1336                    .iter()
1337                    .enumerate()
1338                    .filter(|(_, instruction)| {
1339                        matches!(
1340                            instruction,
1341                            SIRInstruction::Store(..) | SIRInstruction::Commit(..)
1342                        )
1343                    })
1344                    .map(move |(index, _)| (block_id, index))
1345            })
1346            .collect::<Vec<_>>();
1347        if store_sites.is_empty() {
1348            return vec![unit.clone()];
1349        }
1350
1351        let instruction_at = |site: (BlockId, usize)| &unit.blocks[&site.0].instructions[site.1];
1352        let register_dependencies = store_sites
1353            .iter()
1354            .copied()
1355            .map(|store_site| {
1356                let mut dependencies = HashSet::default();
1357                let mut pending = instruction_registers(instruction_at(store_site));
1358                while let Some(register) = pending.pop() {
1359                    if !dependencies.insert(register) {
1360                        continue;
1361                    }
1362                    if let Some(&definition) = definitions.get(&register) {
1363                        pending.extend(instruction_registers(instruction_at(definition)));
1364                    }
1365                }
1366                (store_site, dependencies)
1367            })
1368            .collect::<HashMap<_, _>>();
1369        let store_source = |site| match instruction_at(site) {
1370            SIRInstruction::Store(_, _, _, source, _, _) => Some(*source),
1371            _ => None,
1372        };
1373        let mut remaining = store_sites;
1374        let mut ordered_stores = Vec::with_capacity(remaining.len());
1375        while !remaining.is_empty() {
1376            let next = remaining
1377                .iter()
1378                .position(|candidate| {
1379                    let candidate_source = store_source(*candidate);
1380                    !remaining.iter().any(|predecessor| {
1381                        if predecessor == candidate {
1382                            return false;
1383                        }
1384                        store_source(*predecessor).is_some_and(|source| {
1385                            Some(source) != candidate_source
1386                                && register_dependencies[candidate].contains(&source)
1387                        })
1388                    })
1389                })
1390                .unwrap_or(0);
1391            ordered_stores.push(remaining.remove(next));
1392        }
1393
1394        let split = ordered_stores
1395            .iter()
1396            .enumerate()
1397            .map(|(order, &target)| {
1398                let reloads = ordered_stores[..order]
1399                    .iter()
1400                    .filter_map(|&prior_site| {
1401                        let SIRInstruction::Store(address, offset, bits, source, _, _) =
1402                            instruction_at(prior_site)
1403                        else {
1404                            return None;
1405                        };
1406                        (register_dependencies[&target].contains(source)
1407                            && store_source(target) != Some(*source)
1408                            && unit.register_map[source].width() == *bits)
1409                            .then(|| (*source, (*address, offset.clone(), *bits)))
1410                    })
1411                    .collect::<HashMap<_, _>>();
1412                let mut extracted = unit.clone();
1413                for (block_id, block) in &mut extracted.blocks {
1414                    block.instructions = std::mem::take(&mut block.instructions)
1415                        .into_iter()
1416                        .enumerate()
1417                        .filter_map(|(index, instruction)| {
1418                            let site = (*block_id, index);
1419                            match instruction {
1420                                SIRInstruction::Store(..) | SIRInstruction::Commit(..) => {
1421                                    (site == target).then_some(instruction)
1422                                }
1423                                SIRInstruction::RuntimeEvent { .. }
1424                                | SIRInstruction::CombCaptureEvent { .. }
1425                                | SIRInstruction::CombCaptureEnableIfChanged { .. } => None,
1426                                _ => {
1427                                    if let Some(register) = instruction.defined_register()
1428                                        && let Some((address, offset, bits)) =
1429                                            reloads.get(&register)
1430                                    {
1431                                        Some(SIRInstruction::Load(
1432                                            register,
1433                                            *address,
1434                                            offset.clone(),
1435                                            *bits,
1436                                        ))
1437                                    } else {
1438                                        Some(instruction)
1439                                    }
1440                                }
1441                            }
1442                        })
1443                        .collect();
1444                }
1445                extracted
1446            })
1447            .collect::<Vec<_>>();
1448
1449        return interleave_comb_runtime_effects(unit, &ordered_stores, split);
1450    }
1451    let block = &unit.blocks[&unit.entry_block_id];
1452    if !block.params.is_empty() || block.terminator != SIRTerminator::Return {
1453        return vec![unit.clone()];
1454    }
1455
1456    let definitions = block
1457        .instructions
1458        .iter()
1459        .enumerate()
1460        .filter_map(|(index, instruction)| {
1461            instruction
1462                .defined_register()
1463                .map(|register| (register, index))
1464        })
1465        .collect::<HashMap<_, _>>();
1466    let store_indices = block
1467        .instructions
1468        .iter()
1469        .enumerate()
1470        .filter_map(|(index, instruction)| {
1471            matches!(
1472                instruction,
1473                SIRInstruction::Store(..) | SIRInstruction::Commit(..)
1474            )
1475            .then_some(index)
1476        })
1477        .collect::<Vec<_>>();
1478    if store_indices.is_empty() {
1479        return vec![unit.clone()];
1480    }
1481
1482    let register_dependencies = store_indices
1483        .iter()
1484        .copied()
1485        .map(|store_index| {
1486            let mut dependencies = HashSet::default();
1487            let mut pending = instruction_registers(&block.instructions[store_index]);
1488            while let Some(register) = pending.pop() {
1489                if !dependencies.insert(register) {
1490                    continue;
1491                }
1492                if let Some(&definition) = definitions.get(&register) {
1493                    pending.extend(instruction_registers(&block.instructions[definition]));
1494                }
1495            }
1496            (store_index, dependencies)
1497        })
1498        .collect::<HashMap<_, _>>();
1499    let store_source = |index| match &block.instructions[index] {
1500        SIRInstruction::Store(_, _, _, source, _, _) => Some(*source),
1501        _ => None,
1502    };
1503    let mut remaining = store_indices.clone();
1504    let mut ordered_stores = Vec::with_capacity(remaining.len());
1505    while !remaining.is_empty() {
1506        let next = remaining
1507            .iter()
1508            .position(|candidate| {
1509                let candidate_source = store_source(*candidate);
1510                !remaining.iter().any(|predecessor| {
1511                    if predecessor == candidate {
1512                        return false;
1513                    }
1514                    store_source(*predecessor).is_some_and(|source| {
1515                        Some(source) != candidate_source
1516                            && register_dependencies[candidate].contains(&source)
1517                    })
1518                })
1519            })
1520            .unwrap_or(0);
1521        ordered_stores.push(remaining.remove(next));
1522    }
1523
1524    let split = ordered_stores
1525        .iter()
1526        .enumerate()
1527        .map(|(order, &store_index)| {
1528            let reloads = ordered_stores[..order]
1529                .iter()
1530                .filter_map(|&prior_index| {
1531                    let SIRInstruction::Store(address, offset, bits, source, _, _) =
1532                        &block.instructions[prior_index]
1533                    else {
1534                        return None;
1535                    };
1536                    (register_dependencies[&store_index].contains(source)
1537                        && store_source(store_index) != Some(*source)
1538                        && unit.register_map[source].width() == *bits)
1539                        .then(|| (*source, (*address, offset.clone(), *bits)))
1540                })
1541                .collect::<HashMap<_, _>>();
1542            let mut prefix = HashSet::<usize>::default();
1543            let mut pending = instruction_registers(&block.instructions[store_index]);
1544            while let Some(register) = pending.pop() {
1545                let Some(&definition) = definitions.get(&register) else {
1546                    continue;
1547                };
1548                if !prefix.insert(definition) {
1549                    continue;
1550                }
1551                if let Some((_, offset, _)) = reloads.get(&register) {
1552                    offset_registers(offset, &mut pending);
1553                    continue;
1554                }
1555                pending.extend(instruction_registers(&block.instructions[definition]));
1556            }
1557            let mut prefix = prefix.into_iter().collect::<Vec<_>>();
1558            prefix.sort_unstable();
1559            let mut instructions = prefix
1560                .into_iter()
1561                .map(|index| {
1562                    let instruction = &block.instructions[index];
1563                    if let Some(register) = instruction.defined_register()
1564                        && let Some((address, offset, bits)) = reloads.get(&register)
1565                    {
1566                        return SIRInstruction::Load(register, *address, offset.clone(), *bits);
1567                    }
1568                    instruction.clone()
1569                })
1570                .collect::<Vec<_>>();
1571            instructions.push(block.instructions[store_index].clone());
1572            let split_block = celox_sir::BasicBlock {
1573                id: BlockId(0),
1574                params: Vec::new(),
1575                instructions,
1576                terminator: SIRTerminator::Return,
1577            };
1578            ExecutionUnit {
1579                entry_block_id: BlockId(0),
1580                blocks: [(BlockId(0), split_block)].into_iter().collect(),
1581                register_map: unit.register_map.clone(),
1582            }
1583        })
1584        .collect::<Vec<_>>();
1585    let ordered_store_sites = ordered_stores
1586        .iter()
1587        .map(|&index| (unit.entry_block_id, index))
1588        .collect::<Vec<_>>();
1589    interleave_comb_runtime_effects(unit, &ordered_store_sites, split)
1590}
1591
1592fn compile_program(
1593    laid_out: &LaidOutProgram,
1594    options: &SimulatorOptions,
1595    capture_trace: bool,
1596) -> Result<(NativeProgramImage, Option<NativeCodegenTrace>), SimulatorError> {
1597    const MAX_PARALLEL_NATIVE_FUNCTIONS: usize = 4;
1598
1599    let sir = laid_out;
1600    let layout = laid_out.layout();
1601    let (compile_tasks, task_bindings) = collect_ff_compile_tasks(sir);
1602    let next_task = AtomicUsize::new(0);
1603    let (comb_jit, compiled_ff_codes) = std::thread::scope(|scope| {
1604        let four_state = options.four_state;
1605        let x86_options = &options.x86_options;
1606        let comb_handle = scope.spawn(move || {
1607            compile_units(
1608                &sir.sir.eval_comb,
1609                layout,
1610                four_state,
1611                "eval_comb",
1612                x86_options,
1613                capture_trace,
1614                &options.optimize_options.diagnostics,
1615            )
1616        });
1617        let task_worker_count = compile_tasks
1618            .len()
1619            .min(MAX_PARALLEL_NATIVE_FUNCTIONS.saturating_sub(1));
1620        let task_handles = (0..task_worker_count)
1621            .map(|_| {
1622                let next_task = &next_task;
1623                let compile_tasks = &compile_tasks;
1624                scope.spawn(move || {
1625                    let mut compiled = Vec::new();
1626                    loop {
1627                        let task_id = next_task.fetch_add(1, Ordering::Relaxed);
1628                        let Some(task) = compile_tasks.get(task_id) else {
1629                            break;
1630                        };
1631                        let code = compile_unit_refs(
1632                            &task.units,
1633                            layout,
1634                            four_state,
1635                            task.label,
1636                            task.first_ff_unit,
1637                            x86_options,
1638                            capture_trace,
1639                            &options.optimize_options.diagnostics,
1640                        )?;
1641                        compiled.push((task_id, code));
1642                    }
1643                    Ok::<_, SimulatorError>(compiled)
1644                })
1645            })
1646            .collect::<Vec<_>>();
1647
1648        let comb_jit = comb_handle
1649            .join()
1650            .map_err(|_| codegen_message("native eval_comb compile thread panicked"))??;
1651        let mut compiled_ff_codes = HashMap::default();
1652        for handle in task_handles {
1653            let compiled = handle
1654                .join()
1655                .map_err(|_| codegen_message("native FF compile thread panicked"))??;
1656            compiled_ff_codes.extend(compiled);
1657        }
1658        Ok::<_, SimulatorError>((comb_jit, compiled_ff_codes))
1659    })?;
1660    // A foreign-interface image can request per-unit entries so force/release
1661    // can reapply overrides between procedural store boundaries. Ordinary
1662    // images do not compile or retain this duplicate combinational code.
1663    let force_store_boundaries = options.optimize_options.opt_level() == crate::OptLevel::O0;
1664    let comb_runtime_units = if options.native_force_support {
1665        sir.sir
1666            .eval_comb
1667            .iter()
1668            .flat_map(|unit| {
1669                if force_store_boundaries {
1670                    split_comb_execution_unit(unit)
1671                } else {
1672                    vec![unit.clone()]
1673                }
1674            })
1675            .collect::<Vec<_>>()
1676    } else {
1677        Vec::new()
1678    };
1679    let comb_unit_jits = comb_runtime_units
1680        .iter()
1681        .enumerate()
1682        .map(|(index, unit)| {
1683            compile_unit_refs(
1684                &[unit],
1685                layout,
1686                options.four_state,
1687                &format!("eval_comb_unit[{index}]"),
1688                None,
1689                &options.x86_options,
1690                false,
1691                &options.optimize_options.diagnostics,
1692            )
1693        })
1694        .collect::<Result<Vec<_>, _>>()?;
1695    let codegen_trace = capture_trace
1696        .then(|| format_native_codegen_trace(&comb_jit, &compiled_ff_codes, &compile_tasks));
1697    let semantic_memory_size = layout
1698        .merged_total_size
1699        .checked_add(layout.triggered_bits_total_size)
1700        .expect("native semantic-memory size overflow");
1701    let native_memory_size = std::iter::once(comb_jit.required_state_size)
1702        .chain(
1703            compiled_ff_codes
1704                .values()
1705                .map(|compiled| compiled.required_state_size),
1706        )
1707        .chain(
1708            comb_unit_jits
1709                .iter()
1710                .map(|compiled| compiled.required_state_size),
1711        )
1712        .fold(semantic_memory_size, usize::max);
1713    let required_native_features = std::iter::once(comb_jit.required_native_features)
1714        .chain(
1715            compiled_ff_codes
1716                .values()
1717                .map(|compiled| compiled.required_native_features),
1718        )
1719        .chain(
1720            comb_unit_jits
1721                .iter()
1722                .map(|compiled| compiled.required_native_features),
1723        )
1724        .fold(0, |features, required| features | required);
1725    let mut packed_image = Vec::new();
1726    let mut code_entries = Vec::with_capacity(1 + compiled_ff_codes.len());
1727    let mut image_symbols = Vec::new();
1728    let comb_offset = append_native_code(
1729        &mut packed_image,
1730        &mut code_entries,
1731        &mut image_symbols,
1732        "eval_comb".into(),
1733        &comb_jit,
1734    )?;
1735    let mut comb_unit_offsets = Vec::with_capacity(comb_unit_jits.len());
1736    for (index, compiled) in comb_unit_jits.iter().enumerate() {
1737        comb_unit_offsets.push(append_native_code(
1738            &mut packed_image,
1739            &mut code_entries,
1740            &mut image_symbols,
1741            format!("eval_comb_unit[{index}]"),
1742            compiled,
1743        )?);
1744    }
1745    let mut compiled_ff_keys = compiled_ff_codes.keys().copied().collect::<Vec<_>>();
1746    compiled_ff_keys.sort_unstable();
1747    let mut task_offsets = HashMap::default();
1748    let mut label_indices = HashMap::<&str, usize>::default();
1749    for &task_id in &compiled_ff_keys {
1750        let task = &compile_tasks[task_id];
1751        let index = label_indices.entry(task.label).or_default();
1752        let name = format!("{}[{index}]", task.label);
1753        *index += 1;
1754        let offset = append_native_code(
1755            &mut packed_image,
1756            &mut code_entries,
1757            &mut image_symbols,
1758            name,
1759            &compiled_ff_codes[&task_id],
1760        )?;
1761        task_offsets.insert(task_id, offset);
1762    }
1763    // Bind semantic event identities to image-relative function offsets. The
1764    // precompiled runtime turns these into process-local pointers after it has
1765    // copied the image into executable memory.
1766    let mut next_id = 0usize;
1767    let mut id_to_addr = Vec::new();
1768    let mut id_to_event = Vec::new();
1769    let mut event_map = HashMap::default();
1770    let mut eval_only_event_map = HashMap::default();
1771    let mut apply_event_map = HashMap::default();
1772    let mut addr_to_id = HashMap::default();
1773    let compile_ff_group = |ff_map: &HashMap<
1774        AbsoluteAddr,
1775        Vec<crate::ir::ExecutionUnit<crate::ir::RegionedAbsoluteAddr>>,
1776    >,
1777                            label: &'static str,
1778                            event_map_out: &mut HashMap<AbsoluteAddr, NativeEventImageRef>,
1779                            addr_to_id: &mut HashMap<AbsoluteAddr, usize>,
1780                            compiled_ff_cache: &HashMap<usize, usize>,
1781                            comb_apply_label: Option<&'static str>,
1782                            next_id: &mut usize,
1783                            id_to_addr: &mut Vec<AbsoluteAddr>,
1784                            id_to_event: &mut Vec<NativeEventImageRef>|
1785     -> Result<(), SimulatorError> {
1786        for addr in ff_map.keys() {
1787            let canonical = sir.design.events.canonical(*addr);
1788            if let Some(&event) = event_map_out.get(&canonical) {
1789                event_map_out.insert(*addr, event);
1790                continue;
1791            }
1792
1793            let task_id = task_bindings[&(label, *addr)];
1794            let func_offset = compiled_ff_cache[&task_id];
1795            let comb_apply_offset = comb_apply_label
1796                .map(|label| {
1797                    let task_id = task_bindings[&(label, *addr)];
1798                    compiled_ff_cache[&task_id]
1799                })
1800                .unwrap_or(func_offset);
1801
1802            let (id, is_new_id) = if let Some(&id) = addr_to_id.get(&canonical) {
1803                (id, false)
1804            } else {
1805                let id = *next_id;
1806                *next_id += 1;
1807                addr_to_id.insert(canonical, id);
1808                id_to_addr.push(canonical);
1809                (id, true)
1810            };
1811
1812            let event = NativeEventImageRef {
1813                func_offset,
1814                comb_apply_offset,
1815                addr: canonical,
1816                id,
1817            };
1818            event_map_out.insert(canonical, event);
1819            if *addr != canonical {
1820                event_map_out.insert(*addr, event);
1821            }
1822            if is_new_id {
1823                id_to_event.push(event);
1824            }
1825        }
1826        Ok(())
1827    };
1828
1829    compile_ff_group(
1830        &sir.sir.eval_apply_ffs,
1831        "eval_apply_ff",
1832        &mut event_map,
1833        &mut addr_to_id,
1834        &task_offsets,
1835        Some("eval_comb_apply_ff"),
1836        &mut next_id,
1837        &mut id_to_addr,
1838        &mut id_to_event,
1839    )?;
1840    compile_ff_group(
1841        &sir.sir.eval_only_ffs,
1842        "eval_only_ff",
1843        &mut eval_only_event_map,
1844        &mut addr_to_id,
1845        &task_offsets,
1846        None,
1847        &mut next_id,
1848        &mut id_to_addr,
1849        &mut id_to_event,
1850    )?;
1851    compile_ff_group(
1852        &sir.sir.apply_ffs,
1853        "apply_ff",
1854        &mut apply_event_map,
1855        &mut addr_to_id,
1856        &task_offsets,
1857        None,
1858        &mut next_id,
1859        &mut id_to_addr,
1860        &mut id_to_event,
1861    )?;
1862    // Pre-compute 4-state initialization regions
1863    let mut four_state_inits = Vec::new();
1864    if options.four_state {
1865        for (addr, &offset) in &layout.offsets {
1866            let is_4state = layout.is_4states.get(addr).copied().unwrap_or(false);
1867            if is_4state {
1868                let allocated_size = layout.plane_size(addr);
1869                four_state_inits.push((offset, allocated_size));
1870            }
1871        }
1872        for (addr, &rel_offset) in &layout.working_offsets {
1873            let offset = layout.working_base_offset + rel_offset;
1874            let is_4state = layout.is_4states.get(addr).copied().unwrap_or(false);
1875            if is_4state {
1876                let allocated_size = layout.plane_size(addr);
1877                four_state_inits.push((offset, allocated_size));
1878            }
1879        }
1880    }
1881
1882    Ok((
1883        NativeProgramImage {
1884            code: packed_image,
1885            code_entries,
1886            symbols: image_symbols,
1887            comb_offset,
1888            comb_unit_offsets,
1889            required_native_features,
1890            event_map,
1891            eval_only_event_map,
1892            apply_event_map,
1893            id_to_addr,
1894            id_to_event,
1895            reflection: sir.runtime().build_design_reflection(layout),
1896            design: sir.runtime().design.clone(),
1897            testbench: sir.runtime().testbench.clone(),
1898            runtime_schema: NativeRuntimeSchema {
1899                runtime_errors: sir.runtime().runtime_schema.runtime_errors.clone(),
1900                runtime_event_sites: sir.runtime().runtime_schema.runtime_event_sites.clone(),
1901                comb_observers: sir.runtime().runtime_schema.comb_observers.clone(),
1902                testbench_read_roots: sir.runtime().runtime_schema.testbench_read_roots.clone(),
1903                rtl_writes: sir.runtime().runtime_schema.rtl_writes.clone(),
1904            },
1905            layout: layout.clone(),
1906            native_memory_size,
1907            options: NativeRuntimeOptions {
1908                four_state: options.four_state,
1909                native_tick_loop: options.x86_options.native_tick_loop,
1910                native_force_support: options.native_force_support,
1911                perf_map: options.x86_options.diagnostics.perf_map,
1912            },
1913            four_state_inits,
1914        },
1915        codegen_trace,
1916    ))
1917}
1918
1919// ────────────────────────────────────────────────────────────────
1920// NativeBackend
1921// ────────────────────────────────────────────────────────────────
1922
1923pub struct NativeBackend {
1924    compiled: Arc<SharedNativeCode>,
1925    memory: Vec<u64>,
1926    runtime_event_buffer: Arc<RuntimeEventBuffer>,
1927    comb_capture_enabled: Vec<u8>,
1928    execution_timing: Option<NativeExecutionTiming>,
1929}
1930
1931fn write_bits_to_memory_from(
1932    memory: &mut [u8],
1933    destination_bit_offset: usize,
1934    bit_width: usize,
1935    source: &[u8],
1936    source_bit_offset: usize,
1937) {
1938    for bit in 0..bit_width {
1939        let source_bit = source_bit_offset + bit;
1940        let value = (source[source_bit / 8] >> (source_bit % 8)) & 1;
1941        let destination_bit = destination_bit_offset + bit;
1942        let destination = &mut memory[destination_bit / 8];
1943        let mask = 1u8 << (destination_bit % 8);
1944        if value == 0 {
1945            *destination &= !mask;
1946        } else {
1947            *destination |= mask;
1948        }
1949    }
1950}
1951
1952fn write_bits_to_memory(
1953    memory: &mut [u8],
1954    destination_bit_offset: usize,
1955    bit_width: usize,
1956    source: &[u8],
1957) {
1958    write_bits_to_memory_from(memory, destination_bit_offset, bit_width, source, 0);
1959}
1960
1961fn write_initial_run_to_plane(
1962    memory: &mut [u8],
1963    signal: SignalRef,
1964    mask_plane: bool,
1965    run: &InitialStateWriteRun,
1966    source: &[u8],
1967) {
1968    let Some(array) = signal.array_layout else {
1969        let plane_size = signal.width.div_ceil(8);
1970        let destination_bit_offset =
1971            (signal.offset + usize::from(mask_plane) * plane_size) * 8 + run.bit_offset;
1972        write_bits_to_memory(memory, destination_bit_offset, run.bit_width, source);
1973        return;
1974    };
1975
1976    let plane_offset = signal.offset + usize::from(mask_plane) * array.plane_size;
1977    let mut consumed = 0usize;
1978    while consumed < run.bit_width {
1979        let logical_offset = run.bit_offset + consumed;
1980        let element = logical_offset / array.element_width;
1981        let intra_element = logical_offset % array.element_width;
1982        let part_width = (run.bit_width - consumed).min(array.element_width - intra_element);
1983        let destination_bit_offset =
1984            (plane_offset + element * array.element_stride) * 8 + intra_element;
1985
1986        if consumed.is_multiple_of(8)
1987            && destination_bit_offset.is_multiple_of(8)
1988            && part_width.is_multiple_of(8)
1989        {
1990            let source_byte = consumed / 8;
1991            let destination_byte = destination_bit_offset / 8;
1992            let byte_width = part_width / 8;
1993            memory[destination_byte..destination_byte + byte_width]
1994                .copy_from_slice(&source[source_byte..source_byte + byte_width]);
1995        } else {
1996            write_bits_to_memory_from(memory, destination_bit_offset, part_width, source, consumed);
1997        }
1998        consumed += part_width;
1999    }
2000}
2001
2002impl NativeBackend {
2003    pub(crate) fn eval_comb_units_with(
2004        &mut self,
2005        mut after_unit: impl FnMut(&mut Self),
2006    ) -> Result<(), SimulatorErrorCode> {
2007        let funcs = self.compiled.comb_unit_funcs.clone();
2008        if funcs.is_empty() {
2009            return self.eval_comb();
2010        }
2011        for func in funcs {
2012            self.call_func_timed(func)?;
2013            after_unit(self);
2014        }
2015        Ok(())
2016    }
2017
2018    /// Compile a pointer-free native image without attaching it to executable
2019    /// memory. A precompiled runtime can load the result with
2020    /// [`SharedNativeCode::from_image`].
2021    pub fn compile_image(
2022        laid_out: &LaidOutProgram,
2023        options: &SimulatorOptions,
2024    ) -> Result<NativeProgramImage, SimulatorError> {
2025        let (image, trace) = compile_program(laid_out, options, false)?;
2026        debug_assert!(trace.is_none());
2027        Ok(image)
2028    }
2029
2030    pub(crate) fn compile_image_with_codegen_trace(
2031        laid_out: &LaidOutProgram,
2032        options: &SimulatorOptions,
2033    ) -> Result<(NativeProgramImage, NativeCodegenTrace), SimulatorError> {
2034        let (image, trace) = compile_program(laid_out, options, true)?;
2035        Ok((
2036            image,
2037            trace.expect("trace-enabled native compilation must return a trace"),
2038        ))
2039    }
2040
2041    /// Load a compiler-produced image into executable memory and create a
2042    /// backend instance for it.
2043    ///
2044    /// # Safety
2045    ///
2046    /// The image's machine code must come from a trusted compiler or image
2047    /// container. Structural validation does not authenticate code before it
2048    /// is mapped executable and invoked.
2049    pub unsafe fn from_image(image: NativeProgramImage) -> Result<Self, SimulatorError> {
2050        // Safety: upheld by this constructor's caller.
2051        let shared = Arc::new(unsafe { SharedNativeCode::from_image(image)? });
2052        Ok(Self::from_shared(shared))
2053    }
2054
2055    pub fn new(
2056        laid_out: &LaidOutProgram,
2057        options: &SimulatorOptions,
2058    ) -> Result<Self, SimulatorError> {
2059        let image = Self::compile_image(laid_out, options)?;
2060        // Safety: `image` was produced in-process by the Celox compiler above.
2061        unsafe { Self::from_image(image) }
2062    }
2063
2064    #[cfg(any(
2065        all(target_arch = "x86_64", not(feature = "arm64-codegen")),
2066        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
2067    ))]
2068    pub(crate) fn new_with_codegen_trace(
2069        laid_out: &LaidOutProgram,
2070        options: &SimulatorOptions,
2071    ) -> Result<(Self, NativeCodegenTrace), SimulatorError> {
2072        let (image, trace) = Self::compile_image_with_codegen_trace(laid_out, options)?;
2073        // Safety: `image` was produced in-process by the Celox compiler above.
2074        let shared = unsafe { SharedNativeCode::from_image(image)? };
2075        let backend = Self::from_shared(Arc::new(shared));
2076        Ok((backend, trace))
2077    }
2078
2079    /// Create a new backend instance from shared compiled code.
2080    /// Each instance gets its own simulation state memory.
2081    pub fn from_shared(shared: Arc<SharedNativeCode>) -> Self {
2082        let mem_size_words = shared.native_memory_size.div_ceil(8);
2083        let mut memory = vec![0u64; mem_size_words + 1]; // +1 for safety
2084        let runtime_event_buffer = Arc::new(RuntimeEventBuffer::new(
2085            shared.layout.runtime_event_buffer_size,
2086        ));
2087        let comb_capture_enabled = vec![0; shared.layout.runtime_event_site_layouts.len().max(1)];
2088
2089        // Initialize 4-state regions to X (v=1, m=1)
2090        for &(offset, allocated_size) in &shared.four_state_inits {
2091            unsafe {
2092                let base_ptr = (memory.as_mut_ptr() as *mut u8).add(offset);
2093                std::ptr::write_bytes(base_ptr, 0xFF, allocated_size);
2094                let mask_ptr = base_ptr.add(allocated_size);
2095                std::ptr::write_bytes(mask_ptr, 0xFF, allocated_size);
2096            }
2097        }
2098
2099        let mut backend = Self {
2100            compiled: shared,
2101            memory,
2102            runtime_event_buffer,
2103            comb_capture_enabled,
2104            execution_timing: None,
2105        };
2106        backend.install_event_buffers();
2107        let compiled = Arc::clone(&backend.compiled);
2108        backend.apply_initial_values(&compiled.program_image.design.initial_state);
2109        backend
2110    }
2111
2112    fn apply_initial_values(&mut self, initial_state: &[InitialStateValue<AbsoluteAddr>]) {
2113        for init in initial_state {
2114            let signal = self.resolve_signal(&init.address);
2115            match &init.data {
2116                InitialStateData::Packed {
2117                    value,
2118                    mask,
2119                    written_mask,
2120                } => {
2121                    let width_mask = if signal.width == 0 {
2122                        BigUint::default()
2123                    } else {
2124                        (BigUint::from(1u8) << signal.width) - BigUint::from(1u8)
2125                    };
2126                    let preserve_mask = &width_mask ^ (written_mask & &width_mask);
2127                    let (current_value, current_mask) = self.get_four_state(signal);
2128                    let value = (current_value & &preserve_mask) | (value & written_mask);
2129                    let mask = (current_mask & &preserve_mask) | (mask & written_mask);
2130                    if self.compiled.options.four_state && signal.is_4state {
2131                        self.set_four_state(signal, value, mask);
2132                    } else {
2133                        let known_mask = &width_mask ^ (&mask & &width_mask);
2134                        self.set_wide(signal, value & known_mask);
2135                    }
2136                }
2137                InitialStateData::Writes(runs) => self.apply_initial_memory_writes(signal, runs),
2138            }
2139        }
2140    }
2141
2142    fn apply_initial_memory_writes(&mut self, signal: SignalRef, runs: &[InitialStateWriteRun]) {
2143        let value_byte_size = signal.width.div_ceil(8);
2144        let write_mask = self.compiled.options.four_state && signal.is_4state;
2145        let mem = self.mem_bytes_mut();
2146
2147        for run in runs {
2148            if run.bit_width == 0 {
2149                continue;
2150            }
2151            if signal.array_layout.is_some() {
2152                write_initial_run_to_plane(mem, signal, false, run, &run.value_bytes);
2153                if write_mask {
2154                    write_initial_run_to_plane(mem, signal, true, run, &run.mask_bytes);
2155                }
2156                continue;
2157            }
2158            if run.bit_offset.is_multiple_of(8) && run.bit_width.is_multiple_of(8) {
2159                let byte_offset = run.bit_offset / 8;
2160                let byte_width = run.bit_width / 8;
2161                let value_offset = signal.offset + byte_offset;
2162                mem[value_offset..value_offset + byte_width]
2163                    .copy_from_slice(&run.value_bytes[..byte_width]);
2164                if write_mask {
2165                    let mask_offset = signal.offset + value_byte_size + byte_offset;
2166                    mem[mask_offset..mask_offset + byte_width]
2167                        .copy_from_slice(&run.mask_bytes[..byte_width]);
2168                }
2169                continue;
2170            }
2171
2172            write_bits_to_memory(
2173                mem,
2174                signal.offset * 8 + run.bit_offset,
2175                run.bit_width,
2176                &run.value_bytes,
2177            );
2178            if write_mask {
2179                write_bits_to_memory(
2180                    mem,
2181                    (signal.offset + value_byte_size) * 8 + run.bit_offset,
2182                    run.bit_width,
2183                    &run.mask_bytes,
2184                );
2185            }
2186        }
2187    }
2188
2189    /// Start a fresh opt-in measurement of generated native function calls.
2190    pub fn start_execution_timing(&mut self) {
2191        self.execution_timing = Some(NativeExecutionTiming::default());
2192    }
2193
2194    /// Stop timing and return the accumulated generated-code interval.
2195    pub fn finish_execution_timing(&mut self) -> Option<NativeExecutionTiming> {
2196        self.execution_timing.take()
2197    }
2198
2199    fn install_event_buffers(&mut self) {
2200        use crate::backend::memory_layout::{
2201            STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET, STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET,
2202        };
2203
2204        let addr = self.runtime_event_buffer.as_mut_ptr() as u64;
2205        let ptr = unsafe {
2206            (self.memory.as_mut_ptr() as *mut u8).add(STATE_HEADER_RUNTIME_EVENT_ADDR_OFFSET)
2207                as *mut u64
2208        };
2209        unsafe {
2210            std::ptr::write_unaligned(ptr, addr);
2211        }
2212        let addr = self.comb_capture_enabled.as_ptr() as u64;
2213        let ptr = unsafe {
2214            (self.memory.as_mut_ptr() as *mut u8).add(STATE_HEADER_COMB_CAPTURE_ENABLED_ADDR_OFFSET)
2215                as *mut u64
2216        };
2217        unsafe {
2218            std::ptr::write_unaligned(ptr, addr);
2219        }
2220    }
2221
2222    /// Get the shared compiled code handle.
2223    pub fn shared_code(&self) -> Arc<SharedNativeCode> {
2224        Arc::clone(&self.compiled)
2225    }
2226
2227    fn mem_ptr(&self) -> *const u8 {
2228        self.memory.as_ptr() as *const u8
2229    }
2230
2231    fn mem_mut_ptr(&mut self) -> *mut u8 {
2232        self.memory.as_mut_ptr() as *mut u8
2233    }
2234
2235    fn mem_bytes(&self) -> &[u8] {
2236        let ptr = self.mem_ptr();
2237        let len = self.memory.len() * 8;
2238        unsafe { std::slice::from_raw_parts(ptr, len) }
2239    }
2240
2241    fn mem_bytes_mut(&mut self) -> &mut [u8] {
2242        let ptr = self.mem_mut_ptr();
2243        let len = self.memory.len() * 8;
2244        unsafe { std::slice::from_raw_parts_mut(ptr, len) }
2245    }
2246
2247    fn read_signal_plane(&self, signal: SignalRef, mask_plane: bool) -> BigUint {
2248        let bytes = self.mem_bytes();
2249        let Some(array) = signal.array_layout else {
2250            let byte_size = get_byte_size(signal.width);
2251            let plane_offset = signal.offset + usize::from(mask_plane) * byte_size;
2252            let mut value = BigUint::from_bytes_le(&bytes[plane_offset..plane_offset + byte_size]);
2253            if !signal.width.is_multiple_of(8) {
2254                value &= (BigUint::from(1u8) << signal.width) - BigUint::from(1u8);
2255            }
2256            return value;
2257        };
2258
2259        let plane_offset = signal.offset + usize::from(mask_plane) * array.plane_size;
2260        let element_bytes = get_byte_size(array.element_width);
2261        let element_mask = (BigUint::from(1u8) << array.element_width) - BigUint::from(1u8);
2262        let mut value = BigUint::from(0u8);
2263        for element in 0..array.element_count {
2264            let start = plane_offset + element * array.element_stride;
2265            let element_value =
2266                BigUint::from_bytes_le(&bytes[start..start + element_bytes]) & &element_mask;
2267            value |= element_value << (element * array.element_width);
2268        }
2269        value
2270    }
2271
2272    fn write_signal_plane(&mut self, signal: SignalRef, mask_plane: bool, value: &BigUint) {
2273        let Some(array) = signal.array_layout else {
2274            let byte_size = get_byte_size(signal.width);
2275            let plane_offset = signal.offset + usize::from(mask_plane) * byte_size;
2276            let bytes = self.mem_bytes_mut();
2277            bytes[plane_offset..plane_offset + byte_size].fill(0);
2278            let value_bytes = value.to_bytes_le();
2279            let copy_len = value_bytes.len().min(byte_size);
2280            bytes[plane_offset..plane_offset + copy_len].copy_from_slice(&value_bytes[..copy_len]);
2281            if !signal.width.is_multiple_of(8) && byte_size != 0 {
2282                bytes[plane_offset + byte_size - 1] &= (1u8 << (signal.width % 8)) - 1;
2283            }
2284            return;
2285        };
2286
2287        let plane_offset = signal.offset + usize::from(mask_plane) * array.plane_size;
2288        let element_bytes = get_byte_size(array.element_width);
2289        let element_mask = (BigUint::from(1u8) << array.element_width) - BigUint::from(1u8);
2290        let bytes = self.mem_bytes_mut();
2291        bytes[plane_offset..plane_offset + array.plane_size].fill(0);
2292        for element in 0..array.element_count {
2293            let element_value = (value >> (element * array.element_width)) & &element_mask;
2294            let value_bytes = element_value.to_bytes_le();
2295            let copy_len = value_bytes.len().min(element_bytes);
2296            let start = plane_offset + element * array.element_stride;
2297            bytes[start..start + copy_len].copy_from_slice(&value_bytes[..copy_len]);
2298        }
2299    }
2300
2301    fn call_func(memory: &mut [u64], func: NativeSimFunc) -> Result<(), SimulatorErrorCode> {
2302        let ptr = memory.as_mut_ptr() as *mut u8;
2303        let ret = unsafe { func(ptr) };
2304        match ret {
2305            0 => Ok(()),
2306            code if code > 0 => Err(SimulatorErrorCode::DetectedTrueLoopCode(code)),
2307            _ => Err(SimulatorErrorCode::InternalError),
2308        }
2309    }
2310
2311    fn call_func_timed(&mut self, func: NativeSimFunc) -> Result<(), SimulatorErrorCode> {
2312        let Some(_) = self.execution_timing else {
2313            return Self::call_func(&mut self.memory, func);
2314        };
2315        let start = Instant::now();
2316        let result = Self::call_func(&mut self.memory, func);
2317        let elapsed = start.elapsed();
2318        let timing = self
2319            .execution_timing
2320            .as_mut()
2321            .expect("native execution timing was enabled before the call");
2322        timing.elapsed = timing.elapsed.saturating_add(elapsed);
2323        timing.calls = timing.calls.saturating_add(1);
2324        result
2325    }
2326
2327    fn call_func_many(
2328        memory: &mut [u64],
2329        func: NativeSimFunc,
2330        count: u64,
2331    ) -> (u64, Result<(), SimulatorErrorCode>) {
2332        use crate::backend::memory_layout::STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET;
2333
2334        if count == 0 {
2335            return (0, Ok(()));
2336        }
2337        let remaining_word = STATE_HEADER_NATIVE_LOOP_REMAINING_OFFSET / 8;
2338        memory[remaining_word] = count;
2339        let ptr = memory.as_mut_ptr() as *mut u8;
2340        let ret = unsafe { func(ptr) };
2341        let completed = count.saturating_sub(memory[remaining_word]);
2342        let result = match ret {
2343            0 => Ok(()),
2344            code if code > 0 => Err(SimulatorErrorCode::DetectedTrueLoopCode(code)),
2345            _ => Err(SimulatorErrorCode::InternalError),
2346        };
2347        (completed, result)
2348    }
2349
2350    fn call_func_many_timed(
2351        &mut self,
2352        func: NativeSimFunc,
2353        count: u64,
2354    ) -> (u64, Result<(), SimulatorErrorCode>) {
2355        if self.execution_timing.is_none() || count == 0 {
2356            return Self::call_func_many(&mut self.memory, func, count);
2357        }
2358        let start = Instant::now();
2359        let result = Self::call_func_many(&mut self.memory, func, count);
2360        let elapsed = start.elapsed();
2361        let timing = self
2362            .execution_timing
2363            .as_mut()
2364            .expect("native execution timing was enabled before the call");
2365        timing.elapsed = timing.elapsed.saturating_add(elapsed);
2366        timing.calls = timing.calls.saturating_add(1);
2367        result
2368    }
2369}
2370
2371impl super::super::SimBackend for NativeBackend {
2372    type Event = NativeEventRef;
2373
2374    fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode> {
2375        let func = self.compiled.comb_func;
2376        self.call_func_timed(func)
2377    }
2378
2379    fn eval_apply_ff_at(&mut self, event: NativeEventRef) -> Result<(), SimulatorErrorCode> {
2380        self.call_func_timed(event.func)
2381    }
2382
2383    fn eval_comb_apply_ff_at(&mut self, event: NativeEventRef) -> Result<(), SimulatorErrorCode> {
2384        self.call_func_timed(event.comb_apply_func)
2385    }
2386
2387    fn eval_comb_apply_ff_many_at(
2388        &mut self,
2389        event: NativeEventRef,
2390        count: u64,
2391    ) -> (u64, Result<(), SimulatorErrorCode>) {
2392        if self.compiled.options.native_tick_loop {
2393            self.call_func_many_timed(event.comb_apply_func, count)
2394        } else if count == 0 {
2395            (0, Ok(()))
2396        } else {
2397            (1, self.call_func_timed(event.comb_apply_func))
2398        }
2399    }
2400
2401    fn eval_only_ff_at(&mut self, event: NativeEventRef) -> Result<(), SimulatorErrorCode> {
2402        self.call_func_timed(event.func)
2403    }
2404
2405    fn apply_ff_at(&mut self, event: NativeEventRef) -> Result<(), SimulatorErrorCode> {
2406        self.call_func_timed(event.func)
2407    }
2408
2409    fn resolve_signal(&self, addr: &AbsoluteAddr) -> SignalRef {
2410        let layout = &self.compiled.layout;
2411        let offset = layout.offsets.get(addr).copied().unwrap_or(0);
2412        let width = layout.widths.get(addr).copied().unwrap_or(0);
2413        let is_4state = layout.is_4states.get(addr).copied().unwrap_or(false);
2414        let array_layout = layout
2415            .unpacked_arrays
2416            .get(addr)
2417            .map(|array| SignalArrayLayout {
2418                element_width: array.element_width,
2419                element_count: array.element_count,
2420                element_stride: array.element_stride,
2421                plane_size: array.plane_size,
2422            });
2423        SignalRef {
2424            offset,
2425            width,
2426            is_4state,
2427            array_layout,
2428        }
2429    }
2430
2431    fn resolve_event(&self, addr: &AbsoluteAddr) -> NativeEventRef {
2432        *self
2433            .compiled
2434            .event_map
2435            .get(addr)
2436            .unwrap_or_else(|| panic!("event not found for {:?}", addr))
2437    }
2438
2439    fn resolve_event_opt(&self, addr: &AbsoluteAddr) -> Option<NativeEventRef> {
2440        self.compiled.event_map.get(addr).copied()
2441    }
2442
2443    fn resolve_eval_only_event(&self, addr: &AbsoluteAddr) -> Option<NativeEventRef> {
2444        self.compiled.eval_only_event_map.get(addr).copied()
2445    }
2446
2447    fn resolve_apply_event(&self, addr: &AbsoluteAddr) -> Option<NativeEventRef> {
2448        self.compiled.apply_event_map.get(addr).copied()
2449    }
2450
2451    fn set<T: Copy>(&mut self, signal: SignalRef, val: T) {
2452        let allocated_size = get_byte_size(signal.width);
2453        let provided_size = std::mem::size_of::<T>();
2454        let clear_mask = self.compiled.options.four_state && signal.is_4state;
2455
2456        assert!(provided_size <= allocated_size);
2457
2458        if signal.array_layout.is_some() {
2459            let value_bytes =
2460                unsafe { std::slice::from_raw_parts(&val as *const T as *const u8, provided_size) };
2461            self.write_signal_plane(signal, false, &BigUint::from_bytes_le(value_bytes));
2462            if clear_mask {
2463                self.write_signal_plane(signal, true, &BigUint::from(0u8));
2464            }
2465            return;
2466        }
2467
2468        unsafe {
2469            let base_ptr = (self.memory.as_mut_ptr() as *mut u8).add(signal.offset);
2470            if !clear_mask && allocated_size == 1 {
2471                let raw = *(&val as *const T as *const u8);
2472                let byte = if signal.width < 8 {
2473                    raw & ((1u8 << signal.width) - 1)
2474                } else {
2475                    raw
2476                };
2477                *base_ptr = byte;
2478                return;
2479            }
2480
2481            if provided_size < allocated_size {
2482                std::ptr::write_bytes(base_ptr, 0, allocated_size);
2483            }
2484            std::ptr::write_unaligned(base_ptr as *mut T, val);
2485
2486            if clear_mask {
2487                let mask_ptr = base_ptr.add(allocated_size);
2488                std::ptr::write_bytes(mask_ptr, 0, allocated_size);
2489            }
2490        }
2491    }
2492
2493    fn set_wide(&mut self, signal: SignalRef, val: BigUint) {
2494        let clear_mask = self.compiled.options.four_state && signal.is_4state;
2495        self.write_signal_plane(signal, false, &val);
2496        if clear_mask {
2497            self.write_signal_plane(signal, true, &BigUint::from(0u8));
2498        }
2499    }
2500
2501    fn set_four_state(&mut self, signal: SignalRef, val: BigUint, mask: BigUint) {
2502        let write_mask = self.compiled.options.four_state && signal.is_4state;
2503        self.write_signal_plane(signal, false, &val);
2504        if write_mask {
2505            self.write_signal_plane(signal, true, &mask);
2506        }
2507    }
2508
2509    fn get(&self, signal: SignalRef) -> BigUint {
2510        self.read_signal_plane(signal, false)
2511    }
2512
2513    fn get_as<T: Default + Copy>(&self, signal: SignalRef) -> T {
2514        let bs = get_byte_size(signal.width);
2515        let provided_size = std::mem::size_of::<T>();
2516        if signal.array_layout.is_some() {
2517            let mut val = T::default();
2518            let value = self.read_signal_plane(signal, false).to_bytes_le();
2519            let val_bytes = unsafe {
2520                std::slice::from_raw_parts_mut(&mut val as *mut T as *mut u8, provided_size)
2521            };
2522            let copy_len = value.len().min(val_bytes.len());
2523            val_bytes[..copy_len].copy_from_slice(&value[..copy_len]);
2524            return val;
2525        }
2526        let ptr = unsafe { (self.memory.as_ptr() as *const u8).add(signal.offset) };
2527        if provided_size <= bs {
2528            return unsafe { std::ptr::read_unaligned(ptr as *const T) };
2529        }
2530
2531        let bytes = self.mem_bytes();
2532        let mut val = T::default();
2533        let val_bytes =
2534            unsafe { std::slice::from_raw_parts_mut(&mut val as *mut T as *mut u8, provided_size) };
2535        let copy_len = val_bytes.len().min(bs);
2536        val_bytes[..copy_len].copy_from_slice(&bytes[signal.offset..signal.offset + copy_len]);
2537        val
2538    }
2539
2540    fn get_four_state(&self, signal: SignalRef) -> (BigUint, BigUint) {
2541        let val = self.read_signal_plane(signal, false);
2542        let mask = if self.compiled.options.four_state && signal.is_4state {
2543            self.read_signal_plane(signal, true)
2544        } else {
2545            BigUint::from(0u32)
2546        };
2547        (val, mask)
2548    }
2549
2550    fn memory_as_ptr(&self) -> (*const u8, usize) {
2551        (self.mem_ptr(), self.memory.len() * 8)
2552    }
2553
2554    fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
2555        (self.mem_mut_ptr(), self.memory.len() * 8)
2556    }
2557
2558    fn runtime_event_buffer_as_ptr(&self) -> (*const u8, usize) {
2559        (
2560            self.runtime_event_buffer.as_ptr(),
2561            self.runtime_event_buffer.byte_size(),
2562        )
2563    }
2564
2565    fn runtime_event_buffer(&self) -> Option<Arc<RuntimeEventBuffer>> {
2566        Some(Arc::clone(&self.runtime_event_buffer))
2567    }
2568
2569    fn set_comb_capture_event_enabled(&mut self, active_sites: &[bool]) {
2570        self.comb_capture_enabled.fill(0);
2571        for (idx, active) in active_sites.iter().copied().enumerate() {
2572            if active && idx < self.comb_capture_enabled.len() {
2573                self.comb_capture_enabled[idx] = 1;
2574            }
2575        }
2576    }
2577
2578    fn stable_region_size(&self) -> usize {
2579        self.compiled.layout.total_size
2580    }
2581
2582    fn layout(&self) -> &MemoryLayout {
2583        &self.compiled.layout
2584    }
2585
2586    fn id_to_addr_slice(&self) -> &[AbsoluteAddr] {
2587        &self.compiled.id_to_addr
2588    }
2589
2590    fn id_to_event_slice(&self) -> &[NativeEventRef] {
2591        &self.compiled.id_to_event
2592    }
2593
2594    fn num_events(&self) -> usize {
2595        self.compiled.id_to_event.len()
2596    }
2597
2598    fn clear_triggered_bits(&mut self) {
2599        let offset = self.compiled.layout.triggered_bits_offset;
2600        let size = self.compiled.layout.triggered_bits_total_size;
2601        let bytes = self.mem_bytes_mut();
2602        bytes[offset..offset + size].fill(0);
2603    }
2604
2605    fn mark_triggered_bit(&mut self, id: usize) {
2606        let offset = self.compiled.layout.triggered_bits_offset;
2607        let byte_idx = offset + id / 8;
2608        let bit_idx = id % 8;
2609        self.mem_bytes_mut()[byte_idx] |= 1 << bit_idx;
2610    }
2611
2612    fn get_triggered_bits(&self) -> BitSet {
2613        let offset = self.compiled.layout.triggered_bits_offset;
2614        let size = self.compiled.layout.triggered_bits_total_size;
2615        let bytes = self.mem_bytes();
2616        let mut bs = BitSet::with_capacity(size * 8);
2617        for i in 0..size * 8 {
2618            let byte_idx = offset + i / 8;
2619            let bit_idx = i % 8;
2620            if bytes[byte_idx] & (1 << bit_idx) != 0 {
2621                bs.insert(i);
2622            }
2623        }
2624        bs
2625    }
2626}