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        self.entities.remove(&id)
176    }
177
178    pub(crate) fn insert_component(&mut self, key: (EntityId, TypeCode), bytes: Bytes) {
179        self.components.insert(key, bytes);
180    }
181
182    pub(crate) fn remove_component(&mut self, key: (EntityId, TypeCode)) -> Option<Bytes> {
183        self.components.remove(&key)
184    }
185
186    pub(crate) fn scheduler_mut(&mut self) -> &mut Scheduler {
187        &mut self.scheduler
188    }
189
190    pub(crate) fn ledger_mut(&mut self) -> &mut ResourceLedger {
191        &mut self.ledger
192    }
193
194    pub(crate) fn id_counters_mut(&mut self) -> &mut IdCounters {
195        &mut self.id_counters
196    }
197
198    pub(crate) fn inflight_refs_mut(&mut self) -> &mut BTreeMap<RouteId, u32> {
199        &mut self.inflight_refs
200    }
201
202    pub(crate) fn advance_wall_remainder(&mut self, delta: u128) {
203        self.wall_remainder = self.wall_remainder.saturating_add(delta);
204    }
205
206    pub(crate) fn advance_local_tick(&mut self, delta: u64) {
207        self.local_tick = self.local_tick.saturating_add(delta);
208    }
209
210    /// Snapshot of `IdCounters` (read-only clone) for callers that need
211    /// pre-stage values without holding a borrow on Instance.
212    pub(crate) fn id_counters_snapshot(&self) -> IdCounters {
213        self.id_counters.clone()
214    }
215
216    /// Capture all instance fields into a serializable `InstanceSnapshot`.
217    /// Excludes nothing — every Instance field is round-tripped.
218    pub(crate) fn to_snapshot(&self) -> InstanceSnapshot {
219        InstanceSnapshot {
220            id: self.id,
221            config: self.config.clone(),
222            entities: self.entities.clone(),
223            components: self.components.clone(),
224            scheduler: self.scheduler.clone(),
225            id_counters: self.id_counters.clone(),
226            ledger: self.ledger.clone(),
227            inflight_refs: self.inflight_refs.clone(),
228            wall_remainder: self.wall_remainder,
229            local_tick: self.local_tick,
230        }
231    }
232
233    /// Reconstruct an Instance from a snapshot. Inverse of `to_snapshot`.
234    pub(crate) fn from_snapshot(snap: InstanceSnapshot) -> Self {
235        Self {
236            id: snap.id,
237            config: snap.config,
238            entities: snap.entities,
239            components: snap.components,
240            scheduler: snap.scheduler,
241            id_counters: snap.id_counters,
242            ledger: snap.ledger,
243            inflight_refs: snap.inflight_refs,
244            wall_remainder: snap.wall_remainder,
245            local_tick: snap.local_tick,
246        }
247    }
248}
249
250/// Round-tripable shape of a single `Instance`. Pub struct, pub(crate)
251/// fields — visible as a type at the crate boundary (so `KernelSnapshot`
252/// can name it) but not constructible/inspectable from outside the
253/// kernel crate.
254#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct InstanceSnapshot {
256    pub(crate) id: InstanceId,
257    pub(crate) config: InstanceConfig,
258    pub(crate) entities: BTreeMap<EntityId, EntityMeta>,
259    pub(crate) components: BTreeMap<(EntityId, TypeCode), Bytes>,
260    pub(crate) scheduler: Scheduler,
261    pub(crate) id_counters: IdCounters,
262    pub(crate) ledger: ResourceLedger,
263    pub(crate) inflight_refs: BTreeMap<RouteId, u32>,
264    pub(crate) wall_remainder: u128,
265    pub(crate) local_tick: u64,
266}
267
268#[cfg(test)]
269mod tests {
270    use super::*;
271    use crate::abi::CapabilityMask;
272    use crate::state::quota::QuotaReductionPolicy;
273
274    fn id(n: u64) -> InstanceId {
275        InstanceId::new(n).unwrap()
276    }
277
278    fn cfg() -> InstanceConfig {
279        InstanceConfig {
280            default_caps: CapabilityMask::default(),
281            max_entities: 100,
282            max_scheduled: 1000,
283            memory_budget_bytes: 1 << 20,
284            parent: None,
285            quota_reduction: QuotaReductionPolicy::default(),
286        }
287    }
288
289    #[test]
290    fn instance_new_initial_state() {
291        let inst = Instance::new(id(1), cfg());
292        assert_eq!(inst.id(), id(1));
293        assert_eq!(inst.entities_len(), 0);
294        assert_eq!(inst.components_len(), 0);
295        assert_eq!(inst.local_tick(), 0);
296        assert_eq!(inst.wall_remainder(), 0);
297        assert_eq!(inst.ledger().total_entities(), 0);
298        assert!(inst.scheduler().is_empty());
299        assert_eq!(inst.inflight_refs_len(), 0);
300    }
301
302    #[test]
303    fn instance_id_counters_default_zero() {
304        let inst = Instance::new(id(1), cfg());
305        let c = inst.id_counters();
306        assert_eq!(c.next_entity, 0);
307        assert_eq!(c.next_scheduled, 0);
308        assert_eq!(c.next_source_seq, 0);
309    }
310
311    #[test]
312    fn instance_config_carried_through() {
313        let mut config = cfg();
314        config.max_entities = 7;
315        config.parent = id(99).into(); // Some(_)
316        let inst = Instance::new(id(2), config);
317        assert_eq!(inst.config().max_entities, 7);
318        assert_eq!(inst.config().parent, Some(id(99)));
319    }
320
321    #[test]
322    fn multiple_instances_independent() {
323        let inst1 = Instance::new(id(1), cfg());
324        let inst2 = Instance::new(id(2), cfg());
325        assert_ne!(inst1.id(), inst2.id());
326        assert!(inst1.scheduler().is_empty());
327        assert!(inst2.scheduler().is_empty());
328        assert_eq!(inst1.ledger().total_bytes(), 0);
329        assert_eq!(inst2.ledger().total_bytes(), 0);
330    }
331
332    #[test]
333    fn entity_meta_clone_preserves_fields() {
334        let m1 = EntityMeta {
335            owner: Principal::System,
336            created: Tick(5),
337        };
338        let m2 = m1.clone();
339        assert!(matches!(m2.owner, Principal::System));
340        assert_eq!(m2.created, Tick(5));
341    }
342
343    #[test]
344    fn id_counters_default_clone_independent() {
345        let c1 = IdCounters::default();
346        let mut c2 = c1.clone();
347        c2.next_entity = 10;
348        assert_eq!(c1.next_entity, 0);
349        assert_eq!(c2.next_entity, 10);
350    }
351
352    // apply_stage / discard_stage tests live in `runtime/apply.rs`
353    // (R4-X: Instance does not import `runtime::StepStage`).
354}