Skip to main content

arkhe_kernel/state/
instance.rs

1//! `Instance` — per-instance kernel-state container.
2//!
3//! Holds entities, components, scheduler, ID counters, ledger, in-flight
4//! module refs, wall-remainder, and local tick. Mutation flows through the
5//! `pub(crate)` accessor surface; `runtime::apply::apply_stage` is the sole
6//! caller that drives `StepStage` (10-bucket commit-or-rollback) into
7//! Instance state.
8
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13use crate::abi::{EntityId, InstanceId, Principal, RouteId, Tick, TypeCode};
14use crate::state::config::InstanceConfig;
15use crate::state::ledger::ResourceLedger;
16use crate::state::scheduler::Scheduler;
17
18/// Per-entity metadata. Surfaced to L1 via `runtime::InstanceView`.
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
20pub struct EntityMeta {
21    /// Principal that owned the entity at spawn time.
22    pub owner: Principal,
23    /// Tick at which the entity was spawned.
24    pub created: Tick,
25}
26
27#[derive(Debug, Default, Clone, Serialize, Deserialize)]
28pub(crate) struct IdCounters {
29    pub next_entity: u64,
30    pub next_scheduled: u64,
31    pub next_source_seq: u64,
32}
33
34pub(crate) struct Instance {
35    // `id` is retained for the future IntrospectHandle (deferred). The kernel
36    // already keys by `InstanceId` externally, so production read paths
37    // for the field itself are deferred.
38    #[cfg_attr(not(test), allow(dead_code))]
39    id: InstanceId,
40    config: InstanceConfig,
41    entities: BTreeMap<EntityId, EntityMeta>,
42    /// Component data keyed by `(entity, type)` — canonical postcard bytes.
43    components: BTreeMap<(EntityId, TypeCode), Bytes>,
44    scheduler: Scheduler,
45    id_counters: IdCounters,
46    ledger: ResourceLedger,
47    /// Drain-refcount per registered route.
48    inflight_refs: BTreeMap<RouteId, u32>,
49    /// Sub-tick wall-clock remainder accumulator.
50    wall_remainder: u128,
51    /// Logical local tick advancing per `step()`.
52    local_tick: u64,
53}
54
55impl Instance {
56    pub(crate) fn new(id: InstanceId, config: InstanceConfig) -> Self {
57        Self {
58            id,
59            config,
60            entities: BTreeMap::new(),
61            components: BTreeMap::new(),
62            scheduler: Scheduler::new(),
63            id_counters: IdCounters::default(),
64            ledger: ResourceLedger::new(),
65            inflight_refs: BTreeMap::new(),
66            wall_remainder: 0,
67            local_tick: 0,
68        }
69    }
70
71    // ---- read accessors ----
72    //
73    // Kernel-internal observability surface used by tests and
74    // `runtime::registry` test fixtures. Production introspection
75    // wiring lands with the future IntrospectHandle interface (deferred).
76    #[inline]
77    pub(crate) fn id(&self) -> InstanceId {
78        self.id
79    }
80    #[inline]
81    pub(crate) fn config(&self) -> &InstanceConfig {
82        &self.config
83    }
84    #[inline]
85    pub(crate) fn entities_len(&self) -> usize {
86        self.entities.len()
87    }
88    #[inline]
89    pub(crate) fn components_len(&self) -> usize {
90        self.components.len()
91    }
92    #[inline]
93    pub(crate) fn local_tick(&self) -> u64 {
94        self.local_tick
95    }
96    #[cfg_attr(not(test), allow(dead_code))]
97    #[inline]
98    pub(crate) fn wall_remainder(&self) -> u128 {
99        self.wall_remainder
100    }
101    #[inline]
102    pub(crate) fn ledger(&self) -> &ResourceLedger {
103        &self.ledger
104    }
105    #[cfg_attr(not(test), allow(dead_code))]
106    #[inline]
107    pub(crate) fn scheduler(&self) -> &Scheduler {
108        &self.scheduler
109    }
110    #[cfg_attr(not(test), allow(dead_code))]
111    #[inline]
112    pub(crate) fn id_counters(&self) -> &IdCounters {
113        &self.id_counters
114    }
115    #[cfg_attr(not(test), allow(dead_code))]
116    #[inline]
117    pub(crate) fn inflight_refs_len(&self) -> usize {
118        self.inflight_refs.len()
119    }
120    #[cfg_attr(not(test), allow(dead_code))]
121    #[inline]
122    pub(crate) fn inflight_refs_for(&self, route: RouteId) -> u32 {
123        *self.inflight_refs.get(&route).unwrap_or(&0)
124    }
125
126    /// Per-entity metadata lookup. Surfaced through `runtime::InstanceView`.
127    pub(crate) fn entity_meta(&self, entity: EntityId) -> Option<&EntityMeta> {
128        self.entities.get(&entity)
129    }
130
131    /// Component bytes for an `(entity, type_code)` pair. Surfaced through
132    /// `runtime::InstanceView`.
133    pub(crate) fn component(&self, entity: EntityId, type_code: TypeCode) -> Option<&Bytes> {
134        self.components.get(&(entity, type_code))
135    }
136
137    /// Iterate every entity in ascending `EntityId` order (BTreeMap
138    /// canonical iteration; A23 deterministic). Surfaced through
139    /// `runtime::InstanceView`.
140    pub(crate) fn entities_iter(&self) -> impl Iterator<Item = (EntityId, &EntityMeta)> + '_ {
141        self.entities.iter().map(|(id, meta)| (*id, meta))
142    }
143
144    /// Iterate every `(entity, bytes)` pair whose `TypeCode` matches.
145    /// Order is ascending `(EntityId, TypeCode)` lex, so for a fixed
146    /// `type_code` the effective order is ascending `EntityId`.
147    pub(crate) fn components_by_type_iter(
148        &self,
149        type_code: TypeCode,
150    ) -> impl Iterator<Item = (EntityId, &Bytes)> + '_ {
151        self.components
152            .iter()
153            .filter_map(move |((eid, tc), bytes)| {
154                if *tc == type_code {
155                    Some((*eid, bytes))
156                } else {
157                    None
158                }
159            })
160    }
161
162    // ---- pub(crate) mutators ----
163    //
164    // R4-X DAG fix: `apply_stage` lives in `runtime::apply`, not on `Instance`,
165    // so `state` does not import `runtime`. These accessors are the sole
166    // surface through which `StepStage` deltas reach Instance state.
167    // External crates do not see them — only sibling kernel modules
168    // (notably `runtime::apply`) can mutate.
169
170    pub(crate) fn insert_entity(&mut self, id: EntityId, meta: EntityMeta) {
171        self.entities.insert(id, meta);
172    }
173
174    pub(crate) fn remove_entity(&mut self, id: EntityId) -> Option<EntityMeta> {
175        // Despawn cascades: drop every component the entity still holds so
176        // no orphaned `(entity, *)` rows survive in `components`. The
177        // ledger mirrors this via its own cascade in `ResourceLedger::
178        // remove_entity`, keeping the two maps consistent.
179        let orphaned: Vec<(EntityId, TypeCode)> = self
180            .components
181            .range((id, TypeCode(0))..=(id, TypeCode(u32::MAX)))
182            .map(|(k, _)| *k)
183            .collect();
184        for k in orphaned {
185            self.components.remove(&k);
186        }
187        self.entities.remove(&id)
188    }
189
190    pub(crate) fn insert_component(&mut self, key: (EntityId, TypeCode), bytes: Bytes) {
191        self.components.insert(key, bytes);
192    }
193
194    pub(crate) fn remove_component(&mut self, key: (EntityId, TypeCode)) -> Option<Bytes> {
195        self.components.remove(&key)
196    }
197
198    pub(crate) fn scheduler_mut(&mut self) -> &mut Scheduler {
199        &mut self.scheduler
200    }
201
202    pub(crate) fn ledger_mut(&mut self) -> &mut ResourceLedger {
203        &mut self.ledger
204    }
205
206    pub(crate) fn id_counters_mut(&mut self) -> &mut IdCounters {
207        &mut self.id_counters
208    }
209
210    pub(crate) fn inflight_refs_mut(&mut self) -> &mut BTreeMap<RouteId, u32> {
211        &mut self.inflight_refs
212    }
213
214    pub(crate) fn advance_wall_remainder(&mut self, delta: u128) {
215        self.wall_remainder = self.wall_remainder.saturating_add(delta);
216    }
217
218    pub(crate) fn advance_local_tick(&mut self, delta: u64) {
219        self.local_tick = self.local_tick.saturating_add(delta);
220    }
221
222    /// Snapshot of `IdCounters` (read-only clone) for callers that need
223    /// pre-stage values without holding a borrow on Instance.
224    pub(crate) fn id_counters_snapshot(&self) -> IdCounters {
225        self.id_counters.clone()
226    }
227
228    /// Capture all instance fields into a serializable `InstanceSnapshot`.
229    /// Excludes nothing — every Instance field is round-tripped.
230    pub(crate) fn to_snapshot(&self) -> InstanceSnapshot {
231        InstanceSnapshot {
232            id: self.id,
233            config: self.config.clone(),
234            entities: self.entities.clone(),
235            components: self.components.clone(),
236            scheduler: self.scheduler.clone(),
237            id_counters: self.id_counters.clone(),
238            ledger: self.ledger.clone(),
239            inflight_refs: self.inflight_refs.clone(),
240            wall_remainder: self.wall_remainder,
241            local_tick: self.local_tick,
242        }
243    }
244
245    /// Reconstruct an Instance from a snapshot. Inverse of `to_snapshot`.
246    pub(crate) fn from_snapshot(snap: InstanceSnapshot) -> Self {
247        Self {
248            id: snap.id,
249            config: snap.config,
250            entities: snap.entities,
251            components: snap.components,
252            scheduler: snap.scheduler,
253            id_counters: snap.id_counters,
254            ledger: snap.ledger,
255            inflight_refs: snap.inflight_refs,
256            wall_remainder: snap.wall_remainder,
257            local_tick: snap.local_tick,
258        }
259    }
260}
261
262/// Round-tripable shape of a single `Instance`. Pub struct, pub(crate)
263/// fields — visible as a type at the crate boundary (so `KernelSnapshot`
264/// can name it) but not constructible/inspectable from outside the
265/// kernel crate.
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct InstanceSnapshot {
268    pub(crate) id: InstanceId,
269    pub(crate) config: InstanceConfig,
270    pub(crate) entities: BTreeMap<EntityId, EntityMeta>,
271    pub(crate) components: BTreeMap<(EntityId, TypeCode), Bytes>,
272    pub(crate) scheduler: Scheduler,
273    pub(crate) id_counters: IdCounters,
274    pub(crate) ledger: ResourceLedger,
275    pub(crate) inflight_refs: BTreeMap<RouteId, u32>,
276    pub(crate) wall_remainder: u128,
277    pub(crate) local_tick: u64,
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::abi::CapabilityMask;
284    use crate::state::quota::QuotaReductionPolicy;
285
286    fn id(n: u64) -> InstanceId {
287        InstanceId::new(n).unwrap()
288    }
289
290    fn cfg() -> InstanceConfig {
291        InstanceConfig {
292            default_caps: CapabilityMask::default(),
293            max_entities: 100,
294            max_scheduled: 1000,
295            memory_budget_bytes: 1 << 20,
296            parent: None,
297            quota_reduction: QuotaReductionPolicy::default(),
298        }
299    }
300
301    #[test]
302    fn instance_new_initial_state() {
303        let inst = Instance::new(id(1), cfg());
304        assert_eq!(inst.id(), id(1));
305        assert_eq!(inst.entities_len(), 0);
306        assert_eq!(inst.components_len(), 0);
307        assert_eq!(inst.local_tick(), 0);
308        assert_eq!(inst.wall_remainder(), 0);
309        assert_eq!(inst.ledger().total_entities(), 0);
310        assert!(inst.scheduler().is_empty());
311        assert_eq!(inst.inflight_refs_len(), 0);
312    }
313
314    #[test]
315    fn instance_id_counters_default_zero() {
316        let inst = Instance::new(id(1), cfg());
317        let c = inst.id_counters();
318        assert_eq!(c.next_entity, 0);
319        assert_eq!(c.next_scheduled, 0);
320        assert_eq!(c.next_source_seq, 0);
321    }
322
323    #[test]
324    fn instance_config_carried_through() {
325        let mut config = cfg();
326        config.max_entities = 7;
327        config.parent = id(99).into(); // Some(_)
328        let inst = Instance::new(id(2), config);
329        assert_eq!(inst.config().max_entities, 7);
330        assert_eq!(inst.config().parent, Some(id(99)));
331    }
332
333    #[test]
334    fn multiple_instances_independent() {
335        let inst1 = Instance::new(id(1), cfg());
336        let inst2 = Instance::new(id(2), cfg());
337        assert_ne!(inst1.id(), inst2.id());
338        assert!(inst1.scheduler().is_empty());
339        assert!(inst2.scheduler().is_empty());
340        assert_eq!(inst1.ledger().total_bytes(), 0);
341        assert_eq!(inst2.ledger().total_bytes(), 0);
342    }
343
344    #[test]
345    fn entity_meta_clone_preserves_fields() {
346        let m1 = EntityMeta {
347            owner: Principal::System,
348            created: Tick(5),
349        };
350        let m2 = m1.clone();
351        assert!(matches!(m2.owner, Principal::System));
352        assert_eq!(m2.created, Tick(5));
353    }
354
355    #[test]
356    fn id_counters_default_clone_independent() {
357        let c1 = IdCounters::default();
358        let mut c2 = c1.clone();
359        c2.next_entity = 10;
360        assert_eq!(c1.next_entity, 0);
361        assert_eq!(c2.next_entity, 10);
362    }
363
364    // apply_stage / discard_stage tests live in `runtime/apply.rs`
365    // (R4-X: Instance does not import `runtime::StepStage`).
366}