Skip to main content

bb_runtime/engine/
bootstrap.rs

1//! Engine bootstrap state — consolidated owner of every bootstrap
2//! field the engine reads during install + poll.
3//!
4//! Host-driven: install records targets on `install_order` +
5//! `module_bootstraps` but leaves `pending` disarmed. The host arms
6//! the queue via [`crate::node::Node::run_bootstrap`] (which calls
7//! [`Self::arm_install_order`] + `Engine::seed_bootstrap_call`) or
8//! by staging a `BootstrapRequest` (which arms via
9//! [`Self::enqueue_request`]).
10//!
11//! ## Field roles
12//!
13//! - `install_order` — append-only target-name sequence the install
14//!   path stamps when a `module_phase = bootstrap` FunctionProto
15//!   lands. Drives the seeder front-to-back so multi-target installs
16//!   surface one BootstrapComplete per target in the order the host
17//!   supplied to `bytesandbrains::install`.
18//! - `module_bootstraps` — per-target metadata (function key + touch
19//!   set) keyed on the target name. The touch set is the closure of
20//!   every `ComponentRef` referenced by the bootstrap function body
21//!   (slot-id NodeProtos + transitive FunctionCalls); the install
22//!   path stamps it via `Engine::compute_touch_set` so the
23//!   host-driven driver can pre-acquire bound components without
24//!   re-walking the program at run time.
25//! - `component_bootstraps` — per-slot Component bootstrap registry.
26//! - `pending_requests` — host-supplied `BootstrapRequest`s awaiting
27//!   validation + staging.
28//! - `in_flight` — currently executing bootstraps (Module or
29//!   Component). Today at most one entry — the currently seeded
30//!   module bootstrap. The Vec shape readies the host-driven path
31//!   for concurrent Component bootstraps.
32//! - `waiting` — validated + staged `QueuedBootstrap`s ready to fire
33//!   once the in-flight set drains. Empty today; F4 populates.
34//! - `next_idx` — seed pointer into `install_order`. Bumps each time
35//!   `Engine::maybe_complete_bootstrap` observes a phase drained.
36//!   `Engine::seed_bootstrap_call` reads it to pick the next target
37//!   once the host kicks the queue.
38//! - `pending` — coarse "queue still has work" flag the body-op gate
39//!   + `maybe_complete_bootstrap` consult. Armed by
40//!     `arm_install_order` (host kick) or `enqueue_request` (staged
41//!     `BootstrapRequest`), cleared once every queued phase drains.
42
43use std::collections::{HashMap, HashSet, VecDeque};
44
45use crate::engine::dispatch_entry::FunctionKey;
46use crate::ids::{ComponentRef, ExecId};
47
48/// Per-target Module bootstrap metadata. Stamped into the engine
49/// bootstrap state when the install path sees a
50/// `module_phase = bootstrap` FunctionProto.
51#[derive(Clone, Debug)]
52pub struct ModuleBootstrap {
53    /// Canonical `(domain, name, overload)` key of the bootstrap
54    /// FunctionProto. Used to look up the GraphSlot at seed time.
55    pub function_key: FunctionKey,
56
57    /// Closure of every `ComponentRef` referenced by the bootstrap
58    /// body (slot-id NodeProtos + every transitively-called
59    /// FunctionProto's slot-id NodeProtos). Computed by the engine
60    /// at install time; the host-driven driver consults it to pre-
61    /// acquire bound components without re-walking the program.
62    pub touch_set: HashSet<ComponentRef>,
63}
64
65/// Per-slot Component bootstrap metadata. Stamped into the engine
66/// bootstrap state when a Component registers a `Bootstrap`
67/// Contract impl. Empty today; F5 wires the registration path.
68#[derive(Clone, Debug)]
69pub struct ComponentBootstrap {
70    /// Component reference the bootstrap dispatches against.
71    pub cref: ComponentRef,
72}
73
74/// Discriminator for the two bootstrap dispatch kinds the engine
75/// drives. Module bootstraps splice into the FunctionCall path under
76/// a fresh ExecId; Component bootstraps invoke a Contract method on
77/// the bound runtime impl directly.
78#[derive(Clone, Debug)]
79pub enum BootstrapKind {
80    /// Module bootstrap — `target` names the FunctionProto whose
81    /// body the engine seeds onto the frontier.
82    Module {
83        /// Target function name (matches an entry in the engine's
84        /// `install_order` queue).
85        target: String,
86    },
87
88    /// Component bootstrap — `slot` names the binding slot whose
89    /// bound Component the engine invokes.
90    Component {
91        /// Slot name (matches a key in the engine's
92        /// `component_bootstraps` registry).
93        slot: String,
94    },
95}
96
97/// Host-facing bootstrap lifecycle status. Returned by
98/// `Node::bootstrap_status` so the caller can decide whether to keep
99/// polling or surface a "wait for input" prompt. F3 fills the
100/// observable surface.
101#[derive(Clone, Debug, PartialEq, Eq)]
102pub enum BootstrapStatus {
103    /// No bootstrap queued or in-flight — `Node::poll` runs the
104    /// body phase freely.
105    Idle,
106
107    /// Bootstrap is queued + executing. Body-phase ops park until
108    /// the queue drains.
109    Running,
110
111    /// Bootstrap is queued but waiting on host-supplied input
112    /// formals. The host must call `Node::run_bootstrap`
113    /// (`BootstrapTarget::ModuleRequests` or `Slots`) to advance.
114    WaitingForInput,
115}
116
117/// Host-supplied bootstrap input staging request — borrowed shape per
118/// the F5 host-driven bootstrap spec
119/// (`docs/internal/superpowers/specs/2026-06-25-host-driven-bootstrap.md`
120/// §3.1). The host hands the engine a `target` name plus an ordered
121/// `(input_name, value_bytes)` slice; the engine validates against the
122/// target's declared formal input ports and runs the Principle 1a copy
123/// (cap-check → `try_reserve_exact` → `extend_from_slice`) so the
124/// caller's borrowed buffers can drop the moment
125/// [`crate::engine::core::Engine::enqueue_bootstrap_request`] returns.
126///
127/// Owned-form storage on the engine's internal conflict queue lives in
128/// [`OwnedBootstrapRequest`]; the borrowed form here is the host-facing
129/// request shape only.
130pub struct BootstrapRequest<'a> {
131    /// Target function name. Must match an entry in the engine's
132    /// `install_order` queue (i.e. a Module whose `bootstrap`
133    /// FunctionProto landed via `install_function_library`).
134    pub target: &'a str,
135
136    /// Ordered `(input_name, value_bytes)` pairs. Validated against
137    /// the target's declared input formals; missing required inputs
138    /// surface as `BootstrapError::MissingInput` and unknown ones as
139    /// `BootstrapError::UnknownInput` before any staging happens.
140    pub inputs: &'a [(&'a str, &'a [u8])],
141}
142
143/// Owned-form bootstrap input request used by the engine's internal
144/// conflict-queue path (`BootstrapState::pending_requests`). The host-
145/// facing borrowed [`BootstrapRequest`] is the canonical staging shape;
146/// this owned mirror exists so the conflict queue can keep parked
147/// requests alive across poll cycles. Kept internal-ish to the engine —
148/// the [`BootstrapState::enqueue_request`] consumer is exercised by
149/// sibling tests only.
150#[derive(Clone, Debug)]
151pub struct OwnedBootstrapRequest {
152    /// Target function name. Same semantics as
153    /// [`BootstrapRequest::target`].
154    pub target_name: String,
155
156    /// Ordered `(input_name, value_bytes)` pairs. Same semantics as
157    /// [`BootstrapRequest::inputs`] but with owned strings + buffers
158    /// so the queue survives poll cycles.
159    pub inputs: Vec<(String, Vec<u8>)>,
160}
161
162/// Currently executing bootstrap. The host-driven seeder
163/// (`Engine::seed_bootstrap_call`) pops one Module target per phase
164/// and pushes it here; the Vec shape supports concurrent disjoint
165/// Component bootstraps fired through the conflict-queue path.
166#[derive(Clone, Debug)]
167pub struct InFlightBootstrap {
168    /// Kind discriminator — Module vs Component.
169    pub kind: BootstrapKind,
170
171    /// ExecId allocated for the bootstrap's body. The body-op gate
172    /// walks the `parent_exec_id` chain on `pending_calls` and
173    /// fires an op when the chain terminates at this ExecId.
174    pub exec_id: ExecId,
175
176    /// `ComponentRef` closure this bootstrap locks against body-phase
177    /// ops. Populated from `ModuleBootstrap.touch_set` (Module) or the
178    /// single bound `ComponentRef` (Component) at fire time. The
179    /// body-op gate (`Engine::is_op_locked`) parks any body op whose
180    /// touched `ComponentRef` falls in this set so disjoint Components
181    /// can keep firing while bootstrap runs.
182    pub touch_set: HashSet<ComponentRef>,
183
184    /// Names already covered by staged input values. Empty today;
185    /// F4 populates from `BootstrapRequest::inputs` as the host
186    /// supplies formals.
187    pub staged_inputs: HashSet<String>,
188}
189
190/// Validated + staged bootstrap that cleared the touch-set conflict
191/// check and is ready for the engine to assign an ExecId + push its
192/// body onto the frontier. The conflict queue
193/// (`BootstrapState::process_pending_requests` /
194/// `on_bootstrap_drained`) emits `ReadyBootstrap`s back to the engine
195/// instead of mutating in-flight state directly so the seed step
196/// (ExecId allocation, frontier population) stays on the engine
197/// side where the OpRef tables live.
198#[derive(Clone, Debug)]
199pub struct ReadyBootstrap {
200    /// Kind discriminator — Module vs Component.
201    pub kind: BootstrapKind,
202
203    /// `ComponentRef` closure the engine should record on the new
204    /// `InFlightBootstrap` so the gate locks the right slots.
205    pub touch_set: HashSet<ComponentRef>,
206
207    /// Names already covered by staged input values (carried over
208    /// from the originating `QueuedBootstrap`). Empty today;
209    /// `Node::run_bootstrap` (`BootstrapTarget::ModuleRequests`) populates once F4 lands.
210    pub staged_inputs: HashSet<String>,
211}
212
213/// Validated + staged bootstrap awaiting an in-flight slot. F4
214/// drains `waiting` once `in_flight` clears. Empty today.
215#[derive(Clone, Debug)]
216pub struct QueuedBootstrap {
217    /// Kind discriminator — Module vs Component.
218    pub kind: BootstrapKind,
219
220    /// `ComponentRef` closure this queued bootstrap will lock when it
221    /// promotes to in-flight. Mirrors `InFlightBootstrap.touch_set`
222    /// so the conflict-queue check (F3 Commit 2) can compare a
223    /// waiter's touch set against currently-in-flight touch sets.
224    pub touch_set: HashSet<ComponentRef>,
225
226    /// Names already covered by staged input values. Filled by
227    /// `Node::run_bootstrap` (`BootstrapTarget::ModuleRequests`) once F4 lands.
228    pub staged_inputs: HashSet<String>,
229}
230
231/// Engine-owned bootstrap state. One field on `Engine`, replacing
232/// the four `bootstrap_*` fields the prior shape carried.
233pub(crate) struct BootstrapState {
234    /// Per-target Module bootstrap metadata. Populated by the
235    /// install path when a `module_phase = bootstrap` FunctionProto
236    /// lands.
237    pub(crate) module_bootstraps: HashMap<String, ModuleBootstrap>,
238
239    /// Per-slot Component bootstrap metadata. Empty today; F5 wires
240    /// the registration path.
241    pub(crate) component_bootstraps: HashMap<String, ComponentBootstrap>,
242
243    /// Append-only sequence of Module bootstrap target names in
244    /// install order. The seeder walks front-to-back so multi-
245    /// target installs surface one BootstrapComplete per target.
246    /// Append-only across a Node's lifetime; the seeder advances
247    /// `next_idx` rather than mutating the Vec so introspection
248    /// keeps reporting every queued target across phases.
249    pub(crate) install_order: Vec<String>,
250
251    /// Host-supplied bootstrap input staging requests awaiting
252    /// validation + staging. Empty today; F3 populates from
253    /// `Node::run_bootstrap` (`BootstrapTarget::ModuleRequests`). The owned-form mirror of
254    /// [`BootstrapRequest`] keeps parked entries alive across poll
255    /// cycles; the engine's F5 immediate-fire entry point
256    /// ([`crate::engine::core::Engine::enqueue_bootstrap_request`])
257    /// bypasses this queue and stages directly.
258    pub(crate) pending_requests: VecDeque<OwnedBootstrapRequest>,
259
260    /// Currently executing bootstraps. The host-driven seeder
261    /// (`Engine::seed_bootstrap_call`) records one Module target per
262    /// phase; the conflict-queue path can fire additional disjoint
263    /// Component bootstraps so multiple entries coexist when
264    /// `pending_requests` fans out.
265    pub(crate) in_flight: Vec<InFlightBootstrap>,
266
267    /// Validated + staged bootstraps ready to fire once `in_flight`
268    /// drains. Empty today; F4 populates.
269    pub(crate) waiting: VecDeque<QueuedBootstrap>,
270
271    /// Seed pointer into `install_order`. Bumps each time
272    /// [`crate::engine::core::Engine::maybe_complete_bootstrap`]
273    /// observes a phase drained. The host kick via
274    /// [`Self::arm_install_order`] rewinds — or rather, picks up at —
275    /// this offset so re-arming a partially-drained queue continues
276    /// from the next unseeded target.
277    pub(crate) next_idx: usize,
278
279    /// Coarse "queue still has work" flag. Armed by
280    /// [`Self::arm_install_order`] (host kick) or
281    /// [`Self::enqueue_request`] (staged input), cleared by
282    /// `maybe_complete_bootstrap` after the last queued phase drains.
283    /// `Engine::poll` consults it to skip the bootstrap path entirely
284    /// on cycles with no queued work.
285    pub(crate) pending: bool,
286}
287
288impl BootstrapState {
289    /// Construct an empty bootstrap state — every map / Vec /
290    /// VecDeque empty, `next_idx = 0`, `pending = false`.
291    pub(crate) fn new() -> Self {
292        Self {
293            module_bootstraps: HashMap::new(),
294            component_bootstraps: HashMap::new(),
295            install_order: Vec::new(),
296            pending_requests: VecDeque::new(),
297            in_flight: Vec::new(),
298            waiting: VecDeque::new(),
299            next_idx: 0,
300            pending: false,
301        }
302    }
303
304    /// Reset transient fields ahead of a `Node::restore` call.
305    /// `install_order` and `module_bootstraps` stay populated (the
306    /// install path stamps both at register time, and restore
307    /// preserves install metadata); `pending`, `in_flight`,
308    /// `pending_requests`, `waiting`, and `next_idx` reset so the
309    /// restored Node does not re-fire bootstraps it already ran.
310    pub(crate) fn clear_for_restore(&mut self) {
311        self.pending = false;
312        self.in_flight.clear();
313        self.pending_requests.clear();
314        self.waiting.clear();
315        self.next_idx = self.install_order.len();
316    }
317
318    /// First queued Module bootstrap's function key, or `None` when
319    /// the queue is empty. Mirrors the prior `bootstrap_function_key()`
320    /// accessor.
321    pub(crate) fn first_function_key(&self) -> Option<&FunctionKey> {
322        let name = self.install_order.first()?;
323        self.module_bootstraps.get(name).map(|m| &m.function_key)
324    }
325
326    /// All queued Module bootstrap function keys in install order.
327    /// Mirrors the prior `bootstrap_function_keys()` accessor.
328    /// Allocates a fresh Vec each call — the caller uses this for
329    /// introspection (snapshot dumps, host-side asserts), not the
330    /// hot path.
331    pub(crate) fn function_keys(&self) -> Vec<FunctionKey> {
332        self.install_order
333            .iter()
334            .filter_map(|name| {
335                self.module_bootstraps
336                    .get(name)
337                    .map(|m| m.function_key.clone())
338            })
339            .collect()
340    }
341
342    /// ExecId of the currently in-flight Module bootstrap. Mirrors
343    /// the prior `bootstrap_exec_id` field. Returns `None` when no
344    /// Module bootstrap is in-flight; if multiple in-flight entries
345    /// exist (future Component bootstrap path), returns the first
346    /// Module-kind ExecId.
347    pub(crate) fn module_exec_id(&self) -> Option<ExecId> {
348        self.in_flight.iter().find_map(|b| match b.kind {
349            BootstrapKind::Module { .. } => Some(b.exec_id),
350            BootstrapKind::Component { .. } => None,
351        })
352    }
353
354    /// Record a Module bootstrap target. Appends to `install_order`,
355    /// inserts `module_bootstraps[name]`. Idempotent per target name —
356    /// re-registering the same name updates the metadata in place
357    /// without re-appending to `install_order`.
358    ///
359    /// Does not arm `pending`. Host-driven F4 model: install records
360    /// the bootstrap, the host calls [`crate::node::Node::run_bootstrap`]
361    /// (or stages a `BootstrapRequest`) to actually fire it. `Engine::poll`
362    /// no longer auto-seeds the queue on first call.
363    pub(crate) fn register_module(&mut self, function_key: FunctionKey) {
364        let name = function_key.1.clone();
365        let entry = self
366            .module_bootstraps
367            .entry(name.clone())
368            .or_insert_with(|| ModuleBootstrap {
369                function_key: function_key.clone(),
370                touch_set: HashSet::new(),
371            });
372        // Refresh the function_key in case install path passes a
373        // different (domain, overload) for the same name — the most
374        // recent install wins.
375        entry.function_key = function_key;
376        if !self.install_order.iter().any(|n| n == &name) {
377            self.install_order.push(name);
378        }
379    }
380
381    /// Arm `pending` and rewind `next_idx` to the first unseeded target
382    /// so [`crate::engine::core::Engine::seed_bootstrap_call`] picks up
383    /// where the previous drain left off. The host calls this through
384    /// `Node::run_bootstrap` to kick the install-time bootstrap queue;
385    /// returns `false` when no install-order target remains (idempotent
386    /// on a fully drained Node).
387    pub(crate) fn arm_install_order(&mut self) -> bool {
388        if self.next_idx >= self.install_order.len() {
389            return false;
390        }
391        self.pending = true;
392        true
393    }
394
395    /// Mark a Module bootstrap target as in-flight. Pushes an
396    /// `InFlightBootstrap` with kind `Module { target }` carrying
397    /// the supplied ExecId + the `ComponentRef` closure the body-op
398    /// gate consults to park overlapping body ops.
399    pub(crate) fn mark_module_in_flight(
400        &mut self,
401        target: String,
402        exec_id: ExecId,
403        touch_set: HashSet<ComponentRef>,
404    ) {
405        self.in_flight.push(InFlightBootstrap {
406            kind: BootstrapKind::Module { target },
407            exec_id,
408            touch_set,
409            staged_inputs: HashSet::new(),
410        });
411    }
412
413    /// Look up a Component bootstrap by slot name. F5 populates
414    /// `component_bootstraps` from the Bootstrap Contract
415    /// registration path; today the map stays empty so this lookup
416    /// always returns `None`. Wired now so the field has a non-test
417    /// reader and the F5 dispatch path doesn't need to expand the
418    /// accessor surface — it just calls this method.
419    pub(crate) fn component_bootstrap(&self, slot: &str) -> Option<&ComponentBootstrap> {
420        self.component_bootstraps.get(slot)
421    }
422
423    /// Push a host-supplied [`OwnedBootstrapRequest`] onto the pending
424    /// queue + arm `pending` so the body gate stays parked until the
425    /// staged work drains. Direct entry-point exists for the engine's
426    /// internal conflict-queue tests; production callers use
427    /// [`crate::engine::core::Engine::enqueue_bootstrap_request`]
428    /// (F5 immediate-fire) which validates + stages without going
429    /// through the parked queue.
430    #[cfg(test)]
431    pub(crate) fn enqueue_request(&mut self, req: OwnedBootstrapRequest) {
432        self.pending_requests.push_back(req);
433        self.pending = true;
434    }
435
436    /// Drain `pending_requests` once. For each request, resolve its
437    /// target's `ComponentRef` touch set (Module bootstraps look it
438    /// up via `module_bootstraps`); compare against every currently
439    /// in-flight bootstrap's touch set. On disjoint → return a
440    /// [`ReadyBootstrap`] for the engine to seed. On overlap →
441    /// enqueue as [`QueuedBootstrap`] in `waiting` and let
442    /// [`Self::on_bootstrap_drained`] promote it later. Requests
443    /// whose target is unknown drop silently — caller-side
444    /// validation is responsible for surfacing the error before the
445    /// request reaches the queue.
446    pub(crate) fn process_pending_requests(&mut self) -> Vec<ReadyBootstrap> {
447        let mut ready = Vec::new();
448        while let Some(req) = self.pending_requests.pop_front() {
449            // Look up the touch set for this target. Module
450            // bootstraps cache it on `module_bootstraps`. Unknown
451            // targets drop here — the host-facing API
452            // (`Node::run_bootstrap` (`BootstrapTarget::ModuleRequests`)) returns
453            // `BootstrapError::UnknownTarget` before enqueueing, so
454            // reaching this branch indicates a stale request and we
455            // skip rather than wedge.
456            let Some(meta) = self.module_bootstraps.get(&req.target_name) else {
457                continue;
458            };
459            let touch_set = meta.touch_set.clone();
460            let kind = BootstrapKind::Module {
461                target: req.target_name.clone(),
462            };
463            // Staged-inputs placeholder — F4 populates from
464            // `req.inputs` once input staging lands. For Commit 2
465            // the conflict-queue path is the only consumer and it
466            // ignores the names.
467            let staged_inputs: HashSet<String> =
468                req.inputs.iter().map(|(name, _)| name.clone()).collect();
469            if Self::overlaps_any_in_flight(&self.in_flight, &touch_set) {
470                self.waiting.push_back(QueuedBootstrap {
471                    kind,
472                    touch_set,
473                    staged_inputs,
474                });
475            } else {
476                ready.push(ReadyBootstrap {
477                    kind,
478                    touch_set,
479                    staged_inputs,
480                });
481            }
482        }
483        ready
484    }
485
486    /// Drop the in-flight bootstrap whose body ExecId matches
487    /// `exec_id`, then walk `waiting` once and promote any waiter
488    /// whose touch set no longer conflicts with the remaining
489    /// in-flight set. Returns promoted waiters in queue order so
490    /// the engine can seed them. The engine calls this from
491    /// `maybe_complete_bootstrap` after a phase drains.
492    pub(crate) fn on_bootstrap_drained(&mut self, exec_id: ExecId) -> Vec<ReadyBootstrap> {
493        self.in_flight.retain(|b| b.exec_id != exec_id);
494        let mut promoted = Vec::new();
495        let mut remaining = VecDeque::new();
496        while let Some(waiter) = self.waiting.pop_front() {
497            if Self::overlaps_any_in_flight(&self.in_flight, &waiter.touch_set) {
498                remaining.push_back(waiter);
499            } else {
500                promoted.push(ReadyBootstrap {
501                    kind: waiter.kind,
502                    touch_set: waiter.touch_set,
503                    staged_inputs: waiter.staged_inputs,
504                });
505            }
506        }
507        self.waiting = remaining;
508        promoted
509    }
510
511    /// `true` when `touch_set` overlaps any in-flight bootstrap's
512    /// touch set. Empty touch sets never conflict — Module
513    /// bootstraps whose body touches no Component can fan out
514    /// alongside any in-flight target.
515    fn overlaps_any_in_flight(
516        in_flight: &[InFlightBootstrap],
517        touch_set: &HashSet<ComponentRef>,
518    ) -> bool {
519        if touch_set.is_empty() {
520            return false;
521        }
522        in_flight
523            .iter()
524            .any(|b| !b.touch_set.is_disjoint(touch_set))
525    }
526}
527