Skip to main content

hara_native/instrumentation/
hbc.rs

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
15/// Exact Rust HBC capabilities backed by the production Machine dispatch path.
16pub 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
54/// One instrumentation target around the actual validated HBC machine.
55pub 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    /// Executes one real instruction or documented transition/terminal
127    /// boundary through `Machine::dispatch` without eager snapshots.
128    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    /// Settles the exact Promise retained at the machine's current `Await`.
199    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;