use std::cell::{Cell, RefCell};
use std::sync::Arc;

use genmc_sys::{
    EstimationResult, GENMC_GLOBAL_ADDRESSES_MASK, GenmcScalar, MemOrdering, MiriGenmcShim,
    RMWBinOp, UniquePtr, create_genmc_driver_handle,
};
use rustc_abi::{Align, Size};
use rustc_const_eval::interpret::{AllocId, InterpCx, InterpResult, interp_ok};
use rustc_data_structures::fx::FxHashMap;
use rustc_middle::{throw_ub_format, throw_unsup_format};
// FIXME(genmc,tracing): Implement some work-around for enabling debug/trace level logging (currently disabled statically in rustc).
use tracing::debug;

use self::global_allocations::{EvalContextExt as _, GlobalAllocationHandler};
use self::helper::{
    MAX_ACCESS_SIZE, genmc_scalar_to_scalar, maybe_upgrade_compare_exchange_success_orderings,
    scalar_to_genmc_scalar, to_genmc_rmw_op,
};
use self::run::GenmcMode;
use self::thread_id_map::ThreadIdMap;
use crate::concurrency::genmc::helper::split_access;
use crate::diagnostics::SpanDedupDiagnostic;
use crate::intrinsics::AtomicRmwOp;
use crate::*;

mod config;
mod global_allocations;
mod helper;
mod run;
pub(crate) mod scheduling;
mod shims;
mod thread_id_map;

pub use genmc_sys::GenmcParams;

pub use self::config::GenmcConfig;
pub use self::run::run_genmc_mode;
pub use self::shims::EvalContextExt as GenmcEvalContextExt;

#[derive(Debug)]
pub enum ExecutionEndResult {
    /// An error occurred at the end of the execution.
    Error(String),
    /// No errors occurred, and there are more executions to explore.
    Continue,
    /// No errors occurred and we are finished.
    Stop,
}

#[derive(Clone, Copy, Debug)]
pub enum ExitType {
    MainThreadFinish,
    ExitCalled,
}

/// The exit status of a program.
/// GenMC must store this if a thread exits while any others can still run.
/// The other threads must also be explored before the program is terminated.
#[derive(Clone, Copy, Debug)]
struct ExitStatus {
    exit_code: i32,
    exit_type: ExitType,
}

/// State that is reset at the start of every execution.
#[derive(Debug, Default)]
struct PerExecutionState {
    /// Thread id management, such as mapping between Miri `ThreadId` and GenMC's thread ids, or selecting GenMC thread ids.
    thread_id_manager: RefCell<ThreadIdMap>,

    /// A flag to indicate that we should not forward non-atomic accesses to genmc, e.g. because we
    /// are executing an atomic operation.
    allow_data_races: Cell<bool>,

    /// The exit status of the program. We keep running other threads even after `exit` to ensure
    /// we cover all possible executions.
    /// `None` if no thread has called `exit` and the main thread isn't finished yet.
    exit_status: Cell<Option<ExitStatus>>,

    /// Allocations in this map have been sent to GenMC, and should thus be kept around, since future loads from GenMC may return this allocation again.
    genmc_shared_allocs_map: RefCell<FxHashMap<u64, AllocId>>,
}

impl PerExecutionState {
    fn reset(&self) {
        self.allow_data_races.replace(false);
        self.thread_id_manager.borrow_mut().reset();
        self.exit_status.set(None);
        self.genmc_shared_allocs_map.borrow_mut().clear();
    }
}

struct GlobalState {
    /// Keep track of global allocations, to ensure they keep the same address across different executions, even if the order of allocations changes.
    /// The `AllocId` for globals is stable across executions, so we can use it as an identifier.
    global_allocations: GlobalAllocationHandler,
}

impl GlobalState {
    fn new(target_usize_max: u64) -> Self {
        Self { global_allocations: GlobalAllocationHandler::new(target_usize_max) }
    }
}

