Skip to main content

celox/backend/
tiered.rs

1//! Tiered execution backend: interpret first, promote to compiled code.
2//!
3//! [`TieredBackend`] starts every simulation on the Tier-0 interpreter so
4//! execution begins the moment the state layout is finalized, hiding the
5//! code-generation latency behind the first simulated steps. A worker thread
6//! compiles the program for the host's default compiled tier in the
7//! background — the direct native backend where available (matching
8//! [`crate::DefaultBackend`]'s selection), Cranelift otherwise — and the next
9//! scheduler safe point after completion adopts the compiled code, moving the
10//! live memory image across without any translation because both tiers run
11//! against the same finalized layout.
12//!
13//! Promotion is whole-program: once the compiled tier is adopted it is used
14//! for the remainder of the simulation. The interpreter remains the permanent
15//! fallback whenever background compilation fails, and
16//! [`TierPromotion::Never`] skips background compilation entirely so the
17//! simulation stays interpreted without paying for a worker thread.
18//! [`TierPromotion::AfterSteps`] keeps compiling eagerly but delays adoption
19//! until the interpreter has executed a minimum number of evaluation steps,
20//! protecting short simulations from the promotion cost.
21//!
22//! Adoption never moves the live memory image: tiered builds reserve arena
23//! headroom up front and grow within that allocation, so views handed out
24//! before promotion (for example zero-copy host buffers) stay valid. If a
25//! compiled image would exceed the reserved arena, adoption is declined and
26//! the reason is reported through [`TieredBackend::promotion_error`].
27//!
28//! Background compilation is cooperatively cancellable on the native tier:
29//! dropping the backend (or calling
30//! [`TieredBackend::cancel_background_compilation`]) flags the worker, which
31//! unwinds at the next task boundary instead of finishing the image. Drop
32//! joins the owned worker so compilation never leaks beyond the backend's
33//! lifetime. The Cranelift tier runs to completion once started, so its drop
34//! may wait for that compilation to finish.
35
36#![cfg(feature = "host-runtime")]
37
38use std::sync::Arc;
39use std::sync::mpsc;
40use std::thread::JoinHandle;
41use std::time::{Duration, Instant};
42
43use num_bigint::BigUint;
44
45use super::compile_cancel::CompileCancel;
46use super::{
47    EventHandle, MemoryLayout, RuntimeEventBuffer, SharedJitCode, SimBackend, SimulatorErrorCode,
48    memory_image::MemoryImage,
49};
50#[cfg(any(
51    target_arch = "x86_64",
52    feature = "arm64-codegen",
53    target_arch = "aarch64"
54))]
55use crate::backend::native::SharedNativeCode;
56use crate::backend::{InterpBackend, JitBackend};
57use crate::{
58    SimulatorError, SimulatorOptions,
59    ir::{AbsoluteAddr, LaidOutProgram, SignalRef},
60};
61
62/// Whether this host's default compiled tier is the direct native backend
63/// (mirroring [`crate::DefaultBackend`]'s selection) rather than Cranelift.
64pub(crate) fn native_is_default_target() -> bool {
65    #[cfg(any(
66        all(target_arch = "x86_64", not(feature = "arm64-codegen")),
67        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
68    ))]
69    {
70        true
71    }
72    #[cfg(not(any(
73        all(target_arch = "x86_64", not(feature = "arm64-codegen")),
74        all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
75    )))]
76    {
77        false
78    }
79}
80
81/// The memory-layout mode for tiered execution.
82///
83/// Uses the selected compiled tier's required mode. On native hosts this is
84/// ElementStrided (a native-tier optimization for unpacked arrays); the
85/// interpreter implements matching strided addressing.
86pub(crate) fn default_target_layout_mode() -> crate::backend::memory_layout::MemoryLayoutMode {
87    if native_is_default_target() {
88        crate::backend::memory_layout::MemoryLayoutMode::ElementStrided
89    } else {
90        crate::backend::memory_layout::MemoryLayoutMode::Packed
91    }
92}
93
94/// The layout mode for one tiered build.
95///
96/// VCD tracing reads whole signals as contiguous packed bytes, so a build
97/// that records traces selects the packed layout; element-strided arrays are
98/// not representable in the current VCD descriptor format.
99pub(crate) fn tiered_layout_mode(
100    vcd_recording: bool,
101) -> crate::backend::memory_layout::MemoryLayoutMode {
102    if vcd_recording {
103        crate::backend::memory_layout::MemoryLayoutMode::Packed
104    } else {
105        default_target_layout_mode()
106    }
107}
108
109/// The adopted compiled tier.
110enum CompiledTier {
111    Jit(Box<JitBackend>),
112    #[cfg(any(
113        target_arch = "x86_64",
114        feature = "arm64-codegen",
115        target_arch = "aarch64"
116    ))]
117    Native(Box<crate::backend::native::NativeBackend>),
118}
119
120impl CompiledCode {
121    /// Image size in `u64` words the compiled tier requires its host
122    /// allocation to cover.
123    fn required_image_words(&self) -> usize {
124        match self {
125            // A MemorySpilled Cranelift plan extends the layout with backend
126            // scratch beyond the semantic state, and adoption resizes the
127            // transferred image to the compiled layout's total size; report
128            // that requirement so promotion stays inside the reservation.
129            CompiledCode::Cranelift(shared) => shared.layout.merged_total_size.div_ceil(8),
130            #[cfg(any(
131                target_arch = "x86_64",
132                feature = "arm64-codegen",
133                target_arch = "aarch64"
134            ))]
135            CompiledCode::Native(shared) => shared.native_memory_size.div_ceil(8) + 1,
136        }
137    }
138}
139
140impl CompiledTier {
141    /// Bind compiled code to a live simulation state handed over by the
142    /// interpreter during promotion.
143    ///
144    /// Callers must have verified [`CompiledCode::required_image_words`]
145    /// against the image's spare capacity first: growth stays within the
146    /// existing allocation so promotion never moves the live memory image,
147    /// which external views (zero-copy host buffers) may still reference.
148    fn adopt(
149        code: CompiledCode,
150        mut memory: MemoryImage,
151        runtime_event_buffer: Arc<RuntimeEventBuffer>,
152        comb_capture_enabled: Vec<u8>,
153    ) -> Self {
154        debug_assert!(memory.capacity_words() >= code.required_image_words());
155        if memory.len_words() < code.required_image_words() {
156            memory.resize_zeroed_within_capacity(code.required_image_words());
157        }
158        match code {
159            CompiledCode::Cranelift(shared) => {
160                Self::Jit(Box::new(JitBackend::adopt_shared_with_state(
161                    shared,
162                    memory,
163                    runtime_event_buffer,
164                    comb_capture_enabled,
165                )))
166            }
167            #[cfg(any(
168                target_arch = "x86_64",
169                feature = "arm64-codegen",
170                target_arch = "aarch64"
171            ))]
172            CompiledCode::Native(shared) => Self::Native(Box::new(
173                crate::backend::native::NativeBackend::adopt_shared_with_state(
174                    shared,
175                    memory,
176                    runtime_event_buffer,
177                    comb_capture_enabled,
178                ),
179            )),
180        }
181    }
182
183    fn layout(&self) -> &MemoryLayout {
184        match self {
185            Self::Jit(jit) => jit.layout(),
186            #[cfg(any(
187                target_arch = "x86_64",
188                feature = "arm64-codegen",
189                target_arch = "aarch64"
190            ))]
191            Self::Native(native) => native.layout(),
192        }
193    }
194
195    fn memory_base_mut(&mut self) -> *mut u8 {
196        match self {
197            Self::Jit(jit) => jit.memory_as_ptr().0.cast_mut(),
198            #[cfg(any(
199                target_arch = "x86_64",
200                feature = "arm64-codegen",
201                target_arch = "aarch64"
202            ))]
203            Self::Native(native) => native.memory_as_ptr().0.cast_mut(),
204        }
205    }
206
207    fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode> {
208        match self {
209            Self::Jit(jit) => jit.eval_comb(),
210            #[cfg(any(
211                target_arch = "x86_64",
212                feature = "arm64-codegen",
213                target_arch = "aarch64"
214            ))]
215            Self::Native(native) => native.eval_comb(),
216        }
217    }
218
219    /// Resolve the tier's own event handle for a shared trigger id. Every
220    /// backend indexes its event table by the same deterministic id space,
221    /// so translation is an array lookup.
222    fn eval_apply_ff_at(&mut self, id: usize) -> Result<(), SimulatorErrorCode> {
223        match self {
224            Self::Jit(jit) => {
225                let event = jit.id_to_event_slice()[id];
226                jit.eval_apply_ff_at(event)
227            }
228            #[cfg(any(
229                target_arch = "x86_64",
230                feature = "arm64-codegen",
231                target_arch = "aarch64"
232            ))]
233            Self::Native(native) => {
234                let event = native.id_to_event_slice()[id];
235                native.eval_apply_ff_at(event)
236            }
237        }
238    }
239
240    fn eval_comb_apply_ff_at(&mut self, id: usize) -> Result<(), SimulatorErrorCode> {
241        match self {
242            Self::Jit(jit) => {
243                let event = jit.id_to_event_slice()[id];
244                jit.eval_comb_apply_ff_at(event)
245            }
246            #[cfg(any(
247                target_arch = "x86_64",
248                feature = "arm64-codegen",
249                target_arch = "aarch64"
250            ))]
251            Self::Native(native) => {
252                let event = native.id_to_event_slice()[id];
253                native.eval_comb_apply_ff_at(event)
254            }
255        }
256    }
257
258    /// Execute up to `count` fused ticks. The Cranelift tier has no
259    /// in-generated-code batch loop, so it completes one tick per call and
260    /// lets the caller re-poll; the native tier runs the whole batch inside
261    /// generated code when `native_tick_loop` is enabled.
262    fn eval_comb_apply_ff_many_at(
263        &mut self,
264        id: usize,
265        count: u64,
266    ) -> (u64, Result<(), SimulatorErrorCode>) {
267        match self {
268            Self::Jit(jit) => (1, {
269                let event = jit.id_to_event_slice()[id];
270                jit.eval_comb_apply_ff_at(event)
271            }),
272            #[cfg(any(
273                target_arch = "x86_64",
274                feature = "arm64-codegen",
275                target_arch = "aarch64"
276            ))]
277            Self::Native(native) => {
278                native.eval_comb_apply_ff_many_at(native.id_to_event_slice()[id], count)
279            }
280        }
281    }
282
283    /// Resolve the phase-specific evaluate-only event for a shared trigger
284    /// address and execute it.
285    ///
286    /// Evaluate-only and apply functions live in their own per-phase id
287    /// spaces, so the combined `id_to_event_slice` entry must not be used
288    /// here; the address resolves through each tier's phase-specific map.
289    fn eval_only_ff_at_addr(&mut self, addr: &AbsoluteAddr) -> Result<(), SimulatorErrorCode> {
290        match self {
291            Self::Jit(jit) => {
292                let event = jit
293                    .resolve_eval_only_event(addr)
294                    .unwrap_or_else(|| panic!("eval-only event not found for {addr:?}"));
295                jit.eval_only_ff_at(event)
296            }
297            #[cfg(any(
298                target_arch = "x86_64",
299                feature = "arm64-codegen",
300                target_arch = "aarch64"
301            ))]
302            Self::Native(native) => {
303                let event = native
304                    .resolve_eval_only_event(addr)
305                    .unwrap_or_else(|| panic!("eval-only event not found for {addr:?}"));
306                native.eval_only_ff_at(event)
307            }
308        }
309    }
310
311    /// Resolve the phase-specific apply event for a shared trigger address
312    /// and execute it.
313    fn apply_ff_at_addr(&mut self, addr: &AbsoluteAddr) -> Result<(), SimulatorErrorCode> {
314        match self {
315            Self::Jit(jit) => {
316                let event = jit
317                    .resolve_apply_event(addr)
318                    .unwrap_or_else(|| panic!("apply event not found for {addr:?}"));
319                jit.apply_ff_at(event)
320            }
321            #[cfg(any(
322                target_arch = "x86_64",
323                feature = "arm64-codegen",
324                target_arch = "aarch64"
325            ))]
326            Self::Native(native) => {
327                let event = native
328                    .resolve_apply_event(addr)
329                    .unwrap_or_else(|| panic!("apply event not found for {addr:?}"));
330                native.apply_ff_at(event)
331            }
332        }
333    }
334}
335
336/// Compiled code produced by the background worker, before it is bound to a
337/// live simulation state at promotion time.
338pub(crate) enum CompiledCode {
339    Cranelift(Arc<SharedJitCode>),
340    #[cfg(any(
341        target_arch = "x86_64",
342        feature = "arm64-codegen",
343        target_arch = "aarch64"
344    ))]
345    Native(Arc<SharedNativeCode>),
346}
347
348/// Stable event handle for the tiered backend.
349///
350/// Both inner backends assign identical trigger id spaces for the same
351/// laid-out program (deterministic `FxHashMap` iteration over identical
352/// maps), so the address and id observed before promotion stay meaningful
353/// after adopting the compiled tier.
354#[derive(Clone, Copy, Debug)]
355pub struct TieredEventRef {
356    addr: AbsoluteAddr,
357    id: usize,
358}
359
360impl EventHandle for TieredEventRef {
361    fn id(&self) -> usize {
362        self.id
363    }
364
365    fn addr(&self) -> AbsoluteAddr {
366        self.addr
367    }
368}
369
370enum Phase {
371    Interpreting(Option<Box<InterpBackend>>),
372    Compiled(CompiledTier),
373}
374
375enum Promotion {
376    /// Background compilation still running; the receiver yields exactly one
377    /// result when it finishes.
378    Pending(mpsc::Receiver<Result<CompiledCode, SimulatorError>>),
379    /// Compilation failed permanently; remain on the interpreter forever.
380    Failed(SimulatorError),
381    /// [`TierPromotion::Never`] — no worker was ever spawned and the
382    /// simulation stays interpreted by policy (not by failure).
383    Disabled,
384    Promoted,
385}
386
387/// Active execution tier reported by [`TieredExecutionStats`].
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389#[non_exhaustive]
390pub enum TieredExecutionTier {
391    /// Evaluation is currently performed by the SIR interpreter.
392    Interpreter,
393    /// Evaluation is currently performed by generated code.
394    Compiled,
395}
396
397/// State of background compilation and promotion.
398#[derive(Clone, Copy, Debug, PartialEq, Eq)]
399#[non_exhaustive]
400pub enum TieredPromotionStatus {
401    /// The background compiler has not yet delivered a result.
402    Pending,
403    /// Compilation or adoption failed and execution will remain interpreted.
404    Failed,
405    /// Promotion was disabled by policy, so no compiler worker was started.
406    Disabled,
407    /// Generated code has been adopted.
408    Promoted,
409}
410
411/// Deterministic execution measurements for one tiered simulator.
412///
413/// This snapshot deliberately contains counts and lifecycle state rather than
414/// wall-clock samples. Applications can correlate it with structured
415/// `celox.tiered.compile` and `celox.tiered.promote` tracing spans, while
416/// performance tests should measure operations with a statistical benchmark
417/// harness such as Criterion. Reading a snapshot has no side effects and does
418/// not poll the compiler worker.
419#[derive(Clone, Copy, Debug, PartialEq, Eq)]
420#[non_exhaustive]
421pub struct TieredExecutionStats {
422    /// Tier that will execute the next evaluation.
423    pub tier: TieredExecutionTier,
424    /// Background compilation and promotion state.
425    pub promotion: TieredPromotionStatus,
426    /// Evaluation iterations attempted by the interpreter.
427    pub interpreted_evaluations: u64,
428    /// Evaluation iterations attempted by generated code.
429    pub compiled_evaluations: u64,
430    /// Interpreter evaluation count at the point generated code was adopted.
431    pub promoted_after_interpreted_evaluations: Option<u64>,
432    /// Number of scheduler safe-point polls performed while interpreted.
433    pub safe_point_polls: u64,
434    /// Polls that could not promote because an evaluate/apply pair was open.
435    pub split_apply_deferrals: u64,
436    /// Polls that were held below the configured promotion threshold.
437    pub threshold_deferrals: u64,
438}
439
440/// Opt-in wall-clock measurement for a tiered execution interval.
441///
442/// Ordinary simulations do not read the host clock. Benchmark callers start
443/// timing immediately before executing a workload and finish it afterwards;
444/// `promotion_elapsed` is the time from that start until generated code is
445/// adopted at a scheduler safe point.
446#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
447pub struct TieredExecutionTiming {
448    promotion_elapsed: Option<Duration>,
449}
450
451impl TieredExecutionTiming {
452    /// Time from the measurement start until the compiled tier was adopted.
453    pub fn promotion_elapsed(self) -> Option<Duration> {
454        self.promotion_elapsed
455    }
456}
457
458struct ActiveExecutionTiming {
459    started_at: Instant,
460    promotion_elapsed: Option<Duration>,
461}
462
463/// A [`SimBackend`] that interprets immediately and promotes to generated
464/// code as soon as background compilation completes.
465pub struct TieredBackend {
466    phase: Phase,
467    promotion: Promotion,
468    /// Event handles in trigger-id order, resolved once from the initial
469    /// interpreter and valid across promotion thanks to the shared id space.
470    events: Vec<TieredEventRef>,
471    /// Shared with the background worker; flagged on drop or on an explicit
472    /// cancellation request so the worker unwinds at the next task boundary.
473    cancel: CompileCancel,
474    /// Owned worker handle. Keeping this attached to the backend lets drop
475    /// cancel and join compilation instead of leaking work into later users
476    /// of the process (notably subsequent benchmark samples).
477    compiler_worker: Option<JoinHandle<()>>,
478    /// Evaluate-only events whose apply phase has not run yet. Promotion is
479    /// deferred while this is non-zero so the compiled apply phase always
480    /// observes the interpreted evaluate-only results through an intact
481    /// sparse-metadata image.
482    pending_split_applies: usize,
483    /// Minimum number of interpreted evaluation steps required before
484    /// adoption ([`TierPromotion::AfterSteps`]). Zero means "as soon as the
485    /// compiled tier is ready".
486    promotion_threshold: u64,
487    /// Evaluation steps executed on the interpreted tier so far.
488    interpreted_steps: u64,
489    /// Evaluation iterations executed after adopting generated code.
490    compiled_steps: u64,
491    /// Interpreter count captured at the successful promotion boundary.
492    promoted_after_interpreted_steps: Option<u64>,
493    /// Scheduler safe-point polls made while the interpreter was active.
494    safe_point_polls: u64,
495    /// Safe-point polls deferred by a split evaluate/apply pair.
496    split_apply_deferrals: u64,
497    /// Safe-point polls deferred by the configured step threshold.
498    threshold_deferrals: u64,
499    /// Opt-in benchmark timing. Kept out of deterministic execution stats so
500    /// normal simulations neither read the clock nor expose noisy state.
501    execution_timing: Option<ActiveExecutionTiming>,
502}
503
504impl TieredBackend {
505    /// Build a tiered simulation targeting this host's default compiled
506    /// tier: ready to run immediately on the interpreter while the compiled
507    /// tier is prepared in the background.
508    ///
509    /// The native tier observes cancellation (see
510    /// [`TieredBackend::cancel_background_compilation`]); the Cranelift tier
511    /// runs to completion once started.
512    pub fn new(laid_out: &LaidOutProgram, options: &SimulatorOptions) -> Self {
513        if native_is_default_target() {
514            #[cfg(any(
515                target_arch = "x86_64",
516                feature = "arm64-codegen",
517                target_arch = "aarch64"
518            ))]
519            {
520                Self::with_compiler(laid_out, options, |laid_out, options, cancel| {
521                    use crate::backend::native::{NativeBackend, SharedNativeCode};
522                    // Honor native/MIR tracing requests the same way the
523                    // direct compile_native path does: capture the codegen
524                    // trace and print it when output was requested.
525                    let wants_native_trace = options.trace.native || options.trace.mir;
526                    let image = if wants_native_trace {
527                        let (image, native_trace) =
528                            NativeBackend::compile_image_with_cancel_and_trace(
529                                laid_out, options, cancel,
530                            )?;
531                        let compilation = crate::debug::CompilationTrace {
532                            native_optimized_sir: Some(native_trace.optimized_sir),
533                            mir: Some(native_trace.mir),
534                            reactive_event_graph: Some(native_trace.reactive_graph),
535                            native_state_layout: Some(native_trace.state_layout),
536                            ..crate::debug::CompilationTrace::default()
537                        };
538                        if options.trace.output_to_stdout {
539                            compilation.print();
540                        }
541                        image
542                    } else {
543                        NativeBackend::compile_image_with_cancel(laid_out, options, cancel)?
544                    };
545                    // Safety: the image was produced in-process by the Celox
546                    // compiler above.
547                    let shared = Arc::new(unsafe { SharedNativeCode::from_image(image)? });
548                    Ok(CompiledCode::Native(shared))
549                })
550            }
551            #[cfg(not(any(
552                target_arch = "x86_64",
553                feature = "arm64-codegen",
554                target_arch = "aarch64"
555            )))]
556            {
557                unreachable!("native tier selected on a host without native support")
558            }
559        } else {
560            Self::with_compiler(laid_out, options, |laid_out, options, _cancel| {
561                let mut trace = crate::debug::CompilationTrace::default();
562                let wants_codegen_trace = options.trace.pre_optimized_clif
563                    || options.trace.post_optimized_clif
564                    || options.trace.native;
565                let shared = Arc::new(JitBackend::compile(
566                    laid_out,
567                    options,
568                    wants_codegen_trace.then_some(&mut trace),
569                )?);
570                if options.trace.output_to_stdout {
571                    trace.print();
572                }
573                Ok(CompiledCode::Cranelift(shared))
574            })
575        }
576    }
577
578    /// Build a tiered simulation with a custom compilation step.
579    ///
580    /// `compile` runs on the worker thread and receives the backend's
581    /// cancellation token; a cancelled compile should return an error so the
582    /// simulation stays on the interpreter. Tests use this to gate or stub
583    /// compilation so promotion timing is deterministic.
584    pub(crate) fn with_compiler<F>(
585        laid_out: &LaidOutProgram,
586        options: &SimulatorOptions,
587        compile: F,
588    ) -> Self
589    where
590        F: FnOnce(
591                &LaidOutProgram,
592                &SimulatorOptions,
593                &CompileCancel,
594            ) -> Result<CompiledCode, SimulatorError>
595            + Send
596            + 'static,
597    {
598        let mut interp = Box::new(
599            InterpBackend::new(laid_out, options)
600                .expect("interpreter construction cannot fail for a laid-out program"),
601        );
602        // Reserve promotion headroom before anything can observe the image:
603        // the compiled tier's adoption may grow the image beyond the semantic
604        // state (native spill/scratch arenas, Cranelift MemorySpilled plans),
605        // and adoption must grow within this allocation so the live image
606        // never moves. The slack covers the measured arena sizes with
607        // margin; a design that outgrows it declines promotion (recorded via
608        // `promotion_error`) instead of reallocating.
609        if !matches!(
610            options.tier_promotion,
611            crate::simulator::TierPromotion::Never
612        ) {
613            let len = interp.image_word_len();
614            let slack = len.max(1024) / 8;
615            interp.reserve_image_capacity(len + slack.max(1024));
616        }
617        let events = interp
618            .id_to_event_slice()
619            .iter()
620            .map(|ev| TieredEventRef {
621                addr: ev.addr(),
622                id: ev.id(),
623            })
624            .collect();
625
626        let cancel = CompileCancel::new();
627        let worker_cancel = cancel.clone();
628        // A failed spawn keeps the interpreter as the permanent tier with the
629        // reason recorded, matching the background-failure policy instead of
630        // panicking the embedding application.
631        let (promotion, compiler_worker) = if matches!(
632            options.tier_promotion,
633            crate::simulator::TierPromotion::Never
634        ) {
635            // Policy says interpreted-only: skip the worker entirely so a
636            // never-promoted simulation pays nothing beyond the interpreter.
637            (Promotion::Disabled, None)
638        } else {
639            let (sender, receiver) = mpsc::channel();
640            let background_laid_out = laid_out.clone();
641            let background_options = options.clone();
642            match std::thread::Builder::new()
643                .name("celox-jit-compile".to_string())
644                .spawn(move || {
645                    // The result is delivered through the channel instead of the
646                    // join handle so safe-point polls never block on compilation.
647                    // A panicked worker surfaces as a disconnected channel and
648                    // keeps the simulation on the interpreter permanently.
649                    let span = tracing::info_span!(
650                        "celox.tiered.compile",
651                        target = if native_is_default_target() {
652                            "native"
653                        } else {
654                            "cranelift"
655                        }
656                    );
657                    let result = span.in_scope(|| {
658                        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
659                            compile(&background_laid_out, &background_options, &worker_cancel)
660                        }))
661                        .unwrap_or_else(|_| {
662                            Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
663                        });
664                        match &result {
665                            Ok(_) => tracing::info!("tiered background compilation completed"),
666                            Err(error) => tracing::warn!(
667                                error = %error,
668                                "tiered background compilation failed"
669                            ),
670                        }
671                        result
672                    });
673                    let _ = sender.send(result);
674                }) {
675                Ok(handle) => (Promotion::Pending(receiver), Some(handle)),
676                Err(error) => (
677                    Promotion::Failed(SimulatorError::new(crate::SimulatorErrorKind::Codegen(
678                        crate::CodegenError::message(format!(
679                            "failed to spawn the background compiler thread: {error}"
680                        )),
681                    ))),
682                    None,
683                ),
684            }
685        };
686
687        let promotion_threshold = match options.tier_promotion {
688            crate::simulator::TierPromotion::AfterSteps(steps) => steps,
689            _ => 0,
690        };
691
692        Self {
693            phase: Phase::Interpreting(Some(interp)),
694            promotion,
695            events,
696            cancel,
697            compiler_worker,
698            pending_split_applies: 0,
699            promotion_threshold,
700            interpreted_steps: 0,
701            compiled_steps: 0,
702            promoted_after_interpreted_steps: None,
703            safe_point_polls: 0,
704            split_apply_deferrals: 0,
705            threshold_deferrals: 0,
706            execution_timing: None,
707        }
708    }
709
710    /// Whether the compiled tier has been adopted.
711    pub fn is_compiled(&self) -> bool {
712        matches!(self.phase, Phase::Compiled(_))
713    }
714
715    /// Return deterministic lifecycle and execution measurements.
716    pub fn execution_stats(&self) -> TieredExecutionStats {
717        TieredExecutionStats {
718            tier: if self.is_compiled() {
719                TieredExecutionTier::Compiled
720            } else {
721                TieredExecutionTier::Interpreter
722            },
723            promotion: match &self.promotion {
724                Promotion::Pending(_) => TieredPromotionStatus::Pending,
725                Promotion::Failed(_) => TieredPromotionStatus::Failed,
726                Promotion::Disabled => TieredPromotionStatus::Disabled,
727                Promotion::Promoted => TieredPromotionStatus::Promoted,
728            },
729            interpreted_evaluations: self.interpreted_steps,
730            compiled_evaluations: self.compiled_steps,
731            promoted_after_interpreted_evaluations: self.promoted_after_interpreted_steps,
732            safe_point_polls: self.safe_point_polls,
733            split_apply_deferrals: self.split_apply_deferrals,
734            threshold_deferrals: self.threshold_deferrals,
735        }
736    }
737
738    /// Start measuring time to promotion for a benchmark workload.
739    pub fn start_execution_timing(&mut self) {
740        self.execution_timing = Some(ActiveExecutionTiming {
741            started_at: Instant::now(),
742            promotion_elapsed: self.is_compiled().then_some(Duration::ZERO),
743        });
744    }
745
746    /// Stop timing and return the observed promotion interval.
747    pub fn finish_execution_timing(&mut self) -> Option<TieredExecutionTiming> {
748        self.execution_timing
749            .take()
750            .map(|timing| TieredExecutionTiming {
751                promotion_elapsed: timing.promotion_elapsed,
752            })
753    }
754
755    /// Why promotion has not happened yet, for diagnostics.
756    ///
757    /// Returns `None` once running fully compiled or while background
758    /// compilation is still in progress.
759    pub fn promotion_error(&self) -> Option<&SimulatorError> {
760        match &self.promotion {
761            Promotion::Failed(error) => Some(error),
762            _ => None,
763        }
764    }
765
766    /// Request cancellation of background compilation.
767    ///
768    /// The native-tier worker unwinds at its next task boundary and the
769    /// simulation stays on the interpreter permanently; the reason becomes
770    /// retrievable through [`TieredBackend::promotion_error`]. A result that
771    /// already reached the channel (or arrives afterwards, for example from
772    /// a tier that ignores the token) is rejected so cancellation is
773    /// honored regardless of compiler timing. Returns whether a background
774    /// compilation was still pending, so callers that only want to reclaim
775    /// a finished worker can ignore the call cheaply.
776    pub fn cancel_background_compilation(&mut self) -> bool {
777        let pending = matches!(self.promotion, Promotion::Pending(_));
778        self.cancel.cancel();
779        if pending {
780            self.promotion = Promotion::Failed(super::compile_cancel::cancelled_error());
781        }
782        pending
783    }
784
785    /// Record one evaluation iteration against the currently active tier.
786    /// Interpreter iterations also feed the [`TierPromotion::AfterSteps`]
787    /// adoption threshold.
788    fn count_evaluation_for_current_tier(&mut self) {
789        if matches!(self.phase, Phase::Interpreting(Some(_))) {
790            self.interpreted_steps = self.interpreted_steps.saturating_add(1);
791        } else if matches!(self.phase, Phase::Compiled(_)) {
792            self.compiled_steps = self.compiled_steps.saturating_add(1);
793        }
794    }
795
796    /// Adopt the compiled tier if background compilation finished. Called at
797    /// scheduler safe points (between evaluation phases) where no unit is
798    /// mid-execution and the memory image can move atomically from the
799    /// caller's perspective.
800    fn maybe_promote(&mut self) {
801        if !matches!(self.phase, Phase::Interpreting(Some(_))) {
802            return;
803        }
804        self.safe_point_polls = self.safe_point_polls.saturating_add(1);
805        // Never promote inside a split evaluate/apply pair: the compiled
806        // apply phase must observe the interpreted evaluate-only results,
807        // and adoption clears the sparse metadata they are tracked in.
808        if self.pending_split_applies > 0 {
809            self.split_apply_deferrals = self.split_apply_deferrals.saturating_add(1);
810            return;
811        }
812        // Honor the minimum interpreted-step count so short simulations are
813        // not disrupted by an adoption their remaining run cannot amortize.
814        if self.interpreted_steps < self.promotion_threshold {
815            self.threshold_deferrals = self.threshold_deferrals.saturating_add(1);
816            return;
817        }
818        let Promotion::Pending(receiver) = &self.promotion else {
819            return;
820        };
821        // Cancellation wins over a queued success: a compile that finished
822        // before (or ignores) the flag must not adopt after the request.
823        if self.cancel.is_cancelled() {
824            self.promotion = Promotion::Failed(super::compile_cancel::cancelled_error());
825            return;
826        }
827        let Ok(result) = receiver.try_recv() else {
828            return;
829        };
830
831        let Ok(code) = result else {
832            // Compilation errors keep the simulation on the interpreter;
833            // the reason stays retrievable via promotion_error().
834            self.promotion = match result {
835                Err(error) => Promotion::Failed(error),
836                Ok(_) => unreachable!("checked above"),
837            };
838            return;
839        };
840        // Adoption grows the transferred image within its existing allocation
841        // so promotion never moves the live memory image (external views such
842        // as zero-copy host buffers may still reference it). If the compiled
843        // tier outgrew the reserved interpreter arena, decline promotion and
844        // stay interpreted instead of reallocating.
845        if let Phase::Interpreting(Some(interp)) = &mut self.phase {
846            let required = code.required_image_words();
847            if interp.image_word_capacity() < required {
848                self.promotion = Promotion::Failed(SimulatorError::new(
849                    crate::SimulatorErrorKind::Codegen(crate::CodegenError::message(format!(
850                        "native image needs {required} words but the reserved interpreter \
851                         image only has {} words of capacity; refusing to move the live \
852                         memory image and staying interpreted",
853                        interp.image_word_capacity()
854                    ))),
855                ));
856                return;
857            }
858        }
859        let mut adopted = None;
860        if let Phase::Interpreting(slot) = &mut self.phase {
861            if let Some(mut interp) = slot.take() {
862                let (memory, runtime_event_buffer, comb_capture_enabled) = interp.tier_transfer();
863                drop(interp);
864                adopted = Some(CompiledTier::adopt(
865                    code,
866                    memory,
867                    runtime_event_buffer,
868                    comb_capture_enabled,
869                ));
870            }
871        }
872        if let Some(mut compiled) = adopted {
873            let promotion_span = tracing::info_span!(
874                "celox.tiered.promote",
875                interpreted_evaluations = self.interpreted_steps
876            );
877            let _entered = promotion_span.enter();
878            // Clear sparse metadata so the compiled tier starts with clean
879            // tracking; stale dirty bits from interpreted-tier evaluation
880            // could confuse the compiled commit logic.
881            let sparse_metas: Vec<_> = {
882                let layout = compiled.layout();
883                layout
884                    .sparse_layouts
885                    .values()
886                    .map(|s| {
887                        (
888                            s.dirty_words_offset,
889                            s.dirty_word_count * 8,
890                            s.summary_words_offset,
891                            s.summary_word_count * 8,
892                        )
893                    })
894                    .collect()
895            };
896            unsafe {
897                let base = compiled.memory_base_mut();
898                for &(dwo, dwc, swo, swc) in &sparse_metas {
899                    std::ptr::write_bytes(base.add(dwo), 0, dwc);
900                    std::ptr::write_bytes(base.add(swo), 0, swc);
901                }
902            }
903            self.phase = Phase::Compiled(compiled);
904            self.promotion = Promotion::Promoted;
905            self.promoted_after_interpreted_steps = Some(self.interpreted_steps);
906            if let Some(timing) = &mut self.execution_timing
907                && timing.promotion_elapsed.is_none()
908            {
909                timing.promotion_elapsed = Some(timing.started_at.elapsed());
910            }
911            tracing::info!("tiered backend adopted generated code");
912        }
913    }
914}
915
916impl Drop for TieredBackend {
917    fn drop(&mut self) {
918        self.cancel.cancel();
919        if let Some(worker) = self.compiler_worker.take() {
920            let _ = worker.join();
921        }
922    }
923}
924
925impl SimBackend for TieredBackend {
926    type Event = TieredEventRef;
927
928    fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode> {
929        self.maybe_promote();
930        self.count_evaluation_for_current_tier();
931        match &mut self.phase {
932            Phase::Interpreting(Some(interp)) => interp.eval_comb(),
933            Phase::Compiled(compiled) => compiled.eval_comb(),
934            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
935        }
936    }
937
938    fn eval_apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
939        self.maybe_promote();
940        self.count_evaluation_for_current_tier();
941        match &mut self.phase {
942            Phase::Interpreting(Some(interp)) => interp.eval_apply_ff_at(
943                super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
944            ),
945            Phase::Compiled(compiled) => compiled.eval_apply_ff_at(event.id()),
946            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
947        }
948    }
949
950    fn eval_comb_apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
951        self.maybe_promote();
952        self.count_evaluation_for_current_tier();
953        match &mut self.phase {
954            Phase::Interpreting(Some(interp)) => interp.eval_comb_apply_ff_at(
955                super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
956            ),
957            Phase::Compiled(compiled) => compiled.eval_comb_apply_ff_at(event.id()),
958            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
959        }
960    }
961
962    fn eval_comb_apply_ff_many_at(
963        &mut self,
964        event: TieredEventRef,
965        count: u64,
966    ) -> (u64, Result<(), SimulatorErrorCode>) {
967        // Poll promotion once for the whole batch so an adopted tier runs
968        // every remaining iteration inside generated code.
969        if count == 0 {
970            return (0, Ok(()));
971        }
972        self.maybe_promote();
973        match &mut self.phase {
974            Phase::Interpreting(Some(interp)) => {
975                self.interpreted_steps = self.interpreted_steps.saturating_add(1);
976                (
977                    1,
978                    interp.eval_comb_apply_ff_at(super::interp::InterpEventRef::from_parts(
979                        event.addr(),
980                        event.id(),
981                    )),
982                )
983            }
984            Phase::Compiled(compiled) => {
985                let (completed, result) = compiled.eval_comb_apply_ff_many_at(event.id(), count);
986                self.compiled_steps = self.compiled_steps.saturating_add(completed);
987                (completed, result)
988            }
989            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
990        }
991    }
992
993    fn eval_only_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
994        self.maybe_promote();
995        self.count_evaluation_for_current_tier();
996        let result = match &mut self.phase {
997            Phase::Interpreting(Some(interp)) => interp.eval_only_ff_at(
998                super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
999            ),
1000            Phase::Compiled(compiled) => compiled.eval_only_ff_at_addr(&event.addr()),
1001            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1002        };
1003        // The paired apply phase must run on the same tier; defer promotion
1004        // until it completes.
1005        self.pending_split_applies = self.pending_split_applies.saturating_add(1);
1006        result
1007    }
1008
1009    fn apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
1010        // Never promote between the evaluate-only and apply phases.
1011        self.maybe_promote();
1012        self.count_evaluation_for_current_tier();
1013        let result = match &mut self.phase {
1014            Phase::Interpreting(Some(interp)) => interp.apply_ff_at(
1015                super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
1016            ),
1017            Phase::Compiled(compiled) => compiled.apply_ff_at_addr(&event.addr()),
1018            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1019        };
1020        self.pending_split_applies = self.pending_split_applies.saturating_sub(1);
1021        result
1022    }
1023
1024    fn resolve_signal(&self, addr: &AbsoluteAddr) -> SignalRef {
1025        match &self.phase {
1026            Phase::Interpreting(Some(interp)) => interp.resolve_signal(addr),
1027            Phase::Compiled(CompiledTier::Jit(jit)) => jit.resolve_signal(addr),
1028            #[cfg(any(
1029                target_arch = "x86_64",
1030                feature = "arm64-codegen",
1031                target_arch = "aarch64"
1032            ))]
1033            Phase::Compiled(CompiledTier::Native(native)) => native.resolve_signal(addr),
1034            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1035        }
1036    }
1037
1038    fn resolve_event(&self, addr: &AbsoluteAddr) -> TieredEventRef {
1039        match &self.phase {
1040            Phase::Interpreting(Some(interp)) => {
1041                let ev = interp.resolve_event(addr);
1042                TieredEventRef {
1043                    addr: ev.addr(),
1044                    id: ev.id(),
1045                }
1046            }
1047            Phase::Compiled(CompiledTier::Jit(jit)) => {
1048                let ev = jit.resolve_event(addr);
1049                TieredEventRef {
1050                    addr: ev.addr(),
1051                    id: ev.id(),
1052                }
1053            }
1054            #[cfg(any(
1055                target_arch = "x86_64",
1056                feature = "arm64-codegen",
1057                target_arch = "aarch64"
1058            ))]
1059            Phase::Compiled(CompiledTier::Native(native)) => {
1060                let ev = native.resolve_event(addr);
1061                TieredEventRef {
1062                    addr: ev.addr(),
1063                    id: ev.id(),
1064                }
1065            }
1066            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1067        }
1068    }
1069
1070    fn resolve_event_opt(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
1071        match &self.phase {
1072            Phase::Interpreting(Some(interp)) => {
1073                interp.resolve_event_opt(addr).map(|ev| TieredEventRef {
1074                    addr: ev.addr(),
1075                    id: ev.id(),
1076                })
1077            }
1078            Phase::Compiled(CompiledTier::Jit(jit)) => {
1079                jit.resolve_event_opt(addr).map(|ev| TieredEventRef {
1080                    addr: ev.addr(),
1081                    id: ev.id(),
1082                })
1083            }
1084            #[cfg(any(
1085                target_arch = "x86_64",
1086                feature = "arm64-codegen",
1087                target_arch = "aarch64"
1088            ))]
1089            Phase::Compiled(CompiledTier::Native(native)) => {
1090                native.resolve_event_opt(addr).map(|ev| TieredEventRef {
1091                    addr: ev.addr(),
1092                    id: ev.id(),
1093                })
1094            }
1095            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1096        }
1097    }
1098
1099    fn resolve_eval_only_event(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
1100        match &self.phase {
1101            Phase::Interpreting(Some(interp)) => {
1102                interp
1103                    .resolve_eval_only_event(addr)
1104                    .map(|ev| TieredEventRef {
1105                        addr: ev.addr(),
1106                        id: ev.id(),
1107                    })
1108            }
1109            Phase::Compiled(CompiledTier::Jit(jit)) => {
1110                jit.resolve_eval_only_event(addr).map(|ev| TieredEventRef {
1111                    addr: ev.addr(),
1112                    id: ev.id(),
1113                })
1114            }
1115            #[cfg(any(
1116                target_arch = "x86_64",
1117                feature = "arm64-codegen",
1118                target_arch = "aarch64"
1119            ))]
1120            Phase::Compiled(CompiledTier::Native(native)) => native
1121                .resolve_eval_only_event(addr)
1122                .map(|ev| TieredEventRef {
1123                    addr: ev.addr(),
1124                    id: ev.id(),
1125                }),
1126            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1127        }
1128    }
1129
1130    fn resolve_apply_event(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
1131        match &self.phase {
1132            Phase::Interpreting(Some(interp)) => {
1133                interp.resolve_apply_event(addr).map(|ev| TieredEventRef {
1134                    addr: ev.addr(),
1135                    id: ev.id(),
1136                })
1137            }
1138            Phase::Compiled(CompiledTier::Jit(jit)) => {
1139                jit.resolve_apply_event(addr).map(|ev| TieredEventRef {
1140                    addr: ev.addr(),
1141                    id: ev.id(),
1142                })
1143            }
1144            #[cfg(any(
1145                target_arch = "x86_64",
1146                feature = "arm64-codegen",
1147                target_arch = "aarch64"
1148            ))]
1149            Phase::Compiled(CompiledTier::Native(native)) => {
1150                native.resolve_apply_event(addr).map(|ev| TieredEventRef {
1151                    addr: ev.addr(),
1152                    id: ev.id(),
1153                })
1154            }
1155            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1156        }
1157    }
1158
1159    fn set<T: Copy>(&mut self, signal: SignalRef, value: T) {
1160        match &mut self.phase {
1161            Phase::Interpreting(Some(interp)) => interp.set(signal, value),
1162            Phase::Compiled(CompiledTier::Jit(jit)) => jit.set(signal, value),
1163            #[cfg(any(
1164                target_arch = "x86_64",
1165                feature = "arm64-codegen",
1166                target_arch = "aarch64"
1167            ))]
1168            Phase::Compiled(CompiledTier::Native(native)) => native.set(signal, value),
1169            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1170        }
1171    }
1172
1173    fn set_wide(&mut self, signal: SignalRef, value: BigUint) {
1174        match &mut self.phase {
1175            Phase::Interpreting(Some(interp)) => interp.set_wide(signal, value),
1176            Phase::Compiled(CompiledTier::Jit(jit)) => jit.set_wide(signal, value),
1177            #[cfg(any(
1178                target_arch = "x86_64",
1179                feature = "arm64-codegen",
1180                target_arch = "aarch64"
1181            ))]
1182            Phase::Compiled(CompiledTier::Native(native)) => native.set_wide(signal, value),
1183            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1184        }
1185    }
1186
1187    fn set_four_state(&mut self, signal: SignalRef, value: BigUint, mask: BigUint) {
1188        match &mut self.phase {
1189            Phase::Interpreting(Some(interp)) => interp.set_four_state(signal, value, mask),
1190            Phase::Compiled(CompiledTier::Jit(jit)) => jit.set_four_state(signal, value, mask),
1191            #[cfg(any(
1192                target_arch = "x86_64",
1193                feature = "arm64-codegen",
1194                target_arch = "aarch64"
1195            ))]
1196            Phase::Compiled(CompiledTier::Native(native)) => {
1197                native.set_four_state(signal, value, mask)
1198            }
1199            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1200        }
1201    }
1202
1203    fn get(&self, signal: SignalRef) -> BigUint {
1204        match &self.phase {
1205            Phase::Interpreting(Some(interp)) => interp.get(signal),
1206            Phase::Compiled(CompiledTier::Jit(jit)) => jit.get(signal),
1207            #[cfg(any(
1208                target_arch = "x86_64",
1209                feature = "arm64-codegen",
1210                target_arch = "aarch64"
1211            ))]
1212            Phase::Compiled(CompiledTier::Native(native)) => native.get(signal),
1213            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1214        }
1215    }
1216
1217    fn get_as<T: Default + Copy>(&self, signal: SignalRef) -> T {
1218        match &self.phase {
1219            Phase::Interpreting(Some(interp)) => interp.get_as(signal),
1220            Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_as(signal),
1221            #[cfg(any(
1222                target_arch = "x86_64",
1223                feature = "arm64-codegen",
1224                target_arch = "aarch64"
1225            ))]
1226            Phase::Compiled(CompiledTier::Native(native)) => native.get_as(signal),
1227            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1228        }
1229    }
1230
1231    fn get_four_state(&self, signal: SignalRef) -> (BigUint, BigUint) {
1232        match &self.phase {
1233            Phase::Interpreting(Some(interp)) => interp.get_four_state(signal),
1234            Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_four_state(signal),
1235            #[cfg(any(
1236                target_arch = "x86_64",
1237                feature = "arm64-codegen",
1238                target_arch = "aarch64"
1239            ))]
1240            Phase::Compiled(CompiledTier::Native(native)) => native.get_four_state(signal),
1241            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1242        }
1243    }
1244
1245    fn memory_as_ptr(&self) -> (*const u8, usize) {
1246        match &self.phase {
1247            Phase::Interpreting(Some(interp)) => interp.memory_as_ptr(),
1248            Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_as_ptr(),
1249            #[cfg(any(
1250                target_arch = "x86_64",
1251                feature = "arm64-codegen",
1252                target_arch = "aarch64"
1253            ))]
1254            Phase::Compiled(CompiledTier::Native(native)) => native.memory_as_ptr(),
1255            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1256        }
1257    }
1258
1259    fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
1260        match &mut self.phase {
1261            Phase::Interpreting(Some(interp)) => interp.memory_as_mut_ptr(),
1262            Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_as_mut_ptr(),
1263            #[cfg(any(
1264                target_arch = "x86_64",
1265                feature = "arm64-codegen",
1266                target_arch = "aarch64"
1267            ))]
1268            Phase::Compiled(CompiledTier::Native(native)) => native.memory_as_mut_ptr(),
1269            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1270        }
1271    }
1272
1273    fn memory_owner(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
1274        match &self.phase {
1275            Phase::Interpreting(Some(interp)) => interp.memory_owner(),
1276            Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_owner(),
1277            #[cfg(any(
1278                target_arch = "x86_64",
1279                feature = "arm64-codegen",
1280                target_arch = "aarch64"
1281            ))]
1282            Phase::Compiled(CompiledTier::Native(native)) => native.memory_owner(),
1283            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1284        }
1285    }
1286
1287    fn vcd_tracking_enabled(&self) -> bool {
1288        match &self.phase {
1289            Phase::Interpreting(Some(interp)) => interp.vcd_tracking_enabled(),
1290            Phase::Compiled(CompiledTier::Jit(jit)) => jit.vcd_tracking_enabled(),
1291            #[cfg(any(
1292                target_arch = "x86_64",
1293                feature = "arm64-codegen",
1294                target_arch = "aarch64"
1295            ))]
1296            Phase::Compiled(CompiledTier::Native(native)) => native.vcd_tracking_enabled(),
1297            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1298        }
1299    }
1300
1301    fn runtime_event_buffer_as_ptr(&self) -> (*const u8, usize) {
1302        match &self.phase {
1303            Phase::Interpreting(Some(interp)) => interp.runtime_event_buffer_as_ptr(),
1304            Phase::Compiled(CompiledTier::Jit(jit)) => jit.runtime_event_buffer_as_ptr(),
1305            #[cfg(any(
1306                target_arch = "x86_64",
1307                feature = "arm64-codegen",
1308                target_arch = "aarch64"
1309            ))]
1310            Phase::Compiled(CompiledTier::Native(native)) => native.runtime_event_buffer_as_ptr(),
1311            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1312        }
1313    }
1314
1315    fn runtime_event_buffer(&self) -> Option<Arc<RuntimeEventBuffer>> {
1316        match &self.phase {
1317            Phase::Interpreting(Some(interp)) => interp.runtime_event_buffer(),
1318            Phase::Compiled(CompiledTier::Jit(jit)) => Some(jit.runtime_event_buffer().clone()),
1319            #[cfg(any(
1320                target_arch = "x86_64",
1321                feature = "arm64-codegen",
1322                target_arch = "aarch64"
1323            ))]
1324            Phase::Compiled(CompiledTier::Native(native)) => native.runtime_event_buffer(),
1325            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1326        }
1327    }
1328
1329    fn set_comb_capture_event_enabled(&mut self, active_sites: &[bool]) {
1330        match &mut self.phase {
1331            Phase::Interpreting(Some(interp)) => {
1332                interp.set_comb_capture_event_enabled(active_sites)
1333            }
1334            Phase::Compiled(CompiledTier::Jit(jit)) => {
1335                jit.set_comb_capture_event_enabled(active_sites)
1336            }
1337            #[cfg(any(
1338                target_arch = "x86_64",
1339                feature = "arm64-codegen",
1340                target_arch = "aarch64"
1341            ))]
1342            Phase::Compiled(CompiledTier::Native(native)) => {
1343                native.set_comb_capture_event_enabled(active_sites)
1344            }
1345            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1346        }
1347    }
1348
1349    fn stable_region_size(&self) -> usize {
1350        match &self.phase {
1351            Phase::Interpreting(Some(interp)) => interp.stable_region_size(),
1352            Phase::Compiled(CompiledTier::Jit(jit)) => jit.stable_region_size(),
1353            #[cfg(any(
1354                target_arch = "x86_64",
1355                feature = "arm64-codegen",
1356                target_arch = "aarch64"
1357            ))]
1358            Phase::Compiled(CompiledTier::Native(native)) => native.stable_region_size(),
1359            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1360        }
1361    }
1362
1363    fn layout(&self) -> &MemoryLayout {
1364        match &self.phase {
1365            Phase::Interpreting(Some(interp)) => interp.layout(),
1366            Phase::Compiled(CompiledTier::Jit(jit)) => jit.layout(),
1367            #[cfg(any(
1368                target_arch = "x86_64",
1369                feature = "arm64-codegen",
1370                target_arch = "aarch64"
1371            ))]
1372            Phase::Compiled(CompiledTier::Native(native)) => native.layout(),
1373            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1374        }
1375    }
1376
1377    fn id_to_addr_slice(&self) -> &[AbsoluteAddr] {
1378        match &self.phase {
1379            Phase::Interpreting(Some(interp)) => interp.id_to_addr_slice(),
1380            Phase::Compiled(CompiledTier::Jit(jit)) => jit.id_to_addr_slice(),
1381            #[cfg(any(
1382                target_arch = "x86_64",
1383                feature = "arm64-codegen",
1384                target_arch = "aarch64"
1385            ))]
1386            Phase::Compiled(CompiledTier::Native(native)) => native.id_to_addr_slice(),
1387            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1388        }
1389    }
1390
1391    fn id_to_event_slice(&self) -> &[TieredEventRef] {
1392        &self.events
1393    }
1394
1395    fn num_events(&self) -> usize {
1396        match &self.phase {
1397            Phase::Interpreting(Some(interp)) => interp.num_events(),
1398            Phase::Compiled(CompiledTier::Jit(jit)) => jit.num_events(),
1399            #[cfg(any(
1400                target_arch = "x86_64",
1401                feature = "arm64-codegen",
1402                target_arch = "aarch64"
1403            ))]
1404            Phase::Compiled(CompiledTier::Native(native)) => native.num_events(),
1405            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1406        }
1407    }
1408
1409    fn clear_triggered_bits(&mut self) {
1410        match &mut self.phase {
1411            Phase::Interpreting(Some(interp)) => interp.clear_triggered_bits(),
1412            Phase::Compiled(CompiledTier::Jit(jit)) => jit.clear_triggered_bits(),
1413            #[cfg(any(
1414                target_arch = "x86_64",
1415                feature = "arm64-codegen",
1416                target_arch = "aarch64"
1417            ))]
1418            Phase::Compiled(CompiledTier::Native(native)) => native.clear_triggered_bits(),
1419            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1420        }
1421    }
1422
1423    fn mark_triggered_bit(&mut self, id: usize) {
1424        match &mut self.phase {
1425            Phase::Interpreting(Some(interp)) => interp.mark_triggered_bit(id),
1426            Phase::Compiled(CompiledTier::Jit(jit)) => jit.mark_triggered_bit(id),
1427            #[cfg(any(
1428                target_arch = "x86_64",
1429                feature = "arm64-codegen",
1430                target_arch = "aarch64"
1431            ))]
1432            Phase::Compiled(CompiledTier::Native(native)) => native.mark_triggered_bit(id),
1433            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1434        }
1435    }
1436
1437    fn get_triggered_bits(&self) -> bit_set::BitSet {
1438        match &self.phase {
1439            Phase::Interpreting(Some(interp)) => interp.get_triggered_bits(),
1440            Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_triggered_bits(),
1441            #[cfg(any(
1442                target_arch = "x86_64",
1443                feature = "arm64-codegen",
1444                target_arch = "aarch64"
1445            ))]
1446            Phase::Compiled(CompiledTier::Native(native)) => native.get_triggered_bits(),
1447            Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
1448        }
1449    }
1450}
1451
1452#[cfg(test)]
1453mod tests {
1454    use super::*;
1455    use crate::backend::native::NativeBackend;
1456    use crate::{CodegenError, RuntimeEvent, Simulator, SimulatorBuilder, SimulatorErrorKind};
1457    use std::sync::atomic::{AtomicBool, Ordering};
1458
1459    /// Two-stage pipeline: `q` trails `d` by two clock edges.
1460    const PIPELINE: &str = r#"
1461module Top (
1462    clk: input clock,
1463    rst: input reset,
1464    d: input logic<8>,
1465    q: output logic<8>,
1466) {
1467    var stage1: logic<8>;
1468    var stage2: logic<8>;
1469    always_ff (clk, rst) {
1470        if_reset {
1471            stage1 = 0;
1472            stage2 = 0;
1473        } else {
1474            stage1 = d;
1475            stage2 = stage1;
1476        }
1477    }
1478    assign q = stage2;
1479}
1480"#;
1481
1482    struct Gate(Arc<AtomicBool>);
1483
1484    impl Gate {
1485        fn closed() -> Self {
1486            Self(Arc::new(AtomicBool::new(false)))
1487        }
1488
1489        fn open(&self) {
1490            self.0.store(true, Ordering::SeqCst);
1491        }
1492    }
1493
1494    fn wait_for_gate_or_cancel(
1495        gate: &AtomicBool,
1496        cancel: &CompileCancel,
1497    ) -> Result<(), SimulatorError> {
1498        while !gate.load(Ordering::Acquire) {
1499            if cancel.is_cancelled() {
1500                return Err(super::super::compile_cancel::cancelled_error());
1501            }
1502            std::thread::sleep(std::time::Duration::from_millis(1));
1503        }
1504        Ok(())
1505    }
1506
1507    /// Compile through the Cranelift tier behind a gate. The native tier is
1508    /// covered end-to-end by the public `build_tiered` integration tests on
1509    /// native hosts; gating lets these unit tests pin promotion timing.
1510    fn build_gated(gate: &Gate) -> Simulator<TieredBackend> {
1511        let worker_gate = gate.0.clone();
1512        SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
1513            .build_tiered_with_compiler(move |laid_out, options, cancel| {
1514                wait_for_gate_or_cancel(&worker_gate, cancel)?;
1515                {
1516                    let image = NativeBackend::compile_image(laid_out, options)?;
1517                    let shared = unsafe { SharedNativeCode::from_image(image)? };
1518                    Ok(CompiledCode::Native(Arc::new(shared)))
1519                }
1520            })
1521            .unwrap()
1522    }
1523
1524    /// Drive the pipeline through a reset and an input sweep, recording `q`
1525    /// after every tick. Identical inputs must produce identical outputs no
1526    /// matter when (or whether) promotion lands, because both tiers are
1527    /// bit-exact.
1528    fn drive(sim: &mut Simulator<TieredBackend>, inputs: &[u8]) -> Vec<u8> {
1529        let clk = sim.event("clk");
1530        let rst = sim.signal("rst");
1531        let d = sim.signal("d");
1532
1533        sim.modify(|io| {
1534            io.set(rst, 0u8);
1535            io.set(d, inputs[0]);
1536        })
1537        .unwrap();
1538        sim.tick(clk).unwrap();
1539        sim.tick(clk).unwrap();
1540        sim.modify(|io| io.set(rst, 1u8)).unwrap();
1541
1542        let mut observed = Vec::new();
1543        for &value in inputs {
1544            sim.modify(|io| io.set(d, value)).unwrap();
1545            sim.tick(clk).unwrap();
1546            sim.tick(clk).unwrap();
1547            observed.push(sim.get_as::<u8>(sim.signal("q")));
1548        }
1549        observed
1550    }
1551
1552    fn reference_outputs(inputs: &[u8]) -> Vec<u8> {
1553        // Each iteration ticks twice before sampling, which fully absorbs
1554        // the pipeline's two-cycle latency: the sampled output equals the
1555        // current input.
1556        inputs.to_vec()
1557    }
1558
1559    #[test]
1560    fn unpacked_array_works_across_promotion_on_strided_layout() {
1561        // On native-default hosts this runs the interpreter against an
1562        // element-strided layout, exercising strided element addressing and
1563        // plane-sized state before and after promotion.
1564        let code = r#"
1565module Top (
1566    clk: input clock,
1567    we: input logic,
1568    waddr: input logic<3>,
1569    wdata: input logic<8>,
1570    raddr: input logic<3>,
1571    q: output logic<8>,
1572) {
1573    var mem: logic<8>[8];
1574    always_ff (clk) {
1575        if we {
1576            mem[waddr] = wdata;
1577        }
1578    }
1579    assign q = mem[raddr];
1580}
1581"#;
1582        let gate = Gate::closed();
1583        let worker_gate = gate.0.clone();
1584        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
1585            .build_tiered_with_compiler(move |laid_out, options, cancel| {
1586                wait_for_gate_or_cancel(&worker_gate, cancel)?;
1587                {
1588                    let image = NativeBackend::compile_image(laid_out, options)?;
1589                    let shared = unsafe { SharedNativeCode::from_image(image)? };
1590                    Ok(CompiledCode::Native(Arc::new(shared)))
1591                }
1592            })
1593            .unwrap();
1594        let clk = sim.event("clk");
1595        let we = sim.signal("we");
1596        let waddr = sim.signal("waddr");
1597        let wdata = sim.signal("wdata");
1598        let raddr = sim.signal("raddr");
1599        let q = sim.signal("q");
1600
1601        // Fill every element while still interpreted.
1602        for i in 0..8u8 {
1603            sim.modify(|io| {
1604                io.set(we, 1u8);
1605                io.set(waddr, i);
1606                io.set(wdata, i * 7 + 1);
1607            })
1608            .unwrap();
1609            sim.tick(clk).unwrap();
1610        }
1611        sim.modify(|io| io.set(we, 0u8)).unwrap();
1612
1613        let read_back = |sim: &mut Simulator<TieredBackend>, index: u8| -> u8 {
1614            sim.modify(|io| io.set(raddr, index)).unwrap();
1615            sim.get_as::<u8>(q)
1616        };
1617
1618        // Verify interpreted results...
1619        for i in 0..8u8 {
1620            assert_eq!(read_back(&mut sim, i), i * 7 + 1);
1621        }
1622
1623        // ...release the worker and promote...
1624        gate.open();
1625        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
1626        while !sim.is_compiled() && std::time::Instant::now() < deadline {
1627            sim.tick(clk).unwrap();
1628            std::thread::sleep(std::time::Duration::from_millis(1));
1629        }
1630        assert!(sim.is_compiled());
1631
1632        // ...and confirm the whole array survived the memory handoff.
1633        for i in 0..8u8 {
1634            assert_eq!(read_back(&mut sim, i), i * 7 + 1, "element {i}");
1635        }
1636
1637        // Writes keep working on the compiled tier too.
1638        sim.modify(|io| {
1639            io.set(we, 1u8);
1640            io.set(waddr, 3u8);
1641            io.set(wdata, 250u8);
1642        })
1643        .unwrap();
1644        sim.tick(clk).unwrap();
1645        sim.modify(|io| io.set(we, 0u8)).unwrap();
1646        assert_eq!(read_back(&mut sim, 3), 250u8);
1647    }
1648
1649    /// Four-state dynamically-indexed array through native promotion.
1650    ///
1651    /// Exercises element-strided addressing, sparse commit, and mask plane
1652    /// handling across the tier boundary.
1653    #[test]
1654    fn four_state_unpacked_array_planes_initialize_and_survive_promotion() {
1655        let code = r#"
1656module Top (
1657    clk: input clock,
1658    we: input logic,
1659    waddr: input logic<2>,
1660    wdata: input logic<8>,
1661    raddr: input logic<2>,
1662    q: output logic<8>,
1663) {
1664    var mem: logic<8>[4];
1665    always_ff (clk) {
1666        if we {
1667            mem[waddr] = wdata;
1668        }
1669    }
1670    assign q = mem[raddr];
1671}
1672"#;
1673        let gate = Gate::closed();
1674        let worker_gate = gate.0.clone();
1675        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
1676            .four_state(true)
1677            .build_tiered_with_compiler(move |laid_out, options, cancel| {
1678                wait_for_gate_or_cancel(&worker_gate, cancel)?;
1679                let image = NativeBackend::compile_image(laid_out, options)?;
1680                let shared = unsafe { SharedNativeCode::from_image(image)? };
1681                Ok(CompiledCode::Native(Arc::new(shared)))
1682            })
1683            .unwrap();
1684        let clk = sim.event("clk");
1685        let we = sim.signal("we");
1686        let waddr = sim.signal("waddr");
1687        let wdata = sim.signal("wdata");
1688        let raddr = sim.signal("raddr");
1689        let q = sim.signal("q");
1690
1691        // Write all elements on the interpreted tier.
1692        for i in 0..4u8 {
1693            sim.modify(|io| {
1694                io.set(we, 1u8);
1695                io.set(waddr, i);
1696                io.set(wdata, i * 60 + 5);
1697            })
1698            .unwrap();
1699            sim.tick(clk).unwrap();
1700        }
1701        sim.modify(|io| io.set(we, 0u8)).unwrap();
1702
1703        // Verify interpreted-tier self-consistency.
1704        for i in 0..4u8 {
1705            sim.modify(|io| io.set(raddr, i)).unwrap();
1706            assert_eq!(sim.get_as::<u8>(q), i * 60 + 5);
1707        }
1708
1709        // Promote to native tier.
1710        gate.open();
1711        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
1712        while !sim.is_compiled() && std::time::Instant::now() < deadline {
1713            sim.tick(clk).unwrap();
1714            std::thread::sleep(std::time::Duration::from_millis(2));
1715        }
1716        assert!(sim.is_compiled());
1717
1718        // Verify all elements survived promotion and are readable on native.
1719        for i in 0..4u8 {
1720            sim.modify(|io| io.set(raddr, i)).unwrap();
1721            assert_eq!(
1722                sim.get_as::<u8>(q),
1723                i * 60 + 5,
1724                "element {i} after native promotion"
1725            );
1726        }
1727
1728        // Writes keep working on the native tier.
1729        sim.modify(|io| {
1730            io.set(we, 1u8);
1731            io.set(waddr, 2u8);
1732            io.set(wdata, 200u8);
1733        })
1734        .unwrap();
1735        sim.tick(clk).unwrap();
1736        sim.modify(|io| io.set(we, 0u8)).unwrap();
1737        sim.modify(|io| io.set(raddr, 2u8)).unwrap();
1738        assert_eq!(sim.get_as::<u8>(q), 200u8, "native-tier write");
1739    }
1740
1741    /// Compare Packed-mode interpreter vs ElementStrided-mode interpreter
1742    /// (no promotion). If these differ, the strided addressing formula is
1743    /// wrong independent of any tier transition.
1744    #[test]
1745    fn strided_interp_matches_packed_interp_for_four_state_array() {
1746        let code = r#"
1747module Top (
1748    clk: input clock,
1749    we: input logic,
1750    waddr: input logic<2>,
1751    wdata: input logic<8>,
1752    raddr: input logic<2>,
1753    q: output logic<8>,
1754) {
1755    var mem: logic<8>[4];
1756    always_ff (clk) {
1757        if we {
1758            mem[waddr] = wdata;
1759        }
1760    }
1761    assign q = mem[raddr];
1762}
1763"#;
1764        // Reference: Packed-mode interpreter (build_interpreter always uses Packed).
1765        // NOTE: build_interpreter is pub so we can call it from integration tests,
1766        // but from unit tests inside the crate we use the builder directly.
1767        let mut packed_sim = SimulatorBuilder::<Simulator>::new(code, "Top")
1768            .four_state(true)
1769            .build_interpreter()
1770            .unwrap();
1771
1772        // Strided: ElementStrided interpreter via gated tiered (never promotes).
1773        let gate = Gate::closed();
1774        let worker_gate = gate.0.clone();
1775        let mut strided_sim: Simulator<TieredBackend> =
1776            SimulatorBuilder::<Simulator>::new(code, "Top")
1777                .four_state(true)
1778                .build_tiered_with_compiler(move |laid_out, options, cancel| {
1779                    wait_for_gate_or_cancel(&worker_gate, cancel)?;
1780                    {
1781                        let image = NativeBackend::compile_image(laid_out, options)?;
1782                        let shared = unsafe { SharedNativeCode::from_image(image)? };
1783                        Ok(CompiledCode::Native(Arc::new(shared)))
1784                    }
1785                })
1786                .unwrap();
1787
1788        let packed_clk = packed_sim.event("clk");
1789        let strided_clk = strided_sim.event("clk");
1790        let packed_we = packed_sim.signal("we");
1791        let strided_we = strided_sim.signal("we");
1792        let packed_waddr = packed_sim.signal("waddr");
1793        let strided_waddr = strided_sim.signal("waddr");
1794        let packed_wdata = packed_sim.signal("wdata");
1795        let strided_wdata = strided_sim.signal("wdata");
1796        let packed_raddr = packed_sim.signal("raddr");
1797        let strided_raddr = strided_sim.signal("raddr");
1798
1799        // Write identical data to both simulators.
1800        for i in 0..4u8 {
1801            // Drive packed
1802            packed_sim
1803                .modify(|io| {
1804                    io.set(packed_we, 1u8);
1805                    io.set(packed_waddr, i);
1806                    io.set(packed_wdata, i * 60 + 5);
1807                })
1808                .unwrap();
1809            packed_sim.tick(packed_clk).unwrap();
1810            // Drive strided
1811            strided_sim
1812                .modify(|io| {
1813                    io.set(strided_we, 1u8);
1814                    io.set(strided_waddr, i);
1815                    io.set(strided_wdata, i * 60 + 5);
1816                })
1817                .unwrap();
1818            strided_sim.tick(strided_clk).unwrap();
1819        }
1820        packed_sim.modify(|io| io.set(packed_we, 0u8)).unwrap();
1821        strided_sim.modify(|io| io.set(strided_we, 0u8)).unwrap();
1822
1823        // Read back and compare.
1824        for i in 0..4u8 {
1825            packed_sim.modify(|io| io.set(packed_raddr, i)).unwrap();
1826            strided_sim.modify(|io| io.set(strided_raddr, i)).unwrap();
1827            let pv = packed_sim.get_as::<u8>(packed_sim.signal("q"));
1828            let sv = strided_sim.get_as::<u8>(strided_sim.signal("q"));
1829            assert_eq!(
1830                pv, sv,
1831                "element {i} diverges between Packed and ElementStrided interp"
1832            );
1833        }
1834    }
1835
1836    #[test]
1837    fn outputs_match_reference_when_promotion_never_happens() {
1838        let inputs: Vec<u8> = (10..40).collect();
1839        let gate = Gate::closed();
1840        let mut sim = build_gated(&gate);
1841        let observed = drive(&mut sim, &inputs);
1842        assert_eq!(observed, reference_outputs(&inputs));
1843        assert!(!sim.is_compiled());
1844        assert!(sim.promotion_error().is_none(), "gate still closed");
1845    }
1846
1847    #[test]
1848    fn outputs_match_reference_when_promotion_lands_mid_run() {
1849        let inputs: Vec<u8> = (10..40).collect();
1850        let gate = Gate::closed();
1851        let mut sim = build_gated(&gate);
1852
1853        let clk = sim.event("clk");
1854        let rst = sim.signal("rst");
1855        let d = sim.signal("d");
1856        sim.modify(|io| {
1857            io.set(rst, 0u8);
1858            io.set(d, inputs[0]);
1859        })
1860        .unwrap();
1861        sim.tick(clk).unwrap();
1862        sim.tick(clk).unwrap();
1863        sim.modify(|io| io.set(rst, 1u8)).unwrap();
1864
1865        // Run the first half strictly interpreted.
1866        let mut observed = Vec::new();
1867        for &value in &inputs[..inputs.len() / 2] {
1868            sim.modify(|io| io.set(d, value)).unwrap();
1869            sim.tick(clk).unwrap();
1870            sim.tick(clk).unwrap();
1871            observed.push(sim.get_as::<u8>(sim.signal("q")));
1872        }
1873
1874        // Release the worker; the next safe points adopt the compiled tier.
1875        gate.open();
1876        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
1877        while !sim.is_compiled() && std::time::Instant::now() < deadline {
1878            // Hold the input steady so any extra ticks are unobservable.
1879            sim.tick(clk).unwrap();
1880            std::thread::sleep(std::time::Duration::from_millis(1));
1881        }
1882        assert!(sim.is_compiled());
1883        assert!(sim.promotion_error().is_none());
1884
1885        for &value in &inputs[inputs.len() / 2..] {
1886            sim.modify(|io| io.set(d, value)).unwrap();
1887            sim.tick(clk).unwrap();
1888            sim.tick(clk).unwrap();
1889            observed.push(sim.get_as::<u8>(sim.signal("q")));
1890        }
1891
1892        assert_eq!(observed, reference_outputs(&inputs));
1893    }
1894
1895    #[test]
1896    fn waveform_activity_survives_promotion_before_dump() {
1897        for expose_raw in [false, true] {
1898            let dir = tempfile::tempdir().unwrap();
1899            let path = dir.path().join("promotion.vcd");
1900            let gate = Gate::closed();
1901            let worker_gate = gate.0.clone();
1902            let mut sim: Simulator<TieredBackend> =
1903                SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
1904                    .vcd(&path)
1905                    .build_tiered_with_compiler(move |laid_out, options, cancel| {
1906                        wait_for_gate_or_cancel(&worker_gate, cancel)?;
1907                        let image = NativeBackend::compile_image(laid_out, options)?;
1908                        let shared = unsafe { SharedNativeCode::from_image(image)? };
1909                        Ok(CompiledCode::Native(Arc::new(shared)))
1910                    })
1911                    .unwrap();
1912            let descs = sim.build_vcd_descs(false);
1913            let mut reference = crate::VcdWriter::from_writer(Vec::new(), &descs);
1914            let dump = |sim: &mut Simulator<TieredBackend>,
1915                        reference: &mut crate::VcdWriter<Vec<u8>>,
1916                        time| {
1917                sim.dump(time);
1918                let (ptr, size) = sim.memory_as_ptr();
1919                reference
1920                    .dump(time, unsafe { std::slice::from_raw_parts(ptr, size) })
1921                    .unwrap();
1922            };
1923            let clk = sim.event("clk");
1924            let rst = sim.signal("rst");
1925            let d = sim.signal("d");
1926            sim.set(rst, 0u8);
1927            sim.tick(clk).unwrap();
1928            dump(&mut sim, &mut reference, 0);
1929            let raw = expose_raw.then(|| {
1930                let (ptr, size) = sim.memory_as_ptr();
1931                // Snapshot before exposing a pointer, then restore the full image.
1932                let snapshot = unsafe { std::slice::from_raw_parts(ptr, size) }.to_vec();
1933                let (ptr, _) = sim.memory_as_mut_ptr();
1934                unsafe {
1935                    std::ptr::copy_nonoverlapping(snapshot.as_ptr(), ptr, size);
1936                }
1937                ptr
1938            });
1939            sim.set(rst, 1u8);
1940            sim.set(d, 0xa5u8);
1941            sim.tick(clk).unwrap();
1942            sim.tick(clk).unwrap();
1943            assert!(!sim.is_compiled());
1944            // Leave interpreted writes pending while the compiled tier is adopted.
1945            gate.open();
1946            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
1947            while !sim.is_compiled() && std::time::Instant::now() < deadline {
1948                sim.tick(clk).unwrap();
1949                std::thread::sleep(std::time::Duration::from_millis(1));
1950            }
1951            assert!(sim.is_compiled(), "{:?}", sim.promotion_error());
1952            dump(&mut sim, &mut reference, 1);
1953            let before = sim.vcd_statistics().unwrap().comparisons;
1954            if let Some(raw) = raw {
1955                // Reuse the pointer obtained while interpreting, without exposing
1956                // a new pointer from the compiled backend or notifying a setter.
1957                unsafe {
1958                    *raw.add(d.offset) = 0x3c;
1959                }
1960            }
1961            dump(&mut sim, &mut reference, 2);
1962            assert_eq!(
1963                sim.vcd_statistics().unwrap().comparisons,
1964                before + if expose_raw { descs.len() as u64 } else { 0 }
1965            );
1966            sim.flush_vcd().unwrap();
1967            let parse = |bytes: Vec<u8>| {
1968                let mut parser = vcd::Parser::new(bytes.as_slice());
1969                parser.parse_header().unwrap();
1970                parser.map(Result::unwrap).collect::<Vec<_>>()
1971            };
1972            assert_eq!(
1973                parse(std::fs::read(path).unwrap()),
1974                parse(reference.into_inner().unwrap())
1975            );
1976        }
1977    }
1978
1979    #[test]
1980    fn compilation_failure_keeps_the_interpreter() {
1981        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
1982            .build_tiered_with_compiler(|_, _, _| {
1983                Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
1984            })
1985            .unwrap();
1986
1987        let inputs: Vec<u8> = (10..20).collect();
1988        let observed = drive(&mut sim, &inputs);
1989        assert_eq!(observed, reference_outputs(&inputs));
1990        assert!(!sim.is_compiled());
1991
1992        // The worker reports its failure through the channel; keep polling
1993        // safe points until it lands (thread start may lag under load).
1994        let clk = sim.event("clk");
1995        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
1996        while sim.promotion_error().is_none() && std::time::Instant::now() < deadline {
1997            sim.tick(clk).unwrap();
1998            std::thread::sleep(std::time::Duration::from_millis(1));
1999        }
2000        assert!(sim.promotion_error().is_some());
2001    }
2002
2003    #[test]
2004    fn runtime_events_stay_continuous_across_promotion() {
2005        let code = r#"
2006module Top (
2007    clk: input clock,
2008    cnt: output logic<8>,
2009) {
2010    var c: logic<8>;
2011    always_ff (clk) {
2012        c = c + 1;
2013        $display("tick %0d", c);
2014    }
2015    assign cnt = c;
2016}
2017"#;
2018        let gate = Gate::closed();
2019        let worker_gate = gate.0.clone();
2020        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
2021            .build_tiered_with_compiler(move |laid_out, options, cancel| {
2022                wait_for_gate_or_cancel(&worker_gate, cancel)?;
2023                {
2024                    let image = NativeBackend::compile_image(laid_out, options)?;
2025                    let shared = unsafe { SharedNativeCode::from_image(image)? };
2026                    Ok(CompiledCode::Native(Arc::new(shared)))
2027                }
2028            })
2029            .unwrap();
2030        let clk = sim.event("clk");
2031
2032        // Five interpreted ticks emit tick 0..4 (display observes the
2033        // pre-increment value).
2034        for _ in 0..5 {
2035            sim.tick(clk).unwrap();
2036        }
2037        let pre = sim.drain_runtime_events();
2038        assert_eq!(pre.len(), 5);
2039        for (index, event) in pre.iter().enumerate() {
2040            let RuntimeEvent::Display { message } = event else {
2041                panic!("unexpected event {event:?}");
2042            };
2043            assert_eq!(message.as_str(), format!("tick {index}"));
2044        }
2045
2046        gate.open();
2047        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
2048        while !sim.is_compiled() && std::time::Instant::now() < deadline {
2049            sim.tick(clk).unwrap();
2050            std::thread::sleep(std::time::Duration::from_millis(1));
2051        }
2052        assert!(sim.is_compiled());
2053        // Discard the unbounded wait-loop events.
2054        let _ = sim.drain_runtime_events();
2055
2056        // Eight compiled-tier ticks continue the counter seamlessly.
2057        const POST_TICKS: u32 = 8;
2058        for _ in 0..POST_TICKS {
2059            sim.tick(clk).unwrap();
2060        }
2061        let base = sim
2062            .get_as::<u8>(sim.signal("cnt"))
2063            .wrapping_sub(POST_TICKS as u8);
2064        let post = sim.drain_runtime_events();
2065        assert_eq!(post.len(), POST_TICKS as usize);
2066        for (index, event) in post.iter().enumerate() {
2067            let RuntimeEvent::Display { message } = event else {
2068                panic!("unexpected event {event:?}");
2069            };
2070            assert_eq!(
2071                message.as_str(),
2072                format!("tick {}", base.wrapping_add(index as u8))
2073            );
2074        }
2075    }
2076
2077    /// Explicit cancellation unwinds background compilation and leaves the
2078    /// interpreter as the permanent tier, with the cancellation retrievable
2079    /// through `promotion_error`.
2080    #[test]
2081    fn cancel_background_compilation_stays_on_the_interpreter() {
2082        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
2083            .build_tiered_with_compiler(|laid_out, options, cancel| {
2084                while !cancel.is_cancelled() {
2085                    std::thread::sleep(std::time::Duration::from_millis(1));
2086                }
2087                let image = NativeBackend::compile_image_with_cancel(laid_out, options, cancel)?;
2088                let shared = unsafe { SharedNativeCode::from_image(image)? };
2089                Ok(CompiledCode::Native(Arc::new(shared)))
2090            })
2091            .unwrap();
2092
2093        assert!(sim.cancel_background_compilation());
2094
2095        let clk = sim.event("clk");
2096        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
2097        while sim.promotion_error().is_none() && std::time::Instant::now() < deadline {
2098            sim.tick(clk).unwrap();
2099            std::thread::sleep(std::time::Duration::from_millis(1));
2100        }
2101        assert!(!sim.is_compiled());
2102        match sim.promotion_error() {
2103            Some(error) => assert!(matches!(
2104                error.kind(),
2105                SimulatorErrorKind::Codegen(CodegenError::Cancelled)
2106            )),
2107            None => panic!("cancellation was not reported through promotion_error"),
2108        }
2109
2110        // The interpreted tier keeps producing correct results afterwards.
2111        let inputs: Vec<u8> = (0..8).collect();
2112        assert_eq!(drive(&mut sim, &inputs), reference_outputs(&inputs));
2113    }
2114
2115    /// Dropping the simulator cancels and joins the worker before returning.
2116    #[test]
2117    fn drop_cancels_pending_background_compilation() {
2118        let saw_cancel = Arc::new(AtomicBool::new(false));
2119        let worker_saw_cancel = Arc::clone(&saw_cancel);
2120        let sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
2121            .build_tiered_with_compiler(move |_laid_out, _options, cancel| {
2122                while !cancel.is_cancelled() {
2123                    std::thread::sleep(std::time::Duration::from_millis(1));
2124                }
2125                worker_saw_cancel.store(true, Ordering::SeqCst);
2126                Err(SimulatorError::new(SimulatorErrorKind::Codegen(
2127                    CodegenError::Cancelled,
2128                )))
2129            })
2130            .unwrap();
2131        drop(sim);
2132        assert!(saw_cancel.load(Ordering::Acquire));
2133    }
2134
2135    /// `TierPromotion::Never` never spawns the background worker and keeps
2136    /// the simulation interpreted permanently without recording an error.
2137    #[test]
2138    fn tier_promotion_never_skips_background_compilation() {
2139        let invoked = Arc::new(AtomicBool::new(false));
2140        let worker_invoked = Arc::clone(&invoked);
2141        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
2142            .tier_promotion(crate::TierPromotion::Never)
2143            .build_tiered_with_compiler(move |_laid_out, _options, _cancel| {
2144                worker_invoked.store(true, Ordering::SeqCst);
2145                Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
2146            })
2147            .unwrap();
2148
2149        // Give a hypothetical worker ample time to start; the policy must
2150        // have prevented the spawn entirely.
2151        std::thread::sleep(std::time::Duration::from_millis(50));
2152        assert!(!invoked.load(Ordering::SeqCst));
2153        assert!(!sim.is_compiled());
2154        assert!(sim.promotion_error().is_none());
2155        let before = sim.tiered_execution_stats();
2156        assert_eq!(before.tier, TieredExecutionTier::Interpreter);
2157        assert_eq!(before.promotion, TieredPromotionStatus::Disabled);
2158        assert_eq!(before.compiled_evaluations, 0);
2159
2160        // The interpreted tier still produces correct results.
2161        let inputs: Vec<u8> = (0..8).collect();
2162        assert_eq!(drive(&mut sim, &inputs), reference_outputs(&inputs));
2163        let after = sim.tiered_execution_stats();
2164        assert!(after.interpreted_evaluations > before.interpreted_evaluations);
2165        assert_eq!(after.compiled_evaluations, 0);
2166    }
2167
2168    /// `TierPromotion::AfterSteps` lets background compilation finish but
2169    /// defers adoption until the interpreter crosses the step threshold.
2170    #[test]
2171    fn tier_promotion_after_steps_defers_adoption() {
2172        const THRESHOLD: u64 = 1000;
2173        let sim_build: Simulator<TieredBackend> =
2174            SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
2175                .tier_promotion(crate::TierPromotion::AfterSteps(THRESHOLD))
2176                .build_tiered_with_compiler(|laid_out, options, _cancel| {
2177                    let image = NativeBackend::compile_image(laid_out, options)?;
2178                    let shared = unsafe { SharedNativeCode::from_image(image)? };
2179                    Ok(CompiledCode::Native(Arc::new(shared)))
2180                })
2181                .unwrap();
2182        let mut sim = sim_build;
2183        let clk = sim.event("clk");
2184        let initial = sim.tiered_execution_stats();
2185        assert!(initial.interpreted_evaluations < THRESHOLD);
2186
2187        // A handful of ticks stay far below the threshold; even once the
2188        // compiled tier is ready, adoption must not land.
2189        for _ in 0..4 {
2190            sim.tick(clk).unwrap();
2191        }
2192        assert!(
2193            !sim.is_compiled(),
2194            "adopted before the interpreted-step threshold"
2195        );
2196
2197        // Crossing the threshold lets subsequent safe points adopt.
2198        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
2199        while !sim.is_compiled() && std::time::Instant::now() < deadline {
2200            sim.tick(clk).unwrap();
2201            std::thread::sleep(std::time::Duration::from_millis(1));
2202        }
2203        assert!(sim.is_compiled());
2204        assert!(sim.promotion_error().is_none());
2205        let promoted = sim.tiered_execution_stats();
2206        assert_eq!(promoted.tier, TieredExecutionTier::Compiled);
2207        assert_eq!(promoted.promotion, TieredPromotionStatus::Promoted);
2208        assert!(promoted.interpreted_evaluations >= THRESHOLD);
2209        assert_eq!(
2210            promoted.promoted_after_interpreted_evaluations,
2211            Some(promoted.interpreted_evaluations)
2212        );
2213        assert!(promoted.threshold_deferrals > 0);
2214
2215        sim.tick(clk).unwrap();
2216        assert!(sim.tiered_execution_stats().compiled_evaluations > promoted.compiled_evaluations);
2217    }
2218
2219    /// Promoting to the Cranelift tier must report an image requirement that
2220    /// covers adoption's resize target (a MemorySpilled plan grows the layout
2221    /// beyond the interpreter's semantic state), and the growth must stay
2222    /// within the reserved arena so the live image never moves.
2223    #[test]
2224    fn cranelift_promotion_covers_its_image_requirement_without_moving_the_image() {
2225        use std::sync::Mutex;
2226
2227        let gate = Gate::closed();
2228        let worker_gate = gate.0.clone();
2229        let observed: Arc<Mutex<Option<(usize, usize)>>> = Arc::new(Mutex::new(None));
2230        let worker_observed = observed.clone();
2231
2232        let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
2233            .build_tiered_with_compiler(move |laid_out, options, cancel| {
2234                wait_for_gate_or_cancel(&worker_gate, cancel)?;
2235                let shared = Arc::new(JitBackend::compile(laid_out, options, None)?);
2236                let code = CompiledCode::Cranelift(Arc::clone(&shared));
2237                // (adoption resize target in words, reported requirement)
2238                *worker_observed.lock().unwrap() = Some((
2239                    shared.layout.merged_total_size.div_ceil(8),
2240                    code.required_image_words(),
2241                ));
2242                Ok(code)
2243            })
2244            .unwrap();
2245
2246        let clk = sim.event("clk");
2247        for _ in 0..2 {
2248            sim.tick(clk).unwrap();
2249        }
2250        let (base_before, _) = sim.memory_as_ptr();
2251
2252        gate.open();
2253        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
2254        while !sim.is_compiled() && std::time::Instant::now() < deadline {
2255            sim.tick(clk).unwrap();
2256            std::thread::sleep(std::time::Duration::from_millis(1));
2257        }
2258        assert!(sim.is_compiled());
2259        assert!(sim.promotion_error().is_none());
2260
2261        let (adopt_target, required) = observed.lock().unwrap().expect("compiler ran");
2262        assert!(
2263            required >= adopt_target,
2264            "required_image_words ({required}) must cover the Cranelift \
2265             adoption resize target ({adopt_target})"
2266        );
2267
2268        let (base_after, _) = sim.memory_as_ptr();
2269        assert_eq!(
2270            base_before, base_after,
2271            "promotion must not move the live memory image"
2272        );
2273    }
2274}