cove_runtime/vm/debug.rs
1//! Stopping the machine, and looking at it.
2//!
3//! Issue #241's first half: the machine side of a debugger, with no session,
4//! no commands and no policy in it. What is here is a place to stand — the
5//! machine asks a [`Debugger`] before every instruction while one is
6//! installed, and honours [`Resume::Go`] or [`Resume::Halt`] — and a way to
7//! look, which is a set of owned snapshots this module builds and hands out.
8//!
9//! # The machine calls the debugger, and never the other way round
10//!
11//! There is no "suspended machine" here to hold, and there could not be. The
12//! dispatch loop runs inside [`Machine::drive`]'s `std::thread::scope`, and
13//! it holds a `&'s Scope<'s, 'a>` — the borrow a `spawn` starts its children
14//! in — which by construction cannot outlive that call. A handle to a paused
15//! machine would be a value carrying that borrow out of the scope that
16//! created it, which is exactly what the scope exists to refuse.
17//!
18//! So the call is inverted. The machine reaches a stop, builds a [`Stop`]
19//! that borrows it for the length of one call, and asks. Everything a
20//! debugger wants to keep, it copies out of that call — which is why every
21//! view below is owned, and why none of them borrows the machine.
22//!
23//! # A stop costs the loop nothing when nobody is stopping
24//!
25//! The loop's one per-instruction comparison already existed, as
26//! `instructions % SAFEPOINT_STRIDE == 0`. `Machine::next_question` folds
27//! the debugger's question into that same comparison rather than adding a
28//! second: with no debugger installed the next check is the next multiple of
29//! [`SAFEPOINT_STRIDE`](crate::vm::exec::SAFEPOINT_STRIDE) and the loop does
30//! what it always did; with one installed it is the very next instruction.
31//! `docs/VM_ARCHITECTURE.md` measured what a second per-instruction branch
32//! costs — 2.4% on `arith` for a `bool` guarding the counter — and that is
33//! the price this arrangement does not pay.
34//!
35//! It was measured rather than argued, by `scripts/vm-time.sh`: fifteen runs
36//! of one benchmark, medians of `execute=`, three interleaved rounds, every
37//! build from the same working tree so that nothing but the named line
38//! differs. On `arith`, against a base of that tree with this change removed:
39//!
40//! | build | median |
41//! | --- | ---: |
42//! | base | 80.7 ms |
43//! | the fold, with the question written out in the loop | 85.0 ms |
44//! | the fold, with the question in `Machine::ask` | 79.4 ms |
45//! | base plus the two fields, loop untouched (a control) | 79.6 ms |
46//!
47//! Two things came out of it, and only the second was expected. **The
48//! comparison is free**: the same tree with the loop's condition put back to
49//! `instructions % SAFEPOINT_STRIDE == 0` measured 82.9 ms against the fold's
50//! 82.7 ms, indistinguishable. **Where the question is written is not**:
51//! building the [`Stop`] and making the indirect call inside the dispatch
52//! body cost 4.3%, which is more than the branch this whole arrangement was
53//! shaped to avoid, and moving it behind `#[inline(never)]` recovered all of
54//! it. The control is the reason that is reported as a code-layout effect
55//! rather than as work: adding the same two fields and reading neither of
56//! them moved `arith` by 1.1% on its own. The shipped shape measures 1.5%
57//! *below* the base on `arith` and 1.6% below it on `field`, which is to say
58//! inside that band and not outside it.
59//!
60//! The safepoint's own schedule does not move by one instruction either.
61//! [ADR 0040](../../../../docs/adr/0040-a-bound-outlives-its-backend.md)
62//! states every stop mode's bound in multiples of the stride, and
63//! `crates/cove-runtime/tests/responsiveness.rs` measures each of them.
64//! `the_safepoint_fires_at_the_same_counts_as_it_did_before`, below, is that
65//! rule pinned at the instruction rather than left to the bounds.
66//!
67//! # What may be handed out
68//!
69//! `crates/cove-runtime/tests/representation_is_private.rs` is the arbiter,
70//! and it decided the shape of everything below: no slot, no layout id, no
71//! frame base, no word of VM storage leaves in a public signature. A frame
72//! word is named by its *position* ([`Local::at`]), a family by its *name*
73//! ([`Object::name`]), and a value by what it *renders as* — a `String` this
74//! crate produced, not a piece of the representation that produced it.
75//!
76//! One raw word does cross, in one direction only: [`Stop::object`] takes a
77//! word a [`Word`] view already showed and answers what it names, for the VM
78//! development the sketch asks for. It is not a handle in the sense
79//! ADR 0031 forbids — nothing roots it, nothing stores it, it is not valid
80//! after the run, and a word that names no object answers `None` rather than
81//! misbehaving.
82//!
83//! [`Local::words`] widens *which* words a [`Word`] view can show and not
84//! what one is: the words a name covers, in the same projection as the words
85//! no name covers, so that a named reference can be followed the way an
86//! unnamed one already could. The argument above is unchanged by it, which
87//! is the test of whether it belonged here.
88//!
89//! One number that is not a representation crosses too. [`Stop::task`] is
90//! the id `crate::trace` writes on its events, and it is here because
91//! everything else a [`Stop`] answers — the count, the depth, the frames —
92//! belongs to one task and nothing said which. It names a task; it is not a
93//! way to reach one.
94
95use cove_diag::Span;
96use cove_ir::{print, FunctionId, Pc};
97
98use crate::error::RuntimeError;
99use crate::trace::RunOutcome;
100use crate::vm::exec::Machine;
101use crate::vm::render;
102
103/// What a debugger says when the machine asks.
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum Resume {
106 /// Run the instruction the stop is at, and go on.
107 Go,
108 /// End the run here, as a stop and not as a failure of the program.
109 Halt,
110}
111
112/// Something that watches a run, one instruction at a time.
113///
114/// The machine's whole contract with it is [`Debugger::at`]: while a debugger
115/// is installed the machine asks before every instruction, and honours the
116/// answer. **Every policy is the implementor's** — a breakpoint is an `at`
117/// that answers `Go` until the pc is one it is looking for, a step is one
118/// that counts, a `finish` is one that watches the depth — because a policy
119/// in the loop is a policy that cannot be changed without touching the loop.
120///
121/// `Send + Sync` because a spawned task's machine is handed the same
122/// reference and asks it from that task's own thread. A debugger that keeps
123/// state therefore keeps it behind a lock or an atomic, as any two threads
124/// sharing anything must.
125pub trait Debugger: Send + Sync {
126 /// Called before each instruction while this debugger is installed.
127 fn at(&self, stop: &Stop<'_>) -> Resume;
128}
129
130/// One instruction's worth of standing still.
131///
132/// It borrows the machine for the length of the call and nothing longer,
133/// which is the whole of why the call is inverted. Every method answers with
134/// an owned snapshot, so a debugger keeps what it asks for and holds nothing
135/// of the machine.
136///
137/// The frame's `pc` is truthful here: the dispatch loop syncs it before
138/// asking, which it does not do between instructions, and a view built
139/// anywhere else would name the instruction after the one being stopped at.
140pub struct Stop<'m> {
141 machine: &'m Machine<'m>,
142 function: FunctionId,
143 pc: usize,
144}
145
146/// One frame the debugger shows, which is not always one the machine pushed.
147///
148/// `function` and `base` say where the *words* are; `pc` says which of that
149/// function's instructions this frame is at; `named` says whose body those
150/// instructions are, which is `function` itself unless `lower::inline`
151/// expanded one here; `span` is where this frame's own call was written; and
152/// `locals` is the table of names to read at that pc, which for an expanded
153/// body is the one the expansion recorded rather than the function's own.
154struct Frame<'p> {
155 function: FunctionId,
156 base: u64,
157 pc: Pc,
158 named: FunctionId,
159 span: Span,
160 locals: &'p [cove_ir::Local],
161}
162
163impl<'m> Stop<'m> {
164 /// The stop the dispatch loop is at, with `pc` already synced.
165 pub(crate) fn new(machine: &'m Machine<'m>, function: FunctionId, pc: usize) -> Stop<'m> {
166 Stop {
167 machine,
168 function,
169 pc,
170 }
171 }
172
173 /// How many instructions this task has run, this one included.
174 ///
175 /// The count [`crate::Vm::instructions`] reports, read at the moment the
176 /// instruction about to run was counted. It is *this task's*, and
177 /// [`Stop::task`] is what says whose.
178 pub fn instructions(&self) -> u64 {
179 self.machine.instructions()
180 }
181
182 /// Words this task's heap has handed out, reuse counted each time.
183 ///
184 /// Read at the moment the instruction about to run was counted, so the
185 /// difference between two consecutive stops is what the instruction
186 /// between them allocated. That is how [`crate::vm::profile::Profiler`]
187 /// attributes a heap to the code that asked for it: there is no seam
188 /// inside `Memory::alloc` that knows which instruction it is serving, and
189 /// a difference needs none.
190 pub fn allocated_words(&self) -> u64 {
191 self.machine.allocated_words()
192 }
193
194 /// Objects this task's heap has handed out, reuse counted each time.
195 ///
196 /// Beside [`Stop::allocated_words`] because allocating often and
197 /// allocating large are different faults with different repairs.
198 pub fn allocations(&self) -> u64 {
199 self.machine.allocations()
200 }
201
202 /// Which task this stop is in.
203 ///
204 /// Everything else a `Stop` answers is one task's — the count, the
205 /// depth, the frames — because a spawned task runs on a machine of its
206 /// own, and until this was here a debugger could not tell two of them
207 /// apart. A policy stated in frame depth is then a policy that a second
208 /// task can satisfy by accident: a `step` asked in the entry, finished
209 /// by an instruction of a task the entry spawned.
210 ///
211 /// The number is [`crate::ENTRY_TASK`] for the entry and a spawned
212 /// task's own id otherwise, which is to say it is the number
213 /// `crate::trace`'s events carry under `task`. That is deliberate and it
214 /// is the whole of the choice here: a debugger and a trace of the same
215 /// run must name the same task the same way, or a person holding both
216 /// has to work out the correspondence themselves.
217 ///
218 /// It is opaque. Two stops with the same id are the same task and two
219 /// with different ids are not; nothing else about the number is
220 /// promised, and no ordering of it means anything.
221 pub fn task(&self) -> u64 {
222 self.machine.task()
223 }
224
225 /// `module.name` of the function this stop is in.
226 ///
227 /// The function the *source* would say it is in, which is not always the
228 /// one whose frame the machine pushed: an instruction inside a body
229 /// `lower::inline` expanded belongs to the body that was written, and a
230 /// session that named the caller would tell a person their breakpoint had
231 /// stopped somewhere they had not asked about. `Function::inlined` is what
232 /// says otherwise, and [`Stop::function_id`] is deliberately the other
233 /// answer.
234 ///
235 /// A `String`, built here, because that is what a session prints and a
236 /// name is what a person reads. A caller that will look the function up
237 /// again — a profiler counting instructions, above all — wants
238 /// [`Stop::function_id`] instead: this allocates, and a debugger that
239 /// stops at every instruction would allocate at every instruction.
240 pub fn function(&self) -> String {
241 let program = self.machine.program();
242 let named = program
243 .function(self.function)
244 .inlined_at(self.pc as Pc)
245 .last()
246 .map_or(self.function, |held| held.callee);
247 program.function(named).qualified()
248 }
249
250 /// Which function this stop is in, as the program names it.
251 ///
252 /// The identity rather than the name: two stops in one function answer
253 /// the same id, and an id indexes `Program::functions` — so a caller can
254 /// hold one per stop without holding a string per stop.
255 ///
256 /// It is the *machine's* answer and not the source's: an instruction of an
257 /// expanded body reports the function whose frame and whose code hold it,
258 /// where [`Stop::function`] reports the body that was written. A profiler
259 /// keys a count by this and prints `function+pc`, and a pc is a counter of
260 /// the function this names; naming the callee there would make the pair
261 /// disagree.
262 pub fn function_id(&self) -> FunctionId {
263 self.function
264 }
265
266 /// Which instruction of that function is about to run.
267 ///
268 /// A counter of the function whose *code* holds it, which for an
269 /// instruction inside an expanded body is the caller's rather than the
270 /// body's: `lower::inline` wrote the body there and there is no other
271 /// numbering. So a stop reported as `m.inner` at pc 1 is not `m.inner`'s
272 /// second instruction, it is `m.inner`'s first, standing at `m.outer`'s
273 /// counter 1. [`Stop::code`] numbers the same way, which is what keeps a
274 /// listing and the pc beside it agreeing.
275 pub fn pc(&self) -> u32 {
276 self.pc as u32
277 }
278
279 /// Where that instruction was written.
280 pub fn span(&self) -> Span {
281 self.machine
282 .program()
283 .function(self.function)
284 .span_at(self.pc)
285 }
286
287 /// How many calls are live, this one included.
288 ///
289 /// An expanded body counts. `lower::inline` writes a small leaf's
290 /// instructions into its caller's code and pushes no frame for them, and
291 /// a debugger that counted only the frames the machine pushed would say a
292 /// stop inside such a body was a stop in the caller — `finish` would run
293 /// past the body it was asked to finish, `next` would step over nothing,
294 /// and a backtrace would be one name short. `Function::inlined` is what
295 /// says otherwise, and this is the one place the count comes from.
296 ///
297 /// Counted rather than built, because `State::wanted` asks this at every
298 /// stop of a stepping session and the frames themselves are only wanted
299 /// when something is shown.
300 pub fn depth(&self) -> usize {
301 let program = self.machine.program();
302 self.machine
303 .calls()
304 .iter()
305 .enumerate()
306 .map(|(at, (id, _, pc))| {
307 let function = program.function(*id);
308 1 + function.inlined_at(Self::shown(at, *pc)).count()
309 })
310 .sum()
311 }
312
313 /// Every frame the debugger shows, innermost first — which is not every
314 /// frame the machine pushed.
315 ///
316 /// One real frame becomes one shown frame per expanded body its pc is
317 /// inside, plus itself. Each of them reads the *same* words of the *same*
318 /// frame, because that is where an expansion put them; what differs is
319 /// whose code the pc belongs to, which name to print, and where the call
320 /// below it was written.
321 ///
322 /// The pcs walk outwards the way the ranges nest. The innermost shown
323 /// frame is at the pc the machine is at; each one further out is at the
324 /// first counter of the body below it, which is where that body's call
325 /// stood before it was expanded — the exact analogue of a suspended real
326 /// frame being shown at its call rather than at its resume address.
327 fn shown_frames(&self) -> Vec<Frame<'m>> {
328 let program = self.machine.program();
329 let mut held = Vec::new();
330 for (at, (id, base, pc)) in self.machine.calls().iter().enumerate() {
331 let function = program.function(*id);
332 let pc = Self::shown(at, *pc);
333 let ranges: Vec<_> = function.inlined_at(pc).collect();
334 // Innermost first: the deepest range's callee is stopped at `pc`,
335 // and every range further out is stopped where the range inside
336 // it begins.
337 let mut here = pc;
338 let mut span = function.span_at(here as usize);
339 for range in ranges.iter().rev() {
340 held.push(Frame {
341 function: *id,
342 base: *base,
343 pc: here,
344 named: range.callee,
345 span,
346 locals: &range.locals,
347 });
348 here = range.from;
349 span = range.site;
350 }
351 held.push(Frame {
352 function: *id,
353 base: *base,
354 pc: here,
355 named: *id,
356 span,
357 locals: &function.locals,
358 });
359 }
360 held
361 }
362
363 /// Every live call, innermost first.
364 ///
365 /// This renders every local of every frame — and every word of every
366 /// local — so a debugger that stops at every instruction and takes a
367 /// whole backtrace at each one is doing real work per instruction.
368 /// [`Stop::frame`] is the same view of one call, for a session that only
369 /// shows what it was asked for.
370 pub fn backtrace(&self) -> Vec<Call> {
371 // Built once and walked, rather than `frame(at)` per level: an
372 // expanded body is not a frame the machine holds, so a shown frame
373 // has to be worked out rather than indexed, and asking for the `at`th
374 // of them works out the first `at` on the way.
375 self.shown_frames().iter().map(|f| self.call(f)).collect()
376 }
377
378 /// The call `at` levels out from this one, or `None` past the outermost.
379 ///
380 /// A frame that is not the innermost is suspended at *the instruction
381 /// after* the call that led one level deeper, and that resume address is
382 /// not where the frame *is*. The instruction it names is whatever runs
383 /// next — frequently the next statement's, or the `return` the body ends
384 /// with — so a line built from it points away from the call as often as
385 /// at it, and a name the call site had in scope may already have gone out
386 /// of it. `- 1` is always the call itself, because a `pc` is only ever
387 /// synced after the instruction it dispatched.
388 ///
389 /// This is the rule `Machine::calls`'s own documentation states for the
390 /// error chain, which had made the same choice for the same reason and
391 /// left this view alone. Issue #302 is what settled that the two should
392 /// agree: with the copy after a call gone, the resume address in a
393 /// one-expression body *is* the `return`, and a suspended frame answered
394 /// that its caller's locals were out of scope.
395 ///
396 /// The innermost frame keeps its own `pc`. It is not suspended at a
397 /// resume address — the machine is about to execute the instruction it
398 /// names — so there is nothing to look back past.
399 pub fn frame(&self, at: usize) -> Option<Call> {
400 Some(self.call(&self.shown_frames().into_iter().nth(at)?))
401 }
402
403 /// The pc a frame is *shown* at, out of the pc the machine holds for it.
404 ///
405 /// They are the same for the innermost frame and differ by one for every
406 /// other, for the reason [`Stop::frame`] gives. It is one function so
407 /// that the backtrace and the disassembly cannot answer differently.
408 fn shown(at: usize, pc: Pc) -> Pc {
409 match at {
410 0 => pc,
411 _ => pc.saturating_sub(1),
412 }
413 }
414
415 /// What the word `at` names, if it names an object of this run's heap.
416 ///
417 /// The one place a raw word crosses, and it crosses inward: what comes
418 /// back is a rendered snapshot. It is for the view VM development wants —
419 /// a [`Word`] showed an address, and this says what is there — and it
420 /// promises nothing about what a word means. A word that is not an
421 /// object this memory holds answers `None`.
422 pub fn object(&self, at: u64) -> Option<Object> {
423 let (name, fields) = render::parts(self.machine, at)?;
424 Some(Object {
425 name,
426 fields: fields
427 .into_iter()
428 .map(|(name, value)| Field { name, value })
429 .collect(),
430 })
431 }
432
433 /// The instructions around frame `at`'s pc, `reach` either side of it,
434 /// or nothing past the outermost frame.
435 ///
436 /// The disassembly a session shows beside a stop.
437 /// [`cove_ir::print::one`] renders each, which is the same rendering
438 /// `cove ir` prints, so a debugger and a dump do not disagree about what
439 /// an instruction is called.
440 ///
441 /// `at` names a frame the way [`Stop::frame`] names one, and for the
442 /// same reason: a session that lets a person select a frame has to be
443 /// able to show that frame's code, and one that could only ever
444 /// disassemble the innermost would answer `frame 2` with frame 0's
445 /// instructions. It is a parameter here rather than a method on [`Call`]
446 /// because a `Call` is an owned snapshot: giving it a disassembly would
447 /// mean rendering every instruction of every live function at every
448 /// stop, and a backtrace is already the expensive view.
449 ///
450 /// The pc it reads is the one [`Stop::frame`] reports, which for a
451 /// suspended frame is the call it is waiting on rather than the resume
452 /// address after it. The two panes of a session are one view: a
453 /// backtrace naming a line and a disassembly marking a different
454 /// instruction would be the debugger disagreeing with itself.
455 pub fn code(&self, at: usize, reach: usize) -> Vec<Line> {
456 let Some(frame) = self.shown_frames().into_iter().nth(at) else {
457 return Vec::new();
458 };
459 let frame_pc = frame.pc as usize;
460 let program = self.machine.program();
461 let function = program.function(frame.function);
462 // Both ends saturate. Only `from` did at first, which reads as a
463 // decision and was an oversight: on a 32-bit target — which
464 // `wasm32-unknown-unknown` is — a `reach` of `u32::MAX` made
465 // `pc + reach + 1` wrap to zero, and the answer became the empty
466 // range `0..pc` rather than the whole function. `cargo test` on a
467 // 64-bit host cannot see it. A caller asking for "all of it" by
468 // naming a very large reach is the obvious way to ask, so this
469 // answers it here rather than leaving each caller to know the width
470 // of a `usize` on the target it will run on.
471 let from = frame_pc.saturating_sub(reach);
472 let to = frame_pc
473 .saturating_add(reach)
474 .saturating_add(1)
475 .min(function.code.len());
476 (from..to)
477 .map(|pc| Line {
478 pc: pc as u32,
479 text: print::one(program, function, &function.code[pc]),
480 span: function.span_at(pc),
481 current: pc == frame_pc,
482 })
483 .collect()
484 }
485
486 /// One frame, projected.
487 fn call(&self, frame: &Frame) -> Call {
488 let (base, pc) = (frame.base, frame.pc);
489 let program = self.machine.program();
490 let function = program.function(frame.function);
491 let mut named = vec![false; function.frame_size() as usize];
492 let mut locals = Vec::new();
493 for local in frame.locals {
494 if !(local.from <= pc && pc < local.to) {
495 continue;
496 }
497 let width = program.layout(local.layout).width();
498 let words = self.machine.frame_run(base, local.slot, width);
499 for word in local.slot..(local.slot + width).min(function.frame_size()) {
500 named[word as usize] = true;
501 }
502 locals.push(Local {
503 name: local.name.to_string(),
504 value: render::lossy(self.machine, local.layout, &words),
505 at: local.slot,
506 width,
507 words: words
508 .iter()
509 .enumerate()
510 .map(|(offset, raw)| self.word(function, local.slot + offset as u32, *raw))
511 .collect(),
512 });
513 }
514 // What no name covers, read as the frame itself describes it. This is
515 // the VM development view: a compiler temporary, a slot whose live
516 // range has ended, a word the lowering wrote and no source name ever
517 // held.
518 let words = named
519 .iter()
520 .enumerate()
521 .filter(|(_, named)| !**named)
522 .map(|(at, _)| {
523 let at = at as u32;
524 let raw = self.machine.frame_run(base, at, 1)[0];
525 self.word(function, at, raw)
526 })
527 .collect();
528 Call {
529 function: program.function(frame.named).qualified(),
530 within: function.qualified(),
531 pc,
532 span: frame.span,
533 locals,
534 words,
535 }
536 }
537
538 /// One word of a frame, read as the frame itself says it should be.
539 ///
540 /// The same projection whether a name covers the word or nothing does,
541 /// which is what lets [`Local::words`] and [`Call::words`] be one type:
542 /// a position, what the frame says is in it, the word, and that word
543 /// rendered. A frame that says nothing about a word — one past the
544 /// declared frame, which the lowering does not produce — is read as an
545 /// `Int`, because a raw word shown as a number is the least a reader can
546 /// be told and it is still true.
547 fn word(&self, function: &cove_ir::Function, at: u32, raw: u64) -> Word {
548 let repr = function.repr(at).unwrap_or(cove_ir::Repr::Int);
549 Word {
550 at,
551 holds: repr.name(),
552 raw,
553 value: render::raw(self.machine, repr, raw),
554 }
555 }
556}
557
558/// One live call, as a debugger sees it.
559#[derive(Clone, Debug)]
560pub struct Call {
561 function: String,
562 within: String,
563 pc: Pc,
564 span: Span,
565 locals: Vec<Local>,
566 words: Vec<Word>,
567}
568
569impl Call {
570 /// `module.name` of the function running here.
571 ///
572 /// The body that was *written*, which for a frame `lower::inline`
573 /// expanded is the leaf and not the function that holds its
574 /// instructions. [`Call::within`] is that other answer.
575 pub fn function(&self) -> &str {
576 &self.function
577 }
578
579 /// `module.name` of the function whose code [`Call::pc`] is a counter of.
580 ///
581 /// The same as [`Call::function`] for a frame the machine pushed, and the
582 /// *caller* for one that is an expanded body: the expansion wrote the
583 /// leaf's instructions into the caller and there is no other numbering
584 /// for them.
585 ///
586 /// A reader showing a disassembly needs both, and showing one under the
587 /// other's name is the mistake this exists to stop — a pane titled
588 /// `playground.twice` holding `playground.main`'s four instructions,
589 /// which is what a table keyed on [`Call::function`] alone produced.
590 pub fn within(&self) -> &str {
591 &self.within
592 }
593
594 /// Where in it this call is: the instruction about to run for the
595 /// innermost call, and the one to return to for every other.
596 ///
597 /// A counter of [`Call::within`], not of [`Call::function`].
598 pub fn pc(&self) -> Pc {
599 self.pc
600 }
601
602 /// Where that instruction was written.
603 pub fn span(&self) -> Span {
604 self.span
605 }
606
607 /// The names the source bound that are in scope at this pc, in
608 /// declaration order.
609 ///
610 /// One name may appear twice. Shadowing is *recorded* rather than
611 /// resolved — see [`cove_ir::Local`] — so `let x = 1; let x = x + 41` is
612 /// two live bindings of two words, and both are here. [`Call::local`] is
613 /// what chooses between them.
614 pub fn locals(&self) -> &[Local] {
615 &self.locals
616 }
617
618 /// The frame's own words that no name in scope covers.
619 ///
620 /// For a frame that is an expanded body, that includes every word the
621 /// *caller* holds. They are in the same physical frame — an expansion
622 /// appends the callee's run to the caller's rather than pushing one — and
623 /// they are not names this body bound, which is exactly what this reports:
624 /// a word no name in scope covers, whoever else may have a name for it.
625 pub fn words(&self) -> &[Word] {
626 &self.words
627 }
628
629 /// The local called `name` that the source means at this pc, if one is
630 /// in scope here.
631 ///
632 /// **The last match wins**, which is
633 /// [`cove_ir::Function::local_at`]'s rule and is the rule because the
634 /// lowering resolves a name by searching its scope backwards. Two
635 /// bindings of one name are live at once and the later one is what the
636 /// source at this pc means; taking the first match would answer with the
637 /// *shadowed* binding, which is a debugger that is wrong about the value
638 /// of a name exactly where a person is most likely to ask.
639 ///
640 /// The earlier binding is not hidden — it is still in [`Call::locals`],
641 /// where a reader can see both — because it is still in the frame, and a
642 /// view of the machine that quietly dropped a word would be a worse
643 /// lie than a view that shows two.
644 pub fn local(&self, name: &str) -> Option<&Local> {
645 self.locals.iter().rev().find(|local| local.name == name)
646 }
647}
648
649/// One name the source bound, and what it holds.
650#[derive(Clone, Debug)]
651pub struct Local {
652 name: String,
653 value: String,
654 at: u32,
655 width: u32,
656 words: Vec<Word>,
657}
658
659impl Local {
660 /// What the source called it.
661 pub fn name(&self) -> &str {
662 &self.name
663 }
664
665 /// What it holds, rendered.
666 ///
667 /// Always an answer, which is the whole difference from a boundary
668 /// crossing. A value that could not be
669 /// read carries a marker saying which way it could not — `<reclaimed>`,
670 /// `<cycle>`, `<case 7 of 3>` — rather than being absent.
671 pub fn value(&self) -> &str {
672 &self.value
673 }
674
675 /// Which word of the frame it begins at.
676 ///
677 /// A position, not a name of anything this crate owns: it is what lets a
678 /// session say *the same word as that one* without being handed the type
679 /// the machine indexes frames by.
680 pub fn at(&self) -> u32 {
681 self.at
682 }
683
684 /// How many words it occupies. A value is a run of words, so a name
685 /// covers `width` of them from [`Local::at`], and [`Local::words`] is
686 /// that run.
687 pub fn width(&self) -> u32 {
688 self.width
689 }
690
691 /// The words the name covers, read as the frame says they should be.
692 ///
693 /// [`Local::value`] is what the name holds *rendered*, and that is where
694 /// a reader stops. This is where a debugger does not: a name bound to a
695 /// vector renders as its elements and holds a reference, and until this
696 /// was here there was no way to get from the name to the reference —
697 /// [`Stop::object`] follows a word, and [`Call::words`] by construction
698 /// excludes every word a name covers. So `print xs` could show a vector
699 /// and nothing could then look at the object, which is a hole in a
700 /// debugger rather than a missing convenience.
701 ///
702 /// It is the same [`Word`] view [`Call::words`] hands out, and
703 /// deliberately: a word of a frame is a word of a frame whether a name
704 /// covers it or not, and a second shape for the same thing would be a
705 /// second thing to keep true. `words().len()` is [`Local::width`], and
706 /// the first of them is at [`Local::at`].
707 pub fn words(&self) -> &[Word] {
708 &self.words
709 }
710}
711
712/// One word of a frame.
713///
714/// [`Call::words`] hands out the ones no name in scope covers, which is what
715/// the view exists for; [`Local::words`] hands out the ones a name does. The
716/// projection is the same either way — a position, what the frame says is in
717/// it, the word, and that word rendered — because whether a name happens to
718/// cover a word does not change what the word is.
719#[derive(Clone, Debug)]
720pub struct Word {
721 at: u32,
722 holds: &'static str,
723 raw: u64,
724 value: String,
725}
726
727impl Word {
728 /// Which word of the frame it is.
729 pub fn at(&self) -> u32 {
730 self.at
731 }
732
733 /// What the frame says is in it — `Int`, `Ref`, `Duration`.
734 pub fn holds(&self) -> &'static str {
735 self.holds
736 }
737
738 /// The word itself.
739 pub fn raw(&self) -> u64 {
740 self.raw
741 }
742
743 /// The word read as what the frame says it is. A reference renders as
744 /// the address it is and is not followed; [`Stop::object`] is what
745 /// follows one.
746 pub fn value(&self) -> &str {
747 &self.value
748 }
749}
750
751/// One object of the run's heap, rendered.
752#[derive(Clone, Debug)]
753pub struct Object {
754 name: String,
755 fields: Vec<Field>,
756}
757
758impl Object {
759 /// What the family it belongs to is called.
760 pub fn name(&self) -> &str {
761 &self.name
762 }
763
764 /// Its parts, named the way its family names them: a struct's fields by
765 /// their source names, a run of elements by its indices, an enum by
766 /// `case` and then its payload's positions.
767 pub fn fields(&self) -> &[Field] {
768 &self.fields
769 }
770}
771
772/// One named part of an object.
773#[derive(Clone, Debug)]
774pub struct Field {
775 name: String,
776 value: String,
777}
778
779impl Field {
780 /// What the part is called.
781 pub fn name(&self) -> &str {
782 &self.name
783 }
784
785 /// What it holds, rendered.
786 pub fn value(&self) -> &str {
787 &self.value
788 }
789}
790
791/// One line of disassembly.
792#[derive(Clone, Debug)]
793pub struct Line {
794 pc: Pc,
795 text: String,
796 span: Span,
797 current: bool,
798}
799
800impl Line {
801 /// Which instruction of the function it is.
802 pub fn pc(&self) -> Pc {
803 self.pc
804 }
805
806 /// The instruction, as `cove ir` prints it.
807 pub fn text(&self) -> &str {
808 &self.text
809 }
810
811 /// Where it was written.
812 pub fn span(&self) -> Span {
813 self.span
814 }
815
816 /// Whether it is the one the stop is at.
817 pub fn current(&self) -> bool {
818 self.current
819 }
820}
821
822/// The error a run ends with when a debugger answers [`Resume::Halt`].
823///
824/// A stop and not a failure of the program's work, which is why it is
825/// classified as [`RunOutcome::Debugger`] and not as an invariant a program
826/// broke. `every_stop_mode_is_reported_as_itself_on_both_backends` in
827/// `crates/cove-runtime/tests/responsiveness.rs` is the shape being kept:
828/// each way a run can stop reports itself and not something else.
829pub(crate) fn halted(span: Span) -> RuntimeError {
830 RuntimeError::new("a debugger stopped this run")
831 .at(span)
832 .with_rule("A debugger may halt a run at any instruction, and the run ends there.")
833 .with_outcome(RunOutcome::Debugger)
834}
835
836#[cfg(test)]
837pub(crate) mod tests {
838 use std::collections::BTreeMap;
839 use std::path::PathBuf;
840 use std::sync::atomic::{AtomicU64, Ordering};
841 use std::sync::{Arc, Mutex};
842
843 use cove_diag::SourceMap;
844 use cove_sema::config::Config;
845 use cove_sema::package::{Module, Package, Unit};
846 use cove_sema::resolve::Program as Checked;
847
848 use super::*;
849 use crate::budget::{Budget, Limits};
850 use crate::host::{Grants, HostRegistry};
851 use crate::runtime::Runtime;
852 use crate::vm::exec::SAFEPOINT_STRIDE;
853 use crate::vm::Vm;
854
855 /// A program that runs for longer than two safepoint strides.
856 const LOOP: &str = "
857export fn main() -> Int {
858 var total = 0
859 var i = 0
860 while i < 2000 {
861 total = total + i
862 i = i + 1
863 }
864 total
865}
866";
867
868 /// Three functions, a name bound in each, and — since `lower::inline`
869 /// expands a small leaf and then repeats — one frame at run time, with
870 /// `inner` written into `outer` and `outer` into `main`. That is what
871 /// these cases want: what a debugger says about a stop that has no frame
872 /// of its own is exactly the question.
873 const NESTED: &str = "
874fn inner(a: Int) -> Int {
875 let doubled = a * 2
876 doubled
877}
878
879fn outer(b: Int) -> Int {
880 let raised = b + 1
881 inner(raised)
882}
883
884export fn main() -> Int {
885 outer(20)
886}
887";
888
889 /// One name bound twice in one frame, so that two bindings of it are
890 /// live at the same time.
891 const SHADOWED: &str = "
892export fn main() -> Int {
893 let total = 0
894 let total = total + 20
895 total
896}
897";
898
899 /// A name bound to something on the heap, for following a reference the
900 /// frame holds by the name that holds it.
901 const ARRAY: &str = "
902export fn main() -> Int {
903 let items = [10, 20, 30]
904 items.length()
905}
906";
907
908 /// Two tasks, so that two machines ask the same debugger.
909 const SPAWNED: &str = "
910export fn main() -> Int {
911 var total = 0
912 scope tasks {
913 let first = tasks.spawn { 42 }
914 total = await first
915 }
916 total
917}
918";
919
920 /// Everything one run needs, held together so that the borrows it takes
921 /// of each other outlive the [`Vm`] built on them.
922 pub(crate) struct World {
923 hosts: Arc<HostRegistry>,
924 runtime: Runtime,
925 program: cove_ir::Program,
926 }
927
928 impl World {
929 pub(crate) fn new(source: &str) -> World {
930 let (sources, checked) = checked(source);
931 let program = lowered(&sources, &checked);
932 let hosts = Arc::new(HostRegistry::new(Grants::new(Vec::<&str>::new())));
933 let runtime = Runtime::new(checked, sources, hosts.clone());
934 World {
935 hosts,
936 runtime,
937 program,
938 }
939 }
940
941 /// A run nothing is watching.
942 pub(crate) fn plain(&self) -> Vm<'_> {
943 Vm::new(&self.runtime, &self.hosts, &self.program)
944 }
945
946 /// A run `debugger` is watching.
947 pub(crate) fn watched<'w>(&'w self, debugger: &'w dyn Debugger) -> Vm<'w> {
948 Vm::debugged(&self.runtime, &self.hosts, &self.program, debugger)
949 }
950 }
951
952 /// Parses, resolves and checks one module called `m`.
953 fn checked(source: &str) -> (Arc<SourceMap>, Arc<Checked>) {
954 let mut sources = SourceMap::new();
955 let file = sources.add("m/main.cove", source.to_string());
956 let ast = cove_syntax::parse_file(&sources, file).expect("the fixture parses");
957 let mut modules = BTreeMap::from([(
958 "m".to_string(),
959 Module {
960 name: "m".to_string(),
961 dir: PathBuf::from("m"),
962 units: vec![Unit {
963 file,
964 path: PathBuf::from("m/main.cove"),
965 ast,
966 }],
967 },
968 )]);
969 for (name, module) in cove_sema::stdlib::attach(&mut sources).expect("stdlib parses") {
970 modules.insert(name, module);
971 }
972 let package = Package {
973 root: PathBuf::from("."),
974 config: Config::default(),
975 modules,
976 };
977 let program = cove_sema::Compiler::new()
978 .compile(&package)
979 .expect("the fixture checks");
980 (Arc::new(sources), Arc::new(program))
981 }
982
983 fn lowered(sources: &SourceMap, checked: &Checked) -> cove_ir::Program {
984 cove_ir::lower(checked, sources, &cove_schema::HostSchemas::new())
985 .expect("the fixture lowers")
986 }
987
988 /// A debugger that writes down what it was shown and always says go.
989 #[derive(Default)]
990 struct Seen(Mutex<Vec<u64>>);
991
992 impl Debugger for Seen {
993 fn at(&self, stop: &Stop<'_>) -> Resume {
994 self.0.lock().expect("a lock").push(stop.instructions());
995 Resume::Go
996 }
997 }
998
999 /// A debugger that halts once the run has got somewhere.
1000 struct HaltAt(u64);
1001
1002 impl Debugger for HaltAt {
1003 fn at(&self, stop: &Stop<'_>) -> Resume {
1004 match stop.instructions() >= self.0 {
1005 true => Resume::Halt,
1006 false => Resume::Go,
1007 }
1008 }
1009 }
1010
1011 /// A debugger that takes one backtrace, the first time it is deep enough.
1012 #[derive(Default)]
1013 struct Deepest {
1014 depth: AtomicU64,
1015 taken: Mutex<Option<Vec<Call>>>,
1016 }
1017
1018 impl Debugger for Deepest {
1019 fn at(&self, stop: &Stop<'_>) -> Resume {
1020 let depth = stop.depth() as u64;
1021 if depth > self.depth.load(Ordering::SeqCst) {
1022 self.depth.store(depth, Ordering::SeqCst);
1023 *self.taken.lock().expect("a lock") = Some(stop.backtrace());
1024 }
1025 Resume::Go
1026 }
1027 }
1028
1029 /// **A debugger is asked before every instruction, in order, and the
1030 /// count it is shown is the run's own.**
1031 ///
1032 /// The whole of what the machine promises: not "often enough", not "at
1033 /// every call" — every instruction, once, counted the way
1034 /// [`Vm::instructions`] counts.
1035 #[test]
1036 fn a_debugger_is_asked_once_before_every_instruction_in_order() {
1037 let world = World::new(NESTED);
1038 let seen = Seen::default();
1039 let mut vm = world.watched(&seen);
1040 let answer = vm.invoke("m", "main", Vec::new()).expect("the run answers");
1041
1042 assert_eq!(format!("{answer}"), "42");
1043 let counts = seen.0.lock().expect("a lock").clone();
1044 assert_eq!(
1045 counts.len() as u64,
1046 vm.instructions(),
1047 "one question per instruction the run dispatched"
1048 );
1049 let expected: Vec<u64> = (1..=vm.instructions()).collect();
1050 assert_eq!(counts, expected, "in order, and each one once");
1051 }
1052
1053 /// **A debugger that halts ends the run there, and the run reports
1054 /// itself as the debugger's stop.**
1055 ///
1056 /// Its own outcome, the way every other stop mode has one: a run
1057 /// somebody was stepping through did not break an invariant and did not
1058 /// run out of anything.
1059 #[test]
1060 fn halting_ends_the_run_and_is_reported_as_the_debugger_s_own_stop() {
1061 let world = World::new(LOOP);
1062 let halt = HaltAt(500);
1063 let mut vm = world.watched(&halt);
1064 let error = vm
1065 .invoke("m", "main", Vec::new())
1066 .expect_err("a halted run does not answer");
1067
1068 assert_eq!(error.outcome, crate::trace::RunOutcome::Debugger);
1069 assert_eq!(
1070 vm.instructions(),
1071 500,
1072 "the instruction it halted at was counted and not run"
1073 );
1074 }
1075
1076 /// **A backtrace names the functions it is standing in, innermost
1077 /// first, and a local of an outer frame is found by the name the source
1078 /// gave it.**
1079 ///
1080 /// This is the projection the whole of part 3 exists for, and none of
1081 /// what it answers is a piece of the machine: a qualified name, a pc, and
1082 /// a rendered value.
1083 #[test]
1084 fn a_backtrace_names_its_calls_and_finds_a_local_of_an_outer_frame() {
1085 let world = World::new(NESTED);
1086 let deepest = Deepest::default();
1087 let mut vm = world.watched(&deepest);
1088 vm.invoke("m", "main", Vec::new()).expect("the run answers");
1089
1090 let frames = deepest
1091 .taken
1092 .lock()
1093 .expect("a lock")
1094 .clone()
1095 .expect("the run stopped somewhere");
1096 let names: Vec<&str> = frames.iter().map(Call::function).collect();
1097 assert_eq!(names, vec!["m.inner", "m.outer", "m.main"]);
1098
1099 let outer = &frames[1];
1100 let b = outer.local("b").expect("`outer`'s parameter is in scope");
1101 assert_eq!(b.value(), "20");
1102 assert_eq!(b.width(), 1, "an `Int` is one word");
1103 let raised = outer.local("raised").expect("its `let` is in scope too");
1104 assert_eq!(raised.value(), "21");
1105 assert_ne!(
1106 b.at(),
1107 raised.at(),
1108 "two names in scope at once are two positions"
1109 );
1110 }
1111
1112 /// **A stop can say where it is, and read the instruction it is at.**
1113 ///
1114 /// The other half of what a session shows beside a frame: the pc, and
1115 /// the disassembly around it, rendered the way `cove ir` renders it.
1116 #[test]
1117 fn a_stop_reads_the_instruction_it_is_about_to_run() {
1118 /// Keeps the first stop inside `m.inner`.
1119 #[derive(Default)]
1120 struct First(Mutex<Option<(String, u32, Vec<Line>)>>);
1121
1122 impl Debugger for First {
1123 fn at(&self, stop: &Stop<'_>) -> Resume {
1124 let mut held = self.0.lock().expect("a lock");
1125 if held.is_none() && stop.function() == "m.inner" {
1126 *held = Some((stop.function(), stop.pc(), stop.code(0, 2)));
1127 }
1128 Resume::Go
1129 }
1130 }
1131
1132 let world = World::new(NESTED);
1133 let first = First::default();
1134 let mut vm = world.watched(&first);
1135 vm.invoke("m", "main", Vec::new()).expect("the run answers");
1136
1137 let held = first.0.lock().expect("a lock").clone();
1138 let (function, pc, code) = held.expect("the run entered `m.inner`");
1139 assert_eq!(function, "m.inner");
1140 // Not the callee's zero, because `m.inner` is a small leaf and
1141 // `lower::inline` wrote its body into `m.outer` — and then wrote
1142 // `m.outer`, which had become a leaf itself, into `m.main`. There is
1143 // no frame of its own for a counter to be zero of, and the counter
1144 // this reports is one of `m.main`'s. [`Stop::function`] names the
1145 // innermost body that was written and [`Stop::pc`] numbers the code
1146 // that holds it, which are two answers on purpose — and what this
1147 // case is about is that they agree with the listing, which is the
1148 // next assertion.
1149 assert_eq!(pc, 2, "`m.inner`'s body begins at `m.main`'s counter 2");
1150 let current: Vec<&Line> = code.iter().filter(|line| line.current()).collect();
1151 assert_eq!(current.len(), 1, "exactly one line is the one stopped at");
1152 assert_eq!(current[0].pc(), pc);
1153 assert!(
1154 !current[0].text().is_empty(),
1155 "an instruction renders as something"
1156 );
1157 }
1158
1159 /// **A debugger changes what a run does not at all.**
1160 ///
1161 /// Same answer, same instruction count, watched or not. A machine whose
1162 /// work depended on whether anybody was looking would be a debugger that
1163 /// could not be trusted about the run it was shown.
1164 #[test]
1165 fn watching_a_run_changes_neither_its_answer_nor_its_work() {
1166 let world = World::new(LOOP);
1167 let mut plain = world.plain();
1168 let alone = plain.invoke("m", "main", Vec::new()).expect("it answers");
1169 let instructions = plain.instructions();
1170
1171 let seen = Seen::default();
1172 let mut watched = world.watched(&seen);
1173 let watched_answer = watched.invoke("m", "main", Vec::new()).expect("it answers");
1174
1175 assert_eq!(format!("{alone}"), format!("{watched_answer}"));
1176 assert_eq!(instructions, watched.instructions());
1177 }
1178
1179 /// **The safepoint fires at exactly the instruction counts it fired at
1180 /// before, whether a debugger is installed or not.**
1181 ///
1182 /// `SAFEPOINT_STRIDE` is contract arithmetic:
1183 /// `docs/adr/0040-a-bound-outlives-its-backend.md` states every stop
1184 /// mode's bound in multiples of it and `tests/responsiveness.rs`
1185 /// measures each one, so folding the debugger's question into the
1186 /// loop's one comparison may not move the schedule by a single
1187 /// instruction.
1188 ///
1189 /// The instrument is the fuel limit, because a safepoint is the only
1190 /// place fuel is charged: a run whose budget cannot survive its first
1191 /// charge stops at exactly the instruction the first safepoint is at,
1192 /// and one that can survive that but not the second stops at the second.
1193 #[test]
1194 fn the_safepoint_fires_at_the_same_counts_as_it_did_before() {
1195 let world = World::new(LOOP);
1196 for (fuel, expected) in [
1197 (1, SAFEPOINT_STRIDE),
1198 (SAFEPOINT_STRIDE + 1, 2 * SAFEPOINT_STRIDE),
1199 ] {
1200 let limits = Limits {
1201 fuel: Some(fuel),
1202 ..Limits::default()
1203 };
1204 let mut vm = world.plain();
1205 let error = vm
1206 .run_entry_within(Budget::new(limits.clone()), "m", "main", Vec::new())
1207 .expect_err("a run out of fuel does not answer");
1208 assert_eq!(error.outcome, crate::trace::RunOutcome::Fuel);
1209 assert_eq!(
1210 vm.instructions(),
1211 expected,
1212 "unwatched, under a fuel limit of {fuel}"
1213 );
1214
1215 let seen = Seen::default();
1216 let mut vm = world.watched(&seen);
1217 let error = vm
1218 .run_entry_within(Budget::new(limits), "m", "main", Vec::new())
1219 .expect_err("a run out of fuel does not answer");
1220 assert_eq!(error.outcome, crate::trace::RunOutcome::Fuel);
1221 assert_eq!(
1222 vm.instructions(),
1223 expected,
1224 "watched, under a fuel limit of {fuel}"
1225 );
1226 }
1227 }
1228
1229 /// **A stop reads the frame the machine is really in, not the one it was
1230 /// in an instruction ago.**
1231 ///
1232 /// `frame.pc` is a local of the dispatch loop between safepoints and is
1233 /// only made truthful by `Machine::sync`. A debugger asked before the
1234 /// sync would report the pc of the previous instruction, which is the
1235 /// one bug in this whole arrangement that would not show up as a crash.
1236 #[test]
1237 fn the_pc_a_stop_reports_is_the_one_the_top_frame_holds() {
1238 /// Records the pc twice: as the stop says it, and as the innermost
1239 /// frame of the backtrace says it.
1240 #[derive(Default)]
1241 struct Both(Mutex<Vec<(u32, u32)>>);
1242
1243 impl Debugger for Both {
1244 fn at(&self, stop: &Stop<'_>) -> Resume {
1245 let innermost = stop.frame(0).expect("a stop is inside a call");
1246 self.0
1247 .lock()
1248 .expect("a lock")
1249 .push((stop.pc(), innermost.pc()));
1250 Resume::Go
1251 }
1252 }
1253
1254 let world = World::new(NESTED);
1255 let both = Both::default();
1256 let mut vm = world.watched(&both);
1257 vm.invoke("m", "main", Vec::new()).expect("the run answers");
1258
1259 let seen = both.0.lock().expect("a lock").clone();
1260 assert!(!seen.is_empty(), "the run stopped at least once");
1261 for (stop, frame) in seen {
1262 assert_eq!(stop, frame, "the frame's pc was synced before the question");
1263 }
1264 }
1265
1266 /// **A name bound twice in one frame is answered by the later binding,
1267 /// and the earlier one is still there to be seen.**
1268 ///
1269 /// `cove_ir::Function::local_at` is the rule and the reason: shadowing
1270 /// is recorded rather than resolved, so `let total = 0; let total =
1271 /// total + 20` is two live bindings of two words, and the lowering
1272 /// resolves a name by searching its scope *backwards*. A view that took
1273 /// the first match would answer `total` with the value the source
1274 /// stopped meaning one line earlier — wrong, and wrong silently, since
1275 /// both answers are numbers.
1276 ///
1277 /// The method is pinned here and not only in `cove debug`'s own suite
1278 /// because every reader of a stop calls it: the CLI worked around the
1279 /// first-match rule by reading `Call::locals` backwards itself, and the
1280 /// next reader would have had to know to do the same.
1281 #[test]
1282 fn a_shadowed_name_is_answered_by_the_binding_the_source_means() {
1283 /// Keeps the last frame in which `total` was bound twice at once.
1284 #[derive(Default)]
1285 struct Shadow(Mutex<Option<Call>>);
1286
1287 impl Debugger for Shadow {
1288 fn at(&self, stop: &Stop<'_>) -> Resume {
1289 if let Some(call) = stop.frame(0) {
1290 let bound = call
1291 .locals()
1292 .iter()
1293 .filter(|local| local.name() == "total")
1294 .count();
1295 if bound == 2 {
1296 *self.0.lock().expect("a lock") = Some(call);
1297 }
1298 }
1299 Resume::Go
1300 }
1301 }
1302
1303 let world = World::new(SHADOWED);
1304 let shadow = Shadow::default();
1305 let mut vm = world.watched(&shadow);
1306 let answer = vm.invoke("m", "main", Vec::new()).expect("the run answers");
1307 assert_eq!(format!("{answer}"), "20");
1308
1309 let call = shadow
1310 .0
1311 .lock()
1312 .expect("a lock")
1313 .clone()
1314 .expect("both bindings of `total` were live at once");
1315 let both: Vec<&Local> = call
1316 .locals()
1317 .iter()
1318 .filter(|local| local.name() == "total")
1319 .collect();
1320 assert_eq!(both.len(), 2, "shadowing is recorded, not resolved");
1321 assert_ne!(both[0].at(), both[1].at(), "two bindings are two words");
1322 assert_eq!(both[0].value(), "0", "the shadowed binding is still there");
1323
1324 let meant = call.local("total").expect("`total` is in scope");
1325 assert_eq!(
1326 meant.at(),
1327 both[1].at(),
1328 "the later declaration is the one the source means"
1329 );
1330 assert_eq!(meant.value(), "20");
1331 }
1332
1333 /// **A stop says which task it is in, and the id is the one a trace
1334 /// writes.**
1335 ///
1336 /// Everything else a stop answers is one task's — the instruction count,
1337 /// the depth, the frames — because a spawned task runs on a machine of
1338 /// its own. Without this, a policy stated in frame depth can be
1339 /// satisfied by a task nobody was stepping in, and a count can be read
1340 /// as the run's when it is one thread's.
1341 ///
1342 /// The count is checked per task rather than the id alone, because the
1343 /// id would be worth nothing if the things it names were not really that
1344 /// task's: each machine counts from one, so the first stop of every task
1345 /// is its own first instruction.
1346 #[test]
1347 fn a_stop_names_the_task_it_is_in_and_a_spawned_task_is_not_the_entry() {
1348 /// The lowest instruction count seen in each task.
1349 #[derive(Default)]
1350 struct Whose(Mutex<BTreeMap<u64, u64>>);
1351
1352 impl Debugger for Whose {
1353 fn at(&self, stop: &Stop<'_>) -> Resume {
1354 let mut seen = self.0.lock().expect("a lock");
1355 let first = seen.entry(stop.task()).or_insert(u64::MAX);
1356 *first = (*first).min(stop.instructions());
1357 Resume::Go
1358 }
1359 }
1360
1361 let world = World::new(SPAWNED);
1362 let whose = Whose::default();
1363 let mut vm = world.watched(&whose);
1364 let answer = vm.invoke("m", "main", Vec::new()).expect("the run answers");
1365 assert_eq!(format!("{answer}"), "42");
1366
1367 let seen = whose.0.lock().expect("a lock").clone();
1368 assert_eq!(
1369 seen.len(),
1370 2,
1371 "the entry and the task it spawned are two tasks, not one"
1372 );
1373 assert!(
1374 seen.contains_key(&crate::runtime::ENTRY_TASK),
1375 "the entry names itself the way a trace names it"
1376 );
1377 for (task, first) in seen {
1378 assert_eq!(first, 1, "task {task} counts its own instructions from one");
1379 }
1380 }
1381
1382 /// **A reach that would overflow answers the whole function, on any
1383 /// pointer width.**
1384 ///
1385 /// Asking for "all of it" by naming a very large reach is the obvious
1386 /// way to ask, and it has no other spelling. Only `from` saturated at
1387 /// first, which reads as a decision and was an oversight: on a 32-bit
1388 /// target `pc + reach + 1` wraps, and the answer becomes an *empty*
1389 /// listing rather than a full one — a failure that a 64-bit `cargo test`
1390 /// cannot reach and that `crates/cove-wasm` met in a browser.
1391 #[test]
1392 fn a_reach_that_would_overflow_answers_the_whole_function() {
1393 #[derive(Default)]
1394 struct Whole(Mutex<Option<(Vec<Line>, Vec<Line>)>>);
1395
1396 impl Debugger for Whole {
1397 fn at(&self, stop: &Stop<'_>) -> Resume {
1398 let mut held = self.0.lock().expect("a lock");
1399 if held.is_none() && stop.function() == "m.inner" {
1400 *held = Some((stop.code(0, usize::MAX), stop.code(0, usize::MAX / 2)));
1401 }
1402 Resume::Go
1403 }
1404 }
1405
1406 let world = World::new(NESTED);
1407 let whole = Whole::default();
1408 let mut vm = world.watched(&whole);
1409 vm.invoke("m", "main", Vec::new()).expect("the run answers");
1410
1411 let held = whole.0.lock().expect("a lock").clone();
1412 let (widest, half) = held.expect("the run entered `m.inner`");
1413 assert!(
1414 !widest.is_empty(),
1415 "the widest reach there is answered nothing"
1416 );
1417 assert_eq!(
1418 widest.len(),
1419 half.len(),
1420 "two reaches past the end of the function answered different listings"
1421 );
1422 assert_eq!(
1423 widest.first().expect("a line").pc(),
1424 0,
1425 "the whole function starts at its first instruction"
1426 );
1427 }
1428
1429 /// **A disassembly is of the frame it was asked for, and the outermost
1430 /// is the last one there is.**
1431 ///
1432 /// A session that lets a person select a frame has to be able to show
1433 /// that frame's code. Reading the stopping function's would answer
1434 /// `frame 2` with frame 0's instructions — a listing that looks right,
1435 /// is wrong, and says nothing about which frame it is of.
1436 ///
1437 /// Frame 2 and not frame 1 for the "two listings are two" half of it.
1438 /// Frame 1 is `m.outer` and frame 0 is `m.inner` expanded *into*
1439 /// `m.outer`, so the two are one instruction stream and a disassembly of
1440 /// either shows the same instructions with the same one marked. There is
1441 /// nothing else they could honestly show — what an expansion removed was
1442 /// the frame, not the code — so the case needs a frame the machine
1443 /// actually pushed, and `m.main` is one.
1444 #[test]
1445 fn a_disassembly_is_of_the_frame_it_was_asked_for() {
1446 /// The first stop inside `m.inner`: the innermost frame's code, the
1447 /// outermost's, that frame's own pc, and a frame that is not there.
1448 #[derive(Default)]
1449 #[allow(clippy::type_complexity)]
1450 struct Frames(Mutex<Option<(Vec<Line>, Vec<Line>, Pc, Vec<Line>)>>);
1451
1452 impl Debugger for Frames {
1453 fn at(&self, stop: &Stop<'_>) -> Resume {
1454 let mut held = self.0.lock().expect("a lock");
1455 if held.is_none() && stop.function() == "m.inner" {
1456 let caller = stop.frame(2).expect("`m.outer` was called from `m.main`");
1457 *held = Some((
1458 stop.code(0, 2),
1459 stop.code(2, 2),
1460 caller.pc(),
1461 stop.code(9, 2),
1462 ));
1463 }
1464 Resume::Go
1465 }
1466 }
1467
1468 let world = World::new(NESTED);
1469 let frames = Frames::default();
1470 let mut vm = world.watched(&frames);
1471 vm.invoke("m", "main", Vec::new()).expect("the run answers");
1472
1473 let held = frames.0.lock().expect("a lock").clone();
1474 let (innermost, outer, caller_pc, past) = held.expect("the run entered `m.inner`");
1475 let marked = |lines: &[Line]| {
1476 let found: Vec<Line> = lines
1477 .iter()
1478 .filter(|line| line.current())
1479 .cloned()
1480 .collect();
1481 assert_eq!(found.len(), 1, "exactly one line is the one stopped at");
1482 found[0].clone()
1483 };
1484
1485 assert_eq!(
1486 marked(&innermost).pc(),
1487 2,
1488 "`m.inner`'s body begins at `m.main`'s counter 2"
1489 );
1490 assert_eq!(
1491 marked(&outer).pc(),
1492 caller_pc,
1493 "the caller is shown at the pc it will return to"
1494 );
1495 assert_ne!(
1496 marked(&outer).text(),
1497 marked(&innermost).text(),
1498 "two frames of two functions are two instructions"
1499 );
1500 assert!(past.is_empty(), "there is no ninth frame to disassemble");
1501 }
1502
1503 /// **A name hands over the words it covers, so a reference a name holds
1504 /// can be followed into the heap.**
1505 ///
1506 /// `Stop::object` follows a word, and `Call::words` by construction
1507 /// shows only the words no name covers. Between the two there was a hole
1508 /// exactly where a debugger is most used: a name bound to a vector
1509 /// rendered as its elements, and nothing could then ask what the object
1510 /// was. The word is the same `Word` view an unnamed word gets, because a
1511 /// word of a frame does not change shape by being named.
1512 #[test]
1513 fn a_local_hands_over_its_word_so_a_named_reference_can_be_followed() {
1514 /// The first stop at which `items` holds something.
1515 #[derive(Default)]
1516 #[allow(clippy::type_complexity)]
1517 struct Follow(Mutex<Option<(u32, u32, u32, String, Option<Object>)>>);
1518
1519 impl Debugger for Follow {
1520 fn at(&self, stop: &Stop<'_>) -> Resume {
1521 let mut held = self.0.lock().expect("a lock");
1522 if held.is_some() {
1523 return Resume::Go;
1524 }
1525 let Some(call) = stop.frame(0) else {
1526 return Resume::Go;
1527 };
1528 let Some(items) = call.local("items") else {
1529 return Resume::Go;
1530 };
1531 let [word] = items.words() else {
1532 return Resume::Go;
1533 };
1534 if word.raw() == 0 {
1535 return Resume::Go;
1536 }
1537 *held = Some((
1538 items.at(),
1539 items.width(),
1540 word.at(),
1541 word.holds().to_string(),
1542 stop.object(word.raw()),
1543 ));
1544 Resume::Go
1545 }
1546 }
1547
1548 let world = World::new(ARRAY);
1549 let follow = Follow::default();
1550 let mut vm = world.watched(&follow);
1551 let answer = vm.invoke("m", "main", Vec::new()).expect("the run answers");
1552 assert_eq!(format!("{answer}"), "3");
1553
1554 let held = follow.0.lock().expect("a lock").clone();
1555 let (at, width, word_at, holds, object) = held.expect("`items` held a reference");
1556 assert_eq!(width, 1, "a reference is one word");
1557 assert_eq!(word_at, at, "the name's first word is the name's position");
1558 assert_eq!(holds, "ref", "the frame says the word is a reference");
1559
1560 let object = object.expect("the word names an object of this run's heap");
1561 let values: Vec<&str> = object.fields().iter().map(Field::value).collect();
1562 assert_eq!(
1563 values,
1564 vec!["10", "20", "30"],
1565 "the object `items` names is the array the source wrote"
1566 );
1567 }
1568
1569 /// **A run of a program with no debugger is the run it always was.**
1570 ///
1571 /// The regression this whole part is about: `Vm::new`'s signature is
1572 /// unchanged, and so is what it produces.
1573 #[test]
1574 fn an_unwatched_run_answers_what_it_always_did() {
1575 let world = World::new(NESTED);
1576 let mut vm = world.plain();
1577 let answer = vm.invoke("m", "main", Vec::new()).expect("it answers");
1578 assert_eq!(format!("{answer}"), "42");
1579 assert!(vm.instructions() > 0);
1580 }
1581}