cove_wasm/record.rs
1//! Recording a run so that a page can scrub through it.
2//!
3//! [`cove_runtime::Debugger`] is asked before every instruction, and
4//! `cove debug` answers that question by *blocking on stdin inside the
5//! callback*: the machine calls the debugger and never the other way round,
6//! because the dispatch loop holds a `std::thread::scope` borrow that cannot
7//! leave the call that made it. `crates/cove-runtime/src/vm/debug.rs` argues
8//! that at length, and nothing here changes it.
9//!
10//! # Why this records instead of stepping
11//!
12//! A Web Worker cannot block waiting for a message from the page. There is
13//! no synchronous `receive()`; `onmessage` is delivered by the event loop,
14//! and an event loop that is inside a wasm call is an event loop that is not
15//! running. So the shape `cove debug` uses — stop the machine, ask a person,
16//! resume — has no browser spelling. The one construction that would give it
17//! one is `Atomics.wait` on a `SharedArrayBuffer`, and a `SharedArrayBuffer`
18//! is only constructible on a cross-origin-isolated page, which needs the
19//! server to send `Cross-Origin-Opener-Policy: same-origin` and
20//! `Cross-Origin-Embedder-Policy: require-corp`. GitHub Pages sends neither
21//! and cannot be made to.
22//!
23//! So the direction is inverted a second time. This [`Debugger`] does not
24//! ask anything: it *writes down* what it saw, the run goes to completion (or
25//! to its fuel, or to its deadline), the worker hands the whole recording to
26//! the page in one message, and the page scrubs through it. Nothing blocks,
27//! nothing is shared, and the timeline runs backwards as readily as forwards
28//! — which for reading a program is better than stepping, because "what did
29//! `n` hold two lines ago" is a question live stepping answers only by
30//! starting again.
31//!
32//! This is not a refusal of live stepping forever. It is what is possible
33//! without COOP/COEP. An embedder that serves those two headers could keep
34//! this file and add a second `Debugger` that waits on a `SharedArrayBuffer`,
35//! and the machine side would not change at all.
36//!
37//! # What a moment holds
38//!
39//! One captured stop — a *moment* — is what the four panes need and nothing
40//! else:
41//!
42//! - **Source**: the 1-based line and column the instruction was written at,
43//! and the instruction's span as a pair of UTF-16 offsets. The page marks
44//! that span in the editor itself rather than in a second copy of the text,
45//! so it needs an end and not only a start, and it needs both counted the
46//! way a JavaScript string is indexed. `crates/cove-wasm/src/highlight.rs`
47//! argues UTF-16 at length; the same argument holds here, and an em dash in
48//! a comment above the marked line is enough to make it matter.
49//! - **Instructions**: an index into a shared table of disassembled
50//! functions, and the pc inside it. The table is keyed by whose *code* a
51//! frame runs — `Call::within` — because that is what a disassembly is of.
52//! - **Runtime**: the backtrace, innermost first — each frame's function,
53//! pc, line and every local in scope there with its rendered value, plus
54//! the `body` it is. `body` is whose body the frame is and the index above
55//! is whose code it runs in; the two differ for a small leaf `lower::inline`
56//! expanded into its caller, where naming only the second would show one
57//! function twice in a backtrace and one function's listing under the
58//! other's name.
59//! - **Memory**: every heap object named by a `ref` word of one of those
60//! locals, rendered with its fields, and on each local the addresses of
61//! the words that named them.
62//!
63//! Plus the bookkeeping a timeline needs: the instruction count, the task,
64//! the frame depth, and *why* this instruction was captured.
65//!
66//! The disassembly is in a table beside the moments rather than inside each
67//! one. A recording of a loop is hundreds of moments in one function, and
68//! repeating that function's instructions in each of them was measured to be
69//! most of the answer. Interning is where this format's compression comes
70//! from; see [`crate::debug_json`] for why the answer is still one blob.
71//!
72//! # What is captured, and what is bounded
73//!
74//! **The policy is `cove debug`'s line-change rule**, widened by one clause.
75//! A stop is captured when it is the first, when the frame depth differs
76//! from the last captured moment's — a call or a return — when the task
77//! differs, or when the instruction was written outside the byte range of
78//! the last captured moment's source line. The byte range is compared rather
79//! than the line number for the reason `Session::line_mode` gives: a range
80//! check is two comparisons and a line number is a binary search, and this
81//! runs at every instruction.
82//!
83//! The depth clause is the widening, and it is there because
84//! `Session::misses` names its absence as a defect: a callee whose body is
85//! written on the line that calls it is stepped *over* rather than into,
86//! because the line did not change. A recording that skipped a whole call
87//! would give the Runtime pane a backtrace that jumped. Everything else in
88//! that list still applies here, unchanged — a loop written on one line is
89//! one moment per turn only because the depth or the callee changes, a
90//! statement written across several lines produces several moments in
91//! evaluation order so the line number can go backwards, and a moment is at
92//! the first instruction carrying a new line, which is inside the expression
93//! rather than at the statement's start, so a name assigned on that line
94//! still shows its old value.
95//!
96//! **Three bounds, and each loses something nameable.**
97//!
98//! 1. [`MOMENTS`] moments, the *first* N rather than the last. Past it the
99//! recorder stops capturing and the run *keeps going*, so the outcome,
100//! the output and the answer are still the real ones — the recording is a
101//! prefix of the timeline and says so with `truncated`. First and not
102//! last because a ring would give a timeline that does not begin at the
103//! entry, and because only a prefix lets the recorder go quiet: once full
104//! it answers from an [`AtomicBool`] without taking its lock, which is
105//! what lets a long run reach its own end rather than its deadline.
106//! *What is lost is the end of a long run.* The number of moments that
107//! were dropped is deliberately not reported, because counting them means
108//! keeping the per-instruction check alive for the whole run, which is
109//! the cost this bound exists to stop paying.
110//! 2. [`BYTES`] of rendered recording. Each moment is rendered to JSON as it
111//! is captured, so this bound is exact rather than estimated, and it is
112//! the one that holds when the moments are few and enormous — a deep
113//! stack of frames full of long strings. *What is lost is the same end of
114//! the same timeline*, and `truncated` says which bound stopped it.
115//! 3. [`FRAMES`] frames per moment and [`OBJECTS`] objects per moment.
116//! Without these two the first bound would not bound memory at all: a
117//! thousand moments of a recursion a thousand deep is a million frames.
118//! A moment records its true `depth`, so a pane can say how many frames
119//! it is not showing. *What is lost is the outer end of a deep backtrace,
120//! and the heap past the thirty-second object a frame's locals named.*
121//!
122//! A recording that silently truncated would be worse than one that did not
123//! exist, so every one of these reports itself: `truncated` names the bound,
124//! `kept` counts what is there, and `depth` exceeds `frames.length` exactly
125//! when frames were dropped.
126//!
127//! # What it costs the run
128//!
129//! A mutex acquisition per instruction, as `cove debug` pays, plus a span
130//! and depth comparison. What it does not pay is the rendering: a backtrace
131//! renders every local of every frame, and that happens only at a captured
132//! moment.
133//!
134//! Measured under node against the release wasm, on a counting loop of
135//! fourteen million instructions:
136//!
137//! | | |
138//! | --- | ---: |
139//! | `cove_run` | 119 ms |
140//! | `cove_debug`, 1024 moments | 186 ms |
141//! | `cove_debug`, 16384 moments | 326 ms |
142//!
143//! Two things are in that table. A recorded run of a program that overran
144//! its bound early costs **1.6x** a plain one — that is the quiet path, the
145//! relaxed load and the branch, for the fourteen million instructions after
146//! the recording filled. And the fifteen thousand extra captured moments
147//! cost 144 ms between the second row and the third, which is about **9 µs
148//! per moment**: the rendering, and the price of asking for a longer
149//! recording rather than of being watched at all.
150//!
151//! The third row also says the two bounds are calibrated against each other
152//! rather than one of them being decoration. [`MOST_MOMENTS`] moments of
153//! that loop render to 3.3 MB, just under [`BYTES`]; a program with deeper
154//! frames or longer strings reaches the byte bound first, which is what it
155//! is for.
156
157use std::sync::atomic::{AtomicBool, Ordering};
158use std::sync::{Arc, Mutex};
159
160use cove_diag::{FileId, SourceMap, Span};
161use cove_runtime::{Call, Debugger, Resume, Stop};
162
163use crate::json;
164
165/// How many moments a recording keeps by default.
166///
167/// A thousand moments is more of a program than a person will scrub through
168/// in one sitting, and at the sizes measured in `web/README.md` it is a
169/// recording a page can hold without noticing. A caller may ask for fewer,
170/// or for more up to [`MOST_MOMENTS`].
171pub const MOMENTS: usize = 1_024;
172
173/// The most moments a caller may ask for.
174///
175/// A ceiling and not a suggestion: the whole point of the first bound is
176/// that a page cannot ask for an unbounded recording, and a limit a caller
177/// chooses is not a limit if the caller may choose infinity.
178pub const MOST_MOMENTS: usize = 16_384;
179
180/// The most rendered recording a run may accumulate, in bytes.
181///
182/// Four mebibytes of JSON is roughly what a browser will `postMessage` and
183/// `JSON.parse` without a visible pause. It is a second bound and not a
184/// replacement for the first, because the two fail on different programs:
185/// this one catches a few enormous moments, and [`MOMENTS`] catches many
186/// small ones.
187pub const BYTES: usize = 4 << 20;
188
189/// Frames captured per moment, innermost first.
190pub const FRAMES: usize = 16;
191
192/// Heap objects captured per moment.
193pub const OBJECTS: usize = 32;
194
195/// What the capture rule is, in one line, carried in the answer so that a
196/// page can show it without hard-coding it.
197const POLICY: &str = "the first instruction, every call and return, and the first instruction written on a new source line";
198
199/// A `reach` that covers any function: [`Stop::code`] clamps to the code's
200/// own ends, so this asks for all of it without needing to know its length.
201///
202/// Half of `usize` and not `u32::MAX`, which is what this said first. On a
203/// 64-bit host the two are the same; on `wasm32-unknown-unknown` a `usize`
204/// is 32 bits, `Stop::code` computed `pc + reach + 1`, and `u32::MAX + 1`
205/// wrapped to zero — so every function was disassembled as the empty range
206/// `0..pc`, a different range at every pc, and the interning that keys on a
207/// function's length made a fresh entry for each. It answered correctly
208/// under `cargo test` and wrongly in the browser, which is precisely the
209/// class of bug `web/check.mjs` exists to catch, and it is the one it
210/// caught.
211///
212/// `Stop::code` saturates both ends now, so `u32::MAX` would work too. This
213/// stays as it is because a caller should not have to know that: half of
214/// `usize` cannot overflow for any pc naming an instruction actually held in
215/// memory, on any width.
216const WHOLE: usize = usize::MAX / 2;
217
218/// A [`Debugger`] that writes down what it saw instead of asking what to do.
219pub struct Recorder {
220 sources: Arc<SourceMap>,
221 limit: usize,
222 /// Whether a bound has been reached, read before the lock is taken.
223 ///
224 /// The fast path out. Once this is set the recording will not grow
225 /// again, so the per-instruction question is one relaxed load and a
226 /// branch rather than a mutex acquisition — measured at 1.6x a plain
227 /// run over the fourteen million instructions after a recording filled,
228 /// which is what lets such a run reach its own end rather than its
229 /// deadline.
230 full: AtomicBool,
231 kept: Mutex<Kept>,
232}
233
234/// Everything one recording holds, behind one lock.
235///
236/// One lock and not several for the reason `cove debug`'s session gives: a
237/// spawned task's machine asks the same debugger from that task's own
238/// thread. The playground refuses `spawn`, so in practice there is one
239/// asker; the lock is what makes that a fact about the environment rather
240/// than an assumption in this file.
241#[derive(Default)]
242struct Kept {
243 /// Whether a file's text is all ASCII, remembered after the first look.
244 ///
245 /// A UTF-16 offset is the byte offset when it is, which turns the two
246 /// conversions a moment needs per frame into nothing at all. When it is
247 /// not, the prefix is counted, and the cost is why this cache exists: a
248 /// recording is up to [`MOST_MOMENTS`] moments of up to [`FRAMES`] frames
249 /// and each frame carries a span.
250 ascii: Vec<(FileId, bool)>,
251 /// Each moment, already rendered to JSON.
252 ///
253 /// Rendered at capture rather than at the end so that [`BYTES`] is a
254 /// measurement and not a guess.
255 moments: Vec<String>,
256 functions: Vec<Function>,
257 bytes: usize,
258 /// Which bound stopped the recording, if one did.
259 truncated: Option<&'static str>,
260 /// Where the last captured moment was, for the line-change rule.
261 last: Option<Place>,
262}
263
264/// One disassembled function, interned across the moments that are in it.
265struct Function {
266 name: String,
267 /// How many instructions it has, which is what tells two functions of
268 /// the same qualified name apart when it can.
269 len: u32,
270 json: String,
271}
272
273/// The last captured moment's place, as the per-instruction check reads it.
274struct Place {
275 file: FileId,
276 /// The byte range of the source line, so the check is a comparison
277 /// rather than a search.
278 from: u32,
279 to: u32,
280 depth: usize,
281 task: u64,
282}
283
284impl Recorder {
285 /// A recorder that keeps at most `moments` moments of a run of `sources`.
286 ///
287 /// `moments` is clamped into `1..=`[`MOST_MOMENTS`]; zero asks for the
288 /// default, which is a bound and not the absence of one.
289 pub fn new(sources: Arc<SourceMap>, moments: usize) -> Recorder {
290 let limit = match moments {
291 0 => MOMENTS,
292 asked => asked.min(MOST_MOMENTS),
293 };
294 Recorder {
295 sources,
296 limit,
297 full: AtomicBool::new(false),
298 kept: Mutex::new(Kept::default()),
299 }
300 }
301
302 /// The recording, as the JSON object [`crate::debug_json`] puts under
303 /// `debug`.
304 ///
305 /// ```json
306 /// {"moments":[...],"functions":[...],"kept":int,"limit":int,
307 /// "bytes":int,"truncated":"moments"|"bytes"|null,
308 /// "frames":int,"objects":int,"policy":string}
309 /// ```
310 pub fn json(&self) -> String {
311 let kept = self.held();
312 let moments = format!("[{}]", kept.moments.join(","));
313 let functions = format!(
314 "[{}]",
315 kept.functions
316 .iter()
317 .map(|function| function.json.as_str())
318 .collect::<Vec<_>>()
319 .join(",")
320 );
321 json::object([
322 ("moments", moments),
323 ("functions", functions),
324 ("kept", kept.moments.len().to_string()),
325 ("limit", self.limit.to_string()),
326 ("bytes", kept.bytes.to_string()),
327 ("truncated", json::or_null(kept.truncated.map(json::string))),
328 ("frames", FRAMES.to_string()),
329 ("objects", OBJECTS.to_string()),
330 ("policy", json::string(POLICY)),
331 ])
332 }
333
334 /// The recording, whether or not a panic poisoned the lock.
335 ///
336 /// A poisoned lock here means a moment's rendering panicked, and the
337 /// moments captured before it are still exactly what they were. Losing
338 /// them as well would turn one bug into no recording at all.
339 fn held(&self) -> std::sync::MutexGuard<'_, Kept> {
340 self.kept.lock().unwrap_or_else(|held| held.into_inner())
341 }
342}
343
344impl Debugger for Recorder {
345 fn at(&self, stop: &Stop<'_>) -> Resume {
346 // Before the lock: a full recording has nothing left to decide.
347 if self.full.load(Ordering::Relaxed) {
348 return Resume::Go;
349 }
350 let mut kept = self.held();
351 if let Some(why) = kept.wanted(stop) {
352 if kept.moments.len() >= self.limit {
353 kept.truncated = Some("moments");
354 } else if kept.bytes >= BYTES {
355 kept.truncated = Some("bytes");
356 }
357 if kept.truncated.is_some() {
358 self.full.store(true, Ordering::Relaxed);
359 } else {
360 let moment = kept.capture(stop, &self.sources, why);
361 kept.bytes += moment.len();
362 kept.moments.push(moment);
363 }
364 }
365 // Never `Halt`. A recording that ended the run would answer a
366 // question about a program with a program that did not finish, and
367 // the outcome, the output and the answer beside the recording would
368 // all be about a run nobody asked for.
369 Resume::Go
370 }
371}
372
373impl Kept {
374 /// The per-instruction question: is this instruction a moment, and why?
375 ///
376 /// It reads three things off the stop — the span, the depth and the task
377 /// — all of them copies the machine already had, and compares them
378 /// against integers. It allocates nothing and reads no source.
379 fn wanted(&self, stop: &Stop<'_>) -> Option<&'static str> {
380 let Some(last) = &self.last else {
381 return Some("entry");
382 };
383 let depth = stop.depth();
384 if stop.task() != last.task {
385 return Some("task");
386 }
387 if depth > last.depth {
388 return Some("call");
389 }
390 if depth < last.depth {
391 return Some("return");
392 }
393 let span = stop.span();
394 let same = span.file == last.file && last.from <= span.start && span.start < last.to;
395 (!same).then_some("line")
396 }
397
398 /// Whether `file` holds nothing but ASCII, looked up once per file.
399 fn ascii(&mut self, sources: &SourceMap, file: FileId) -> bool {
400 if let Some((_, held)) = self.ascii.iter().find(|(id, _)| *id == file) {
401 return *held;
402 }
403 let held = sources.get(file).text.is_ascii();
404 self.ascii.push((file, held));
405 held
406 }
407
408 /// `span` as the pair of UTF-16 offsets a page slices its own string by.
409 fn utf16(&mut self, sources: &SourceMap, span: Span) -> (usize, usize) {
410 let ascii = self.ascii(sources, span.file);
411 let text = &sources.get(span.file).text;
412 (
413 at_utf16(text, span.start, ascii),
414 at_utf16(text, span.end, ascii),
415 )
416 }
417
418 /// One moment, rendered.
419 fn capture(&mut self, stop: &Stop<'_>, sources: &SourceMap, why: &'static str) -> String {
420 let span = stop.span();
421 let (line, col) = at_line(sources, span);
422 let (from, to) = self.utf16(sources, span);
423 self.last = Some(Place {
424 file: span.file,
425 from: line_from(sources, span),
426 to: line_to(sources, span),
427 depth: stop.depth(),
428 task: stop.task(),
429 });
430
431 let depth = stop.depth();
432 let mut frames = Vec::new();
433 let mut objects: Vec<(u64, String)> = Vec::new();
434 // The function the moment itself is in, which is the innermost
435 // frame's. It is repeated out of the frames because the Instructions
436 // pane follows the timeline whether or not a reader has selected a
437 // frame, and `null` for the one stop with no frame at all.
438 let mut top = None;
439 for at in 0..depth.min(FRAMES) {
440 let Some(call) = stop.frame(at) else { break };
441 let function = self.intern(stop, sources, at, &call);
442 top.get_or_insert(function);
443 let locals = call
444 .locals()
445 .iter()
446 .map(|local| {
447 let refs = local
448 .words()
449 .iter()
450 // Only a `ref` word names a heap object — it is the
451 // one representation the collector treats as a root
452 // — so this asks about the words that can answer
453 // rather than about every word of every local.
454 .filter(|word| word.holds() == "ref" && word.raw() != 0)
455 .filter_map(|word| remember(stop, &mut objects, word.raw()))
456 .collect::<Vec<_>>();
457 json::object([
458 ("name", json::string(local.name())),
459 ("value", json::string(local.value())),
460 ("at", local.at().to_string()),
461 ("width", local.width().to_string()),
462 ("refs", json::array(refs)),
463 ])
464 })
465 .collect::<Vec<_>>();
466 let (from, to) = self.utf16(sources, call.span());
467 frames.push(json::object([
468 ("function", function.to_string()),
469 // Whose body this frame is, which `function` above does not
470 // say: that is an index into the shared table of
471 // *disassemblies*, and `lower::inline` expands a small leaf
472 // where it is called, so a frame can be the leaf while the
473 // stream it runs in is the caller's. A backtrace built from
474 // the table alone would name the caller twice and lose the
475 // one thing the expansion's record was made to keep.
476 //
477 // `body` rather than `name`, because `name` already means two
478 // things in this payload — a disassembly's title and a
479 // local's spelling — and a third would make none of them
480 // findable.
481 ("body", json::string(call.function())),
482 ("pc", call.pc().to_string()),
483 ("line", at_line(sources, call.span()).0.to_string()),
484 // A selected frame moves the editor's mark to that frame's
485 // own call site, which is the only thing that makes an outer
486 // frame readable in a page with one editor rather than one
487 // Source pane per frame.
488 ("from", from.to_string()),
489 ("to", to.to_string()),
490 ("locals", json::array(locals)),
491 ]));
492 }
493
494 json::object([
495 ("at", stop.instructions().to_string()),
496 ("task", stop.task().to_string()),
497 (
498 "function",
499 json::or_null(top.map(|index| index.to_string())),
500 ),
501 ("pc", stop.pc().to_string()),
502 ("line", line.to_string()),
503 ("col", col.to_string()),
504 ("from", from.to_string()),
505 ("to", to.to_string()),
506 ("depth", depth.to_string()),
507 ("why", json::string(why)),
508 ("frames", json::array(frames)),
509 (
510 "objects",
511 json::array(objects.into_iter().map(|(_, json)| json)),
512 ),
513 ])
514 }
515
516 /// The index of `call`'s function in the shared table, disassembling it
517 /// the first time it is seen.
518 ///
519 /// Keyed by the qualified name, with the pc checked against the length
520 /// of what was disassembled. That is a heuristic and it is the best one
521 /// available here: nothing public identifies a lowered function, and one
522 /// generic function lowered twice produces two functions with one
523 /// qualified name. The check catches the case where they differ in
524 /// length; two instantiations of the same length are shown as one, whose
525 /// instructions are the same modulo the layouts named in them. The pc a
526 /// pane marks is right either way.
527 ///
528 /// The name is [`Call::within`] and not [`Call::function`], because this
529 /// table holds *disassemblies*. A frame that is a body `lower::inline`
530 /// expanded runs the caller's instruction stream, so keying on the leaf's
531 /// name gave a pane titled `playground.twice` holding `playground.main`'s
532 /// four instructions. Which body a frame is remains on the frame, where
533 /// `Call::function` put it.
534 fn intern(&mut self, stop: &Stop<'_>, sources: &SourceMap, at: usize, call: &Call) -> usize {
535 let name = call.within();
536 if let Some(index) = self
537 .functions
538 .iter()
539 .position(|held| held.name == name && call.pc() < held.len)
540 {
541 return index;
542 }
543 let code = stop.code(at, WHOLE);
544 let json = json::object([
545 ("name", json::string(name)),
546 (
547 "code",
548 json::array(code.iter().map(|line| {
549 json::object([
550 ("pc", line.pc().to_string()),
551 ("text", json::string(line.text())),
552 ("line", at_line(sources, line.span()).0.to_string()),
553 ])
554 })),
555 ),
556 ]);
557 self.functions.push(Function {
558 name: name.to_string(),
559 len: code.len() as u32,
560 json,
561 });
562 self.functions.len() - 1
563 }
564}
565
566/// Renders the object at `addr` into `objects` if it is one and there is
567/// room, and answers the address as a JSON number for the local to point at.
568///
569/// The address is answered even when the object was already there, because a
570/// local that names an object something else also names should still say so;
571/// it is not answered when the word names nothing this heap holds, because
572/// then it is not a reference a pane can follow.
573fn remember(stop: &Stop<'_>, objects: &mut Vec<(u64, String)>, addr: u64) -> Option<String> {
574 if objects.iter().any(|(held, _)| *held == addr) {
575 return Some(addr.to_string());
576 }
577 if objects.len() >= OBJECTS {
578 return None;
579 }
580 let object = stop.object(addr)?;
581 objects.push((
582 addr,
583 json::object([
584 ("at", addr.to_string()),
585 ("name", json::string(object.name())),
586 (
587 "fields",
588 json::array(object.fields().iter().map(|field| {
589 json::object([
590 ("name", json::string(field.name())),
591 ("value", json::string(field.value())),
592 ])
593 })),
594 ),
595 ]),
596 ));
597 Some(addr.to_string())
598}
599
600/// A byte offset as a UTF-16 offset, which is what a JavaScript string is
601/// indexed in.
602///
603/// `ascii` is the file's answer to "are the two the same number?", looked up
604/// once by [`Kept::ascii`] rather than per span. Out-of-range offsets are
605/// clamped and a mid-character one is walked back, because a marked span that
606/// is one code unit wrong is better than a panic inside a debugger.
607fn at_utf16(text: &str, at: u32, ascii: bool) -> usize {
608 let mut at = (at as usize).min(text.len());
609 if ascii {
610 return at;
611 }
612 while !text.is_char_boundary(at) {
613 at -= 1;
614 }
615 text[..at].encode_utf16().count()
616}
617
618/// The 1-based line and column `span` starts at.
619fn at_line(sources: &SourceMap, span: Span) -> (usize, usize) {
620 sources.get(span.file).line_col(span.start)
621}
622
623/// The byte offset the source line holding `span` begins at.
624///
625/// Found by scanning outwards from the instruction's own offset, which is
626/// `cove debug`'s idiom and for its reason: `SourceMap` exposes a line's
627/// number and its text but not where it begins, and asking for the number
628/// per instruction is the search this exists to avoid.
629fn line_from(sources: &SourceMap, span: Span) -> u32 {
630 let text = &sources.get(span.file).text;
631 let at = (span.start as usize).min(text.len());
632 text[..at].rfind('\n').map_or(0, |end| end + 1) as u32
633}
634
635/// The byte offset one past the end of that line.
636fn line_to(sources: &SourceMap, span: Span) -> u32 {
637 let text = &sources.get(span.file).text;
638 let at = (span.start as usize).min(text.len());
639 text[at..].find('\n').map_or(text.len(), |end| at + end + 1) as u32
640}