Skip to main content

frame_core/
composition.rs

1//! Correct beamr scheduler composition for hot-loading hosts.
2//!
3//! `Scheduler::with_services` silently installs an EMPTY BIF registry
4//! (beamr 0.15.1 `scheduler/mod.rs:1010-1024`). Import resolution is
5//! bif-registry-first, so every `erlang:*` import in hot-loaded bytecode
6//! resolves Deferred; guard-BIF dispatch requires a Native entry
7//! (beamr `guards.rs:193-195`) and refuses at the first arithmetic
8//! instruction with a process-fatal
9//! `InvalidOperand("guard bif native import")` — the composition defect
10//! attributed in `frame-host/attribution/gcbif-wedge/` and Artemis's
11//! 2026-07-18 gcbif probe report (Round 2).
12//!
13//! Every frame scheduler that hot-loads component bytecode must therefore be
14//! composed through [`compose_scheduler`], which populates the registry via
15//! beamr's canonical gate-1 population path
16//! ([`beamr::native::bifs::register_gate1_bifs`]) and constructs through
17//! [`Scheduler::with_services_and_code_server`]. As defense in depth,
18//! [`crate::registry::ComponentRegistry::start`] refuses any hot-loaded
19//! module whose committed import table still defers an `erlang:*` entry.
20
21use std::sync::Arc;
22
23use beamr::atom::{Atom, AtomTable};
24use beamr::module::{ModuleRegistry, ResolvedImportTarget};
25use beamr::namespace::NamespaceId;
26use beamr::native::{BifRegistryImpl, NativeRegistrationError};
27use beamr::scheduler::{Scheduler, SchedulerConfig, SchedulerServices};
28use thiserror::Error;
29
30/// A typed refusal from scheduler composition.
31#[derive(Debug, Error)]
32pub enum SchedulerCompositionError {
33    /// The canonical BIF population path refused a registration.
34    #[error("BIF registry population failed: {source}")]
35    BifRegistration {
36        /// Exact beamr registration refusal.
37        #[source]
38        source: NativeRegistrationError,
39    },
40    /// beamr refused scheduler construction.
41    #[error("scheduler construction failed: {detail}")]
42    Construction {
43        /// Exact beamr construction refusal.
44        detail: String,
45    },
46}
47
48/// Composes a scheduler whose BIF registry is populated BEFORE any module
49/// can load against it.
50///
51/// The atom table (seeded with beamr's common atoms) and the gate-1-populated
52/// BIF registry are handed to [`Scheduler::with_services_and_code_server`] so
53/// load-time import resolution can bind `erlang:*` imports to Native entries.
54/// Never construct a hot-loading scheduler through bare
55/// [`Scheduler::with_services`]: it installs an empty BIF registry and every
56/// `erlang:*` import silently defers into a process-fatal dispatch refusal.
57///
58/// # Errors
59///
60/// Returns a typed failure when gate-1 BIF population or beamr scheduler
61/// construction refuses.
62pub fn compose_scheduler(
63    config: SchedulerConfig,
64    services: SchedulerServices,
65    module_registry: Arc<ModuleRegistry>,
66) -> Result<Scheduler, SchedulerCompositionError> {
67    let atom_table = Arc::new(AtomTable::with_common_atoms());
68    let bif_registry = Arc::new(BifRegistryImpl::new());
69    beamr::native::bifs::register_gate1_bifs(&bif_registry, &atom_table)
70        .map_err(|source| SchedulerCompositionError::BifRegistration { source })?;
71    Scheduler::with_services_and_code_server(
72        config,
73        services,
74        module_registry,
75        atom_table,
76        bif_registry,
77    )
78    .map_err(|detail| SchedulerCompositionError::Construction { detail })
79}
80
81/// Resolves an atom to its human-readable name, falling back to the debug
82/// rendering when the atom is somehow absent from the table (never a panic).
83pub(crate) fn resolve_atom(scheduler: &Scheduler, atom: Atom) -> String {
84    scheduler
85        .atom_table()
86        .resolve(atom)
87        .map_or_else(|| format!("{atom:?}"), str::to_owned)
88}
89
90/// Scans a committed module's dispatch table for deferred `erlang:*` imports.
91///
92/// Returns `None` when the module is not committed in the default namespace;
93/// otherwise the (possibly empty) list of `erlang:name/arity` entries whose
94/// resolution is still Deferred. A deferred built-in can never resolve — no
95/// BEAM module named `erlang` will ever load — and dispatch kills the calling
96/// process at first use with `InvalidOperand("guard bif native import")`, so
97/// every load path in this crate refuses such a module up front
98/// ([`crate::registry::ComponentRegistry::start`] for component bytecode,
99/// [`crate::runtime::ComponentRuntime::load_support_module`] for support
100/// modules).
101///
102/// The committed [`beamr::module::Module`] retains the loader's per-import
103/// resolution, so this reads the actual dispatch table rather than re-running
104/// the loader.
105pub(crate) fn deferred_erlang_imports(scheduler: &Scheduler, module: Atom) -> Option<Vec<String>> {
106    let committed = scheduler.lookup_module_in(NamespaceId::DEFAULT, module)?;
107    let erlang = scheduler.atom_table().intern("erlang");
108    Some(
109        committed
110            .resolved_imports
111            .iter()
112            .filter(|import| {
113                import.module == erlang
114                    && matches!(import.target, ResolvedImportTarget::Deferred { .. })
115            })
116            .map(|import| {
117                format!(
118                    "erlang:{}/{}",
119                    resolve_atom(scheduler, import.function),
120                    import.arity
121                )
122            })
123            .collect(),
124    )
125}