alma 0.1.1

A Bevy-native modal text editor with Vim-style navigation.
Documentation
use super::error::{
    WasmtimePluginRuntimeError, WasmtimePluginRuntimeFailure, WasmtimePluginRuntimeFailureKind,
};
use crate::plugin::{PluginIdentity, PluginOperationalExport};
use std::{
    fmt::{Debug, Formatter},
    sync::{
        Arc, Mutex,
        atomic::{AtomicBool, Ordering},
    },
    thread::{self, JoinHandle},
    time::{Duration, Instant},
};
use wasmtime::{Engine, Error as WasmtimeError, Trap};

/// Shared epoch driver for Wasmtime interruption.
#[derive(Clone)]
pub(super) struct EpochTicker {
    /// Final clone stops the worker.
    inner: Arc<EpochTickerInner>,
}

impl EpochTicker {
    /// Starts one epoch driver for an engine.
    pub(super) fn start(engine: Engine, interval: Duration) -> std::io::Result<Self> {
        let active = Arc::new(AtomicBool::new(true));
        let worker_active = Arc::clone(&active);
        let handle = thread::Builder::new()
            .name(String::from("alma-plugin-epoch"))
            .spawn(move || {
                while worker_active.load(Ordering::Acquire) {
                    thread::sleep(interval);
                    engine.increment_epoch();
                }
            })?;
        Ok(Self {
            inner: Arc::new(EpochTickerInner {
                active,
                handle: Mutex::new(Some(handle)),
                interval,
            }),
        })
    }

    /// Returns the tick interval.
    pub(super) fn interval(&self) -> Duration {
        self.inner.interval
    }
}

impl Debug for EpochTicker {
    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
        formatter
            .debug_struct("EpochTicker")
            .field("interval", &self.inner.interval)
            .finish_non_exhaustive()
    }
}

/// Shared worker state for Wasmtime epoch interruption.
pub(super) struct EpochTickerInner {
    /// Release/acquire stop signal.
    active: Arc<AtomicBool>,
    /// Worker join ownership.
    handle: Mutex<Option<JoinHandle<()>>>,
    /// Engine epoch cadence.
    interval: Duration,
}

impl Drop for EpochTickerInner {
    fn drop(&mut self) {
        self.active.store(false, Ordering::Release);
        if let Ok(mut handle) = self.handle.lock()
            && let Some(handle) = handle.take()
        {
            let _joined = handle.join();
        }
    }
}

/// Deadline for one guest export call.
#[derive(Clone, Copy, Debug)]
pub(super) struct GuestDeadline {
    /// Monotonic baseline for late-return checks.
    pub(super) started: Instant,
    /// Wall-clock cap mirrored by the epoch trap.
    pub(super) timeout: Duration,
    /// Non-zero Wasmtime epoch budget.
    pub(super) epoch_ticks: u64,
}

impl GuestDeadline {
    /// Computes the epoch deadline for one guest call.
    pub(super) fn new(timeout: Duration, ticker: &EpochTicker) -> Self {
        Self {
            started: Instant::now(),
            timeout,
            epoch_ticks: epoch_ticks_for(timeout, ticker.interval()),
        }
    }

    /// Returns the Wasmtime epoch ticks allowed for this call.
    pub(super) const fn epoch_ticks(self) -> u64 {
        self.epoch_ticks
    }

    /// Returns true when the wall-clock deadline elapsed.
    pub(super) fn expired(self) -> bool {
        self.started.elapsed() >= self.timeout
    }

    /// Returns true when Wasmtime stopped this call through the armed deadline.
    pub(super) fn trapped_by_deadline(self, source: &WasmtimeError) -> bool {
        matches!(source.downcast_ref::<Trap>(), Some(Trap::Interrupt)) || self.expired()
    }
}

/// Deadline classification for one guest export invocation.
#[derive(Clone, Copy, Debug)]
pub(super) struct GuestExecutionGuard {
    /// Export name in redacted diagnostics.
    pub(super) export: PluginOperationalExport,
    /// Config value reported on timeout.
    pub(super) timeout_ms: u64,
    /// Classifies traps and late returns.
    pub(super) deadline: GuestDeadline,
}

impl GuestExecutionGuard {
    /// Builds the deadline half of an already fuel-armed guest call.
    pub(super) fn new(
        export: PluginOperationalExport,
        timeout_ms: u64,
        ticker: &EpochTicker,
    ) -> Self {
        let timeout = Duration::from_millis(timeout_ms);
        Self {
            export,
            timeout_ms,
            deadline: GuestDeadline::new(timeout, ticker),
        }
    }

    /// Returns the Wasmtime epoch ticks allowed for this call.
    pub(super) const fn epoch_ticks(self) -> u64 {
        self.deadline.epoch_ticks()
    }

    /// Classifies a guest call or canonical cleanup result.
    pub(super) fn ensure_result(
        self,
        identity: &PluginIdentity,
        result: Result<(), WasmtimeError>,
        failure_kind: WasmtimePluginRuntimeFailureKind,
    ) -> Result<(), WasmtimePluginRuntimeError> {
        result.map_err(|source| self.runtime_error(identity, &source, failure_kind))
    }

    /// Fails closed if the guest returned after the wall-clock deadline.
    pub(super) fn ensure_not_expired(
        self,
        identity: &PluginIdentity,
    ) -> Result<(), WasmtimePluginRuntimeError> {
        if self.deadline.expired() {
            return Err(self.timeout_error(identity));
        }
        Ok(())
    }

    /// Maps a Wasmtime trap into timeout or guest-trap vocabulary.
    pub(super) fn runtime_error(
        self,
        identity: &PluginIdentity,
        source: &WasmtimeError,
        failure_kind: WasmtimePluginRuntimeFailureKind,
    ) -> WasmtimePluginRuntimeError {
        if self.deadline.trapped_by_deadline(source) {
            return self.timeout_error(identity);
        }
        WasmtimePluginRuntimeError::GuestTrap {
            identity: identity.clone(),
            export: self.export,
            failure: WasmtimePluginRuntimeFailure::from_wasmtime(failure_kind, &source),
        }
    }

    /// Builds the redacted timeout error for this export.
    pub(super) fn timeout_error(self, identity: &PluginIdentity) -> WasmtimePluginRuntimeError {
        WasmtimePluginRuntimeError::Timeout {
            identity: identity.clone(),
            export: self.export,
            timeout_ms: self.timeout_ms,
        }
    }
}

/// Converts a wall-clock timeout and epoch cadence into a non-zero epoch budget.
pub(super) fn epoch_ticks_for(timeout: Duration, interval: Duration) -> u64 {
    let timeout_nanos = timeout.as_nanos();
    let interval_nanos = interval.as_nanos().max(1);
    let ticks = timeout_nanos.saturating_add(interval_nanos.saturating_sub(1)) / interval_nanos;
    u64::try_from(ticks.max(1)).unwrap_or(u64::MAX)
}