frame-core 0.2.0

Component model, lifecycle, process isolation, and WASM module host
Documentation
//! Correct beamr scheduler composition for hot-loading hosts.
//!
//! `Scheduler::with_services` silently installs an EMPTY BIF registry
//! (beamr 0.15.1 `scheduler/mod.rs:1010-1024`). Import resolution is
//! bif-registry-first, so every `erlang:*` import in hot-loaded bytecode
//! resolves Deferred; guard-BIF dispatch requires a Native entry
//! (beamr `guards.rs:193-195`) and refuses at the first arithmetic
//! instruction with a process-fatal
//! `InvalidOperand("guard bif native import")` — the composition defect
//! attributed in `frame-host/attribution/gcbif-wedge/` and Artemis's
//! 2026-07-18 gcbif probe report (Round 2).
//!
//! Every frame scheduler that hot-loads component bytecode must therefore be
//! composed through [`compose_scheduler`], which populates the registry via
//! beamr's canonical gate-1 population path
//! ([`beamr::native::bifs::register_gate1_bifs`]) and constructs through
//! [`Scheduler::with_services_and_code_server`]. As defense in depth,
//! [`crate::registry::ComponentRegistry::start`] refuses any hot-loaded
//! module whose committed import table still defers an `erlang:*` entry.

use std::sync::Arc;

use beamr::atom::{Atom, AtomTable};
use beamr::module::{ModuleRegistry, ResolvedImportTarget};
use beamr::namespace::NamespaceId;
use beamr::native::{BifRegistryImpl, NativeRegistrationError};
use beamr::scheduler::{Scheduler, SchedulerConfig, SchedulerServices};
use thiserror::Error;

/// A typed refusal from scheduler composition.
#[derive(Debug, Error)]
pub enum SchedulerCompositionError {
    /// The canonical BIF population path refused a registration.
    #[error("BIF registry population failed: {source}")]
    BifRegistration {
        /// Exact beamr registration refusal.
        #[source]
        source: NativeRegistrationError,
    },
    /// beamr refused scheduler construction.
    #[error("scheduler construction failed: {detail}")]
    Construction {
        /// Exact beamr construction refusal.
        detail: String,
    },
}

/// Composes a scheduler whose BIF registry is populated BEFORE any module
/// can load against it.
///
/// The atom table (seeded with beamr's common atoms) and the gate-1-populated
/// BIF registry are handed to [`Scheduler::with_services_and_code_server`] so
/// load-time import resolution can bind `erlang:*` imports to Native entries.
/// Never construct a hot-loading scheduler through bare
/// [`Scheduler::with_services`]: it installs an empty BIF registry and every
/// `erlang:*` import silently defers into a process-fatal dispatch refusal.
///
/// # Errors
///
/// Returns a typed failure when gate-1 BIF population or beamr scheduler
/// construction refuses.
pub fn compose_scheduler(
    config: SchedulerConfig,
    services: SchedulerServices,
    module_registry: Arc<ModuleRegistry>,
) -> Result<Scheduler, SchedulerCompositionError> {
    let atom_table = Arc::new(AtomTable::with_common_atoms());
    let bif_registry = Arc::new(BifRegistryImpl::new());
    beamr::native::bifs::register_gate1_bifs(&bif_registry, &atom_table)
        .map_err(|source| SchedulerCompositionError::BifRegistration { source })?;
    Scheduler::with_services_and_code_server(
        config,
        services,
        module_registry,
        atom_table,
        bif_registry,
    )
    .map_err(|detail| SchedulerCompositionError::Construction { detail })
}

/// Resolves an atom to its human-readable name, falling back to the debug
/// rendering when the atom is somehow absent from the table (never a panic).
pub(crate) fn resolve_atom(scheduler: &Scheduler, atom: Atom) -> String {
    scheduler
        .atom_table()
        .resolve(atom)
        .map_or_else(|| format!("{atom:?}"), str::to_owned)
}

/// Scans a committed module's dispatch table for deferred `erlang:*` imports.
///
/// Returns `None` when the module is not committed in the default namespace;
/// otherwise the (possibly empty) list of `erlang:name/arity` entries whose
/// resolution is still Deferred. A deferred built-in can never resolve — no
/// BEAM module named `erlang` will ever load — and dispatch kills the calling
/// process at first use with `InvalidOperand("guard bif native import")`, so
/// every load path in this crate refuses such a module up front
/// ([`crate::registry::ComponentRegistry::start`] for component bytecode,
/// [`crate::runtime::ComponentRuntime::load_support_module`] for support
/// modules).
///
/// The committed [`beamr::module::Module`] retains the loader's per-import
/// resolution, so this reads the actual dispatch table rather than re-running
/// the loader.
pub(crate) fn deferred_erlang_imports(scheduler: &Scheduler, module: Atom) -> Option<Vec<String>> {
    let committed = scheduler.lookup_module_in(NamespaceId::DEFAULT, module)?;
    let erlang = scheduler.atom_table().intern("erlang");
    Some(
        committed
            .resolved_imports
            .iter()
            .filter(|import| {
                import.module == erlang
                    && matches!(import.target, ResolvedImportTarget::Deferred { .. })
            })
            .map(|import| {
                format!(
                    "erlang:{}/{}",
                    resolve_atom(scheduler, import.function),
                    import.arity
                )
            })
            .collect(),
    )
}