Skip to main content

cargo_show_asm/
asm.rs

1#![allow(clippy::missing_errors_doc)]
2use crate::asm::statements::Label;
3use crate::cached_lines::CachedLines;
4use crate::demangle::LabelKind;
5use crate::{
6    CallGraph, Dumpable, Item, RawLines, URange, color, demangle, esafeprintln, get_context_for,
7    safeprintln,
8};
9// TODO, use https://sourceware.org/binutils/docs/as/index.html
10use crate::opts::{Format, NameDisplay, RedundantLabels, SourcesFrom};
11
12mod statements;
13
14use nom::Parser as _;
15use owo_colors::OwoColorize;
16pub use statements::{Directive, GenericDirective, Instruction, Statement};
17use statements::{Loc, parse_statement};
18use std::borrow::Cow;
19use std::cell::RefCell;
20use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
21use std::ops::Range;
22use std::path::{Display, Path, PathBuf};
23
24type SourceFile = (String, Option<(Source, CachedLines)>);
25
26pub fn parse_file(input: &str) -> anyhow::Result<Vec<Statement<'_>>> {
27    // eat all statements until the eof, so we can report the proper errors on failed parse
28    match nom::multi::many0(parse_statement).parse(input) {
29        Ok(("", stmts)) => Ok(stmts),
30        Ok((leftovers, _)) =>
31        {
32            #[allow(clippy::redundant_else)]
33            if leftovers.len() < 1000 {
34                anyhow::bail!("Didn't consume everything, leftovers: {leftovers:?}")
35            } else {
36                let head = &leftovers[..leftovers
37                    .char_indices()
38                    .nth(200)
39                    .expect("Shouldn't have that much unicode here...")
40                    .0];
41                anyhow::bail!("Didn't consume everything, leftovers prefix: {head:?}");
42            }
43        }
44        Err(err) => anyhow::bail!("Couldn't parse the .s file: {err}"),
45    }
46}
47
48#[must_use]
49pub fn find_items(lines: &[Statement]) -> BTreeMap<Item, Range<usize>> {
50    let mut res = BTreeMap::new();
51
52    let mut sec_start = 0;
53    let mut item: Option<Item> = None;
54    let mut names = BTreeMap::new();
55
56    for (ix, line) in lines.iter().enumerate() {
57        if line.is_section_start() {
58            if item.is_none() {
59                sec_start = ix;
60            } else {
61                // on Windows, when panic unwinding is enabled, the compiler can
62                // produce multiple blocks of exception-handling code for a
63                // function, annotated by .seh_* directives (which we ignore).
64                // For some reason (maybe a bug? or maybe we're misunderstanding
65                // something?), each of those blocks starts with a .section
66                // directive identical to the one at the start of the function.
67                // We have to ignore such duplicates here, otherwise we'd output
68                // only the last exception-handling block instead of the whole
69                // function.
70                //
71                // See https://github.com/pacak/cargo-show-asm/issues/110
72            }
73        } else if line.is_global() && sec_start + 3 < ix {
74            // On Linux and Windows every global function gets its own section.
75            // On Mac for some reason this is not the case, so we have to look for
76            // symbols marked globl within the section.  So if we encounter a globl
77            // deep enough within the current section treat it as a new section start.
78            // This little hack allows to include full section on Windows/Linux but
79            // still capture full function body on Mac.
80            sec_start = ix;
81        } else if line.is_end_of_fn() {
82            let sec_end = ix;
83            let range = sec_start..sec_end;
84            if let Some(mut item) = item.take() {
85                item.len = ix - item.len;
86                item.non_blank_len = item.len;
87                res.insert(item, range);
88            }
89        } else if let Statement::Label(label) = line {
90            if let Some(dem) = demangle::demangled(label.id) {
91                let hashed = format!("{dem:?}");
92                let name = format!("{dem:#?}");
93                let name_entry = names.entry(name.clone()).or_insert(0);
94                item = Some(Item {
95                    mangled_name: label.id.to_owned(),
96                    name,
97                    hashed,
98                    index: *name_entry,
99                    len: ix,
100                    non_blank_len: 0,
101                    depth: None,
102                });
103                *name_entry += 1;
104            } else if matches!(label.kind, LabelKind::Unknown | LabelKind::Global) {
105                if let Some(mut i) = handle_non_mangled_labels(lines, ix, label, sec_start) {
106                    let name_entry = names.entry(i.name.clone()).or_insert(0);
107                    i.index = *name_entry;
108                    item = Some(i);
109                    *name_entry += 1;
110                }
111            }
112        }
113    }
114
115    // detect merged functions
116    // we'll define merged function as something with a global label and a reference to a different
117    // global label
118
119    let globals = lines
120        .iter()
121        .enumerate()
122        .filter_map(|(ix, line)| {
123            if let Statement::Directive(Directive::Global(name)) = line {
124                Some((name, ix))
125            } else {
126                None
127            }
128        })
129        .collect::<HashMap<_, _>>();
130
131    for (end, line) in lines.iter().enumerate() {
132        let name = match line {
133            Statement::Directive(Directive::SetValue(name, _)) => name,
134            Statement::Assignment(name, _) => name,
135            _ => continue,
136        };
137        let Some(start) = globals.get(name).copied() else {
138            continue;
139        };
140
141        // Merged function is different on different system, lol.
142        //
143        // Linux: a sequence of 3 items
144        //
145        // .globl  _ZN13sample_merged3two17h0afab563317f9d7bE
146        // .type   _ZN13sample_merged3two17h0afab563317f9d7bE,@function
147        // .set _ZN13sample_merged3two17h0afab563317f9d7bE, _ZN13sample_merged12one_plus_one17h408b56cb936d6f10E
148        //
149        // MacOS: a sequence of 2 items
150        //
151        // .globl  _ZN13sample_merged3two17h0afab563317f9d7bE
152        // .set _ZN13sample_merged3two17h0afab563317f9d7bE, _ZN13sample_merged12one_plus_one17h408b56cb936d6f10E
153        //
154        // Windows: a sequence of 6-ish items, different on CI machine LOL
155        //
156        //  .globl  _ZN13sample_merged7two_num17h2372a6fab541fa02E
157        //  .def    _ZN13sample_merged7two_num17h2372a6fab541fa02E;
158        //  .scl    2;
159        //  .type   32;
160        //  .endef
161        // .set _ZN13sample_merged7two_num17h2372a6fab541fa02E, _ZN13sample_merged12one_plus_one17h96e22123e4e22951E
162
163        // Since rust 1.91.0 ".set FOO, BAR" is replaced by "FOO = BAR", I'm assuming - on all
164        // platforms, so try to parse that as well
165
166        let range = start..end + 1;
167        if range.len() > 10 {
168            // merged function body should contain just a few lines, use
169            // this as a sanity check
170            continue;
171        }
172        let sym = name;
173        if let Some(dem) = demangle::demangled(sym) {
174            let hashed = format!("{dem:?}");
175            let name = format!("{dem:#?}");
176            let name_entry = names.entry(name.clone()).or_insert(0);
177            res.insert(
178                Item {
179                    mangled_name: (*sym).to_string(),
180                    name,
181                    hashed,
182                    index: *name_entry,
183                    len: range.len(),
184                    non_blank_len: range.len(),
185                    depth: None,
186                },
187                range,
188            );
189            *name_entry += 1;
190        }
191    }
192
193    res
194}
195
196/// Handles the non-mangled labels found in the given lines of ASM statements.
197///
198/// Returns item if the label is a valid function item, otherwise returns None.
199/// NOTE: Does not set `item.index`.
200fn handle_non_mangled_labels(
201    lines: &[Statement],
202    ix: usize,
203    label: &Label,
204    sec_start: usize,
205) -> Option<Item> {
206    match lines.get(sec_start) {
207        Some(Statement::Directive(Directive::SectionStart(ss))) => {
208            // The first macOS symbol is found in this section.
209            // Symbols after this are resolved by matching globl Generic Directive below
210            // because of the sec_start hack in `find_items`.
211            const MACOS_TEXT_SECTION: &str = "__TEXT,__text,regular,pure_instructions";
212            // Windows symbols each have their own section with this prefix.
213            const WINDOWS_TEXT_SECTION_PREFIX: &str = ".text,\"xr\",one_only,";
214            let is_mac = *ss == MACOS_TEXT_SECTION;
215            let is_windows = ss.starts_with(WINDOWS_TEXT_SECTION_PREFIX);
216            if is_windows || is_mac {
217                // Search for .globl between sec_start and ix
218                for line in &lines[sec_start..ix] {
219                    if let Statement::Directive(Directive::Global(g)) = line {
220                        // last bool is responsible for stripping leading underscore.
221                        // Stripping is not needed on Linux and 64-bit Windows.
222                        // Currently we want to strip underscore on MacOS
223                        // TODO: on 32-bit Windows we ought to remove underscores
224                        if let Some(item) = get_item_in_section(ix, label, g, is_mac) {
225                            return Some(item);
226                        }
227                    }
228                }
229                None
230            } else {
231                // Linux symbols each have their own section, named with this prefix.
232                get_item_in_section(ix, label, ss.strip_prefix(".text.")?, false)
233            }
234        }
235        Some(Statement::Directive(Directive::Global(g))) => get_item_in_section(ix, label, g, true),
236        _ => None,
237    }
238}
239
240/// Checks if the place (ss) starts with the `label`. Place can be either section or .global
241/// Creates a new [`Item`], but sets `item.index` to 0.
242fn get_item_in_section(ix: usize, label: &Label, ss: &str, strip_underscore: bool) -> Option<Item> {
243    if !ss.starts_with(label.id) {
244        return None;
245    }
246    let name = if strip_underscore && label.id.starts_with('_') {
247        String::from(&label.id[1..])
248    } else {
249        String::from(label.id)
250    };
251    Some(Item {
252        mangled_name: label.id.to_owned(),
253        name: name.clone(),
254        hashed: name,
255        index: 0, // Written later in find_items
256        len: ix,
257        non_blank_len: 0,
258        depth: None,
259    })
260}
261
262fn used_labels<'a>(stmts: &'_ [Statement<'a>]) -> BTreeSet<&'a str> {
263    stmts
264        .iter()
265        .filter_map(|stmt| match stmt {
266            Statement::Label(_) | Statement::Nothing => None,
267            Statement::Directive(dir) => match dir {
268                Directive::File(_)
269                | Directive::Loc(_)
270                | Directive::Global(_)
271                | Directive::SubsectionsViaSym
272                | Directive::SymIsFun(_) => None,
273                Directive::Data(_, val) | Directive::SetValue(_, val) => Some(*val),
274                Directive::Generic(g) => Some(g.0),
275                Directive::SectionStart(ss) => Some(*ss),
276            },
277            Statement::Instruction(i) => i.args,
278            Statement::Assignment(_, _) => None,
279            Statement::Dunno(s) => Some(s),
280        })
281        .flat_map(demangle::local_labels)
282        .collect::<BTreeSet<_>>()
283}
284
285/// Scans for referenced constants
286fn scan_constant(
287    name: &str,
288    sections: &BTreeMap<&str, usize>,
289    body: &[Statement],
290) -> Option<URange> {
291    let start = *sections.get(name)?;
292    let end = start
293        + body[start + 1..]
294            .iter()
295            .take_while(|s| matches!(s, Statement::Directive(Directive::Data(_, _))))
296            .count()
297        + 1;
298    Some(URange { start, end })
299}
300
301fn dump_range(
302    files: &BTreeMap<u64, SourceFile>,
303    fmt: &Format,
304    print_range: Range<usize>,
305    body: &[Statement], // full body
306) -> anyhow::Result<()> {
307    let print_range = URange::from(print_range);
308    let mut prev_loc = Loc::default();
309
310    let stmts = &body[print_range];
311    let used = if fmt.redundant_labels == RedundantLabels::Keep {
312        BTreeSet::new()
313    } else {
314        used_labels(stmts)
315    };
316
317    let mut empty_line = false;
318    for (ix, line) in stmts.iter().enumerate() {
319        if fmt.verbosity > 3 {
320            safeprintln!("{line:?}");
321        }
322        if let Statement::Directive(Directive::File(_)) = &line {
323            // do nothing, this directive was used previously to initialize rust sources
324        } else if let Statement::Directive(Directive::Loc(loc)) = &line {
325            if !fmt.rust {
326                continue;
327            }
328            if loc.line == 0 {
329                continue;
330            }
331            if loc == &prev_loc {
332                continue;
333            }
334            prev_loc = *loc;
335            match files.get(&loc.file) {
336                Some((fname, Some((source, file)))) => {
337                    if source.show_for(fmt.sources_from) {
338                        let pos = format!("\t\t// {fname}:{}", loc.line);
339                        safeprintln!("{}", color!(pos, OwoColorize::cyan));
340                        if let Some(rust_line) = &file.get(loc.line as usize - 1) {
341                            safeprintln!(
342                                "\t\t{}",
343                                color!(rust_line.trim_start(), OwoColorize::bright_red)
344                            );
345                        } else {
346                            safeprintln!(
347                                "\t\t{}",
348                                color!(
349                                    "Corrupted rust-src installation? Try re-adding rust-src component.",
350                                    OwoColorize::red
351                                )
352                            );
353                        }
354                    }
355                }
356                Some((fname, None)) => {
357                    if fmt.verbosity > 1 {
358                        safeprintln!(
359                            "\t\t{} {}",
360                            color!("//", OwoColorize::cyan),
361                            color!(
362                                "Can't locate the file, please open a ticket with cargo-show-asm",
363                                OwoColorize::red
364                            ),
365                        );
366                    }
367                    let pos = format!("\t\t// {fname}:{}", loc.line);
368                    safeprintln!("{}", color!(pos, OwoColorize::cyan));
369                }
370                None => {
371                    anyhow::bail!("DWARF file refers to an undefined location {loc:?}");
372                }
373            }
374            empty_line = false;
375        } else if let Statement::Label(Label {
376            kind: kind @ (LabelKind::Local | LabelKind::Temp),
377            id,
378        }) = line
379        {
380            match fmt.redundant_labels {
381                // We always include used labels and labels at the start
382                // of the fragment - those are used for data declarations
383                _ if ix == 0 || used.contains(id) => {
384                    safeprintln!("{line}");
385                }
386                RedundantLabels::Keep => {
387                    safeprintln!("{line}");
388                }
389                RedundantLabels::Blanks => {
390                    if !empty_line && *kind != LabelKind::Temp {
391                        safeprintln!();
392                        empty_line = true;
393                    }
394                }
395                RedundantLabels::Strip => {}
396            }
397        } else {
398            if fmt.simplify && line.boring() {
399                continue;
400            }
401
402            empty_line = false;
403            match fmt.name_display {
404                NameDisplay::Full => safeprintln!("{line:#}"),
405                NameDisplay::Short => safeprintln!("{line}"),
406                NameDisplay::Mangled => safeprintln!("{line:-}"),
407            }
408        }
409    }
410
411    Ok(())
412}
413
414/// Returns a closure that trims the paths
415fn path_formatter() -> impl for<'p> Fn(&'p Path, &'p mut PathBuf) -> Display<'p> {
416    let current_dir = std::env::current_dir().unwrap_or_default();
417    let home_dir = std::env::home_dir();
418    let home = if std::path::MAIN_SEPARATOR == '/' {
419        "~"
420    } else {
421        "%userprofile%"
422    };
423    move |path, tmp| {
424        if path.is_absolute() {
425            if let Ok(rel) = path.strip_prefix(&current_dir) {
426                rel
427            } else if let Some(path_in_home) = home_dir
428                .as_ref()
429                .and_then(|home| path.strip_prefix(home).ok())
430            {
431                tmp.clear();
432                tmp.push(home);
433                tmp.push(path_in_home);
434                &*tmp
435            } else {
436                path
437            }
438        } else {
439            path
440        }
441        .display()
442    }
443}
444
445#[derive(Debug, Clone)]
446pub enum Source {
447    Crate,
448    External,
449    Stdlib,
450    Rustc,
451}
452
453impl Source {
454    fn show_for(&self, from: SourcesFrom) -> bool {
455        match self {
456            Source::Crate => true,
457            Source::External => match from {
458                SourcesFrom::ThisWorkspace => false,
459                SourcesFrom::AllCrates | SourcesFrom::AllSources => true,
460            },
461            Source::Rustc | Source::Stdlib => match from {
462                SourcesFrom::ThisWorkspace | SourcesFrom::AllCrates => false,
463                SourcesFrom::AllSources => true,
464            },
465        }
466    }
467}
468
469// DWARF information contains references to source files
470// It can point to 3 different items:
471// 1. a real file, cargo-show-asm can just read it
472// 2. a file from rustlib, sources are under $sysroot/lib/rustlib/src/rust/$suffix
473//    Some examples:
474//        /rustc/a55dd71d5fb0ec5a6a3a9e8c27b2127ba491ce52/library/core/src/iter/range.rs
475//        /private/tmp/rust-20230325-7327-rbrpyq/rustc-1.68.1-src/library/core/src/option.rs
476//        /rustc/cc66ad468955717ab92600c770da8c1601a4ff33\\library\\core\\src\\convert\\mod.rs
477// 3. a file from prebuilt (?) hashbrown, sources are probably available under
478//    cargo registry, most likely under ~/.cargo/registry/$suffix
479//    Some examples:
480//        /cargo/registry/src/github.com-1ecc6299db9ec823/hashbrown-0.12.3/src/raw/bitmask.rs
481//        /Users/runner/.cargo/registry/src/github.com-1ecc6299db9ec823/hashbrown-0.12.3/src/map.rs
482// 4. rustc sources:
483//    /rustc/89e2160c4ca5808657ed55392620ed1dbbce78d1/compiler/rustc_span/src/span_encoding.rs
484//    $sysroot/lib/rustlib/rust-src/rust/compiler/rustc_span/src/span_encoding.rs
485fn locate_sources(sysroot: &Path, workspace: &Path, path: &Path) -> Option<(Source, PathBuf)> {
486    let mut path = Cow::Borrowed(path);
487    // a real file that simply exists
488    if path.exists() {
489        let source = if path.starts_with(workspace) {
490            Source::Crate
491        } else {
492            Source::External
493        };
494
495        return Some((source, path.into()));
496    }
497
498    let no_rust_src = || {
499        esafeprintln!(
500            "You need to install rustc sources to be able to see the rust annotations, try\n\
501                                       \trustup component add rust-src"
502        );
503        std::process::exit(1);
504    };
505
506    // then during crosscompilation we can get this cursed mix of path names
507    //
508    // /rustc/cc66ad468955717ab92600c770da8c1601a4ff33\\library\\core\\src\\convert\\mod.rs
509    //
510    // where one bit comes from the host platform and second bit comes from the target platform
511    // This feels like a problem in upstream, but supporting that is not _that_ hard.
512    //
513    // I think this should take care of Linux and MacOS support
514    if (path.starts_with("/rustc/") || path.starts_with("/private/tmp"))
515        && path
516            .as_os_str()
517            .to_str()
518            .is_some_and(|s| s.contains('\\') && s.contains('/'))
519    {
520        let cursed_path = path
521            .as_os_str()
522            .to_str()
523            .expect("They are coming from a text file");
524        path = Cow::Owned(PathBuf::from(cursed_path.replace('\\', "/")));
525    }
526
527    // /rustc/89e2160c4ca5808657ed55392620ed1dbbce78d1/compiler/rustc_span/src/span_encoding.rs
528    if path.starts_with("/rustc") && path.iter().any(|c| c == "compiler") {
529        let mut source = sysroot.join("lib/rustlib/rustc-src/rust");
530        for part in path.components().skip(3) {
531            source.push(part);
532        }
533
534        if source.exists() {
535            return Some((Source::Rustc, source));
536        } else {
537            no_rust_src();
538        }
539    }
540
541    // rust sources, Linux style
542    if path.starts_with("/rustc/") {
543        let mut source = sysroot.join("lib/rustlib/src/rust");
544        for part in path.components().skip(3) {
545            source.push(part);
546        }
547        if source.exists() {
548            return Some((Source::Stdlib, source));
549        } else {
550            no_rust_src();
551        }
552    }
553
554    // rust sources, MacOS style
555    if path.starts_with("/private/tmp") && path.components().any(|c| c.as_os_str() == "library") {
556        let mut source = sysroot.join("lib/rustlib/src/rust");
557        for part in path.components().skip(5) {
558            source.push(part);
559        }
560        if source.exists() {
561            return Some((Source::Stdlib, source));
562        } else {
563            no_rust_src();
564        }
565    }
566
567    // cargo registry, Linux and macOS look for cargo/registry and .cargo/registry
568    if let Some(ix) = path
569        .components()
570        .position(|c| c.as_os_str() == "cargo" || c.as_os_str() == ".cargo")
571        .and_then(|ix| path.components().nth(ix).zip(Some(ix)))
572        .and_then(|(c, ix)| (c.as_os_str() == "registry").then_some(ix))
573    {
574        // It does what I want as far as *nix is concerned, might not work for Windows...
575        #[allow(deprecated)]
576        let mut source = std::env::home_dir().expect("No home dir?");
577
578        source.push(".cargo");
579        for part in path.components().skip(ix) {
580            source.push(part);
581        }
582        if source.exists() {
583            return Some((Source::External, source));
584        } else {
585            panic!(
586                "{path:?} looks like it can be a cargo registry reference but we failed to get it"
587            );
588        }
589    }
590
591    None
592}
593
594fn load_rust_sources(
595    sysroot: &Path,
596    workspace: &Path,
597    statements: &[Statement],
598    fmt: &Format,
599    files: &mut BTreeMap<u64, SourceFile>,
600) {
601    let home_dir = std::env::home_dir();
602    let format_path = path_formatter();
603    let mut tmp = PathBuf::new();
604
605    for line in statements {
606        if let Statement::Directive(Directive::File(f)) = line {
607            files.entry(f.index).or_insert_with(|| {
608                let path = f.path.as_full_path_with_home_dir(home_dir.as_deref());
609                let pretty_path = format_path(&path, &mut tmp).to_string();
610                if fmt.verbosity > 2 {
611                    safeprintln!("Reading file #{} {}", f.index, path.display());
612                }
613
614                if let Some((source, filepath)) = locate_sources(sysroot, workspace, &path) {
615                    if fmt.verbosity > 3 {
616                        safeprintln!("Resolved name is {filepath:?}");
617                    }
618                    let sources = std::fs::read_to_string(&filepath).expect("Can't read a file");
619                    if sources.is_empty() {
620                        if fmt.verbosity > 0 {
621                            safeprintln!("Ignoring empty file {filepath:?}!");
622                        }
623                        (pretty_path, None)
624                    } else {
625                        if fmt.verbosity > 3 {
626                            safeprintln!("Got {} bytes", sources.len());
627                        }
628                        let lines = CachedLines::without_ending(sources);
629                        (pretty_path, Some((source, lines)))
630                    }
631                } else {
632                    if fmt.verbosity > 1 {
633                        safeprintln!("File not found {}", path.display());
634                    }
635                    (pretty_path, None)
636                }
637            });
638        }
639    }
640}
641
642impl RawLines for Statement<'_> {
643    fn lines(&self) -> Option<&str> {
644        match self {
645            Statement::Instruction(i) => i.args,
646            Statement::Directive(Directive::SetValue(_, i)) => Some(i),
647            _ => None,
648        }
649    }
650}
651
652pub struct Asm<'a> {
653    workspace: &'a Path,
654    sysroot: &'a Path,
655    sources: RefCell<BTreeMap<u64, SourceFile>>,
656}
657
658impl<'a> Asm<'a> {
659    pub fn new(workspace: &'a Path, sysroot: &'a Path) -> Self {
660        Self {
661            workspace,
662            sysroot,
663            sources: Default::default(),
664        }
665    }
666}
667
668impl Dumpable for Asm<'_> {
669    type Line<'l> = Statement<'l>;
670
671    fn split_lines(contents: &str) -> anyhow::Result<Vec<Self::Line<'_>>> {
672        parse_file(contents)
673    }
674
675    fn find_items(lines: &[Self::Line<'_>]) -> BTreeMap<Item, Range<usize>> {
676        find_items(lines)
677    }
678
679    fn callgraph<'a>(lines: &[Self::Line<'a>]) -> CallGraph<'a> {
680        let mut graph: HashMap<&str, HashSet<&str>> = HashMap::new();
681        let mut caller: &str = "";
682        for line in lines {
683            match line {
684                Statement::Label(Label { id, kind }) => {
685                    if !matches!(kind, LabelKind::Local | LabelKind::Temp) {
686                        caller = id;
687                    }
688                }
689                Statement::Instruction(Instruction {
690                    args: Some(arg), ..
691                }) => {
692                    for callee in demangle::GLOBAL_LABELS
693                        .captures_iter(arg)
694                        .filter_map(|c| c.get(1))
695                    {
696                        graph.entry(caller).or_default().insert(callee.as_str());
697                    }
698                }
699                _ => {}
700            }
701        }
702        CallGraph(graph)
703    }
704
705    fn dump_range(&self, fmt: &Format, lines: &[Self::Line<'_>]) -> anyhow::Result<()> {
706        dump_range(&self.sources.borrow(), fmt, 0..lines.len(), lines)
707    }
708
709    fn extra_context(
710        &self,
711        fmt: &Format,
712        lines: &[Self::Line<'_>],
713        range: Range<usize>,
714        items: &BTreeMap<Item, Range<usize>>,
715    ) -> Vec<Range<usize>> {
716        let mut res = get_context_for(fmt.context, lines, range.clone(), items);
717        if fmt.rust {
718            load_rust_sources(
719                self.sysroot,
720                self.workspace,
721                lines,
722                fmt,
723                &mut self.sources.borrow_mut(),
724            );
725        }
726
727        if fmt.include_constants {
728            let print_range = URange::from(range);
729            // scan for referenced constants such as strings, scan needs to be done recursively
730            let mut pending = vec![print_range];
731            let mut seen: BTreeSet<URange> = BTreeSet::new();
732
733            // Let's define a constant as a label followed by one or more data declarations
734            let constants = lines
735                .iter()
736                .enumerate()
737                .filter_map(|(ix, stmt)| {
738                    let Statement::Label(Label { id, .. }) = stmt else {
739                        return None;
740                    };
741                    matches!(
742                        lines.get(ix + 1),
743                        Some(Statement::Directive(Directive::Data(_, _)))
744                    )
745                    .then_some((*id, ix))
746                })
747                .collect::<BTreeMap<_, _>>();
748            while let Some(subset) = pending.pop() {
749                seen.insert(subset);
750                for s in &lines[subset] {
751                    if let Statement::Instruction(Instruction {
752                        args: Some(arg), ..
753                    })
754                    | Statement::Directive(Directive::Generic(GenericDirective(arg))) = s
755                    {
756                        for label in crate::demangle::local_labels(arg) {
757                            if let Some(constant_range) = scan_constant(label, &constants, lines) {
758                                if !seen.contains(&constant_range)
759                                    && !print_range.fully_contains(constant_range)
760                                {
761                                    pending.push(constant_range);
762                                }
763                            }
764                        }
765                    }
766                }
767            }
768            seen.remove(&print_range);
769            for range in &seen {
770                res.push(range.start..range.end);
771            }
772        }
773
774        if fmt.simplify {
775            res.retain(|range| {
776                lines[range.start..range.end]
777                    .iter()
778                    .any(|s| !(s.boring() || matches!(s, Statement::Nothing | Statement::Label(_))))
779            });
780        }
781
782        res
783    }
784}