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}