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