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