/// The main interface with GenMC.
/// Each `GenmcCtx` owns one `MiriGenmcShim`, which owns one `GenMCDriver` (the GenMC model checker).
/// For each GenMC run (estimation or verification), one or more `GenmcCtx` can be created (one per Miri thread).
/// However, for now, we only ever have one `GenmcCtx` per run.
///
/// In multithreading, each worker thread has its own `GenmcCtx`, which will have their results combined in the end.
/// FIXME(genmc): implement multithreading.
///
/// Some data is shared across all `GenmcCtx` in the same run, namely data for global allocation handling.
/// Globals must be allocated in a consistent manner, i.e., each global allocation must have the same address in each execution.
///
/// Some state is reset between each execution in the same run.
pub struct GenmcCtx {
    /// Handle to the GenMC model checker.
    handle: RefCell<UniquePtr<MiriGenmcShim>>,

    /// State that is reset at the start of every execution.
    exec_state: PerExecutionState,

    /// State that persists across executions.
    /// All `GenmcCtx` in one verification step share this state.
    global_state: Arc<GlobalState>,
}

/// GenMC Context creation and administrative / query actions
impl GenmcCtx {
    /// Create a new `GenmcCtx` from a given config.
    fn new(miri_config: &MiriConfig, global_state: Arc<GlobalState>, mode: GenmcMode) -> Self {
        let genmc_config = miri_config.genmc_config.as_ref().unwrap();
        let handle = RefCell::new(create_genmc_driver_handle(
            &genmc_config.params,
            genmc_config.log_level,
            /* do_estimation: */ mode == GenmcMode::Estimation,
        ));
        Self { handle, exec_state: Default::default(), global_state }
    }

    fn get_estimation_results(&self) -> EstimationResult {
        self.handle.borrow().get_estimation_results()
    }

    /// Get the number of blocked executions encountered by GenMC.
    fn get_blocked_execution_count(&self) -> u64 {
        self.handle.borrow().get_blocked_execution_count()
    }

    /// Get the number of explored executions encountered by GenMC.
    fn get_explored_execution_count(&self) -> u64 {
        self.handle.borrow().get_explored_execution_count()
    }

    /// Check if GenMC encountered an error that wasn't immediately returned during execution.
    /// Returns a string representation of the error if one occurred.
    fn try_get_error(&self) -> Option<String> {
        self.handle
            .borrow()
            .get_error_string()
            .as_ref()
            .map(|error| error.to_string_lossy().to_string())
    }

    /// Check if GenMC encountered an error that wasn't immediately returned during execution.
    /// Returns a string representation of the error if one occurred.
    fn get_result_message(&self) -> String {
        self.handle
            .borrow()
            .get_result_message()
            .as_ref()
            .map(|error| error.to_string_lossy().to_string())
            .expect("there should always be a message")
    }

    /// Select whether data race free actions should be allowed. This function should be used carefully!
    ///
    /// If `true` is passed, allow for data races to happen without triggering an error, until this function is called again with argument `false`.
    /// This allows for racy non-atomic memory accesses to be ignored (GenMC is not informed about them at all).
    ///
    /// Certain operations are not permitted in GenMC mode with data races disabled and will cause a panic, e.g., atomic accesses or asking for scheduling decisions.
    ///
    /// # Panics
    /// If data race free is attempted to be set more than once (i.e., no nesting allowed).
    pub(super) fn set_ongoing_action_data_race_free(&self, enable: bool) {
        debug!("GenMC: set_ongoing_action_data_race_free ({enable})");
        let old = self.exec_state.allow_data_races.replace(enable);
        assert_ne!(old, enable, "cannot nest allow_data_races");
    }

    /// Check whether data races are currently allowed (e.g., for loading values for validation which are not actually loaded by the program).
    fn get_alloc_data_races(&self) -> bool {
        self.exec_state.allow_data_races.get()
    }

    /// Get the GenMC id of the currently active thread.
    #[must_use]
    fn active_thread_genmc_tid<'tcx>(&self, machine: &MiriMachine<'tcx>) -> i32 {
        let thread_infos = self.exec_state.thread_id_manager.borrow();
        let curr_thread = machine.threads.active_thread();
        thread_infos.get_genmc_tid(curr_thread)
    }
}

/// GenMC event handling. These methods are used to inform GenMC about events happening in the program, and to handle scheduling decisions.
impl GenmcCtx {
    /// Prepare for the next execution and inform GenMC about it.
    /// Must be called before at the start of every execution.
    fn prepare_next_execution(&self) {
        // Reset per-execution state.
        self.exec_state.reset();
        // Inform GenMC about the new execution.
        self.handle.borrow_mut().pin_mut().handle_execution_start();
    }

