brink_runtime/state.rs
1//! Context access trait and write observer.
2//!
3//! The `ContextAccess` trait provides the mutable state interface that the VM
4//! and orchestration use. [`World`](crate::world::World) implements it
5//! directly (zero-cost, monomorphized), as does the routing view
6//! ([`ContextView`](crate::world::ContextView)) that composes `World` with
7//! the (currently empty) per-flow `FlowLocal` layer. `ObservedContext` wraps
8//! any `ContextAccess` implementor and fires `WriteObserver` callbacks on
9//! every mutation.
10
11use alloc::vec::Vec;
12
13use brink_format::{DefinitionId, Value};
14
15use crate::rng::StoryRng;
16
17/// Trait for accessing and mutating story execution state.
18///
19/// This is the interface between the VM and the mutable story state.
20/// [`World`](crate::world::World) implements it directly, as does the
21/// [`ContextView`](crate::world::ContextView) routing view.
22/// [`ObservedContext`] wraps an implementor and fires [`WriteObserver`]
23/// callbacks on mutations. Consumers can also implement this trait
24/// themselves to plug in custom observers (e.g. bevy events) or alternate
25/// storage backends.
26///
27/// This does NOT include `Program`, resolver, or any immutable data — it's
28/// purely the mutable state surface.
29pub trait ContextAccess {
30 fn global(&self, idx: u32) -> &Value;
31 fn set_global(&mut self, idx: u32, value: Value);
32
33 /// Move a global's current value out, leaving [`Value::Null`] behind —
34 /// the take-half of the take → `make_mut` → write-back RMW discipline
35 /// (`docs/value-model-spec.md` §5) that closes the indexed-write COW
36 /// cliff: reading a collection via [`global`](Self::global) clones its
37 /// `Arc` (bumping the refcount `array_make_mut`/`map_make_mut` checks),
38 /// so a subsequent in-place mutation always sees itself as "shared" and
39 /// COW-copies. Taking instead of cloning means a slot that's the *sole*
40 /// owner of its value stays the sole owner all the way to the mutate
41 /// site, so the mutation completes in place — O(1) amortized instead of
42 /// O(n) per write in a loop.
43 ///
44 /// Default implementation: clone + null out — always correct (identical
45 /// observable result to `GetGlobal` followed by `SetGlobal(Null)`), just
46 /// not free of the extra `Arc` clone. [`World`](crate::world::World),
47 /// whose globals are a flat `Vec<Value>`, overrides this with a real
48 /// [`core::mem::replace`] move. [`ContextView`](crate::world::ContextView)
49 /// delegates to `World::take_global` for `World`-scoped units (the
50 /// common case every oracle-corpus program exercises) and falls back to
51 /// this default for `Local`-scoped units, whose per-flow override map
52 /// can't move out of an immutable frozen-base ancestor.
53 fn take_global(&mut self, idx: u32) -> Value {
54 let v = self.global(idx).clone();
55 self.set_global(idx, Value::Null);
56 v
57 }
58
59 fn visit_count(&self, id: DefinitionId) -> u32;
60 fn increment_visit(&mut self, id: DefinitionId);
61 /// Set a visit count directly, rather than incrementing it. Used by
62 /// [`crate::load_state`] to reconcile a durable save, whose entries carry
63 /// absolute counts rather than deltas.
64 fn set_visit_count(&mut self, id: DefinitionId, count: u32);
65
66 fn turn_count(&self, id: DefinitionId) -> Option<u32>;
67 fn set_turn_count(&mut self, id: DefinitionId, turn: u32);
68
69 fn turn_index(&self) -> u32;
70 fn increment_turn_index(&mut self);
71 /// Set the turn index directly, rather than incrementing it. Used by
72 /// [`crate::load_state`] to restore a saved turn index.
73 fn set_turn_index(&mut self, index: u32);
74
75 fn rng_seed(&self) -> i32;
76 fn set_rng_seed(&mut self, seed: i32);
77
78 fn previous_random(&self) -> i32;
79 fn set_previous_random(&mut self, val: i32);
80
81 fn next_random<R: StoryRng>(&self, seed: i32) -> i32;
82 fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32>;
83}
84
85// ── WriteObserver ──────────────────────────────────────────────────────────
86
87/// Observer for state mutations during story execution.
88///
89/// Implement this trait to intercept every write the VM makes to the story
90/// state. All methods have default no-op implementations. The observer
91/// receives the *new* value only — no old-value cloning is performed.
92#[expect(unused_variables)]
93pub trait WriteObserver {
94 fn on_set_global(&mut self, idx: u32, value: &Value) {}
95 fn on_increment_visit(&mut self, id: DefinitionId, new_count: u32) {}
96 fn on_set_visit_count(&mut self, id: DefinitionId, count: u32) {}
97 fn on_set_turn_count(&mut self, id: DefinitionId, turn: u32) {}
98 fn on_increment_turn_index(&mut self, new_value: u32) {}
99 fn on_set_turn_index(&mut self, index: u32) {}
100 fn on_set_rng_seed(&mut self, new_seed: i32) {}
101 fn on_set_previous_random(&mut self, new_val: i32) {}
102}
103
104// ── ObservedContext ────────────────────────────────────────────────────────
105
106/// A `ContextAccess` wrapper that delegates to an inner `ContextAccess`
107/// implementor (typically [`World`](crate::world::World) or the
108/// [`ContextView`](crate::world::ContextView) routing view) and notifies a
109/// `WriteObserver` on every mutation.
110///
111/// Generic over the wrapped implementor so it composes with the routing
112/// view: `ObservedContext::new(&mut ContextView::new(&mut world, &mut
113/// local), observer)` observes exactly what the VM sees, regardless of how
114/// many layers the routing view has behind it.
115pub struct ObservedContext<'a, 'o, C: ContextAccess> {
116 context: &'a mut C,
117 observer: &'o mut dyn WriteObserver,
118}
119
120impl<'a, 'o, C: ContextAccess> ObservedContext<'a, 'o, C> {
121 pub fn new(context: &'a mut C, observer: &'o mut dyn WriteObserver) -> Self {
122 Self { context, observer }
123 }
124}
125
126impl<C: ContextAccess> ContextAccess for ObservedContext<'_, '_, C> {
127 #[inline]
128 fn global(&self, idx: u32) -> &Value {
129 self.context.global(idx)
130 }
131
132 #[inline]
133 fn set_global(&mut self, idx: u32, value: Value) {
134 self.context.set_global(idx, value.clone());
135 self.observer.on_set_global(idx, &value);
136 }
137
138 /// Deliberately does **not** notify the observer: `take_global` is an
139 /// internal RMW implementation detail (the slot transiently holds
140 /// `Value::Null` mid-statement, never a state a host or journal should
141 /// ever see — value-model-spec §3's sharing-unobservable law extends to
142 /// this VM-internal bookkeeping too). The RMW's real semantic write is
143 /// the later `set_global` call that writes the final (mutated, or on a
144 /// mid-RMW fault, restored/`Null`) value back — that one fires
145 /// `on_set_global` exactly as before.
146 #[inline]
147 fn take_global(&mut self, idx: u32) -> Value {
148 self.context.take_global(idx)
149 }
150
151 #[inline]
152 fn visit_count(&self, id: DefinitionId) -> u32 {
153 self.context.visit_count(id)
154 }
155
156 #[inline]
157 fn increment_visit(&mut self, id: DefinitionId) {
158 self.context.increment_visit(id);
159 let new_count = self.context.visit_count(id);
160 self.observer.on_increment_visit(id, new_count);
161 }
162
163 #[inline]
164 fn set_visit_count(&mut self, id: DefinitionId, count: u32) {
165 self.context.set_visit_count(id, count);
166 self.observer.on_set_visit_count(id, count);
167 }
168
169 #[inline]
170 fn turn_count(&self, id: DefinitionId) -> Option<u32> {
171 self.context.turn_count(id)
172 }
173
174 #[inline]
175 fn set_turn_count(&mut self, id: DefinitionId, turn: u32) {
176 self.context.set_turn_count(id, turn);
177 self.observer.on_set_turn_count(id, turn);
178 }
179
180 #[inline]
181 fn turn_index(&self) -> u32 {
182 self.context.turn_index()
183 }
184
185 #[inline]
186 fn increment_turn_index(&mut self) {
187 self.context.increment_turn_index();
188 self.observer
189 .on_increment_turn_index(self.context.turn_index());
190 }
191
192 #[inline]
193 fn set_turn_index(&mut self, index: u32) {
194 self.context.set_turn_index(index);
195 self.observer.on_set_turn_index(index);
196 }
197
198 #[inline]
199 fn rng_seed(&self) -> i32 {
200 self.context.rng_seed()
201 }
202
203 #[inline]
204 fn set_rng_seed(&mut self, seed: i32) {
205 self.context.set_rng_seed(seed);
206 self.observer.on_set_rng_seed(seed);
207 }
208
209 #[inline]
210 fn previous_random(&self) -> i32 {
211 self.context.previous_random()
212 }
213
214 #[inline]
215 fn set_previous_random(&mut self, val: i32) {
216 self.context.set_previous_random(val);
217 self.observer.on_set_previous_random(val);
218 }
219
220 #[inline]
221 fn next_random<R: StoryRng>(&self, seed: i32) -> i32 {
222 self.context.next_random::<R>(seed)
223 }
224
225 fn random_sequence<R: StoryRng>(&self, seed: i32, count: usize) -> Vec<i32> {
226 self.context.random_sequence::<R>(seed, count)
227 }
228}