#![cfg(feature = "host-runtime")]
use std::sync::Arc;
use std::sync::mpsc;
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use num_bigint::BigUint;
use super::compile_cancel::CompileCancel;
use super::{
EventHandle, MemoryLayout, RuntimeEventBuffer, SharedJitCode, SimBackend, SimulatorErrorCode,
memory_image::MemoryImage,
};
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
use crate::backend::native::SharedNativeCode;
use crate::backend::{InterpBackend, JitBackend};
use crate::{
SimulatorError, SimulatorOptions,
ir::{AbsoluteAddr, LaidOutProgram, SignalRef},
};
pub(crate) fn native_is_default_target() -> bool {
#[cfg(any(
all(target_arch = "x86_64", not(feature = "arm64-codegen")),
all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
))]
{
true
}
#[cfg(not(any(
all(target_arch = "x86_64", not(feature = "arm64-codegen")),
all(target_arch = "aarch64", not(feature = "x86_64-codegen"))
)))]
{
false
}
}
pub(crate) fn default_target_layout_mode() -> crate::backend::memory_layout::MemoryLayoutMode {
if native_is_default_target() {
crate::backend::memory_layout::MemoryLayoutMode::ElementStrided
} else {
crate::backend::memory_layout::MemoryLayoutMode::Packed
}
}
pub(crate) fn tiered_layout_mode(
vcd_recording: bool,
) -> crate::backend::memory_layout::MemoryLayoutMode {
if vcd_recording {
crate::backend::memory_layout::MemoryLayoutMode::Packed
} else {
default_target_layout_mode()
}
}
enum CompiledTier {
Jit(Box<JitBackend>),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Native(Box<crate::backend::native::NativeBackend>),
}
impl CompiledCode {
fn required_image_words(&self) -> usize {
match self {
CompiledCode::Cranelift(shared) => shared.layout.merged_total_size.div_ceil(8),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
CompiledCode::Native(shared) => shared.native_memory_size.div_ceil(8) + 1,
}
}
}
impl CompiledTier {
fn adopt(
code: CompiledCode,
mut memory: MemoryImage,
runtime_event_buffer: Arc<RuntimeEventBuffer>,
comb_capture_enabled: Vec<u8>,
) -> Self {
debug_assert!(memory.capacity_words() >= code.required_image_words());
if memory.len_words() < code.required_image_words() {
memory.resize_zeroed_within_capacity(code.required_image_words());
}
match code {
CompiledCode::Cranelift(shared) => {
Self::Jit(Box::new(JitBackend::adopt_shared_with_state(
shared,
memory,
runtime_event_buffer,
comb_capture_enabled,
)))
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
CompiledCode::Native(shared) => Self::Native(Box::new(
crate::backend::native::NativeBackend::adopt_shared_with_state(
shared,
memory,
runtime_event_buffer,
comb_capture_enabled,
),
)),
}
}
fn layout(&self) -> &MemoryLayout {
match self {
Self::Jit(jit) => jit.layout(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => native.layout(),
}
}
fn memory_base_mut(&mut self) -> *mut u8 {
match self {
Self::Jit(jit) => jit.memory_as_ptr().0.cast_mut(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => native.memory_as_ptr().0.cast_mut(),
}
}
fn memory_word_capacity(&self) -> usize {
match self {
Self::Jit(jit) => {
let (_, size_bytes) = jit.memory_as_ptr();
size_bytes.div_ceil(8)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => native.memory_word_capacity(),
}
}
fn replace_native_code(&mut self, code: CompiledCode) -> bool {
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
if let CompiledCode::Native(shared) = code {
return match self {
Self::Native(native) => {
native.replace_shared_code(shared);
true
}
Self::Jit(_) => false,
};
}
#[cfg(not(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
)))]
{
let _ = code;
}
false
}
fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => jit.eval_comb(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => native.eval_comb(),
}
}
fn eval_apply_ff_at(&mut self, id: usize) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => {
let event = jit.id_to_event_slice()[id];
jit.eval_apply_ff_at(event)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => {
let event = native.id_to_event_slice()[id];
native.eval_apply_ff_at(event)
}
}
}
fn eval_comb_apply_ff_at(&mut self, id: usize) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => {
let event = jit.id_to_event_slice()[id];
jit.eval_comb_apply_ff_at(event)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => {
let event = native.id_to_event_slice()[id];
native.eval_comb_apply_ff_at(event)
}
}
}
fn eval_comb_apply_ff_many_at(
&mut self,
id: usize,
count: u64,
) -> (u64, Result<(), SimulatorErrorCode>) {
match self {
Self::Jit(jit) => (1, {
let event = jit.id_to_event_slice()[id];
jit.eval_comb_apply_ff_at(event)
}),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => {
native.eval_comb_apply_ff_many_at(native.id_to_event_slice()[id], count)
}
}
}
fn eval_only_ff_at_addr(&mut self, addr: &AbsoluteAddr) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => {
let event = jit
.resolve_eval_only_event(addr)
.unwrap_or_else(|| panic!("eval-only event not found for {addr:?}"));
jit.eval_only_ff_at(event)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => {
let event = native
.resolve_eval_only_event(addr)
.unwrap_or_else(|| panic!("eval-only event not found for {addr:?}"));
native.eval_only_ff_at(event)
}
}
}
fn run_process(&mut self, index: usize) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => jit.run_process(index),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => native.run_process(index),
}
}
fn apply_ff_at_addr(&mut self, addr: &AbsoluteAddr) -> Result<(), SimulatorErrorCode> {
match self {
Self::Jit(jit) => {
let event = jit
.resolve_apply_event(addr)
.unwrap_or_else(|| panic!("apply event not found for {addr:?}"));
jit.apply_ff_at(event)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Self::Native(native) => {
let event = native
.resolve_apply_event(addr)
.unwrap_or_else(|| panic!("apply event not found for {addr:?}"));
native.apply_ff_at(event)
}
}
}
}
pub(crate) enum CompiledCode {
Cranelift(Arc<SharedJitCode>),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Native(Arc<SharedNativeCode>),
}
#[derive(Clone, Copy, Debug)]
pub struct TieredEventRef {
addr: AbsoluteAddr,
id: usize,
}
impl EventHandle for TieredEventRef {
fn id(&self) -> usize {
self.id
}
fn addr(&self) -> AbsoluteAddr {
self.addr
}
}
enum Phase {
Interpreting(Option<Box<InterpBackend>>),
Compiled(CompiledTier),
}
enum Promotion {
Pending(mpsc::Receiver<Result<CompiledCode, SimulatorError>>),
Failed(SimulatorError),
Disabled,
Promoted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TieredExecutionTier {
Interpreter,
Compiled,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TieredPromotionStatus {
Pending,
Failed,
Disabled,
Promoted,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct TieredExecutionStats {
pub tier: TieredExecutionTier,
pub promotion: TieredPromotionStatus,
pub interpreted_evaluations: u64,
pub compiled_evaluations: u64,
pub promoted_after_interpreted_evaluations: Option<u64>,
pub safe_point_polls: u64,
pub split_apply_deferrals: u64,
pub threshold_deferrals: u64,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct TieredExecutionTiming {
promotion_elapsed: Option<Duration>,
}
impl TieredExecutionTiming {
pub fn promotion_elapsed(self) -> Option<Duration> {
self.promotion_elapsed
}
}
struct ActiveExecutionTiming {
started_at: Instant,
promotion_elapsed: Option<Duration>,
}
pub struct TieredBackend {
phase: Phase,
promotion: Promotion,
events: Vec<TieredEventRef>,
cancel: CompileCancel,
compiler_worker: Option<JoinHandle<()>>,
upgrade_receiver: Option<mpsc::Receiver<Result<CompiledCode, SimulatorError>>>,
pending_split_applies: usize,
promotion_threshold: u64,
interpreted_steps: u64,
compiled_steps: u64,
promoted_after_interpreted_steps: Option<u64>,
safe_point_polls: u64,
split_apply_deferrals: u64,
threshold_deferrals: u64,
execution_timing: Option<ActiveExecutionTiming>,
}
impl TieredBackend {
pub fn new(laid_out: &LaidOutProgram, options: &SimulatorOptions) -> Self {
if native_is_default_target() {
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
{
if cfg!(all(target_arch = "x86_64", not(feature = "arm64-codegen")))
&& !options.trace.native
&& !options.trace.mir
&& !matches!(
options.tier_promotion,
crate::simulator::TierPromotion::Never
)
{
let mut baseline_options = options.clone();
baseline_options.x86_options.baseline = true;
let mut optimizing_options = options.clone();
optimizing_options.x86_options.baseline = false;
return Self::with_two_stage_compiler(
laid_out,
&baseline_options,
optimizing_options,
|laid_out, options, cancel| {
use crate::backend::native::{NativeBackend, SharedNativeCode};
let image = NativeBackend::compile_image_with_cancel(
laid_out, options, cancel,
)?;
let shared = Arc::new(unsafe {
SharedNativeCode::from_image_with_dpi(image, &options.dpi)?
});
Ok(CompiledCode::Native(shared))
},
);
}
Self::with_compiler(laid_out, options, |laid_out, options, cancel| {
use crate::backend::native::{NativeBackend, SharedNativeCode};
let wants_native_trace = options.trace.native || options.trace.mir;
let image = if wants_native_trace {
let (image, native_trace) =
NativeBackend::compile_image_with_cancel_and_trace(
laid_out, options, cancel,
)?;
let compilation = crate::debug::CompilationTrace {
native_optimized_sir: Some(native_trace.optimized_sir),
mir: Some(native_trace.mir),
reactive_event_graph: Some(native_trace.reactive_graph),
native_state_layout: Some(native_trace.state_layout),
..crate::debug::CompilationTrace::default()
};
if options.trace.output_to_stdout {
compilation.print();
}
image
} else {
NativeBackend::compile_image_with_cancel(laid_out, options, cancel)?
};
let shared = Arc::new(unsafe {
SharedNativeCode::from_image_with_dpi(image, &options.dpi)?
});
Ok(CompiledCode::Native(shared))
})
}
#[cfg(not(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
)))]
{
unreachable!("native tier selected on a host without native support")
}
} else {
Self::with_compiler(laid_out, options, |laid_out, options, _cancel| {
let mut trace = crate::debug::CompilationTrace::default();
let wants_codegen_trace = options.trace.pre_optimized_clif
|| options.trace.post_optimized_clif
|| options.trace.native;
let shared = Arc::new(JitBackend::compile(
laid_out,
options,
wants_codegen_trace.then_some(&mut trace),
)?);
if options.trace.output_to_stdout {
trace.print();
}
Ok(CompiledCode::Cranelift(shared))
})
}
}
pub(crate) fn with_compiler<F>(
laid_out: &LaidOutProgram,
options: &SimulatorOptions,
compile: F,
) -> Self
where
F: FnOnce(
&LaidOutProgram,
&SimulatorOptions,
&CompileCancel,
) -> Result<CompiledCode, SimulatorError>
+ Send
+ 'static,
{
let mut interp = Box::new(
InterpBackend::new(laid_out, options)
.expect("interpreter construction cannot fail for a laid-out program"),
);
if !matches!(
options.tier_promotion,
crate::simulator::TierPromotion::Never
) {
let len = interp.image_word_len();
let lanes = options.optimize_options.parallel_lanes() as usize;
let slack = (len.max(1024) / 8).saturating_mul(lanes);
interp.reserve_image_capacity(len + slack.max(1024));
}
let events = interp
.id_to_event_slice()
.iter()
.map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
.collect();
let cancel = CompileCancel::new();
let worker_cancel = cancel.clone();
let (promotion, compiler_worker) = if matches!(
options.tier_promotion,
crate::simulator::TierPromotion::Never
) {
(Promotion::Disabled, None)
} else {
let (sender, receiver) = mpsc::channel();
let background_laid_out = laid_out.clone();
let background_options = options.clone();
match std::thread::Builder::new()
.name("celox-jit-compile".to_string())
.spawn(move || {
let span = tracing::info_span!(
"celox.tiered.compile",
target = if native_is_default_target() {
"native"
} else {
"cranelift"
}
);
let result = span.in_scope(|| {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compile(&background_laid_out, &background_options, &worker_cancel)
}))
.unwrap_or_else(|_| {
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
});
match &result {
Ok(_) => tracing::info!("tiered background compilation completed"),
Err(error) => tracing::warn!(
error = %error,
"tiered background compilation failed"
),
}
result
});
let _ = sender.send(result);
}) {
Ok(handle) => (Promotion::Pending(receiver), Some(handle)),
Err(error) => (
Promotion::Failed(SimulatorError::new(crate::SimulatorErrorKind::Codegen(
crate::CodegenError::message(format!(
"failed to spawn the background compiler thread: {error}"
)),
))),
None,
),
}
};
let promotion_threshold = match options.tier_promotion {
crate::simulator::TierPromotion::AfterSteps(steps) => steps,
_ => 0,
};
Self {
phase: Phase::Interpreting(Some(interp)),
promotion,
events,
cancel,
compiler_worker,
upgrade_receiver: None,
pending_split_applies: 0,
promotion_threshold,
interpreted_steps: 0,
compiled_steps: 0,
promoted_after_interpreted_steps: None,
safe_point_polls: 0,
split_apply_deferrals: 0,
threshold_deferrals: 0,
execution_timing: None,
}
}
pub(crate) fn with_two_stage_compiler<F>(
laid_out: &LaidOutProgram,
baseline_options: &SimulatorOptions,
optimizing_options: SimulatorOptions,
compile: F,
) -> Self
where
F: Fn(
&LaidOutProgram,
&SimulatorOptions,
&CompileCancel,
) -> Result<CompiledCode, SimulatorError>
+ Send
+ 'static,
{
let mut interp = Box::new(
InterpBackend::new(laid_out, baseline_options)
.expect("interpreter construction cannot fail for a laid-out program"),
);
let len = interp.image_word_len();
let slack = len.max(1024) / 4;
interp.reserve_image_capacity(len + slack.max(1024));
let events = interp
.id_to_event_slice()
.iter()
.map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
.collect();
let cancel = CompileCancel::new();
let worker_cancel = cancel.clone();
let (baseline_sender, baseline_receiver) = mpsc::channel();
let (upgrade_sender, upgrade_receiver) = mpsc::channel();
let background_laid_out = laid_out.clone();
let worker_baseline_options = baseline_options.clone();
let (promotion, compiler_worker) = match std::thread::Builder::new()
.name("celox-jit-compile".to_string())
.spawn(move || {
let span = tracing::info_span!("celox.tiered.compile", target = "native");
span.in_scope(|| {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compile(
&background_laid_out,
&worker_baseline_options,
&worker_cancel,
)
}))
.unwrap_or_else(|_| {
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
});
match &result {
Ok(_) => tracing::info!("tiered baseline compilation completed"),
Err(error) => {
tracing::warn!(error = %error, "tiered baseline compilation failed")
}
}
let ready = result.is_ok();
let delivered = baseline_sender.send(result).is_ok();
if !delivered || !ready || worker_cancel.is_cancelled() {
return;
}
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
compile(&background_laid_out, &optimizing_options, &worker_cancel)
}))
.unwrap_or_else(|_| {
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
});
match &result {
Ok(_) => tracing::info!("tiered optimizing compilation completed"),
Err(error) => tracing::warn!(
error = %error,
"tiered optimizing compilation failed"
),
}
let _ = upgrade_sender.send(result);
});
}) {
Ok(handle) => (Promotion::Pending(baseline_receiver), Some(handle)),
Err(error) => (
Promotion::Failed(SimulatorError::new(crate::SimulatorErrorKind::Codegen(
crate::CodegenError::message(format!(
"failed to spawn the background compiler thread: {error}"
)),
))),
None,
),
};
let promotion_threshold = match baseline_options.tier_promotion {
crate::simulator::TierPromotion::AfterSteps(steps) => steps,
_ => 0,
};
Self {
phase: Phase::Interpreting(Some(interp)),
promotion,
events,
cancel,
compiler_worker,
upgrade_receiver: Some(upgrade_receiver),
pending_split_applies: 0,
promotion_threshold,
interpreted_steps: 0,
compiled_steps: 0,
promoted_after_interpreted_steps: None,
safe_point_polls: 0,
split_apply_deferrals: 0,
threshold_deferrals: 0,
execution_timing: None,
}
}
pub fn is_compiled(&self) -> bool {
matches!(self.phase, Phase::Compiled(_))
}
pub fn execution_stats(&self) -> TieredExecutionStats {
TieredExecutionStats {
tier: if self.is_compiled() {
TieredExecutionTier::Compiled
} else {
TieredExecutionTier::Interpreter
},
promotion: match &self.promotion {
Promotion::Pending(_) => TieredPromotionStatus::Pending,
Promotion::Failed(_) => TieredPromotionStatus::Failed,
Promotion::Disabled => TieredPromotionStatus::Disabled,
Promotion::Promoted => TieredPromotionStatus::Promoted,
},
interpreted_evaluations: self.interpreted_steps,
compiled_evaluations: self.compiled_steps,
promoted_after_interpreted_evaluations: self.promoted_after_interpreted_steps,
safe_point_polls: self.safe_point_polls,
split_apply_deferrals: self.split_apply_deferrals,
threshold_deferrals: self.threshold_deferrals,
}
}
pub fn start_execution_timing(&mut self) {
self.execution_timing = Some(ActiveExecutionTiming {
started_at: Instant::now(),
promotion_elapsed: self.is_compiled().then_some(Duration::ZERO),
});
}
pub fn finish_execution_timing(&mut self) -> Option<TieredExecutionTiming> {
self.execution_timing
.take()
.map(|timing| TieredExecutionTiming {
promotion_elapsed: timing.promotion_elapsed,
})
}
pub fn promotion_error(&self) -> Option<&SimulatorError> {
match &self.promotion {
Promotion::Failed(error) => Some(error),
_ => None,
}
}
pub fn cancel_background_compilation(&mut self) -> bool {
let promotion_pending = matches!(self.promotion, Promotion::Pending(_));
let upgrade_pending = self.upgrade_receiver.take().is_some();
self.cancel.cancel();
if promotion_pending {
self.promotion = Promotion::Failed(super::compile_cancel::cancelled_error());
}
promotion_pending || upgrade_pending
}
fn count_evaluation_for_current_tier(&mut self) {
if matches!(self.phase, Phase::Interpreting(Some(_))) {
self.interpreted_steps = self.interpreted_steps.saturating_add(1);
} else if matches!(self.phase, Phase::Compiled(_)) {
self.compiled_steps = self.compiled_steps.saturating_add(1);
}
}
fn maybe_promote(&mut self) {
if !matches!(self.phase, Phase::Interpreting(Some(_))) {
return;
}
self.safe_point_polls = self.safe_point_polls.saturating_add(1);
if self.pending_split_applies > 0 {
self.split_apply_deferrals = self.split_apply_deferrals.saturating_add(1);
return;
}
if self.interpreted_steps < self.promotion_threshold {
self.threshold_deferrals = self.threshold_deferrals.saturating_add(1);
return;
}
let Promotion::Pending(receiver) = &self.promotion else {
return;
};
if self.cancel.is_cancelled() {
self.promotion = Promotion::Failed(super::compile_cancel::cancelled_error());
return;
}
let Ok(result) = receiver.try_recv() else {
return;
};
let Ok(code) = result else {
self.promotion = match result {
Err(error) => Promotion::Failed(error),
Ok(_) => unreachable!("checked above"),
};
self.cancel_background_compilation();
return;
};
if let Phase::Interpreting(Some(interp)) = &mut self.phase {
let required = code.required_image_words();
if interp.image_word_capacity() < required {
self.promotion = Promotion::Failed(SimulatorError::new(
crate::SimulatorErrorKind::Codegen(crate::CodegenError::message(format!(
"native image needs {required} words but the reserved interpreter \
image only has {} words of capacity; refusing to move the live \
memory image and staying interpreted",
interp.image_word_capacity()
))),
));
self.cancel_background_compilation();
return;
}
}
let mut adopted = None;
if let Phase::Interpreting(slot) = &mut self.phase {
if let Some(mut interp) = slot.take() {
let (memory, runtime_event_buffer, comb_capture_enabled) = interp.tier_transfer();
drop(interp);
adopted = Some(CompiledTier::adopt(
code,
memory,
runtime_event_buffer,
comb_capture_enabled,
));
}
}
if let Some(mut compiled) = adopted {
let promotion_span = tracing::info_span!(
"celox.tiered.promote",
interpreted_evaluations = self.interpreted_steps
);
let _entered = promotion_span.enter();
let sparse_metas: Vec<_> = {
let layout = compiled.layout();
layout
.sparse_layouts
.values()
.map(|s| {
(
s.dirty_words_offset,
s.dirty_word_count * 8,
s.summary_words_offset,
s.summary_word_count * 8,
)
})
.collect()
};
unsafe {
let base = compiled.memory_base_mut();
for &(dwo, dwc, swo, swc) in &sparse_metas {
std::ptr::write_bytes(base.add(dwo), 0, dwc);
std::ptr::write_bytes(base.add(swo), 0, swc);
}
}
self.phase = Phase::Compiled(compiled);
self.promotion = Promotion::Promoted;
self.promoted_after_interpreted_steps = Some(self.interpreted_steps);
if let Some(timing) = &mut self.execution_timing
&& timing.promotion_elapsed.is_none()
{
timing.promotion_elapsed = Some(timing.started_at.elapsed());
}
tracing::info!("tiered backend adopted generated code");
}
}
fn maybe_upgrade(&mut self) {
if self.cancel.is_cancelled() {
self.upgrade_receiver = None;
return;
}
if !matches!(self.phase, Phase::Compiled(_)) || self.pending_split_applies > 0 {
return;
}
let Some(receiver) = self.upgrade_receiver.take() else {
return;
};
let result = match receiver.try_recv() {
Ok(result) => result,
Err(mpsc::TryRecvError::Disconnected) => return,
Err(mpsc::TryRecvError::Empty) => {
self.upgrade_receiver = Some(receiver);
return;
}
};
let code = match result {
Ok(code) => code,
Err(error) => {
tracing::warn!(
error = %error,
"tiered optimizing compilation failed; keeping baseline code"
);
return;
}
};
let required = code.required_image_words();
if let Phase::Compiled(compiled) = &mut self.phase
&& compiled.memory_word_capacity() < required
{
tracing::warn!(
required_words = required,
"tiered optimizing image exceeds the reserved image; keeping baseline code"
);
return;
}
if let Phase::Compiled(compiled) = &mut self.phase
&& compiled.replace_native_code(code)
{
tracing::info!("tiered backend adopted optimizing native code");
}
}
}
impl Drop for TieredBackend {
fn drop(&mut self) {
self.cancel.cancel();
if let Some(worker) = self.compiler_worker.take() {
let _ = worker.join();
}
}
}
impl SimBackend for TieredBackend {
type Event = TieredEventRef;
fn eval_comb(&mut self) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.eval_comb(),
Phase::Compiled(compiled) => compiled.eval_comb(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn eval_apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.eval_apply_ff_at(
super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
),
Phase::Compiled(compiled) => compiled.eval_apply_ff_at(event.id()),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn eval_comb_apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.eval_comb_apply_ff_at(
super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
),
Phase::Compiled(compiled) => compiled.eval_comb_apply_ff_at(event.id()),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn eval_comb_apply_ff_many_at(
&mut self,
event: TieredEventRef,
count: u64,
) -> (u64, Result<(), SimulatorErrorCode>) {
if count == 0 {
return (0, Ok(()));
}
self.maybe_promote();
self.maybe_upgrade();
match &mut self.phase {
Phase::Interpreting(Some(interp)) => {
self.interpreted_steps = self.interpreted_steps.saturating_add(1);
(
1,
interp.eval_comb_apply_ff_at(super::interp::InterpEventRef::from_parts(
event.addr(),
event.id(),
)),
)
}
Phase::Compiled(compiled) => {
let (completed, result) = compiled.eval_comb_apply_ff_many_at(event.id(), count);
self.compiled_steps = self.compiled_steps.saturating_add(completed);
(completed, result)
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn eval_only_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
let result = match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.eval_only_ff_at(
super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
),
Phase::Compiled(compiled) => compiled.eval_only_ff_at_addr(&event.addr()),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
};
self.pending_split_applies = self.pending_split_applies.saturating_add(1);
result
}
fn apply_ff_at(&mut self, event: TieredEventRef) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
let result = match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.apply_ff_at(
super::interp::InterpEventRef::from_parts(event.addr(), event.id()),
),
Phase::Compiled(compiled) => compiled.apply_ff_at_addr(&event.addr()),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
};
self.pending_split_applies = self.pending_split_applies.saturating_sub(1);
result
}
fn run_process(&mut self, index: usize) -> Result<(), SimulatorErrorCode> {
self.maybe_promote();
self.maybe_upgrade();
self.count_evaluation_for_current_tier();
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.run_process(index),
Phase::Compiled(compiled) => compiled.run_process(index),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn resolve_signal(&self, addr: &AbsoluteAddr) -> SignalRef {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.resolve_signal(addr),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.resolve_signal(addr),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.resolve_signal(addr),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn resolve_event(&self, addr: &AbsoluteAddr) -> TieredEventRef {
match &self.phase {
Phase::Interpreting(Some(interp)) => {
let ev = interp.resolve_event(addr);
TieredEventRef {
addr: ev.addr(),
id: ev.id(),
}
}
Phase::Compiled(CompiledTier::Jit(jit)) => {
let ev = jit.resolve_event(addr);
TieredEventRef {
addr: ev.addr(),
id: ev.id(),
}
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => {
let ev = native.resolve_event(addr);
TieredEventRef {
addr: ev.addr(),
id: ev.id(),
}
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn resolve_event_opt(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
match &self.phase {
Phase::Interpreting(Some(interp)) => {
interp.resolve_event_opt(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
Phase::Compiled(CompiledTier::Jit(jit)) => {
jit.resolve_event_opt(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => {
native.resolve_event_opt(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn resolve_eval_only_event(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
match &self.phase {
Phase::Interpreting(Some(interp)) => {
interp
.resolve_eval_only_event(addr)
.map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
Phase::Compiled(CompiledTier::Jit(jit)) => {
jit.resolve_eval_only_event(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native
.resolve_eval_only_event(addr)
.map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
}),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn resolve_apply_event(&self, addr: &AbsoluteAddr) -> Option<TieredEventRef> {
match &self.phase {
Phase::Interpreting(Some(interp)) => {
interp.resolve_apply_event(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
Phase::Compiled(CompiledTier::Jit(jit)) => {
jit.resolve_apply_event(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => {
native.resolve_apply_event(addr).map(|ev| TieredEventRef {
addr: ev.addr(),
id: ev.id(),
})
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn set<T: Copy>(&mut self, signal: SignalRef, value: T) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.set(signal, value),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.set(signal, value),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.set(signal, value),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn set_wide(&mut self, signal: SignalRef, value: BigUint) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.set_wide(signal, value),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.set_wide(signal, value),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.set_wide(signal, value),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn set_four_state(&mut self, signal: SignalRef, value: BigUint, mask: BigUint) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.set_four_state(signal, value, mask),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.set_four_state(signal, value, mask),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => {
native.set_four_state(signal, value, mask)
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn get(&self, signal: SignalRef) -> BigUint {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.get(signal),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.get(signal),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.get(signal),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn get_as<T: Default + Copy>(&self, signal: SignalRef) -> T {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.get_as(signal),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_as(signal),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.get_as(signal),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn get_four_state(&self, signal: SignalRef) -> (BigUint, BigUint) {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.get_four_state(signal),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_four_state(signal),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.get_four_state(signal),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn memory_as_ptr(&self) -> (*const u8, usize) {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.memory_as_ptr(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_as_ptr(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.memory_as_ptr(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn memory_as_mut_ptr(&mut self) -> (*mut u8, usize) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.memory_as_mut_ptr(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_as_mut_ptr(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.memory_as_mut_ptr(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn memory_owner(&self) -> Option<Arc<dyn std::any::Any + Send + Sync>> {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.memory_owner(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.memory_owner(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.memory_owner(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn write_memory(&mut self, offset: usize, bytes: &[u8]) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.write_memory(offset, bytes),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.write_memory(offset, bytes),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.write_memory(offset, bytes),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn vcd_tracking_enabled(&self) -> bool {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.vcd_tracking_enabled(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.vcd_tracking_enabled(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.vcd_tracking_enabled(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn runtime_event_buffer_as_ptr(&self) -> (*const u8, usize) {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.runtime_event_buffer_as_ptr(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.runtime_event_buffer_as_ptr(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.runtime_event_buffer_as_ptr(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn runtime_event_buffer(&self) -> Option<Arc<RuntimeEventBuffer>> {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.runtime_event_buffer(),
Phase::Compiled(CompiledTier::Jit(jit)) => Some(jit.runtime_event_buffer().clone()),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.runtime_event_buffer(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn set_comb_capture_event_enabled(&mut self, active_sites: &[bool]) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => {
interp.set_comb_capture_event_enabled(active_sites)
}
Phase::Compiled(CompiledTier::Jit(jit)) => {
jit.set_comb_capture_event_enabled(active_sites)
}
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => {
native.set_comb_capture_event_enabled(active_sites)
}
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn stable_region_size(&self) -> usize {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.stable_region_size(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.stable_region_size(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.stable_region_size(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn layout(&self) -> &MemoryLayout {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.layout(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.layout(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.layout(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn id_to_addr_slice(&self) -> &[AbsoluteAddr] {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.id_to_addr_slice(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.id_to_addr_slice(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.id_to_addr_slice(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn id_to_event_slice(&self) -> &[TieredEventRef] {
&self.events
}
fn num_events(&self) -> usize {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.num_events(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.num_events(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.num_events(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn clear_triggered_bits(&mut self) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.clear_triggered_bits(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.clear_triggered_bits(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.clear_triggered_bits(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn mark_triggered_bit(&mut self, id: usize) {
match &mut self.phase {
Phase::Interpreting(Some(interp)) => interp.mark_triggered_bit(id),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.mark_triggered_bit(id),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.mark_triggered_bit(id),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
fn get_triggered_bits(&self) -> bit_set::BitSet {
match &self.phase {
Phase::Interpreting(Some(interp)) => interp.get_triggered_bits(),
Phase::Compiled(CompiledTier::Jit(jit)) => jit.get_triggered_bits(),
#[cfg(any(
target_arch = "x86_64",
feature = "arm64-codegen",
target_arch = "aarch64"
))]
Phase::Compiled(CompiledTier::Native(native)) => native.get_triggered_bits(),
Phase::Interpreting(None) => unreachable!("promoted backend left no interpreter"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::native::NativeBackend;
use crate::{CodegenError, RuntimeEvent, Simulator, SimulatorBuilder, SimulatorErrorKind};
use std::sync::atomic::{AtomicBool, Ordering};
const PIPELINE: &str = r#"
module Top (
clk: input clock,
rst: input reset,
d: input logic<8>,
q: output logic<8>,
) {
var stage1: logic<8>;
var stage2: logic<8>;
always_ff (clk, rst) {
if_reset {
stage1 = 0;
stage2 = 0;
} else {
stage1 = d;
stage2 = stage1;
}
}
assign q = stage2;
}
"#;
struct Gate(Arc<AtomicBool>);
impl Gate {
fn closed() -> Self {
Self(Arc::new(AtomicBool::new(false)))
}
fn open(&self) {
self.0.store(true, Ordering::SeqCst);
}
}
fn wait_for_gate_or_cancel(
gate: &AtomicBool,
cancel: &CompileCancel,
) -> Result<(), SimulatorError> {
while !gate.load(Ordering::Acquire) {
if cancel.is_cancelled() {
return Err(super::super::compile_cancel::cancelled_error());
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
Ok(())
}
fn build_gated(gate: &Gate) -> Simulator<TieredBackend> {
let worker_gate = gate.0.clone();
SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
{
let image = NativeBackend::compile_image(laid_out, options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
}
})
.unwrap()
}
fn drive(sim: &mut Simulator<TieredBackend>, inputs: &[u8]) -> Vec<u8> {
let clk = sim.event("clk");
let rst = sim.signal("rst");
let d = sim.signal("d");
sim.modify(|io| {
io.set(rst, 0u8);
io.set(d, inputs[0]);
})
.unwrap();
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
sim.modify(|io| io.set(rst, 1u8)).unwrap();
let mut observed = Vec::new();
for &value in inputs {
sim.modify(|io| io.set(d, value)).unwrap();
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
observed.push(sim.get_as::<u8>(sim.signal("q")));
}
observed
}
fn reference_outputs(inputs: &[u8]) -> Vec<u8> {
inputs.to_vec()
}
#[test_case::test_case(false; "optimizing")]
#[test_case::test_case(true; "baseline")]
fn unpacked_array_works_across_promotion_on_strided_layout(baseline: bool) {
let code = r#"
module Top (
clk: input clock,
we: input logic,
waddr: input logic<3>,
wdata: input logic<8>,
raddr: input logic<3>,
q: output logic<8>,
) {
var mem: logic<8>[8];
always_ff (clk) {
if we {
mem[waddr] = wdata;
}
}
assign q = mem[raddr];
}
"#;
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
{
let mut options = options.clone();
options.x86_options.baseline = baseline;
options.x86_options.diagnostics.verify_mir_passes = true;
options.x86_options.diagnostics.verify_regalloc = true;
let image = NativeBackend::compile_image(laid_out, &options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
}
})
.unwrap();
let clk = sim.event("clk");
let we = sim.signal("we");
let waddr = sim.signal("waddr");
let wdata = sim.signal("wdata");
let raddr = sim.signal("raddr");
let q = sim.signal("q");
for i in 0..8u8 {
sim.modify(|io| {
io.set(we, 1u8);
io.set(waddr, i);
io.set(wdata, i * 7 + 1);
})
.unwrap();
sim.tick(clk).unwrap();
}
sim.modify(|io| io.set(we, 0u8)).unwrap();
let read_back = |sim: &mut Simulator<TieredBackend>, index: u8| -> u8 {
sim.modify(|io| io.set(raddr, index)).unwrap();
sim.get_as::<u8>(q)
};
for i in 0..8u8 {
assert_eq!(read_back(&mut sim, i), i * 7 + 1);
}
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled());
for i in 0..8u8 {
assert_eq!(read_back(&mut sim, i), i * 7 + 1, "element {i}");
}
sim.modify(|io| {
io.set(we, 1u8);
io.set(waddr, 3u8);
io.set(wdata, 250u8);
})
.unwrap();
sim.tick(clk).unwrap();
sim.modify(|io| io.set(we, 0u8)).unwrap();
assert_eq!(read_back(&mut sim, 3), 250u8);
}
#[test_case::test_case(false; "optimizing")]
#[test_case::test_case(true; "baseline")]
fn four_state_unpacked_array_planes_initialize_and_survive_promotion(baseline: bool) {
let code = r#"
module Top (
clk: input clock,
we: input logic,
waddr: input logic<2>,
wdata: input logic<8>,
raddr: input logic<2>,
q: output logic<8>,
) {
var mem: logic<8>[4];
always_ff (clk) {
if we {
mem[waddr] = wdata;
}
}
assign q = mem[raddr];
}
"#;
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
.four_state(true)
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
let mut options = options.clone();
options.x86_options.baseline = baseline;
options.x86_options.diagnostics.verify_mir_passes = true;
options.x86_options.diagnostics.verify_regalloc = true;
let image = NativeBackend::compile_image(laid_out, &options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
})
.unwrap();
let clk = sim.event("clk");
let we = sim.signal("we");
let waddr = sim.signal("waddr");
let wdata = sim.signal("wdata");
let raddr = sim.signal("raddr");
let q = sim.signal("q");
for i in 0..4u8 {
sim.modify(|io| {
io.set(we, 1u8);
io.set(waddr, i);
io.set(wdata, i * 60 + 5);
})
.unwrap();
sim.tick(clk).unwrap();
}
sim.modify(|io| io.set(we, 0u8)).unwrap();
for i in 0..4u8 {
sim.modify(|io| io.set(raddr, i)).unwrap();
assert_eq!(sim.get_as::<u8>(q), i * 60 + 5);
}
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(2));
}
assert!(sim.is_compiled());
for i in 0..4u8 {
sim.modify(|io| io.set(raddr, i)).unwrap();
assert_eq!(
sim.get_as::<u8>(q),
i * 60 + 5,
"element {i} after native promotion"
);
}
sim.modify(|io| {
io.set(we, 1u8);
io.set(waddr, 2u8);
io.set(wdata, 200u8);
})
.unwrap();
sim.tick(clk).unwrap();
sim.modify(|io| io.set(we, 0u8)).unwrap();
sim.modify(|io| io.set(raddr, 2u8)).unwrap();
assert_eq!(sim.get_as::<u8>(q), 200u8, "native-tier write");
sim.modify(|io| {
io.set(we, 1u8);
io.set_four_state(wdata, 0xa5u8.into(), 0x3cu8.into());
})
.unwrap();
sim.tick(clk).unwrap();
sim.modify(|io| io.set(we, 0u8)).unwrap();
assert_eq!(sim.get_four_state(q), (0xa5u8.into(), 0x3cu8.into()));
}
#[test]
fn strided_interp_matches_packed_interp_for_four_state_array() {
let code = r#"
module Top (
clk: input clock,
we: input logic,
waddr: input logic<2>,
wdata: input logic<8>,
raddr: input logic<2>,
q: output logic<8>,
) {
var mem: logic<8>[4];
always_ff (clk) {
if we {
mem[waddr] = wdata;
}
}
assign q = mem[raddr];
}
"#;
let mut packed_sim = SimulatorBuilder::<Simulator>::new(code, "Top")
.four_state(true)
.build_interpreter()
.unwrap();
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let mut strided_sim: Simulator<TieredBackend> =
SimulatorBuilder::<Simulator>::new(code, "Top")
.four_state(true)
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
{
let image = NativeBackend::compile_image(laid_out, options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
}
})
.unwrap();
let packed_clk = packed_sim.event("clk");
let strided_clk = strided_sim.event("clk");
let packed_we = packed_sim.signal("we");
let strided_we = strided_sim.signal("we");
let packed_waddr = packed_sim.signal("waddr");
let strided_waddr = strided_sim.signal("waddr");
let packed_wdata = packed_sim.signal("wdata");
let strided_wdata = strided_sim.signal("wdata");
let packed_raddr = packed_sim.signal("raddr");
let strided_raddr = strided_sim.signal("raddr");
for i in 0..4u8 {
packed_sim
.modify(|io| {
io.set(packed_we, 1u8);
io.set(packed_waddr, i);
io.set(packed_wdata, i * 60 + 5);
})
.unwrap();
packed_sim.tick(packed_clk).unwrap();
strided_sim
.modify(|io| {
io.set(strided_we, 1u8);
io.set(strided_waddr, i);
io.set(strided_wdata, i * 60 + 5);
})
.unwrap();
strided_sim.tick(strided_clk).unwrap();
}
packed_sim.modify(|io| io.set(packed_we, 0u8)).unwrap();
strided_sim.modify(|io| io.set(strided_we, 0u8)).unwrap();
for i in 0..4u8 {
packed_sim.modify(|io| io.set(packed_raddr, i)).unwrap();
strided_sim.modify(|io| io.set(strided_raddr, i)).unwrap();
let pv = packed_sim.get_as::<u8>(packed_sim.signal("q"));
let sv = strided_sim.get_as::<u8>(strided_sim.signal("q"));
assert_eq!(
pv, sv,
"element {i} diverges between Packed and ElementStrided interp"
);
}
}
#[test]
fn outputs_match_reference_when_promotion_never_happens() {
let inputs: Vec<u8> = (10..40).collect();
let gate = Gate::closed();
let mut sim = build_gated(&gate);
let observed = drive(&mut sim, &inputs);
assert_eq!(observed, reference_outputs(&inputs));
assert!(!sim.is_compiled());
assert!(sim.promotion_error().is_none(), "gate still closed");
}
#[test]
fn outputs_match_reference_when_promotion_lands_mid_run() {
let inputs: Vec<u8> = (10..40).collect();
let gate = Gate::closed();
let mut sim = build_gated(&gate);
let clk = sim.event("clk");
let rst = sim.signal("rst");
let d = sim.signal("d");
sim.modify(|io| {
io.set(rst, 0u8);
io.set(d, inputs[0]);
})
.unwrap();
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
sim.modify(|io| io.set(rst, 1u8)).unwrap();
let mut observed = Vec::new();
for &value in &inputs[..inputs.len() / 2] {
sim.modify(|io| io.set(d, value)).unwrap();
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
observed.push(sim.get_as::<u8>(sim.signal("q")));
}
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled());
assert!(sim.promotion_error().is_none());
for &value in &inputs[inputs.len() / 2..] {
sim.modify(|io| io.set(d, value)).unwrap();
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
observed.push(sim.get_as::<u8>(sim.signal("q")));
}
assert_eq!(observed, reference_outputs(&inputs));
}
#[test]
fn waveform_activity_survives_promotion_before_dump() {
for expose_raw in [false, true] {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("promotion.vcd");
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let mut sim: Simulator<TieredBackend> =
SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.vcd(&path)
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
let image = NativeBackend::compile_image(laid_out, options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
})
.unwrap();
let descs = sim.build_vcd_descs(false);
let mut reference = crate::VcdWriter::from_writer(Vec::new(), &descs);
let dump = |sim: &mut Simulator<TieredBackend>,
reference: &mut crate::VcdWriter<Vec<u8>>,
time| {
sim.dump(time);
let (ptr, size) = sim.memory_as_ptr();
reference
.dump(time, unsafe { std::slice::from_raw_parts(ptr, size) })
.unwrap();
};
let clk = sim.event("clk");
let rst = sim.signal("rst");
let d = sim.signal("d");
sim.set(rst, 0u8);
sim.tick(clk).unwrap();
dump(&mut sim, &mut reference, 0);
let raw = expose_raw.then(|| {
let (ptr, size) = sim.memory_as_ptr();
let snapshot = unsafe { std::slice::from_raw_parts(ptr, size) }.to_vec();
let (ptr, _) = sim.memory_as_mut_ptr();
unsafe {
std::ptr::copy_nonoverlapping(snapshot.as_ptr(), ptr, size);
}
ptr
});
sim.set(rst, 1u8);
sim.set(d, 0xa5u8);
sim.tick(clk).unwrap();
sim.tick(clk).unwrap();
assert!(!sim.is_compiled());
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled(), "{:?}", sim.promotion_error());
dump(&mut sim, &mut reference, 1);
let before = sim.vcd_statistics().unwrap().comparisons;
if let Some(raw) = raw {
unsafe {
*raw.add(d.offset) = 0x3c;
}
}
dump(&mut sim, &mut reference, 2);
assert_eq!(
sim.vcd_statistics().unwrap().comparisons,
before + if expose_raw { descs.len() as u64 } else { 0 }
);
sim.flush_vcd().unwrap();
let parse = |bytes: Vec<u8>| {
let mut parser = vcd::Parser::new(bytes.as_slice());
parser.parse_header().unwrap();
parser.map(Result::unwrap).collect::<Vec<_>>()
};
assert_eq!(
parse(std::fs::read(path).unwrap()),
parse(reference.into_inner().unwrap())
);
}
}
#[test]
fn compilation_failure_keeps_the_interpreter() {
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.build_tiered_with_compiler(|_, _, _| {
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
})
.unwrap();
let inputs: Vec<u8> = (10..20).collect();
let observed = drive(&mut sim, &inputs);
assert_eq!(observed, reference_outputs(&inputs));
assert!(!sim.is_compiled());
let clk = sim.event("clk");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while sim.promotion_error().is_none() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.promotion_error().is_some());
}
#[test]
fn runtime_events_stay_continuous_across_promotion() {
let code = r#"
module Top (
clk: input clock,
cnt: output logic<8>,
) {
var c: logic<8>;
always_ff (clk) {
c = c + 1;
$display("tick %0d", c);
}
assign cnt = c;
}
"#;
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(code, "Top")
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
{
let image = NativeBackend::compile_image(laid_out, options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
}
})
.unwrap();
let clk = sim.event("clk");
for _ in 0..5 {
sim.tick(clk).unwrap();
}
let pre = sim.drain_runtime_events();
assert_eq!(pre.len(), 5);
for (index, event) in pre.iter().enumerate() {
let RuntimeEvent::Display { message } = event else {
panic!("unexpected event {event:?}");
};
assert_eq!(
std::str::from_utf8(message).unwrap(),
format!("tick {index}")
);
}
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled());
let _ = sim.drain_runtime_events();
const POST_TICKS: u32 = 8;
for _ in 0..POST_TICKS {
sim.tick(clk).unwrap();
}
let base = sim
.get_as::<u8>(sim.signal("cnt"))
.wrapping_sub(POST_TICKS as u8);
let post = sim.drain_runtime_events();
assert_eq!(post.len(), POST_TICKS as usize);
for (index, event) in post.iter().enumerate() {
let RuntimeEvent::Display { message } = event else {
panic!("unexpected event {event:?}");
};
assert_eq!(
std::str::from_utf8(message).unwrap(),
format!("tick {}", base.wrapping_add(index as u8))
);
}
}
#[test]
fn cancel_background_compilation_stays_on_the_interpreter() {
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.build_tiered_with_compiler(|laid_out, options, cancel| {
while !cancel.is_cancelled() {
std::thread::sleep(std::time::Duration::from_millis(1));
}
let image = NativeBackend::compile_image_with_cancel(laid_out, options, cancel)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
})
.unwrap();
assert!(sim.cancel_background_compilation());
let clk = sim.event("clk");
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
while sim.promotion_error().is_none() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(!sim.is_compiled());
match sim.promotion_error() {
Some(error) => assert!(matches!(
error.kind(),
SimulatorErrorKind::Codegen(CodegenError::Cancelled)
)),
None => panic!("cancellation was not reported through promotion_error"),
}
let inputs: Vec<u8> = (0..8).collect();
assert_eq!(drive(&mut sim, &inputs), reference_outputs(&inputs));
}
#[test]
fn drop_cancels_pending_background_compilation() {
let saw_cancel = Arc::new(AtomicBool::new(false));
let worker_saw_cancel = Arc::clone(&saw_cancel);
let sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.build_tiered_with_compiler(move |_laid_out, _options, cancel| {
while !cancel.is_cancelled() {
std::thread::sleep(std::time::Duration::from_millis(1));
}
worker_saw_cancel.store(true, Ordering::SeqCst);
Err(SimulatorError::new(SimulatorErrorKind::Codegen(
CodegenError::Cancelled,
)))
})
.unwrap();
drop(sim);
assert!(saw_cancel.load(Ordering::Acquire));
}
#[test]
fn tier_promotion_never_skips_background_compilation() {
let invoked = Arc::new(AtomicBool::new(false));
let worker_invoked = Arc::clone(&invoked);
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.tier_promotion(crate::TierPromotion::Never)
.build_tiered_with_compiler(move |_laid_out, _options, _cancel| {
worker_invoked.store(true, Ordering::SeqCst);
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
})
.unwrap();
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(!invoked.load(Ordering::SeqCst));
assert!(!sim.is_compiled());
assert!(sim.promotion_error().is_none());
let before = sim.tiered_execution_stats();
assert_eq!(before.tier, TieredExecutionTier::Interpreter);
assert_eq!(before.promotion, TieredPromotionStatus::Disabled);
assert_eq!(before.compiled_evaluations, 0);
let inputs: Vec<u8> = (0..8).collect();
assert_eq!(drive(&mut sim, &inputs), reference_outputs(&inputs));
let after = sim.tiered_execution_stats();
assert!(after.interpreted_evaluations > before.interpreted_evaluations);
assert_eq!(after.compiled_evaluations, 0);
}
#[test]
fn tier_promotion_after_steps_defers_adoption() {
const THRESHOLD: u64 = 1000;
let sim_build: Simulator<TieredBackend> =
SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.tier_promotion(crate::TierPromotion::AfterSteps(THRESHOLD))
.build_tiered_with_compiler(|laid_out, options, _cancel| {
let image = NativeBackend::compile_image(laid_out, options)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
Ok(CompiledCode::Native(Arc::new(shared)))
})
.unwrap();
let mut sim = sim_build;
let clk = sim.event("clk");
let initial = sim.tiered_execution_stats();
assert!(initial.interpreted_evaluations < THRESHOLD);
for _ in 0..4 {
sim.tick(clk).unwrap();
}
assert!(
!sim.is_compiled(),
"adopted before the interpreted-step threshold"
);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled());
assert!(sim.promotion_error().is_none());
let promoted = sim.tiered_execution_stats();
assert_eq!(promoted.tier, TieredExecutionTier::Compiled);
assert_eq!(promoted.promotion, TieredPromotionStatus::Promoted);
assert!(promoted.interpreted_evaluations >= THRESHOLD);
assert_eq!(
promoted.promoted_after_interpreted_evaluations,
Some(promoted.interpreted_evaluations)
);
assert!(promoted.threshold_deferrals > 0);
sim.tick(clk).unwrap();
assert!(sim.tiered_execution_stats().compiled_evaluations > promoted.compiled_evaluations);
}
#[test]
fn cranelift_promotion_covers_its_image_requirement_without_moving_the_image() {
use std::sync::Mutex;
let gate = Gate::closed();
let worker_gate = gate.0.clone();
let observed: Arc<Mutex<Option<(usize, usize)>>> = Arc::new(Mutex::new(None));
let worker_observed = observed.clone();
let mut sim: Simulator<TieredBackend> = SimulatorBuilder::<Simulator>::new(PIPELINE, "Top")
.build_tiered_with_compiler(move |laid_out, options, cancel| {
wait_for_gate_or_cancel(&worker_gate, cancel)?;
let shared = Arc::new(JitBackend::compile(laid_out, options, None)?);
let code = CompiledCode::Cranelift(Arc::clone(&shared));
*worker_observed.lock().unwrap() = Some((
shared.layout.merged_total_size.div_ceil(8),
code.required_image_words(),
));
Ok(code)
})
.unwrap();
let clk = sim.event("clk");
for _ in 0..2 {
sim.tick(clk).unwrap();
}
let (base_before, _) = sim.memory_as_ptr();
gate.open();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(60);
while !sim.is_compiled() && std::time::Instant::now() < deadline {
sim.tick(clk).unwrap();
std::thread::sleep(std::time::Duration::from_millis(1));
}
assert!(sim.is_compiled());
assert!(sim.promotion_error().is_none());
let (adopt_target, required) = observed.lock().unwrap().expect("compiler ran");
assert!(
required >= adopt_target,
"required_image_words ({required}) must cover the Cranelift \
adoption resize target ({adopt_target})"
);
let (base_after, _) = sim.memory_as_ptr();
assert_eq!(
base_before, base_after,
"promotion must not move the live memory image"
);
}
#[cfg(all(target_arch = "x86_64", not(feature = "arm64-codegen")))]
fn build_gated_two_stage(
baseline_gate: &Gate,
upgrade_gate: &Gate,
fail_upgrade: bool,
) -> (Simulator<TieredBackend>, mpsc::Receiver<()>) {
let (layout_sender, layout_receiver) = mpsc::channel();
let code = PIPELINE.replace(
"assign q = stage2;",
"always_comb { q = stage2; $display(\"q %0d\", q); }",
);
let mut sim = SimulatorBuilder::<Simulator>::new(&code, "Top")
.build_tiered_with_compiler(move |laid_out, options, _| {
layout_sender
.send((laid_out.clone(), options.clone()))
.unwrap();
Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError))
})
.unwrap();
let (laid_out, mut baseline_options) = layout_receiver
.recv_timeout(Duration::from_secs(60))
.unwrap();
let mut optimizing_options = baseline_options.clone();
baseline_options.x86_options.baseline = true;
optimizing_options.x86_options.baseline = false;
let worker_baseline_gate = baseline_gate.0.clone();
let worker_upgrade_gate = upgrade_gate.0.clone();
let (baseline_ready_sender, baseline_ready_receiver) = mpsc::channel();
sim.backend = TieredBackend::with_two_stage_compiler(
&laid_out,
&baseline_options,
optimizing_options,
move |laid_out, options, cancel| {
if options.x86_options.baseline {
wait_for_gate_or_cancel(&worker_baseline_gate, cancel)?;
} else {
baseline_ready_sender.send(()).unwrap();
wait_for_gate_or_cancel(&worker_upgrade_gate, cancel)?;
if fail_upgrade {
return Err(SimulatorError::from(crate::RuntimeErrorCode::InternalError));
}
}
let image = NativeBackend::compile_image_with_cancel(laid_out, options, cancel)?;
let shared = unsafe { SharedNativeCode::from_image(image)? };
if !options.x86_options.baseline {
baseline_ready_sender.send(()).unwrap();
}
Ok(CompiledCode::Native(Arc::new(shared)))
},
);
(sim, baseline_ready_receiver)
}
#[cfg(all(target_arch = "x86_64", not(feature = "arm64-codegen")))]
fn adopted_native_code(backend: &TieredBackend) -> Arc<SharedNativeCode> {
let Phase::Compiled(CompiledTier::Native(native)) = &backend.phase else {
panic!("native code was not adopted");
};
native.shared_code()
}
#[cfg(all(target_arch = "x86_64", not(feature = "arm64-codegen")))]
#[test]
fn native_two_stage_preserves_live_pipeline_across_split_apply() {
let baseline_gate = Gate::closed();
let upgrade_gate = Gate::closed();
let (mut sim, baseline_ready) = build_gated_two_stage(&baseline_gate, &upgrade_gate, false);
let clk = sim.event("clk");
let rst = sim.signal("rst");
let d = sim.signal("d");
let q = sim.signal("q");
let backend = &mut sim.backend;
let base = backend.memory_as_ptr().0;
let events: Vec<_> = backend
.id_to_event_slice()
.iter()
.map(|e| (e.addr(), e.id()))
.collect();
backend.set(rst, 0u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.set(rst, 1u8);
backend.set(d, 17u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert_eq!(backend.get_as::<u8>(q), 0);
baseline_gate.open();
baseline_ready
.recv_timeout(Duration::from_secs(60))
.unwrap();
backend.maybe_promote();
let baseline = adopted_native_code(backend);
assert_eq!(backend.memory_as_ptr().0, base);
backend.set(d, 29u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert_eq!(backend.get_as::<u8>(q), 17, "after2");
let eval = backend.resolve_eval_only_event(&clk.addr()).unwrap();
let apply = backend.resolve_apply_event(&clk.addr()).unwrap();
backend.set(d, 43u8);
backend.eval_only_ff_at(eval).unwrap();
upgrade_gate.open();
backend.compiler_worker.take().unwrap().join().unwrap();
backend.maybe_upgrade();
assert!(Arc::ptr_eq(&baseline, &adopted_native_code(backend)));
assert_eq!(backend.memory_as_ptr().0, base);
backend.apply_ff_at(apply).unwrap();
assert!(Arc::ptr_eq(&baseline, &adopted_native_code(backend)));
backend.set_comb_capture_event_enabled(&[true]);
let capture_start = crate::simulator::runtime_event_write_seq_for_backend(backend);
if let Phase::Compiled(compiled) = &mut backend.phase {
compiled.eval_comb().unwrap();
}
assert_eq!(backend.get_as::<u8>(q), 29);
let event_buffer = backend.runtime_event_buffer().unwrap();
let (buffer_ptr, buffer_size) = backend.runtime_event_buffer_as_ptr();
let queued = unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) }.to_vec();
let before = crate::simulator::runtime_event_write_seq_for_backend(backend);
assert_eq!(before, capture_start + 1);
backend.set_comb_capture_event_enabled(&[true]);
backend.clear_triggered_bits();
backend.mark_triggered_bit(clk.id());
let triggered = backend.get_triggered_bits();
backend.maybe_upgrade();
assert!(!Arc::ptr_eq(&baseline, &adopted_native_code(backend)));
assert_eq!(backend.memory_as_ptr().0, base);
assert_eq!(backend.get_as::<u8>(q), 29);
assert_eq!(backend.get_triggered_bits(), triggered);
assert_eq!(
backend.runtime_event_buffer_as_ptr(),
(buffer_ptr, buffer_size)
);
assert!(Arc::ptr_eq(
&event_buffer,
&backend.runtime_event_buffer().unwrap()
));
assert_eq!(
unsafe { std::slice::from_raw_parts(buffer_ptr, buffer_size) },
queued
);
assert_eq!(
backend
.id_to_event_slice()
.iter()
.map(|e| (e.addr(), e.id()))
.collect::<Vec<_>>(),
events
);
for (addr, id) in events {
assert_eq!(backend.resolve_event(&addr).id(), id);
}
backend.eval_comb().unwrap();
assert_eq!(
crate::simulator::runtime_event_write_seq_for_backend(backend),
before + 1
);
backend.set_comb_capture_event_enabled(&[false]);
backend.eval_comb().unwrap();
assert_eq!(
crate::simulator::runtime_event_write_seq_for_backend(backend),
before + 1
);
backend.set(d, 61u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert_eq!(backend.get_as::<u8>(q), 43);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert_eq!(backend.get_as::<u8>(q), 61);
assert_eq!(backend.memory_as_ptr().0, base);
}
#[cfg(all(target_arch = "x86_64", not(feature = "arm64-codegen")))]
#[test_case::test_case(false; "cancelled")]
#[test_case::test_case(true; "failed")]
fn unsuccessful_optimization_keeps_baseline_code(fail_upgrade: bool) {
let baseline_gate = Gate::closed();
let upgrade_gate = Gate::closed();
let (mut sim, baseline_ready) =
build_gated_two_stage(&baseline_gate, &upgrade_gate, fail_upgrade);
let clk = sim.event("clk");
let rst = sim.signal("rst");
let d = sim.signal("d");
let q = sim.signal("q");
let backend = &mut sim.backend;
backend.set(rst, 0u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.set(rst, 1u8);
backend.set(d, 37u8);
backend.eval_apply_ff_at(clk).unwrap();
baseline_gate.open();
baseline_ready
.recv_timeout(Duration::from_secs(60))
.unwrap();
backend.maybe_promote();
let baseline = adopted_native_code(backend);
let base = backend.memory_as_ptr().0;
upgrade_gate.open();
backend.compiler_worker.take().unwrap().join().unwrap();
if fail_upgrade {
backend.maybe_upgrade();
} else {
assert!(backend.cancel_background_compilation());
}
assert!(!backend.cancel_background_compilation());
backend.set(d, 53u8);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert!(Arc::ptr_eq(&baseline, &adopted_native_code(backend)));
assert_eq!(backend.get_as::<u8>(q), 37);
backend.eval_apply_ff_at(clk).unwrap();
backend.eval_comb().unwrap();
assert_eq!(backend.get_as::<u8>(q), 53);
assert_eq!(backend.memory_as_ptr().0, base);
assert_eq!(
backend.execution_stats().promotion,
TieredPromotionStatus::Promoted
);
assert!(backend.promotion_error().is_none());
}
}