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
12pub enum VmBoundaryOutcome {
14 Continue,
15 Suspended(Promise),
16 Yielded(Value),
17 Returned(Value),
18 Failed(VmError),
19}
20
21pub 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 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 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}