Skip to main content

hara_native/vm/machine/instrumentation/
step.rs

1use std::collections::BTreeMap;
2
3use super::super::{Dispatch, Machine, VmSlot};
4use super::{
5    InstructionEvent, Opcode, TerminalEvent, TerminalKind, TransitionEvent, TransitionKind, VmProbe,
6};
7use crate::core::{Promise, PromiseState, Value};
8use crate::instrumentation::{EventLocation, PortableProjection, ProjectionLimits, SourceSpan};
9use crate::vm::error::VmError;
10use crate::vm::opcode::Instruction;
11
12/// Runtime outcome retained separately from portable instrumentation data.
13pub enum VmBoundaryOutcome {
14    Continue,
15    Suspended(Promise),
16    Yielded(Value),
17    Returned(Value),
18    Failed(VmError),
19}
20
21/// One actual HBC dispatch boundary with only scalar event metadata.
22pub struct VmBoundary {
23    pub instruction: Option<InstructionEvent>,
24    pub transition: Option<TransitionEvent>,
25    pub terminal: Option<TerminalEvent>,
26    pub outcome: VmBoundaryOutcome,
27}
28
29impl Machine {
30    /// Executes exactly one production dispatch boundary without projecting VM
31    /// state. The caller may lazily inspect the machine after event/filter
32    /// matching through the shared instrumentation hub.
33    pub fn step_instrumented_boundary<P: VmProbe>(&mut self, probe: &mut P) -> VmBoundary {
34        self.clear_step_jit_state();
35        let program = self.program.clone();
36        let Some(function) = program.functions.get(self.function) else {
37            let error = VmError::new("function index out of range", 0, None);
38            let terminal = self.boundary_terminal_event(TerminalKind::Fail);
39            probe.on_terminal(terminal);
40            return VmBoundary {
41                instruction: None,
42                transition: None,
43                terminal: Some(terminal),
44                outcome: VmBoundaryOutcome::Failed(error),
45            };
46        };
47        let Some(instruction) = function.code.get(self.ip).cloned() else {
48            let error = self.error(function, "instruction pointer out of range");
49            let terminal = self.boundary_terminal_event(TerminalKind::Fail);
50            probe.on_terminal(terminal);
51            return VmBoundary {
52                instruction: None,
53                transition: None,
54                terminal: Some(terminal),
55                outcome: VmBoundaryOutcome::Failed(error),
56            };
57        };
58        let instruction_event = self.boundary_instruction_event(&instruction);
59        probe.on_instruction(instruction_event);
60        let from_function = self.function;
61        let from_ip = self.ip;
62
63        match self.dispatch(&program, function, &instruction) {
64            Dispatch::Next(ip) => {
65                self.ip = ip;
66                VmBoundary {
67                    instruction: Some(instruction_event),
68                    transition: None,
69                    terminal: None,
70                    outcome: VmBoundaryOutcome::Continue,
71                }
72            }
73            Dispatch::Unwound(ip) => {
74                self.ip = ip;
75                self.transition_boundary(
76                    probe,
77                    instruction_event,
78                    TransitionKind::ExceptionUnwind,
79                    from_function,
80                    from_ip,
81                    VmBoundaryOutcome::Continue,
82                )
83            }
84            Dispatch::Call { callee, args } => {
85                if let Err(message) = self.enter_callable(&program, callee, args) {
86                    match self.raise(function, message) {
87                        Ok(target) => {
88                            self.ip = target;
89                            self.transition_boundary(
90                                probe,
91                                instruction_event,
92                                TransitionKind::ExceptionUnwind,
93                                from_function,
94                                from_ip,
95                                VmBoundaryOutcome::Continue,
96                            )
97                        }
98                        Err(error) => self.failed_boundary(probe, Some(instruction_event), error),
99                    }
100                } else {
101                    self.transition_boundary(
102                        probe,
103                        instruction_event,
104                        TransitionKind::CallEnter,
105                        from_function,
106                        from_ip,
107                        VmBoundaryOutcome::Continue,
108                    )
109                }
110            }
111            Dispatch::CallStatic {
112                prototype,
113                args,
114                captures,
115            } => {
116                self.enter_or_spawn(&program, prototype, args, captures);
117                self.transition_boundary(
118                    probe,
119                    instruction_event,
120                    TransitionKind::CallEnter,
121                    from_function,
122                    from_ip,
123                    VmBoundaryOutcome::Continue,
124                )
125            }
126            Dispatch::CallStaticDirect { prototype, argc } => {
127                self.enter_static_direct(&program, prototype, argc);
128                self.transition_boundary(
129                    probe,
130                    instruction_event,
131                    TransitionKind::CallEnter,
132                    from_function,
133                    from_ip,
134                    VmBoundaryOutcome::Continue,
135                )
136            }
137            Dispatch::Returned(value) => {
138                self.stack.truncate(self.frame.base());
139                if let Some(caller) = self.calls.pop() {
140                    self.function = caller.function;
141                    let completed = std::mem::replace(&mut self.frame, caller.frame);
142                    self.free_locals.push(completed.into_locals());
143                    self.ip = caller.call_ip + 1;
144                    self.stack.push(value);
145                    self.transition_boundary(
146                        probe,
147                        instruction_event,
148                        TransitionKind::CallReturn,
149                        from_function,
150                        from_ip,
151                        VmBoundaryOutcome::Continue,
152                    )
153                } else {
154                    let terminal = self.boundary_terminal_event(TerminalKind::Return);
155                    probe.on_terminal(terminal);
156                    VmBoundary {
157                        instruction: Some(instruction_event),
158                        transition: None,
159                        terminal: Some(terminal),
160                        outcome: VmBoundaryOutcome::Returned(Self::into_value(
161                            program.clone(),
162                            value,
163                        )),
164                    }
165                }
166            }
167            Dispatch::Suspended(promise) => self.transition_boundary(
168                probe,
169                instruction_event,
170                TransitionKind::MachineSuspend,
171                from_function,
172                from_ip,
173                VmBoundaryOutcome::Suspended(promise),
174            ),
175            Dispatch::Yielded(value) => self.transition_boundary(
176                probe,
177                instruction_event,
178                TransitionKind::MachineSuspend,
179                from_function,
180                from_ip,
181                VmBoundaryOutcome::Yielded(value),
182            ),
183            Dispatch::Failed(error) => self.failed_boundary(probe, Some(instruction_event), error),
184        }
185    }
186
187    /// Applies exactly one settlement at the actual retained `Await` boundary.
188    /// Subsequent instructions are left for later `step_instrumented_boundary`
189    /// calls.
190    pub fn resume_instrumented_boundary<P: VmProbe>(
191        &mut self,
192        state: PromiseState,
193        probe: &mut P,
194    ) -> VmBoundary {
195        self.clear_step_jit_state();
196        let from_function = self.function;
197        let from_ip = self.ip;
198        let Some(function) = self.program.functions.get(self.function).cloned() else {
199            let error = VmError::new("function index out of range", 0, None);
200            return self.failed_boundary(probe, None, error);
201        };
202        if !matches!(function.code.get(self.ip), Some(Instruction::Await)) {
203            let error = self.error(&function, "VM is not suspended at await");
204            return self.failed_boundary(probe, None, error);
205        }
206        match state {
207            PromiseState::Pending => {
208                let promise = match self.stack.last().and_then(VmSlot::runtime_value) {
209                    Some(Value::Promise(promise)) => promise,
210                    _ => {
211                        let error = self.error(&function, "await expects a promise");
212                        return self.failed_boundary(probe, None, error);
213                    }
214                };
215                self.resume_transition_boundary(
216                    probe,
217                    TransitionKind::MachineSuspend,
218                    from_function,
219                    from_ip,
220                    VmBoundaryOutcome::Suspended(promise),
221                )
222            }
223            PromiseState::Fulfilled(value) => {
224                self.stack.pop();
225                self.stack.push(value.into());
226                self.ip += 1;
227                self.resume_transition_boundary(
228                    probe,
229                    TransitionKind::MachineResume,
230                    from_function,
231                    from_ip,
232                    VmBoundaryOutcome::Continue,
233                )
234            }
235            PromiseState::Rejected(error) => {
236                self.stack.pop();
237                match self.raise(&function, crate::core::promise_rejection_error(error)) {
238                    Ok(target) => {
239                        self.ip = target;
240                        self.resume_transition_boundary(
241                            probe,
242                            TransitionKind::ExceptionUnwind,
243                            from_function,
244                            from_ip,
245                            VmBoundaryOutcome::Continue,
246                        )
247                    }
248                    Err(error) => self.failed_boundary(probe, None, error),
249                }
250            }
251        }
252    }
253
254    pub(crate) fn instrumentation_location_at(
255        &self,
256        function: usize,
257        ip: usize,
258        source_id: &str,
259    ) -> EventLocation {
260        let prototype = self.program.functions.get(function);
261        let position = prototype.and_then(|prototype| prototype.source_map.position(ip));
262        EventLocation {
263            source_id: Some(source_id.into()),
264            form_path: None,
265            span: position.map(|position| SourceSpan {
266                start: position.offset,
267                end: position.offset,
268            }),
269            function: prototype.and_then(|prototype| prototype.name.clone()),
270            instruction_pointer: Some(ip),
271        }
272    }
273
274    pub(crate) fn instrumentation_current_frame(
275        &self,
276        limits: ProjectionLimits,
277    ) -> PortableProjection {
278        let mut projection = PortableProjection::new("hbc/current-frame")
279            .with_field("function", self.function.to_string())
280            .with_field("ip", self.ip.to_string())
281            .with_field("stack-base", self.frame.base().to_string());
282        if let Some(name) = self
283            .program
284            .functions
285            .get(self.function)
286            .and_then(|function| function.name.as_ref())
287        {
288            projection
289                .fields
290                .insert("function/name".into(), name.clone());
291        }
292        append_slots(
293            &mut projection.fields,
294            "local",
295            self.frame.locals(),
296            limits,
297            false,
298        );
299        projection
300    }
301
302    pub(crate) fn instrumentation_frames(&self, limits: ProjectionLimits) -> PortableProjection {
303        let mut projection = PortableProjection::new("hbc/frames")
304            .with_field("count", (self.calls.len() + 1).to_string());
305        let start = self.calls.len().saturating_sub(limits.max_items);
306        for (index, frame) in self.calls[start..].iter().enumerate() {
307            projection.fields.insert(
308                format!("frame/{index}/function"),
309                frame.function.to_string(),
310            );
311            projection
312                .fields
313                .insert(format!("frame/{index}/call-ip"), frame.call_ip.to_string());
314            if let Some(name) = self
315                .program
316                .functions
317                .get(frame.function)
318                .and_then(|function| function.name.as_ref())
319            {
320                projection
321                    .fields
322                    .insert(format!("frame/{index}/name"), name.clone());
323            }
324        }
325        projection
326            .fields
327            .insert("omitted".into(), start.to_string());
328        projection
329    }
330
331    pub(crate) fn instrumentation_locals(&self, limits: ProjectionLimits) -> PortableProjection {
332        let mut projection = PortableProjection::new("hbc/locals");
333        append_slots(
334            &mut projection.fields,
335            "local",
336            self.frame.locals(),
337            limits,
338            false,
339        );
340        projection
341    }
342
343    pub(crate) fn instrumentation_stack(&self, limits: ProjectionLimits) -> PortableProjection {
344        let mut projection = PortableProjection::new("hbc/stack");
345        append_slots(&mut projection.fields, "stack", &self.stack, limits, true);
346        projection
347    }
348
349    pub(crate) fn instrumentation_value_preview(
350        &self,
351        limits: ProjectionLimits,
352    ) -> Option<PortableProjection> {
353        let value = self.stack.last()?;
354        Some(
355            PortableProjection::new("hbc/value-preview")
356                .with_field("display", slot_display(value, limits.max_bytes.min(16_384))),
357        )
358    }
359
360    pub(crate) fn instrumentation_snapshot(&self, limits: ProjectionLimits) -> PortableProjection {
361        let mut projection = PortableProjection::new("hbc/snapshot")
362            .with_field("program/entry", self.program.entry.to_string())
363            .with_field(
364                "program/functions",
365                self.program.functions.len().to_string(),
366            )
367            .with_field(
368                "program/constants",
369                self.program.constants.len().to_string(),
370            )
371            .with_field("function", self.function.to_string())
372            .with_field("ip", self.ip.to_string())
373            .with_field("calls", self.calls.len().to_string())
374            .with_field("stack/depth", self.stack.len().to_string())
375            .with_field("locals/count", self.frame.locals().len().to_string());
376        append_slots(&mut projection.fields, "stack", &self.stack, limits, true);
377        projection
378    }
379
380    fn transition_boundary<P: VmProbe>(
381        &self,
382        probe: &mut P,
383        instruction: InstructionEvent,
384        kind: TransitionKind,
385        from_function: usize,
386        from_ip: usize,
387        outcome: VmBoundaryOutcome,
388    ) -> VmBoundary {
389        let transition = self.boundary_transition_event(kind, from_function, from_ip);
390        probe.on_transition(transition);
391        VmBoundary {
392            instruction: Some(instruction),
393            transition: Some(transition),
394            terminal: None,
395            outcome,
396        }
397    }
398
399    fn resume_transition_boundary<P: VmProbe>(
400        &self,
401        probe: &mut P,
402        kind: TransitionKind,
403        from_function: usize,
404        from_ip: usize,
405        outcome: VmBoundaryOutcome,
406    ) -> VmBoundary {
407        let transition = self.boundary_transition_event(kind, from_function, from_ip);
408        probe.on_transition(transition);
409        VmBoundary {
410            instruction: None,
411            transition: Some(transition),
412            terminal: None,
413            outcome,
414        }
415    }
416
417    fn failed_boundary<P: VmProbe>(
418        &self,
419        probe: &mut P,
420        instruction: Option<InstructionEvent>,
421        error: VmError,
422    ) -> VmBoundary {
423        let terminal = self.boundary_terminal_event(TerminalKind::Fail);
424        probe.on_terminal(terminal);
425        VmBoundary {
426            instruction,
427            transition: None,
428            terminal: Some(terminal),
429            outcome: VmBoundaryOutcome::Failed(error),
430        }
431    }
432
433    #[inline(always)]
434    fn boundary_instruction_event(&self, instruction: &Instruction) -> InstructionEvent {
435        InstructionEvent {
436            function: saturating_u16(self.function),
437            ip: saturating_u32(self.ip),
438            opcode: Opcode::from_instruction(instruction),
439            stack_depth: saturating_u32(self.stack.len()),
440            call_depth: saturating_u16(self.calls.len()),
441        }
442    }
443
444    #[inline(always)]
445    fn boundary_transition_event(
446        &self,
447        kind: TransitionKind,
448        from_function: usize,
449        from_ip: usize,
450    ) -> TransitionEvent {
451        TransitionEvent {
452            kind,
453            from_function: saturating_u16(from_function),
454            from_ip: saturating_u32(from_ip),
455            to_function: saturating_u16(self.function),
456            to_ip: saturating_u32(self.ip),
457            stack_depth: saturating_u32(self.stack.len()),
458            call_depth: saturating_u16(self.calls.len()),
459        }
460    }
461
462    #[inline(always)]
463    fn boundary_terminal_event(&self, kind: TerminalKind) -> TerminalEvent {
464        TerminalEvent {
465            kind,
466            function: saturating_u16(self.function),
467            ip: saturating_u32(self.ip),
468            stack_depth: saturating_u32(self.stack.len()),
469            call_depth: saturating_u16(self.calls.len()),
470        }
471    }
472
473    #[cfg(feature = "tracing-jit")]
474    fn clear_step_jit_state(&mut self) {
475        self.jit_path.clear();
476        self.jit_loop_entries.clear();
477        self.jit_suppressed_range = None;
478    }
479
480    #[cfg(not(feature = "tracing-jit"))]
481    fn clear_step_jit_state(&mut self) {}
482}
483
484fn append_slots(
485    fields: &mut BTreeMap<String, String>,
486    prefix: &str,
487    slots: &[VmSlot],
488    limits: ProjectionLimits,
489    tail: bool,
490) {
491    let retained = slots.len().min(limits.max_items);
492    let start = if tail {
493        slots.len().saturating_sub(retained)
494    } else {
495        0
496    };
497    for (output_index, value) in slots[start..start + retained].iter().enumerate() {
498        let source_index = start + output_index;
499        fields.insert(
500            format!("{prefix}/{source_index}"),
501            slot_display(value, limits.max_bytes.min(16_384)),
502        );
503    }
504    fields.insert(format!("{prefix}/count"), slots.len().to_string());
505    fields.insert(
506        format!("{prefix}/omitted"),
507        slots.len().saturating_sub(retained).to_string(),
508    );
509}
510
511fn slot_display(slot: &VmSlot, limit: usize) -> String {
512    let display = match slot {
513        VmSlot::Number(value) => value.to_string(),
514        VmSlot::Bool(value) => value.to_string(),
515        VmSlot::Nil => "nil".into(),
516        VmSlot::Value(value) => value.display(),
517        VmSlot::InlineClosure {
518            prototype,
519            identity,
520        } => {
521            format!("#<hbc-closure {prototype}@{identity}>")
522        }
523        VmSlot::Closure(closure) => format!("#<hbc-closure {}>", closure.prototype),
524        VmSlot::MultiArity(dispatch) => format!("#<hbc-multi-arity {}>", dispatch.name),
525    };
526    bounded_text(&display, limit)
527}
528
529fn bounded_text(value: &str, limit: usize) -> String {
530    if value.chars().count() <= limit {
531        return value.into();
532    }
533    let mut output = value.chars().take(limit).collect::<String>();
534    output.push('…');
535    output
536}
537
538#[inline(always)]
539fn saturating_u16(value: usize) -> u16 {
540    value.min(u16::MAX as usize) as u16
541}
542
543#[inline(always)]
544fn saturating_u32(value: usize) -> u32 {
545    value.min(u32::MAX as usize) as u32
546}