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` (9-bucket commit-or-rollback) into
7//! Instance state.
8
9use bytes::Bytes;
10use serde::{Deserialize, Serialize};
11use std::collections::{BTreeMap, VecDeque};
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/// A signal delivered into an instance's per-route inbox by the kernel
28/// router. Ordered within a route by `seq` (a per-instance monotonic
29/// counter), so inbox iteration is deterministic.
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
31pub(crate) struct InboundSignal {
32    /// Sending instance.
33    pub from: InstanceId,
34    /// Principal under which the sender dispatched the signal.
35    pub principal: Principal,
36    /// Canonical payload bytes.
37    pub payload: Bytes,
38    /// Per-instance monotonic delivery sequence (intra-route ordering).
39    pub seq: u64,
40}
41
42#[derive(Debug, Default, Clone, Serialize, Deserialize)]
43pub(crate) struct IdCounters {
44    pub next_entity: u64,
45    pub next_scheduled: u64,
46    pub next_source_seq: u64,
47}
48
49pub(crate) struct Instance {
50    // `id` is retained for the future IntrospectHandle (deferred). The kernel
51    // already keys by `InstanceId` externally, so production read paths
52    // for the field itself are deferred.
53    #[cfg_attr(not(test), allow(dead_code))]
54    id: InstanceId,
55    config: InstanceConfig,
56    entities: BTreeMap<EntityId, EntityMeta>,
57    /// Component data keyed by `(entity, type)` — canonical postcard bytes.
58    components: BTreeMap<(EntityId, TypeCode), Bytes>,
59    scheduler: Scheduler,
60    id_counters: IdCounters,
61    ledger: ResourceLedger,
62    /// Drain-refcount per registered route.
63    inflight_refs: BTreeMap<RouteId, u32>,
64    /// Per-route inbound-signal queues, populated by the kernel router at
65    /// delivery time. Deterministic: `BTreeMap` over routes, `VecDeque`
66    /// ordered by each signal's `seq`.
67    signal_inbox: BTreeMap<RouteId, VecDeque<InboundSignal>>,
68    /// Monotonic per-instance signal delivery counter (intra-route order).
69    inbox_seq: u64,
70    /// Sub-tick wall-clock remainder accumulator.
71    wall_remainder: u128,
72    /// Logical local tick advancing per `step()`.
73    local_tick: u64,
74}
75
76impl Instance {
77    pub(crate) fn new(id: InstanceId, config: InstanceConfig) -> Self {
78        Self {
79            id,
80            config,
81            entities: BTreeMap::new(),
82            components: BTreeMap::new(),
83            scheduler: Scheduler::new(),
84            id_counters: IdCounters::default(),
85            ledger: ResourceLedger::new(),
86            inflight_refs: BTreeMap::new(),
87            signal_inbox: BTreeMap::new(),
88            inbox_seq: 0,
89            wall_remainder: 0,
90            local_tick: 0,
91        }
92    }
93
94    // ---- read accessors ----
95    //
96    // Kernel-internal observability surface used by tests and
97    // `runtime::registry` test fixtures. Production introspection
98    // wiring lands with the future IntrospectHandle interface (deferred).
99    #[inline]
100    pub(crate) fn id(&self) -> InstanceId {
101        self.id
102    }
103    #[inline]
104    pub(crate) fn config(&self) -> &InstanceConfig {
105        &self.config
106    }
107    #[inline]
108    pub(crate) fn entities_len(&self) -> usize {
109        self.entities.len()
110    }
111    #[inline]
112    pub(crate) fn components_len(&self) -> usize {
113        self.components.len()
114    }
115    #[inline]
116    pub(crate) fn local_tick(&self) -> u64 {
117        self.local_tick
118    }
119    #[cfg_attr(not(test), allow(dead_code))]
120    #[inline]
121    pub(crate) fn wall_remainder(&self) -> u128 {
122        self.wall_remainder
123    }
124    #[inline]
125    pub(crate) fn ledger(&self) -> &ResourceLedger {
126        &self.ledger
127    }
128    #[cfg_attr(not(test), allow(dead_code))]
129    #[inline]
130    pub(crate) fn scheduler(&self) -> &Scheduler {
131        &self.scheduler
132    }
133    #[cfg_attr(not(test), allow(dead_code))]
134    #[inline]
135    pub(crate) fn id_counters(&self) -> &IdCounters {
136        &self.id_counters
137    }
138    #[cfg_attr(not(test), allow(dead_code))]
139    #[inline]
140    pub(crate) fn inflight_refs_len(&self) -> usize {
141        self.inflight_refs.len()
142    }
143    #[cfg_attr(not(test), allow(dead_code))]
144    #[inline]
145    pub(crate) fn inflight_refs_for(&self, route: RouteId) -> u32 {
146        *self.inflight_refs.get(&route).unwrap_or(&0)
147    }
148
149    /// Per-entity metadata lookup. Surfaced through `runtime::InstanceView`.
150    pub(crate) fn entity_meta(&self, entity: EntityId) -> Option<&EntityMeta> {
151        self.entities.get(&entity)
152    }
153
154    /// Component bytes for an `(entity, type_code)` pair. Surfaced through
155    /// `runtime::InstanceView`.
156    pub(crate) fn component(&self, entity: EntityId, type_code: TypeCode) -> Option<&Bytes> {
157        self.components.get(&(entity, type_code))
158    }
159
160    /// Iterate every entity in ascending `EntityId` order (BTreeMap
161    /// canonical iteration; A23 deterministic). Surfaced through
162    /// `runtime::InstanceView`.
163    pub(crate) fn entities_iter(&self) -> impl Iterator<Item = (EntityId, &EntityMeta)> + '_ {
164        self.entities.iter().map(|(id, meta)| (*id, meta))
165    }
166
167    /// Iterate every `(entity, bytes)` pair whose `TypeCode` matches.
168    /// Order is ascending `(EntityId, TypeCode)` lex, so for a fixed
169    /// `type_code` the effective order is ascending `EntityId`.
170    pub(crate) fn components_by_type_iter(
171        &self,
172        type_code: TypeCode,
173    ) -> impl Iterator<Item = (EntityId, &Bytes)> + '_ {
174        self.components
175            .iter()
176            .filter_map(move |((eid, tc), bytes)| {
177                if *tc == type_code {
178                    Some((*eid, bytes))
179                } else {
180                    None
181                }
182            })
183    }
184
185    // ---- pub(crate) mutators ----
186    //
187    // R4-X DAG fix: `apply_stage` lives in `runtime::apply`, not on `Instance`,
188    // so `state` does not import `runtime`. These accessors are the sole
189    // surface through which `StepStage` deltas reach Instance state.
190    // External crates do not see them — only sibling kernel modules
191    // (notably `runtime::apply`) can mutate.
192
193    pub(crate) fn insert_entity(&mut self, id: EntityId, meta: EntityMeta) {
194        self.entities.insert(id, meta);
195    }
196
197    pub(crate) fn remove_entity(&mut self, id: EntityId) -> Option<EntityMeta> {
198        // Despawn cascades: drop every component the entity still holds so
199        // no orphaned `(entity, *)` rows survive in `components`. The
200        // ledger mirrors this via its own cascade in `ResourceLedger::
201        // remove_entity`, keeping the two maps consistent.
202        let orphaned: Vec<(EntityId, TypeCode)> = self
203            .components
204            .range((id, TypeCode(0))..=(id, TypeCode(u32::MAX)))
205            .map(|(k, _)| *k)
206            .collect();
207        for k in orphaned {
208            self.components.remove(&k);
209        }
210        self.entities.remove(&id)
211    }
212
213    pub(crate) fn insert_component(&mut self, key: (EntityId, TypeCode), bytes: Bytes) {
214        self.components.insert(key, bytes);
215    }
216
217    pub(crate) fn remove_component(&mut self, key: (EntityId, TypeCode)) -> Option<Bytes> {
218        self.components.remove(&key)
219    }
220
221    pub(crate) fn scheduler_mut(&mut self) -> &mut Scheduler {
222        &mut self.scheduler
223    }
224
225    pub(crate) fn ledger_mut(&mut self) -> &mut ResourceLedger {
226        &mut self.ledger
227    }
228
229    pub(crate) fn id_counters_mut(&mut self) -> &mut IdCounters {
230        &mut self.id_counters
231    }
232
233    pub(crate) fn inflight_refs_mut(&mut self) -> &mut BTreeMap<RouteId, u32> {
234        &mut self.inflight_refs
235    }
236
237    pub(crate) fn advance_wall_remainder(&mut self, delta: u128) {
238        self.wall_remainder = self.wall_remainder.saturating_add(delta);
239    }
240
241    pub(crate) fn advance_local_tick(&mut self, delta: u64) {
242        self.local_tick = self.local_tick.saturating_add(delta);
243    }
244
245    /// Snapshot of `IdCounters` (read-only clone) for callers that need
246    /// pre-stage values without holding a borrow on Instance.
247    pub(crate) fn id_counters_snapshot(&self) -> IdCounters {
248        self.id_counters.clone()
249    }
250
251    /// Capture all instance fields into a serializable `InstanceSnapshot`.
252    /// Excludes nothing — every Instance field is round-tripped.
253    pub(crate) fn to_snapshot(&self) -> InstanceSnapshot {
254        InstanceSnapshot {
255            id: self.id,
256            config: self.config.clone(),
257            entities: self.entities.clone(),
258            components: self.components.clone(),
259            scheduler: self.scheduler.clone(),
260            id_counters: self.id_counters.clone(),
261            ledger: self.ledger.clone(),
262            inflight_refs: self.inflight_refs.clone(),
263            signal_inbox: self.signal_inbox.clone(),
264            inbox_seq: self.inbox_seq,
265            wall_remainder: self.wall_remainder,
266            local_tick: self.local_tick,
267        }
268    }
269
270    /// Reconstruct an Instance from a snapshot. Inverse of `to_snapshot`.
271    pub(crate) fn from_snapshot(snap: InstanceSnapshot) -> Self {
272        Self {
273            id: snap.id,
274            config: snap.config,
275            entities: snap.entities,
276            components: snap.components,
277            scheduler: snap.scheduler,
278            id_counters: snap.id_counters,
279            ledger: snap.ledger,
280            inflight_refs: snap.inflight_refs,
281            signal_inbox: snap.signal_inbox,
282            inbox_seq: snap.inbox_seq,
283            wall_remainder: snap.wall_remainder,
284            local_tick: snap.local_tick,
285        }
286    }
287
288    /// BLAKE3 digest of the instance's full canonical post-step state — the
289    /// CIL per-step witness. Hashes the postcard encoding of the complete
290    /// `InstanceSnapshot`, i.e. every field in its declaration order: `id`,
291    /// `config`, `entities`, `components`, `scheduler` (incl. `next_seq` /
292    /// `next_id`), `id_counters`, `ledger`, `inflight_refs`, `signal_inbox`,
293    /// `inbox_seq`, `wall_remainder`, `local_tick`. So any state divergence on
294    /// replay is caught — not just counters. (`id` and `config` are constant
295    /// per instance, so they neither mask nor manufacture a divergence; reusing
296    /// the snapshot keeps one canonical serialization for both digest and
297    /// snapshot/restore.) Deterministic: all state is `BTreeMap`/`VecDeque`/
298    /// scalar (no `HashMap`), and postcard encoding is canonical. Returns the
299    /// zero digest only if encoding fails (impossible for this all-derive type
300    /// graph; treated as a fixed sentinel).
301    pub(crate) fn state_digest(&self) -> [u8; 32] {
302        match postcard::to_allocvec(&self.to_snapshot()) {
303            Ok(bytes) => *blake3::hash(&bytes).as_bytes(),
304            Err(_) => [0u8; 32],
305        }
306    }
307
308    /// Deliver `signal` into this instance's `route` inbox, assigning the
309    /// next per-instance `inbox_seq`. Returns the assigned seq.
310    pub(crate) fn deliver_signal(&mut self, route: RouteId, mut signal: InboundSignal) -> u64 {
311        let seq = self.inbox_seq;
312        self.inbox_seq = self.inbox_seq.saturating_add(1);
313        signal.seq = seq;
314        self.signal_inbox.entry(route).or_default().push_back(signal);
315        seq
316    }
317
318    /// Current inbound-signal queue depth for `route` (0 if none).
319    #[cfg_attr(not(test), allow(dead_code))]
320    pub(crate) fn inbox_len(&self, route: RouteId) -> usize {
321        self.signal_inbox.get(&route).map_or(0, VecDeque::len)
322    }
323}
324
325/// Round-tripable shape of a single `Instance`. Pub struct, pub(crate)
326/// fields — visible as a type at the crate boundary (so `KernelSnapshot`
327/// can name it) but not constructible/inspectable from outside the
328/// kernel crate.
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct InstanceSnapshot {
331    pub(crate) id: InstanceId,
332    pub(crate) config: InstanceConfig,
333    pub(crate) entities: BTreeMap<EntityId, EntityMeta>,
334    pub(crate) components: BTreeMap<(EntityId, TypeCode), Bytes>,
335    pub(crate) scheduler: Scheduler,
336    pub(crate) id_counters: IdCounters,
337    pub(crate) ledger: ResourceLedger,
338    pub(crate) inflight_refs: BTreeMap<RouteId, u32>,
339    pub(crate) signal_inbox: BTreeMap<RouteId, VecDeque<InboundSignal>>,
340    pub(crate) inbox_seq: u64,
341    pub(crate) wall_remainder: u128,
342    pub(crate) local_tick: u64,
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348    use crate::abi::CapabilityMask;
349    use crate::state::quota::QuotaReductionPolicy;
350
351    fn id(n: u64) -> InstanceId {
352        InstanceId::new(n).unwrap()
353    }
354
355    fn cfg() -> InstanceConfig {
356        InstanceConfig {
357            default_caps: CapabilityMask::default(),
358            max_entities: 100,
359            max_scheduled: 1000,
360            max_inbox_per_route: 16,
361            memory_budget_bytes: 1 << 20,
362            parent: None,
363            quota_reduction: QuotaReductionPolicy::default(),
364        }
365    }
366
367    #[test]
368    fn instance_new_initial_state() {
369        let inst = Instance::new(id(1), cfg());
370        assert_eq!(inst.id(), id(1));
371        assert_eq!(inst.entities_len(), 0);
372        assert_eq!(inst.components_len(), 0);
373        assert_eq!(inst.local_tick(), 0);
374        assert_eq!(inst.wall_remainder(), 0);
375        assert_eq!(inst.ledger().total_entities(), 0);
376        assert!(inst.scheduler().is_empty());
377        assert_eq!(inst.inflight_refs_len(), 0);
378    }
379
380    #[test]
381    fn instance_id_counters_default_zero() {
382        let inst = Instance::new(id(1), cfg());
383        let c = inst.id_counters();
384        assert_eq!(c.next_entity, 0);
385        assert_eq!(c.next_scheduled, 0);
386        assert_eq!(c.next_source_seq, 0);
387    }
388
389    #[test]
390    fn instance_config_carried_through() {
391        let mut config = cfg();
392        config.max_entities = 7;
393        config.parent = id(99).into(); // Some(_)
394        let inst = Instance::new(id(2), config);
395        assert_eq!(inst.config().max_entities, 7);
396        assert_eq!(inst.config().parent, Some(id(99)));
397    }
398
399    #[test]
400    fn multiple_instances_independent() {
401        let inst1 = Instance::new(id(1), cfg());
402        let inst2 = Instance::new(id(2), cfg());
403        assert_ne!(inst1.id(), inst2.id());
404        assert!(inst1.scheduler().is_empty());
405        assert!(inst2.scheduler().is_empty());
406        assert_eq!(inst1.ledger().total_bytes(), 0);
407        assert_eq!(inst2.ledger().total_bytes(), 0);
408    }
409
410    #[test]
411    fn entity_meta_clone_preserves_fields() {
412        let m1 = EntityMeta {
413            owner: Principal::System,
414            created: Tick(5),
415        };
416        let m2 = m1.clone();
417        assert!(matches!(m2.owner, Principal::System));
418        assert_eq!(m2.created, Tick(5));
419    }
420
421    #[test]
422    fn id_counters_default_clone_independent() {
423        let c1 = IdCounters::default();
424        let mut c2 = c1.clone();
425        c2.next_entity = 10;
426        assert_eq!(c1.next_entity, 0);
427        assert_eq!(c2.next_entity, 10);
428    }
429
430    // apply_stage / discard_stage tests live in `runtime/apply.rs`
431    // (R4-X: Instance does not import `runtime::StepStage`).
432}