Skip to main content

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}