    /// Inform GenMC that the program's execution has ended.
    ///
    /// Don't call this function if an error was found.
    ///
    /// GenMC detects certain errors only when the execution ends.
    /// If an error occurred, a string containing a short error description is returned.
    ///
    /// GenMC currently doesn't return an error in all cases immediately when one happens.
    /// This function will also check for those, and return their error description.
    ///
    /// To get the all messages (warnings, errors) that GenMC produces, use the `get_result_message` method.
    fn handle_execution_end(&self) -> ExecutionEndResult {
        let result = self.handle.borrow_mut().pin_mut().handle_execution_end();
        if let Some(error) = result.as_ref() {
            return ExecutionEndResult::Error(error.to_string_lossy().to_string());
        }

        // GenMC decides if there is more to explore:
        let exploration_done = self.handle.borrow_mut().pin_mut().is_exploration_done();

        // GenMC currently does not return an error value immediately in all cases.
        // Both `handle_execution_end` and `is_exploration_done` can produce such errors.
        // We manually query for any errors here to ensure we don't miss any.
        if let Some(error) = self.try_get_error() {
            ExecutionEndResult::Error(error)
        } else if exploration_done {
            ExecutionEndResult::Stop
        } else {
            ExecutionEndResult::Continue
        }
    }

    /**** Memory access handling ****/

