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