1use std::collections::BTreeSet;
2
3use crate::core::{Promise, PromiseState, Value};
4use crate::vm::machine::instrumentation::{
5 NoProbe, TerminalEvent, TransitionEvent, TransitionKind, VmBoundary, VmBoundaryOutcome,
6};
7use crate::vm::Machine;
8
9use super::{
10 Capability, ControlLease, DispatchReport, EventAccess, EventKind, EventLocation,
11 InstrumentDirective, InstrumentationError, InstrumentationHub, PortableProjection,
12 ProducerEvent, ProjectionLimits, TargetHandle, TargetKind,
13};
14
15pub fn hbc_capabilities() -> BTreeSet<Capability> {
17 BTreeSet::from([
18 Capability::EventInstruction,
19 Capability::EventCall,
20 Capability::EventException,
21 Capability::EventSuspension,
22 Capability::EventLifecycle,
23 Capability::InspectSourceLocation,
24 Capability::InspectCurrentFrame,
25 Capability::InspectFrames,
26 Capability::InspectLocals,
27 Capability::InspectStack,
28 Capability::InspectValuePreview,
29 Capability::InspectSnapshot,
30 Capability::ControlPause,
31 Capability::ControlSingleStep,
32 Capability::ControlResume,
33 Capability::ControlSettle,
34 Capability::ControlTerminate,
35 ])
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct HbcBoundary {
40 pub status: &'static str,
41 pub paused: bool,
42 pub reports: Vec<DispatchReport>,
43}
44
45enum HbcState {
46 Running,
47 Suspended(Promise),
48 Yielded(Value),
49 Returned(Value),
50 Failed(String),
51 Cancelled,
52}
53
54pub struct HbcTarget {
56 target: TargetHandle,
57 source_id: String,
58 machine: Machine,
59 state: HbcState,
60 paused: bool,
61 terminal_emitted: bool,
62}
63
64impl HbcTarget {
65 pub fn new(
66 hub: &InstrumentationHub,
67 target: TargetHandle,
68 source_id: impl Into<String>,
69 machine: Machine,
70 ) -> Result<Self, InstrumentationError> {
71 let descriptor = hub.target_descriptor(&target)?;
72 if descriptor.kind != TargetKind::Hbc {
73 return Err(InstrumentationError::InvalidTarget(
74 "HBC probe requires an HBC target".into(),
75 ));
76 }
77 let source_id = source_id.into();
78 if source_id.trim().is_empty() {
79 return Err(InstrumentationError::InvalidTarget(
80 "HBC source id must be non-empty".into(),
81 ));
82 }
83 Ok(Self {
84 target,
85 source_id,
86 machine,
87 state: HbcState::Running,
88 paused: false,
89 terminal_emitted: false,
90 })
91 }
92
93 pub fn target(&self) -> &TargetHandle {
94 &self.target
95 }
96
97 pub fn status(&self) -> &'static str {
98 state_keyword(&self.state)
99 }
100
101 pub fn paused(&self) -> bool {
102 self.paused
103 }
104
105 pub fn pending(&self) -> Option<Promise> {
106 match &self.state {
107 HbcState::Suspended(promise) => Some(promise.clone()),
108 _ => None,
109 }
110 }
111
112 pub fn result(&self) -> Option<Value> {
113 match &self.state {
114 HbcState::Returned(value) | HbcState::Yielded(value) => Some(value.clone()),
115 _ => None,
116 }
117 }
118
119 pub fn error(&self) -> Option<&str> {
120 match &self.state {
121 HbcState::Failed(error) => Some(error),
122 _ => None,
123 }
124 }
125
126 pub fn step(
129 &mut self,
130 hub: &mut InstrumentationHub,
131 ) -> Result<HbcBoundary, InstrumentationError> {
132 if self.finished() {
133 return Ok(self.boundary(Vec::new()));
134 }
135 let directive = hub.take_directive(&self.target)?;
136 if self.paused && directive == InstrumentDirective::Continue {
137 return Ok(self.boundary(Vec::new()));
138 }
139 let pause_after = directive == InstrumentDirective::StepNext;
140 match directive {
141 InstrumentDirective::Suspend => {
142 self.paused = true;
143 return Ok(self.boundary(Vec::new()));
144 }
145 InstrumentDirective::Terminate => {
146 self.state = HbcState::Cancelled;
147 self.paused = true;
148 let mut reports = Vec::new();
149 self.emit_terminal(hub, "cancelled", None, &mut reports)?;
150 return Ok(self.boundary(reports));
151 }
152 InstrumentDirective::Continue | InstrumentDirective::StepNext => {
153 self.paused = false;
154 }
155 }
156
157 let mut probe = NoProbe;
158 let boundary = self.machine.step_instrumented_boundary(&mut probe);
159 let mut reports = Vec::new();
160 self.emit_boundary(hub, &boundary, &mut reports)?;
161 self.apply_outcome(boundary.outcome);
162 if pause_after {
163 self.paused = true;
164 }
165 Ok(self.boundary(reports))
166 }
167
168 pub fn run(
169 &mut self,
170 hub: &mut InstrumentationHub,
171 boundary_limit: usize,
172 ) -> Result<Vec<HbcBoundary>, InstrumentationError> {
173 let mut boundaries = Vec::new();
174 for _ in 0..boundary_limit {
175 if self.paused || self.finished() || matches!(&self.state, HbcState::Suspended(_)) {
176 break;
177 }
178 let boundary = self.step(hub)?;
179 boundaries.push(boundary);
180 if self.paused || self.finished() || matches!(&self.state, HbcState::Suspended(_)) {
181 break;
182 }
183 }
184 Ok(boundaries)
185 }
186
187 pub fn continue_execution(
188 &mut self,
189 hub: &InstrumentationHub,
190 lease: &ControlLease,
191 ) -> Result<(), InstrumentationError> {
192 hub.authorize_control(lease, Capability::ControlResume)?;
193 self.check_lease_target(lease)?;
194 self.paused = false;
195 Ok(())
196 }
197
198 pub fn settle(
200 &mut self,
201 hub: &mut InstrumentationHub,
202 lease: &ControlLease,
203 state: PromiseState,
204 ) -> Result<HbcBoundary, InstrumentationError> {
205 hub.authorize_control(lease, Capability::ControlSettle)?;
206 self.check_lease_target(lease)?;
207 if !matches!(&self.state, HbcState::Suspended(_)) {
208 return Err(InstrumentationError::Execution(
209 "HBC target is not suspended".into(),
210 ));
211 }
212 if matches!(state, PromiseState::Pending) {
213 return Err(InstrumentationError::Execution(
214 "HBC settlement cannot remain pending".into(),
215 ));
216 }
217 self.paused = false;
218 let mut probe = NoProbe;
219 let boundary = self.machine.resume_instrumented_boundary(state, &mut probe);
220 let mut reports = Vec::new();
221 self.emit_boundary(hub, &boundary, &mut reports)?;
222 self.apply_outcome(boundary.outcome);
223 Ok(self.boundary(reports))
224 }
225
226 fn emit_boundary(
227 &mut self,
228 hub: &mut InstrumentationHub,
229 boundary: &VmBoundary,
230 reports: &mut Vec<DispatchReport>,
231 ) -> Result<(), InstrumentationError> {
232 if let Some(instruction) = boundary.instruction {
233 let location = self.machine.instrumentation_location_at(
234 usize::from(instruction.function),
235 instruction.ip as usize,
236 &self.source_id,
237 );
238 self.emit(
239 hub,
240 ProducerEvent::live(EventKind::InstructionExecute)
241 .with_data("opcode", instruction.opcode.as_keyword())
242 .with_data("stack/depth", instruction.stack_depth.to_string())
243 .with_data("call/depth", instruction.call_depth.to_string()),
244 Some(location),
245 reports,
246 )?;
247 }
248 if let Some(transition) = boundary.transition {
249 self.emit_transition(hub, transition, reports)?;
250 }
251 if let Some(terminal) = boundary.terminal {
252 let status = match terminal.kind {
253 crate::vm::machine::instrumentation::TerminalKind::Return => "returned",
254 crate::vm::machine::instrumentation::TerminalKind::Fail => "failed",
255 };
256 self.emit_terminal(hub, status, Some(terminal), reports)?;
257 }
258 Ok(())
259 }
260
261 fn emit_transition(
262 &self,
263 hub: &mut InstrumentationHub,
264 transition: TransitionEvent,
265 reports: &mut Vec<DispatchReport>,
266 ) -> Result<(), InstrumentationError> {
267 let event = match transition.kind {
268 TransitionKind::CallEnter => EventKind::CallEnter,
269 TransitionKind::CallReturn => EventKind::CallReturn,
270 TransitionKind::ExceptionUnwind => EventKind::ExceptionUnwind,
271 TransitionKind::MachineSuspend => EventKind::MachineSuspend,
272 TransitionKind::MachineResume => EventKind::MachineResume,
273 };
274 let location = self.machine.instrumentation_location_at(
275 usize::from(transition.from_function),
276 transition.from_ip as usize,
277 &self.source_id,
278 );
279 self.emit(
280 hub,
281 ProducerEvent::live(event)
282 .with_data("from/function", transition.from_function.to_string())
283 .with_data("from/ip", transition.from_ip.to_string())
284 .with_data("to/function", transition.to_function.to_string())
285 .with_data("to/ip", transition.to_ip.to_string()),
286 Some(location),
287 reports,
288 )
289 }
290
291 fn emit_terminal(
292 &mut self,
293 hub: &mut InstrumentationHub,
294 status: &str,
295 terminal: Option<TerminalEvent>,
296 reports: &mut Vec<DispatchReport>,
297 ) -> Result<(), InstrumentationError> {
298 if self.terminal_emitted {
299 return Ok(());
300 }
301 let location = terminal.map(|terminal| {
302 self.machine.instrumentation_location_at(
303 usize::from(terminal.function),
304 terminal.ip as usize,
305 &self.source_id,
306 )
307 });
308 let mut event =
309 ProducerEvent::live(EventKind::ExecutionTerminal).with_data("status", status);
310 if let Some(terminal) = terminal {
311 event = event
312 .with_data("stack/depth", terminal.stack_depth.to_string())
313 .with_data("call/depth", terminal.call_depth.to_string());
314 }
315 self.terminal_emitted = true;
316 self.emit(hub, event, location, reports)
317 }
318
319 fn emit(
320 &self,
321 hub: &mut InstrumentationHub,
322 event: ProducerEvent,
323 location: Option<EventLocation>,
324 reports: &mut Vec<DispatchReport>,
325 ) -> Result<(), InstrumentationError> {
326 let mut access = HbcAccess {
327 machine: &self.machine,
328 location,
329 };
330 reports.push(hub.emit(&self.target, event, &mut access)?);
331 Ok(())
332 }
333
334 fn apply_outcome(&mut self, outcome: VmBoundaryOutcome) {
335 self.state = match outcome {
336 VmBoundaryOutcome::Continue => HbcState::Running,
337 VmBoundaryOutcome::Suspended(promise) => HbcState::Suspended(promise),
338 VmBoundaryOutcome::Yielded(value) => HbcState::Yielded(value),
339 VmBoundaryOutcome::Returned(value) => HbcState::Returned(value),
340 VmBoundaryOutcome::Failed(error) => HbcState::Failed(error.to_string()),
341 };
342 }
343
344 fn check_lease_target(&self, lease: &ControlLease) -> Result<(), InstrumentationError> {
345 if lease.target() == &self.target {
346 Ok(())
347 } else {
348 Err(InstrumentationError::InvalidControlLease {
349 target_id: self.target.target_id().into(),
350 instrument_id: lease.instrument().instrument_id().into(),
351 })
352 }
353 }
354
355 fn finished(&self) -> bool {
356 matches!(
357 &self.state,
358 HbcState::Yielded(_)
359 | HbcState::Returned(_)
360 | HbcState::Failed(_)
361 | HbcState::Cancelled
362 )
363 }
364
365 fn boundary(&self, reports: Vec<DispatchReport>) -> HbcBoundary {
366 HbcBoundary {
367 status: state_keyword(&self.state),
368 paused: self.paused,
369 reports,
370 }
371 }
372}
373
374struct HbcAccess<'a> {
375 machine: &'a Machine,
376 location: Option<EventLocation>,
377}
378
379impl EventAccess for HbcAccess<'_> {
380 fn source_location(&mut self) -> Option<EventLocation> {
381 self.location.clone()
382 }
383
384 fn current_frame(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
385 Some(self.machine.instrumentation_current_frame(limits))
386 }
387
388 fn frames(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
389 Some(self.machine.instrumentation_frames(limits))
390 }
391
392 fn locals(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
393 Some(self.machine.instrumentation_locals(limits))
394 }
395
396 fn stack(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
397 Some(self.machine.instrumentation_stack(limits))
398 }
399
400 fn value_preview(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
401 self.machine.instrumentation_value_preview(limits)
402 }
403
404 fn machine_snapshot(&mut self, limits: ProjectionLimits) -> Option<PortableProjection> {
405 Some(self.machine.instrumentation_snapshot(limits))
406 }
407}
408
409fn state_keyword(state: &HbcState) -> &'static str {
410 match state {
411 HbcState::Running => "running",
412 HbcState::Suspended(_) => "suspended",
413 HbcState::Yielded(_) => "yielded",
414 HbcState::Returned(_) => "returned",
415 HbcState::Failed(_) => "failed",
416 HbcState::Cancelled => "cancelled",
417 }
418}
419
420#[cfg(test)]
421#[path = "hbc/tests.rs"]
422mod tests;