    /// Inform GenMC about an atomic load.
    /// Returns that value that the load should read.
    ///
    /// `old_value` is the value that a non-atomic load would read here, or `None` if the memory is uninitialized.
    pub(crate) fn atomic_load<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        ordering: AtomicReadOrd,
        old_val: Option<Scalar>,
    ) -> InterpResult<'tcx, Scalar> {
        assert!(!self.get_alloc_data_races(), "atomic load with data race checking disabled.");
        let genmc_old_value = if let Some(scalar) = old_val {
            scalar_to_genmc_scalar(ecx, self, scalar)?
        } else {
            GenmcScalar::UNINIT
        };
        let read_value =
            self.handle_load(&ecx.machine, address, size, ordering.to_genmc(), genmc_old_value)?;
        genmc_scalar_to_scalar(ecx, self, read_value, size)
    }

    /// Inform GenMC about an atomic store.
    /// Returns `true` if the stored value should be reflected in Miri's memory.
    ///
    /// `old_value` is the value that a non-atomic load would read here, or `None` if the memory is uninitialized.
    pub(crate) fn atomic_store<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        value: Scalar,
        old_value: Option<Scalar>,
        ordering: AtomicWriteOrd,
    ) -> InterpResult<'tcx, bool> {
        assert!(!self.get_alloc_data_races(), "atomic store with data race checking disabled.");
        let genmc_value = scalar_to_genmc_scalar(ecx, self, value)?;
        let genmc_old_value = if let Some(scalar) = old_value {
            scalar_to_genmc_scalar(ecx, self, scalar)?
        } else {
            GenmcScalar::UNINIT
        };
        self.handle_store(
            &ecx.machine,
            address,
            size,
            genmc_value,
            genmc_old_value,
            ordering.to_genmc(),
        )
    }

    /// Inform GenMC about an atomic fence.
    pub(crate) fn atomic_fence<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        ordering: AtomicFenceOrd,
    ) -> InterpResult<'tcx> {
        assert!(!self.get_alloc_data_races(), "atomic fence with data race checking disabled.");
        self.handle
            .borrow_mut()
            .pin_mut()
            .handle_fence(self.active_thread_genmc_tid(machine), ordering.to_genmc());
        interp_ok(())
    }

    /// Inform GenMC about an atomic read-modify-write operation.
    ///
    /// Returns `(old_val, Option<new_val>)`. `new_val` might not be the latest write in coherence order, which is indicated by `None`.
    ///
    /// `old_value` is the value that a non-atomic load would read here, or `None` if the memory is uninitialized.
    pub(crate) fn atomic_rmw_op<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        atomic_op: AtomicRmwOp,
        is_signed: bool,
        ordering: AtomicRwOrd,
        rhs_scalar: Scalar,
        old_value: Scalar,
    ) -> InterpResult<'tcx, (Scalar, Option<Scalar>)> {
        self.handle_atomic_rmw_op(
            ecx,
            address,
            size,
            ordering,
            to_genmc_rmw_op(atomic_op, is_signed),
            scalar_to_genmc_scalar(ecx, self, rhs_scalar)?,
            scalar_to_genmc_scalar(ecx, self, old_value)?,
        )
    }

    /// Returns `(old_val, Option<new_val>)`. `new_val` might not be the latest write in coherence order, which is indicated by `None`.
    ///
    /// `old_value` is the value that a non-atomic load would read here, or `None` if the memory is uninitialized.
    pub(crate) fn atomic_exchange<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        rhs_scalar: Scalar,
        ordering: AtomicRwOrd,
        old_value: Scalar,
    ) -> InterpResult<'tcx, (Scalar, Option<Scalar>)> {
        self.handle_atomic_rmw_op(
            ecx,
            address,
            size,
            ordering,
            /* genmc_rmw_op */ RMWBinOp::Xchg,
            scalar_to_genmc_scalar(ecx, self, rhs_scalar)?,
            scalar_to_genmc_scalar(ecx, self, old_value)?,
        )
    }

    /// Inform GenMC about an atomic compare-exchange operation.
    ///
    /// Returns the old value read by the compare exchange, optionally the value that Miri should write back to its memory, and whether the compare-exchange was a success or not.
    ///
    /// `old_value` is the value that a non-atomic load would read here, or `None` if the memory is uninitialized.
    pub(crate) fn atomic_compare_exchange<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        expected_old_value: Scalar,
        new_value: Scalar,
        success: AtomicRwOrd,
        fail: AtomicReadOrd,
        can_fail_spuriously: bool,
        old_value: Scalar,
    ) -> InterpResult<'tcx, (Scalar, Option<Scalar>, bool)> {
        assert!(
            !self.get_alloc_data_races(),
            "atomic compare-exchange with data race checking disabled."
        );
        assert_ne!(0, size.bytes());
        assert!(
            size.bytes() <= MAX_ACCESS_SIZE,
            "GenMC currently does not support atomic accesses larger than {} bytes (got {} bytes)",
            MAX_ACCESS_SIZE,
            size.bytes()
        );

        // Upgrade the success memory ordering to equal the failure ordering, since GenMC currently ignores the failure ordering.
        // FIXME(genmc): remove this once GenMC properly supports the failure memory ordering.
        let upgraded_success_ordering =
            maybe_upgrade_compare_exchange_success_orderings(success, fail);

        // FIXME(genmc): remove once GenMC supports failure memory ordering in `compare_exchange`.
        let (effective_failure_ordering, _) = upgraded_success_ordering.split_memory_orderings();

        // Return a warning if we cannot explore all behaviors of this operation.
        // Only emit this if the operation is "in user code": walk up across `#[track_caller]`
        // frames, then check if the next frame is local.
        let show_warning = || {
            ecx.active_thread_stack()
                .iter()
                .rev()
                .find(|f| !f.instance().def.requires_caller_location(*ecx.tcx))
                .is_none_or(|f| ecx.machine.is_local(f.instance()))
        };
        if (success != upgraded_success_ordering || effective_failure_ordering != fail)
            && show_warning()
        {
            static DEDUP: SpanDedupDiagnostic = SpanDedupDiagnostic::new();
            ecx.dedup_diagnostic(&DEDUP, |_first| {
                NonHaltingDiagnostic::GenmcCompareExchangeOrderingMismatch {
                    success_ordering: success,
                    upgraded_success_ordering,
                    failure_ordering: fail,
                    effective_failure_ordering,
                }
            });
        }
        // FIXME(genmc): remove once GenMC implements spurious failures for `compare_exchange_weak`.
        if can_fail_spuriously && show_warning() {
            static DEDUP: SpanDedupDiagnostic = SpanDedupDiagnostic::new();
            ecx.dedup_diagnostic(&DEDUP, |_first| NonHaltingDiagnostic::GenmcCompareExchangeWeak);
        }

        debug!(
            "GenMC: atomic_compare_exchange, address: {address:?}, size: {size:?} (expect: {expected_old_value:?}, new: {new_value:?}, old_value: {old_value:?}, {success:?}, orderings: {fail:?}), can fail spuriously: {can_fail_spuriously}"
        );
        let cas_result = self.handle.borrow_mut().pin_mut().handle_compare_exchange(
            self.active_thread_genmc_tid(&ecx.machine),
            address.bytes(),
            size.bytes(),
            scalar_to_genmc_scalar(ecx, self, expected_old_value)?,
            scalar_to_genmc_scalar(ecx, self, new_value)?,
            scalar_to_genmc_scalar(ecx, self, old_value)?,
            upgraded_success_ordering.to_genmc(),
            fail.to_genmc(),
            can_fail_spuriously,
        );

        if let Some(error) = cas_result.error.as_ref() {
            // FIXME(genmc): error handling
            throw_ub_format!("{}", error.to_string_lossy());
        }

        let return_scalar = genmc_scalar_to_scalar(ecx, self, cas_result.old_value, size)?;
        debug!(
            "GenMC: atomic_compare_exchange: result: {cas_result:?}, returning scalar: {return_scalar:?}"
        );
        // The write can only be a co-maximal write if the CAS succeeded.
        assert!(cas_result.is_success || !cas_result.is_coherence_order_maximal_write);
        interp_ok((
            return_scalar,
            cas_result.is_coherence_order_maximal_write.then_some(new_value),
            cas_result.is_success,
        ))
    }

    /// Inform GenMC about a non-atomic memory load
    ///
    /// NOTE: Unlike for *atomic* loads, we don't return a value here. Non-atomic values are still handled by Miri.
    pub(crate) fn memory_load<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        address: Size,
        size: Size,
    ) -> InterpResult<'tcx> {
        debug!(
            "GenMC: received memory_load (non-atomic): address: {:#x}, size: {}",
            address.bytes(),
            size.bytes()
        );
        if self.get_alloc_data_races() {
            debug!("GenMC: data race checking disabled, ignoring non-atomic load.");
            return interp_ok(());
        }
        // GenMC doesn't like ZSTs, and they can't have any data races, so we skip them
        if size.bytes() == 0 {
            return interp_ok(());
        }

        let handle_load = |address, size| {
            // NOTE: Values loaded non-atomically are still handled by Miri, so we discard whatever we get from GenMC
            let _read_value = self.handle_load(
                machine,
                address,
                size,
                MemOrdering::NotAtomic,
                // This value is used to update the co-maximal store event to the same location.
                // We don't need to update that store, since if it is ever read by any atomic loads, the value will be updated then.
                // We use uninit for lack of a better value, since we don't know whether the location we currently load from is initialized or not.
                GenmcScalar::UNINIT,
            )?;
            interp_ok(())
        };

        // This load is small enough so GenMC can handle it.
        if size.bytes() <= MAX_ACCESS_SIZE {
            return handle_load(address, size);
        }

        // This load is too big to be a single GenMC access, we have to split it.
        // FIXME(genmc): This will misbehave if there are non-64bit-atomics in there.
        // Needs proper support on the GenMC side for large and mixed atomic accesses.
        for (address, size) in split_access(address, size) {
            handle_load(Size::from_bytes(address), Size::from_bytes(size))?;
        }
        interp_ok(())
    }

    /// Inform GenMC about a non-atomic memory store
    ///
    /// NOTE: Unlike for *atomic* stores, we don't provide the actual stored values to GenMC here.
    pub(crate) fn memory_store<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        address: Size,
        size: Size,
    ) -> InterpResult<'tcx> {
        debug!(
            "GenMC: received memory_store (non-atomic): address: {:#x}, size: {}",
            address.bytes(),
            size.bytes()
        );
        if self.get_alloc_data_races() {
            debug!("GenMC: data race checking disabled, ignoring non-atomic store.");
            return interp_ok(());
        }
        // GenMC doesn't like ZSTs, and they can't have any data races, so we skip them
        if size.bytes() == 0 {
            return interp_ok(());
        }

        let handle_store = |address, size| {
            // We always write the the stored values to Miri's memory, whether GenMC says the write is co-maximal or not.
            // The GenMC scheduler ensures that replaying an execution happens in porf-respecting order (po := program order, rf: reads-from order).
            // This means that for any non-atomic read Miri performs, the corresponding write has already been replayed.
            let _is_co_max_write = self.handle_store(
                machine,
                address,
                size,
                // We don't know the value that this store will write, but GenMC expects that we give it an actual value.
                // Unfortunately, there are situations where this value can actually become visible
                // to the program: when there is an atomic load reading from a non-atomic store.
                // FIXME(genmc): update once mixed atomic-non-atomic support is added. Afterwards, this value should never be readable.
                GenmcScalar::from_u64(0xDEADBEEF),
                // This value is used to update the co-maximal store event to the same location.
                // This old value cannot be read anymore by any future loads, since we are doing another non-atomic store to the same location.
                // Any future load will either see the store we are adding now, or we have a data race (there can only be one possible non-atomic value to read from at any time).
                // We use uninit for lack of a better value, since we don't know whether the location we currently write to is initialized or not.
                GenmcScalar::UNINIT,
                MemOrdering::NotAtomic,
            )?;
            interp_ok(())
        };

        // This store is small enough so GenMC can handle it.
        if size.bytes() <= MAX_ACCESS_SIZE {
            return handle_store(address, size);
        }

        // This store is too big to be a single GenMC access, we have to split it.
        // FIXME(genmc): This will misbehave if there are non-64bit-atomics in there.
        // Needs proper support on the GenMC side for large and mixed atomic accesses.
        for (address, size) in split_access(address, size) {
            handle_store(Size::from_bytes(address), Size::from_bytes(size))?;
        }
        interp_ok(())
    }

    /**** Memory (de)allocation ****/

    /// This is also responsible for determining the address of the new allocation.
    pub(crate) fn handle_alloc<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        alloc_id: AllocId,
        size: Size,
        alignment: Align,
        memory_kind: MemoryKind,
    ) -> InterpResult<'tcx, u64> {
        assert!(
            !self.get_alloc_data_races(),
            "memory allocation with data race checking disabled."
        );
        let machine = &ecx.machine;
        if memory_kind == MiriMemoryKind::Global.into() {
            return ecx
                .get_global_allocation_address(&self.global_state.global_allocations, alloc_id);
        }
        // GenMC doesn't support ZSTs, so we set the minimum size to 1 byte
        let genmc_size = size.bytes().max(1);
        let chosen_address = self.handle.borrow_mut().pin_mut().handle_malloc(
            self.active_thread_genmc_tid(machine),
            genmc_size,
            alignment.bytes(),
        );
        if chosen_address == 0 {
            throw_exhaust!(AddressSpaceFull);
        }

        // Non-global addresses should not be in the global address space.
        assert_eq!(0, chosen_address & GENMC_GLOBAL_ADDRESSES_MASK);
        // Sanity check the address alignment:
        assert!(
            chosen_address.is_multiple_of(alignment.bytes()),
            "GenMC returned address {chosen_address:#x} with lower alignment than requested ({}).",
            alignment.bytes()
        );

        interp_ok(chosen_address)
    }

    pub(crate) fn handle_dealloc<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        alloc_id: AllocId,
        address: Size,
        kind: MemoryKind,
    ) -> InterpResult<'tcx> {
        assert_ne!(
            kind,
            MiriMemoryKind::Global.into(),
            "we probably shouldn't try to deallocate global allocations (alloc_id: {alloc_id:?})"
        );
        assert!(
            !self.get_alloc_data_races(),
            "memory deallocation with data race checking disabled."
        );
        let free_result = self
            .handle
            .borrow_mut()
            .pin_mut()
            .handle_free(self.active_thread_genmc_tid(machine), address.bytes());
        if let Some(error) = free_result.as_ref() {
            // FIXME(genmc): improve error handling.
            throw_ub_format!("{}", error.to_string_lossy());
        }

        interp_ok(())
    }

    /**** Thread management ****/

    pub(crate) fn handle_thread_create<'tcx>(
        &self,
        threads: &ThreadManager<'tcx>,
        // FIXME(genmc,symmetry reduction): pass info to GenMC
        _start_routine: crate::Pointer,
        _func_arg: &crate::ImmTy<'tcx>,
        new_thread_id: ThreadId,
    ) -> InterpResult<'tcx> {
        assert!(!self.get_alloc_data_races(), "thread creation with data race checking disabled.");
        let mut thread_infos = self.exec_state.thread_id_manager.borrow_mut();

        let curr_thread_id = threads.active_thread();
        let genmc_parent_tid = thread_infos.get_genmc_tid(curr_thread_id);
        let genmc_new_tid = thread_infos.add_thread(new_thread_id);

        self.handle.borrow_mut().pin_mut().handle_thread_create(genmc_new_tid, genmc_parent_tid);
        interp_ok(())
    }

    pub(crate) fn handle_thread_join<'tcx>(
        &self,
        active_thread_id: ThreadId,
        child_thread_id: ThreadId,
    ) -> InterpResult<'tcx> {
        assert!(!self.get_alloc_data_races(), "thread join with data race checking disabled.");
        let thread_infos = self.exec_state.thread_id_manager.borrow();

        let genmc_curr_tid = thread_infos.get_genmc_tid(active_thread_id);
        let genmc_child_tid = thread_infos.get_genmc_tid(child_thread_id);

        self.handle.borrow_mut().pin_mut().handle_thread_join(genmc_curr_tid, genmc_child_tid);

        interp_ok(())
    }

    pub(crate) fn handle_thread_finish<'tcx>(&self, threads: &ThreadManager<'tcx>) {
        assert!(!self.get_alloc_data_races(), "thread finish with data race checking disabled.");
        let curr_thread_id = threads.active_thread();

        let thread_infos = self.exec_state.thread_id_manager.borrow();
        let genmc_tid = thread_infos.get_genmc_tid(curr_thread_id);

        debug!("GenMC: thread {curr_thread_id:?} ({genmc_tid:?}) finished.");
        // NOTE: Miri doesn't support return values for threads, but GenMC expects one, so we return 0.
        self.handle.borrow_mut().pin_mut().handle_thread_finish(genmc_tid, /* ret_val */ 0);
    }

    /// Handle a call to `libc::exit` or the exit of the main thread.
    /// Unless an error is returned, the program should continue executing (in a different thread,
    /// chosen by the next scheduling call).
    pub(crate) fn handle_exit<'tcx>(
        &self,
        thread: ThreadId,
        exit_code: i32,
        exit_type: ExitType,
    ) -> InterpResult<'tcx> {
        if let Some(old_exit_status) = self.exec_state.exit_status.get() {
            throw_ub_format!(
                "`exit` called twice, first with exit code {old_exit_code}, now with status {exit_code}",
                old_exit_code = old_exit_status.exit_code,
            );
        }

        match exit_type {
            ExitType::ExitCalled => {
                // `exit` kills the current thread; we have to tell GenMC about this.
                let thread_infos = self.exec_state.thread_id_manager.borrow();
                let genmc_tid = thread_infos.get_genmc_tid(thread);
                self.handle.borrow_mut().pin_mut().handle_thread_kill(genmc_tid);
            }
            ExitType::MainThreadFinish => {
                // The main thread has already exited so we don't call `handle_thread_kill` again.
                assert_eq!(thread, ThreadId::MAIN_THREAD);
            }
        }
        // To cover all possible behaviors, we have to continue execution the other threads:
        // whatever they do next could also have happened before the `exit` call. So we just
        // remember the exit status and use it when the other threads are done.
        self.exec_state.exit_status.set(Some(ExitStatus { exit_code, exit_type }));
        interp_ok(())
    }
}

