Skip to main content

telltale_vm/vm/runtime_exec/
core.rs

1/// Approximate retained state for the live VM runtime.
2#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
3pub struct VmMemoryUsage {
4    /// Session-store retained state.
5    pub session_store: SessionStoreMemoryUsage,
6    /// Number of coroutine records still retained by the VM.
7    pub coroutine_records: usize,
8    /// Number of terminal coroutine records retained by the VM.
9    pub terminal_coroutines: usize,
10    /// Number of loaded immutable program records.
11    pub program_count: usize,
12    /// Total instruction count across loaded programs.
13    pub program_instruction_count: usize,
14    /// Number of retained observable events.
15    pub obs_events: usize,
16    /// Number of retained effect-trace entries.
17    pub effect_trace_entries: usize,
18    /// Number of retained delegation audit records.
19    pub delegation_audits: usize,
20    /// Number of retained authority witness audit records.
21    pub authority_audits: usize,
22    /// Number of retained replay-consumption artifacts.
23    pub communication_artifacts: usize,
24    /// Number of retained output-condition checks.
25    pub output_condition_checks: usize,
26    /// Estimated retained bytes by VM subsystem.
27    pub retained_bytes: VmRetainedBytes,
28}
29/// Estimated retained bytes for VM subsystems.
30#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
31pub struct VmRetainedBytes {
32    /// Session-store retained bytes.
33    pub session_store: usize,
34    /// Coroutine state.
35    pub coroutines: usize,
36    /// Immutable program storage.
37    pub programs: usize,
38    /// Resource-state storage.
39    pub resource_states: usize,
40    /// Observable/effect trace storage.
41    pub traces: usize,
42    /// Replay-state and replay-artifact storage.
43    pub replay: usize,
44    /// Output-condition diagnostics.
45    pub output_condition_checks: usize,
46    /// Scheduler and control-state bookkeeping.
47    pub scheduler_and_control: usize,
48    /// Symbol interning tables.
49    pub symbols: usize,
50    /// Guard-layer resources.
51    pub guard_layer: usize,
52    /// Session monitor metadata.
53    pub monitor: usize,
54    /// Arena slot storage.
55    pub arena: usize,
56    /// Aggregate retained bytes across VM subsystems.
57    pub total: usize,
58}
59
60fn vm_serialized_bytes<T: Serialize>(value: &T) -> usize {
61    crate::serialization::binary_size(value)
62}
63
64impl VM {
65    fn communication_replay_enabled(&self) -> bool {
66        !matches!(
67            self.config.communication_replay_mode,
68            CommunicationReplayMode::Off
69        )
70    }
71
72    fn intern_load_plan_symbols(&mut self, plan: &crate::session::SessionOpenPlan, sid: SessionId) {
73        for role in plan.roles() {
74            let _: StringId = self.role_symbols.intern(role);
75        }
76        let _: StringId = self
77            .handler_symbols
78            .intern(crate::session::DEFAULT_HANDLER_ID);
79        let edge_handlers: Vec<_> = self
80            .sessions
81            .get(sid)
82            .map(|session| session.edge_handlers.keys().cloned().collect())
83            .unwrap_or_default();
84        for edge in edge_handlers {
85            let _: EdgeId = self.intern_edge(&edge);
86        }
87    }
88
89    /// Create a VM instance from configuration.
90    #[must_use]
91    pub fn new(config: VMConfig) -> Self {
92        Self::new_with_models(config)
93    }
94
95    fn bind_default_handlers_for_session(&mut self, sid: SessionId) {
96        self.sessions
97            .set_default_handler_for_session(sid, crate::session::DEFAULT_HANDLER_ID.to_string());
98        self.handler_symbols
99            .intern(crate::session::DEFAULT_HANDLER_ID);
100    }
101
102    fn ensure_session_capacity(&self) -> Result<(), VMError> {
103        if self.sessions.active_count() >= self.config.max_sessions {
104            return Err(VMError::TooManySessions {
105                max: self.config.max_sessions,
106            });
107        }
108        Ok(())
109    }
110
111    fn coroutine_runtime_eligible(&self, coro_id: usize) -> bool {
112        let Some(idx) = self.coro_index(coro_id) else {
113            return false;
114        };
115        let role = &self.coroutines[idx].role;
116        !(self.paused_coro_ids.contains(&coro_id)
117            || self.paused_roles.contains(role)
118            || self.crashed_sites.contains(role)
119            || self.timed_out_coro_ids.contains(&coro_id)
120            || self.timed_out_sites.contains_key(role))
121    }
122
123    fn mark_eligibility_dirty(&mut self) {
124        self.eligibility_dirty = true;
125    }
126
127    fn sync_ready_eligibility_for(&mut self, coro_id: usize) {
128        let eligible = self.sched.is_ready(coro_id) && self.coroutine_runtime_eligible(coro_id);
129        let eligibility = if eligible {
130            crate::scheduler::ReadyEligibility::Eligible
131        } else {
132            crate::scheduler::ReadyEligibility::Ineligible
133        };
134        self.sched.set_ready_eligibility(coro_id, eligibility);
135        #[cfg(debug_assertions)]
136        {
137            if eligible {
138                self.eligible_ready.insert(coro_id);
139            } else {
140                self.eligible_ready.remove(&coro_id);
141            }
142        }
143    }
144
145    fn refresh_ready_eligibility(&mut self) {
146        self.sched.clear_ready_eligibility();
147        #[cfg(debug_assertions)]
148        self.eligible_ready.clear();
149        for coro_id in self.sched.ready_set_snapshot() {
150            let eligible = self.coroutine_runtime_eligible(coro_id);
151            let eligibility = if eligible {
152                crate::scheduler::ReadyEligibility::Eligible
153            } else {
154                crate::scheduler::ReadyEligibility::Ineligible
155            };
156            self.sched.set_ready_eligibility(coro_id, eligibility);
157            #[cfg(debug_assertions)]
158            if eligible {
159                self.eligible_ready.insert(coro_id);
160            }
161        }
162        self.eligibility_dirty = false;
163    }
164
165    fn ensure_ready_eligibility(&mut self) {
166        if self.eligibility_dirty {
167            self.refresh_ready_eligibility();
168        }
169    }
170
171    #[cfg(debug_assertions)]
172    fn debug_assert_ready_eligibility_consistent(&self) {
173        for coro_id in &self.eligible_ready {
174            debug_assert!(self.sched.is_ready(*coro_id));
175            debug_assert!(self.coroutine_runtime_eligible(*coro_id));
176        }
177    }
178
179    fn sync_communication_consumption_mode(&mut self) {
180        self.communication_consumption
181            .set_mode(self.config.communication_replay_mode);
182    }
183
184    fn allocate_send_sequence(&mut self, edge: &Edge) -> u64 {
185        if !self.communication_replay_enabled() {
186            // Off mode preserves legacy behavior and avoids replay bookkeeping.
187            return 0;
188        }
189        self.sync_communication_consumption_mode();
190        self.communication_consumption.allocate_send_sequence(edge)
191    }
192
193    fn consume_receive_identity(
194        &mut self,
195        identity: CommunicationIdentity,
196    ) -> Result<CommunicationConsumeResult, CommunicationReplayError> {
197        if !self.communication_replay_enabled() {
198            // Off mode intentionally skips replay-consumption state and artifacts.
199            return Ok(CommunicationConsumeResult {
200                mode: CommunicationReplayMode::Off,
201                pre_root: self.communication_consumption.root(),
202                post_root: self.communication_consumption.root(),
203                consumed_nullifier: None,
204            });
205        }
206        self.sync_communication_consumption_mode();
207        let result = self.communication_consumption.consume_receive(&identity)?;
208        self.communication_consumption_artifacts.push(
209            CommunicationConsumptionArtifact {
210                tick: self.clock.tick,
211                identity,
212                mode: result.mode,
213                pre_root: result.pre_root,
214                post_root: result.post_root,
215            },
216            &self.config.observability_retention,
217        );
218        Ok(result)
219    }
220
221    fn session_open_plan(&mut self, image: &CodeImage) -> &crate::session::SessionOpenPlan {
222        let key = format!("{image:p}");
223        self.session_open_plans.entry(key).or_insert_with(|| {
224            crate::session::SessionOpenPlan::new(&image.roles(), &image.local_types)
225        })
226    }
227
228    fn open_choreography_session(
229        &mut self,
230        plan: &crate::session::SessionOpenPlan,
231    ) -> (SessionId, Vec<String>) {
232        let sid = self.sessions.next_session_id();
233        let roles = plan.roles().to_vec();
234        self.sessions
235            .open_with_sid_from_plan(sid, plan, &self.config.buffer_config);
236        (sid, roles)
237    }
238
239    fn finalize_open_choreography_session(
240        &mut self,
241        sid: SessionId,
242        roles: &[String],
243        plan: &crate::session::SessionOpenPlan,
244    ) -> Result<(), VMError> {
245        self.next_session_id = self.sessions.next_session_id();
246        self.bind_default_handlers_for_session(sid);
247        self.intern_load_plan_symbols(plan, sid);
248        self.monitor.set_kind(sid, SessionKind::Peer);
249        self.resource_states.entry(sid).or_default();
250        self.apply_open_delta(sid)
251            .map_err(VMError::PersistenceError)?;
252        self.obs_trace.push(
253            ObsEvent::Opened {
254                tick: self.clock.tick,
255                session: sid,
256                roles: roles.to_vec(),
257            },
258            &self.config.observability_retention,
259        );
260        Ok(())
261    }
262
263    fn spawn_coroutine_for_role(
264        &mut self,
265        image: &CodeImage,
266        sid: SessionId,
267        role: &str,
268    ) -> Result<(), VMError> {
269        if self.coroutines.len() >= self.config.max_coroutines {
270            return Err(VMError::TooManyCoroutines {
271                max: self.config.max_coroutines,
272            });
273        }
274
275        let program_id = self
276            .programs
277            .intern(image.programs.get(role).cloned().unwrap_or_default());
278        if self.code.is_none() {
279            let program = self
280                .programs
281                .get(program_id)
282                .expect("interned program must exist")
283                .clone();
284            self.code = Some(program);
285        }
286
287        let coro_id = self.next_coro_id;
288        self.next_coro_id += 1;
289
290        let endpoint = Endpoint {
291            sid,
292            role: role.to_string(),
293        };
294        self.role_coroutines
295            .entry(role.to_string())
296            .or_default()
297            .push(coro_id);
298        if self.paused_roles.contains(role) {
299            self.paused_coro_ids.insert(coro_id);
300        }
301        if self.timed_out_sites.contains_key(role) {
302            self.timed_out_coro_ids.insert(coro_id);
303        }
304        let mut coro = Coroutine::new(
305            coro_id,
306            program_id,
307            sid,
308            role.to_string(),
309            self.config.num_registers,
310            self.config.initial_cost_budget,
311        );
312        coro.owned_endpoints.push(endpoint.clone());
313        if !coro.regs.is_empty() {
314            coro.regs[0] = Value::Endpoint(endpoint);
315        }
316        self.sched.add_ready(coro_id);
317        self.coroutines.push(coro);
318        self.coro_slots.insert(coro_id, self.coroutines.len() - 1);
319        self.sync_ready_eligibility_for(coro_id);
320        Ok(())
321    }
322
323    fn spawn_session_coroutines(
324        &mut self,
325        image: &CodeImage,
326        sid: SessionId,
327        roles: &[String],
328    ) -> Result<(), VMError> {
329        for role in roles {
330            self.spawn_coroutine_for_role(image, sid, role)?;
331        }
332        Ok(())
333    }
334
335    /// Runtime open primitive for a verified code image.
336    ///
337    /// Creates a session (with local types), spawns coroutines per role,
338    /// and returns the session ID. Type state is initialized in the
339    /// session store with no separate monitor object.
340    ///
341    /// # Errors
342    ///
343    /// Returns an error if session or coroutine limits are exceeded.
344    #[doc(hidden)]
345    pub fn load_choreography(&mut self, image: &CodeImage) -> Result<SessionId, VMError> {
346        self.ensure_session_capacity()?;
347        image
348            .validate_runtime_shape()
349            .map_err(|reason| VMError::InvalidCodeImage { reason })?;
350        let plan = self.session_open_plan(image).clone();
351        let (sid, roles) = self.open_choreography_session(&plan);
352        self.finalize_open_choreography_session(sid, &roles, &plan)?;
353        self.programs.reserve(image.programs.len());
354        self.coroutines.reserve(roles.len());
355        self.spawn_session_coroutines(image, sid, &roles)?;
356        Ok(sid)
357    }
358}