impl GenmcCtx {
    /// Inform GenMC about a load (atomic or non-atomic).
    /// Returns the value that GenMC wants this load to read.
    fn handle_load<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        address: Size,
        size: Size,
        memory_ordering: MemOrdering,
        genmc_old_value: GenmcScalar,
    ) -> InterpResult<'tcx, GenmcScalar> {
        assert!(
            size.bytes() != 0
                && (memory_ordering == MemOrdering::NotAtomic || size.bytes().is_power_of_two())
        );
        if size.bytes() > MAX_ACCESS_SIZE {
            throw_unsup_format!(
                "GenMC mode currently does not support atomics larger than {MAX_ACCESS_SIZE} bytes.",
            );
        }
        debug!(
            "GenMC: load, address: {addr} == {addr:#x}, size: {size:?}, ordering: {memory_ordering:?}, old_value: {genmc_old_value:x?}",
            addr = address.bytes()
        );
        let load_result = self.handle.borrow_mut().pin_mut().handle_load(
            self.active_thread_genmc_tid(machine),
            address.bytes(),
            size.bytes(),
            memory_ordering,
            genmc_old_value,
        );

        if let Some(error) = load_result.error.as_ref() {
            // FIXME(genmc): error handling
            throw_ub_format!("{}", error.to_string_lossy());
        }

        if !load_result.has_value {
            // FIXME(GenMC): Implementing certain GenMC optimizations will lead to this.
            unimplemented!("GenMC: load returned no value.");
        }

        debug!("GenMC: load returned value: {:?}", load_result.read_value);
        interp_ok(load_result.read_value)
    }

    /// Inform GenMC about a store (atomic or non-atomic).
    /// Returns true if the store is co-maximal, i.e., it should be written to Miri's memory too.
    fn handle_store<'tcx>(
        &self,
        machine: &MiriMachine<'tcx>,
        address: Size,
        size: Size,
        genmc_value: GenmcScalar,
        genmc_old_value: GenmcScalar,
        memory_ordering: MemOrdering,
    ) -> InterpResult<'tcx, bool> {
        assert!(
            size.bytes() != 0
                && (memory_ordering == MemOrdering::NotAtomic || size.bytes().is_power_of_two())
        );
        if size.bytes() > MAX_ACCESS_SIZE {
            throw_unsup_format!(
                "GenMC mode currently does not support atomics larger than {MAX_ACCESS_SIZE} bytes."
            );
        }
        debug!(
            "GenMC: store, address: {addr} = {addr:#x}, size: {size:?}, ordering {memory_ordering:?}, value: {genmc_value:?}",
            addr = address.bytes()
        );
        let store_result = self.handle.borrow_mut().pin_mut().handle_store(
            self.active_thread_genmc_tid(machine),
            address.bytes(),
            size.bytes(),
            genmc_value,
            genmc_old_value,
            memory_ordering,
        );

        if let Some(error) = store_result.error.as_ref() {
            // FIXME(genmc): error handling
            throw_ub_format!("{}", error.to_string_lossy());
        }

        interp_ok(store_result.is_coherence_order_maximal_write)
    }

    /// Inform GenMC about an atomic read-modify-write operation.
    /// This includes atomic swap (also often called "exchange"), but does *not*
    /// include compare-exchange (see `RMWBinOp` for full list of operations).
    /// Returns the previous value at that memory location, and optionally the value that should be written back to Miri's memory.
    fn handle_atomic_rmw_op<'tcx>(
        &self,
        ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
        address: Size,
        size: Size,
        ordering: AtomicRwOrd,
        genmc_rmw_op: RMWBinOp,
        genmc_rhs_scalar: GenmcScalar,
        genmc_old_value: GenmcScalar,
    ) -> InterpResult<'tcx, (Scalar, Option<Scalar>)> {
        assert!(
            !self.get_alloc_data_races(),
            "atomic read-modify-write operation with data race checking disabled."
        );
        assert_ne!(0, size.bytes());
        assert!(
            size.bytes() <= MAX_ACCESS_SIZE,
            "GenMC currently does not support atomic accesses larger than {} bytes (got {} bytes)",
            MAX_ACCESS_SIZE,
            size.bytes()
        );
        debug!(
            "GenMC: atomic_rmw_op (op: {genmc_rmw_op:?}, rhs value: {genmc_rhs_scalar:?}), address: {address:?}, size: {size:?}, ordering: {ordering:?}",
        );
        let rmw_result = self.handle.borrow_mut().pin_mut().handle_read_modify_write(
            self.active_thread_genmc_tid(&ecx.machine),
            address.bytes(),
            size.bytes(),
            genmc_rmw_op,
            ordering.to_genmc(),
            genmc_rhs_scalar,
            genmc_old_value,
        );

        if let Some(error) = rmw_result.error.as_ref() {
            // FIXME(genmc): error handling
            throw_ub_format!("{}", error.to_string_lossy());
        }

        let old_value_scalar = genmc_scalar_to_scalar(ecx, self, rmw_result.old_value, size)?;

        let new_value_scalar = if rmw_result.is_coherence_order_maximal_write {
            Some(genmc_scalar_to_scalar(ecx, self, rmw_result.new_value, size)?)
        } else {
            None
        };
        interp_ok((old_value_scalar, new_value_scalar))
    }
}

impl VisitProvenance for GenmcCtx {
    fn visit_provenance(&self, visit: &mut VisitWith<'_>) {
        let genmc_shared_allocs_map = self.exec_state.genmc_shared_allocs_map.borrow();
        for alloc_id in genmc_shared_allocs_map.values().copied() {
            visit(Some(alloc_id), None);
        }
    }
}
