panicgraph 0.2.1

Reports which functions can panic, why, and through what call path.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
//! Walks MIR and records what each function can panic with, and who it calls.

use panicgraph::{
    Body, CallSite, Category, EdgeKind, FuncKey, Guard, Loc, OPEN_PREFIX,
    PanicSite, Reified, Termination, UnwindOrigin,
    util::{Map, Set},
};
use rustc_hir::def_id::DefId;
use rustc_middle::{
    middle::codegen_fn_attrs::CodegenFnAttrFlags,
    mir::{self, AssertKind, BasicBlock, TerminatorKind, UnwindAction},
    ty::{
        self, Instance, TyCtxt, TypeVisitableExt, TypingEnv,
        print::with_no_trimmed_paths,
    },
};
use rustc_span::Spanned;

use self::{
    flow::{resuming, unavoidable, written_in},
    sites::classify_assert,
};
use crate::{
    fold,
    read::instantiate,
    sinks::{Sink, SinkTable},
    summary::Cache,
};

mod candidates;
mod flow;
mod sites;

/// One function to analyse, together with the environment its generic
/// arguments belong to.
///
/// A callee resolved from a generic caller carries that caller's parameters,
/// so the two travel together: normalizing the callee's types demands the
/// environment those parameters were declared in.
#[derive(Clone, Copy)]
struct Work<'tcx> {
    inst: Instance<'tcx>,
    env: TypingEnv<'tcx>,
}

/// Where a terminator sits, and where unwinding out of it lands.
///
/// The three travel together from the moment a terminator is read until the
/// entry it produces is recorded, so they are carried as one value.
#[derive(Clone, Copy)]
struct At {
    bb: BasicBlock,
    unwind: UnwindAction,
    span: rustc_span::Span,
    scope: mir::SourceScope,
}

impl At {
    const fn new(
        bb: BasicBlock,
        unwind: UnwindAction,
        info: mir::SourceInfo,
    ) -> Self {
        Self {
            bb,
            unwind,
            span: info.span,
            scope: info.scope,
        }
    }
}

/// Entries collected from one body before reachability guards are attached.
#[derive(Default)]
struct Raw<'tcx> {
    sites: Vec<PanicSite>,
    site_blocks: Vec<BasicBlock>,
    /// Whether each site raises whenever its block runs, rather than only
    /// when a check in the block fails.
    site_fires: Vec<bool>,
    calls: Vec<CallSite>,
    call_blocks: Vec<BasicBlock>,
    unwind_edges: Vec<(UnwindOrigin, BasicBlock)>,
    successors: Vec<Work<'tcx>>,
    /// Which blocks can unwind out of the body, by block index.
    resuming: Vec<bool>,
}

impl Raw<'_> {
    /// Appends a panic site and the cleanup path unwinding out of it
    /// reaches.
    ///
    /// The origin names the entry by its position, so the index has to be
    /// read before the push, and every caller has to go through here for
    /// the two vectors to stay in step.
    fn add_site(&mut self, at: At, mut site: PanicSite, fires: bool) {
        let index = u32::try_from(self.sites.len()).unwrap_or(u32::MAX);
        site.terminates = !self.leaves(at.unwind);
        self.sites.push(site);
        self.site_blocks.push(at.bb);
        self.site_fires.push(fires);
        self.record_unwind(UnwindOrigin::Site(index), at.unwind);
    }

    /// Appends a call edge and the cleanup path unwinding out of it reaches.
    fn add_call(&mut self, at: At, mut call: CallSite) {
        let index = u32::try_from(self.calls.len()).unwrap_or(u32::MAX);
        call.terminates = !self.leaves(at.unwind);
        self.calls.push(call);
        self.call_blocks.push(at.bb);
        self.record_unwind(UnwindOrigin::Call(index), at.unwind);
    }

    /// Whether unwinding out of a terminator can leave the function.
    ///
    /// The compiler aborts instead in a function that must not unwind, in a
    /// cleanup block, and on a cleanup path that never resumes.
    fn leaves(&self, unwind: UnwindAction) -> bool {
        match unwind {
            UnwindAction::Continue => true,
            UnwindAction::Cleanup(target) => self
                .resuming
                .get(target.as_usize())
                .copied()
                .unwrap_or(true),
            UnwindAction::Unreachable | UnwindAction::Terminate(_) => false,
        }
    }

    /// Notes that unwinding from `origin` transfers control to a cleanup
    /// block.
    fn record_unwind(&mut self, origin: UnwindOrigin, unwind: UnwindAction) {
        if let UnwindAction::Cleanup(target) = unwind {
            self.unwind_edges.push((origin, target));
        }
    }
}

/// Collects panic facts for every function reachable from a crate's roots.
pub struct Extractor<'tcx> {
    tcx: TyCtxt<'tcx>,
    sinks: SinkTable,
    /// What folding each callee against each set of claims found, shared
    /// by every body so a chain of calls is read once rather than at every
    /// site that reaches it.
    cache: Cache<'tcx>,
    /// The name of the library the package under analysis builds, which
    /// is what a test crate's instantiations of its generic functions are
    /// recognised by.
    package_lib: Option<String>,
    /// Whether the crate being compiled is an integration test or a bench,
    /// which links the library rather than being it.
    integration: bool,
    /// Whether panics unwind in this build. Under `panic = "abort"` no
    /// cleanup runs and no catch contains anything.
    unwinds: bool,
    bodies: Vec<Body>,
    seen: Set<String>,
    reified: Vec<Reified>,
    reified_seen: Set<(FuncKey, String)>,
    /// The types reachable code makes into trait objects.
    coerced: Set<String>,
}

/// What the extractor found in one crate.
pub struct Extraction {
    /// Every function body observed.
    pub bodies: Vec<Body>,
    /// Every function observed being reified to a pointer.
    pub reified: Vec<Reified>,
    /// Every type observed being made into a trait object.
    pub coerced: Vec<String>,
}

impl<'tcx> Extractor<'tcx> {
    /// Prepares an extractor for one compilation.
    pub fn new(tcx: TyCtxt<'tcx>) -> Self {
        Self {
            tcx,
            sinks: SinkTable::default(),
            cache: Cache::default(),
            package_lib: std::env::var("CARGO_PKG_NAME")
                .ok()
                .map(|name| name.replace('-', "_")),
            // Cargo sets this for integration tests and benches alone.
            integration: std::env::var_os("CARGO_TARGET_TMPDIR").is_some(),
            unwinds: tcx.sess.panic_strategy().unwinds(),
            bodies: Vec::new(),
            seen: Set::default(),
            reified: Vec::new(),
            reified_seen: Set::default(),
            coerced: Set::default(),
        }
    }

    /// Walks the whole reachable call graph and returns what it found.
    pub fn run(mut self) -> Extraction {
        let mut queue: Vec<Work<'tcx>> = self.roots();
        // Every instance is recorded in `seen` before its callees are
        // queued, so each function is expanded at most once and the walk
        // terminates once the reachable set is exhausted.
        while let Some(work) = queue.pop() {
            let Some(key) = self.symbol_of(work.inst) else {
                continue;
            };
            if !self.seen.insert(key.clone()) {
                continue;
            }
            queue.extend(self.build(work, FuncKey(key)));
        }
        let mut coerced: Vec<String> = self.coerced.into_iter().collect();
        // Sorted so two runs describe one build the same way.
        coerced.sort();
        Extraction {
            bodies: self.bodies,
            reified: self.reified,
            coerced,
        }
    }

    /// Every function defined in the crate under compilation.
    fn roots(&self) -> Vec<Work<'tcx>> {
        let mut out = Vec::new();
        for local in self.tcx.mir_keys(()) {
            let did = local.to_def_id();
            if !self.tcx.is_mir_available(did) {
                continue;
            }
            if !matches!(
                self.tcx.def_kind(did),
                rustc_hir::def::DefKind::Fn
                    | rustc_hir::def::DefKind::AssocFn
                    | rustc_hir::def::DefKind::Closure
            ) {
                continue;
            }
            // Generic items are analysed as written. Their callees often
            // cannot be resolved without concrete arguments, which is
            // recorded honestly as an unresolved edge rather than silently
            // dropping the function from the report.
            let args = ty::GenericArgs::identity_for_item(self.tcx, did);
            out.push(Work {
                inst: Instance::new_raw(did, args),
                env: TypingEnv::post_analysis(self.tcx, did),
            });
        }
        out
    }

    /// Records one function and returns the callees worth expanding.
    fn build(&mut self, work: Work<'tcx>, key: FuncKey) -> Vec<Work<'tcx>> {
        let inst = work.inst;
        let did = inst.def_id();
        let display = self.display_of(did);
        let krate = self.tcx.crate_name(did.krate).to_string();
        if !Self::has_mir_body(self.tcx, inst) {
            let mut body = Body::opaque(key, display, krate);
            // Foreign code has no Rust body to read and never will, so it is
            // reported apart from a Rust function a fuller standard library
            // would have shown.
            body.foreign = self.tcx.is_foreign_item(did);
            // A body the compiler could not produce is still recorded
            // against the crate that declares it, so the two facts have to
            // agree: a foreign item declared here reports the local crate
            // name, and saying it is not local contradicts that.
            body.local = self.reported(inst);
            body.from_tests = self.tcx.sess.opts.test;
            // A function the compiler guarantees does not unwind raises no
            // panic even though its body is unavailable. Allocator shims are
            // the common case.
            body.opaque = !self.never_unwinds(did);
            self.bodies.push(body);
            return Vec::new();
        }

        let mir = self.tcx.instance_mir(inst.def);
        let raw = self.scan(work, mir);
        let origins = Self::propagate_origins(mir, &raw.unwind_edges);

        let mut sites = raw.sites;
        for (site, bb) in sites.iter_mut().zip(&raw.site_blocks) {
            site.guard = Self::guard_for(mir, &origins, *bb);
        }
        let mut calls = raw.calls;
        for (call, bb) in calls.iter_mut().zip(&raw.call_blocks) {
            call.guard = Self::guard_for(mir, &origins, *bb);
        }

        self.bodies.push(Body {
            key,
            display,
            krate,
            loc: self.loc_of(self.tcx.def_span(did)),
            sites,
            calls,
            opaque: false,
            foreign: false,
            local: self.reported(inst),
            from_tests: self.tcx.sess.opts.test,
        });
        raw.successors
    }

    /// Whether a function is reported as the crate under analysis's own.
    ///
    /// In a test crate the functions defined locally are the tests and the
    /// crate's own code compiled again for them, and neither is what was
    /// asked about: the second is already reported from the build that is.
    /// What such a crate adds is the instantiations it makes of the
    /// library's generic functions, which the library's own build could
    /// only read as written. Those belong to the library, whether it is
    /// compiled again for its unit tests or linked by an integration test,
    /// and are reported as its own. A closure is judged by the function it
    /// is written in, so one inside a test stays out. An integration test
    /// may be named after the package it tests, so there the library is
    /// only ever the crate linked in, never the test crate itself. A generic
    /// body as written is the library build's to report.
    fn reported(&self, inst: Instance<'tcx>) -> bool {
        let did = inst.def_id();
        if !self.tcx.sess.opts.test {
            return did.is_local();
        }
        if (self.integration && did.is_local()) || inst.args.has_param() {
            return false;
        }
        let root = self.tcx.typeck_root_def_id(did);
        self.of_package(did)
            && self
                .tcx
                .generics_of(root)
                .requires_monomorphization(self.tcx)
    }

    /// Whether a function belongs to the library the package builds.
    fn of_package(&self, did: DefId) -> bool {
        self.package_lib.as_deref()
            == Some(self.tcx.crate_name(did.krate).as_str())
    }

    /// The path a function reports under.
    ///
    /// A function of the library reached from one of its test crates is
    /// named the way the library's own build names it, without the crate
    /// in front of its paths, so the two reports of one function fall
    /// under one name.
    fn display_of(&self, did: DefId) -> String {
        let path = self.tcx.def_path_str(did);
        if did.is_local() || !self.of_package(did) {
            return path;
        }
        match &self.package_lib {
            Some(lib) => path.replace(&format!("{lib}::"), ""),
            None => path,
        }
    }

    /// The environment types in this body must be normalized against.
    ///
    /// A body still carrying generic parameters has to be read in the
    /// environment those parameters were declared in, which is the caller's,
    /// not the callee's: a trait method resolved from a generic caller knows
    /// only its own `Self`, so normalizing the caller's parameters there asks
    /// the compiler about parameters it has never heard of.
    fn env_for(work: Work<'tcx>) -> TypingEnv<'tcx> {
        if work.inst.args.has_param() {
            work.env
        } else {
            TypingEnv::fully_monomorphized()
        }
    }

    /// Reads every terminator of a body into raw entries.
    fn scan(&mut self, work: Work<'tcx>, mir: &mir::Body<'tcx>) -> Raw<'tcx> {
        let cx = Work {
            inst: work.inst,
            env: Self::env_for(work),
        };
        let reach =
            fold::reachable(self.tcx, cx.inst, cx.env, mir, &mut self.cache);
        let mut raw = Raw {
            resuming: resuming(mir),
            ..Raw::default()
        };
        for (bb, data) in mir.basic_blocks.iter_enumerated() {
            if !reach.is_live(bb) {
                continue;
            }
            for stmt in &data.statements {
                self.note_reified(&mut raw, cx, stmt, mir);
                self.note_coerced(cx, stmt, mir);
            }
            let Some(term) = &data.terminator else {
                continue;
            };
            let info = term.source_info;
            match &term.kind {
                TerminatorKind::Assert { msg, unwind, .. } => {
                    if reach.is_settled(bb) {
                        // The condition holds for these generic arguments,
                        // so the compiler emits no check at all.
                        continue;
                    }
                    let at = At::new(bb, self.unwind_of(*unwind), info);
                    self.push_assert(&mut raw, at, msg, reach.is_failing(bb));
                }
                TerminatorKind::Call {
                    func,
                    args,
                    fn_span,
                    ..
                }
                | TerminatorKind::TailCall {
                    func,
                    args,
                    fn_span,
                    ..
                } => {
                    if reach.is_quiet(bb) {
                        // The callee was walked with the arguments this
                        // call makes and found unable to raise. It runs no
                        // other body under them either, so nothing below it
                        // is reachable through this edge.
                        continue;
                    }
                    // A tail call replaces this frame, so an unwind out of
                    // the callee goes straight on to the caller.
                    let unwind = term.kind.unwind().copied();
                    let unwind = unwind.unwrap_or(UnwindAction::Continue);
                    let mut info = info;
                    info.span = *fn_span;
                    let at = At::new(bb, self.unwind_of(unwind), info);
                    let ty = func.ty(&mir.local_decls, self.tcx);
                    self.push_call(&mut raw, cx, at, ty, args, mir);
                }
                TerminatorKind::Drop { place, unwind, .. } => {
                    let at = At::new(bb, self.unwind_of(*unwind), info);
                    let ty = place.ty(&mir.local_decls, self.tcx).ty;
                    self.push_drop(&mut raw, cx, at, ty);
                }
                // Only assembly declared `may_unwind` has somewhere to
                // unwind to, and its code cannot be read.
                TerminatorKind::InlineAsm { unwind, .. }
                    if !matches!(unwind, UnwindAction::Unreachable) =>
                {
                    let at = At::new(bb, self.unwind_of(*unwind), info);
                    self.push_assembly(&mut raw, at);
                }
                _ => {}
            }
        }
        Self::settle_certain(&mut raw, mir, &reach);
        raw
    }

    /// Where unwinding out of a terminator goes in this build.
    ///
    /// A standard library body is built to unwind even when this build
    /// aborts, and then its cleanup never runs.
    const fn unwind_of(&self, unwind: UnwindAction) -> UnwindAction {
        if self.unwinds {
            unwind
        } else {
            UnwindAction::Unreachable
        }
    }

    /// Marks the sites every execution of the body raises at.
    ///
    /// A site is certain when it raises whenever its block runs and no
    /// execution gets round the block: no path from the entry reaches a
    /// way out of the body without passing through it, and no path can go
    /// round in a loop instead, which is as far as a walk that does not
    /// prove loops terminate can go.
    fn settle_certain(
        raw: &mut Raw<'tcx>,
        mir: &mir::Body<'tcx>,
        reach: &fold::Reach,
    ) {
        let mut raises = vec![false; mir.basic_blocks.len()];
        for bb in &raw.site_blocks {
            if let Some(slot) = raises.get_mut(bb.as_usize()) {
                *slot = true;
            }
        }
        for (index, site) in raw.sites.iter_mut().enumerate() {
            let fires = raw.site_fires.get(index).copied().unwrap_or(false);
            let Some(&bb) = raw.site_blocks.get(index) else {
                continue;
            };
            // A cleanup block runs only while another panic unwinds, so a
            // site in one is never the whole story of a call.
            site.certain = fires
                && !mir.basic_blocks[bb].is_cleanup
                && unavoidable(mir, reach, bb, &raises);
        }
    }

    /// Records inline assembly that may unwind as code with no Rust body.
    fn push_assembly(&self, raw: &mut Raw<'tcx>, at: At) {
        let site = PanicSite {
            category: Category::Foreign,
            termination: Termination::Unwind,
            reason: "runs inline assembly that may unwind".to_owned(),
            sink: None,
            loc: self.loc_of(at.span),
            guard: Guard::default(),
            certain: false,
            terminates: false,
        };
        raw.add_site(at, site, false);
    }

    /// Records a compiler inserted check as a panic site.
    fn push_assert<O>(
        &self,
        raw: &mut Raw<'tcx>,
        at: At,
        msg: &AssertKind<O>,
        fails: bool,
    ) {
        if !self.tcx.sess.overflow_checks() && msg.is_optional_overflow_check()
        {
            // Codegen drops these outright in a build without overflow
            // checks: the arithmetic wraps instead. They survive in the MIR
            // only because a function marked to inherit the setting is built
            // once and used by crates that disagree about it.
            return;
        }
        let (category, termination, reason) = classify_assert(msg);
        let site = PanicSite {
            category,
            termination,
            reason: reason.to_owned(),
            sink: None,
            loc: self.loc_of(at.span),
            guard: Guard::default(),
            certain: false,
            terminates: false,
        };
        raw.add_site(at, site, fails);
    }

    /// Records a call, either as a panic site or as a graph edge.
    fn push_call(
        &mut self,
        raw: &mut Raw<'tcx>,
        cx: Work<'tcx>,
        at: At,
        ty: ty::Ty<'tcx>,
        operands: &[Spanned<mir::Operand<'tcx>>],
        mir: &mir::Body<'tcx>,
    ) {
        let Some(ty) = self.normalize(cx, ty) else {
            self.unresolved(raw, at, "<unresolved>".to_owned());
            return;
        };
        let ty::FnDef(did, args) = *ty.kind() else {
            // A call through a function pointer. The target set is unknown,
            // but the signature narrows which reified functions could be
            // behind it.
            let site = CallSite {
                callee: None,
                callee_display: "<fn pointer>".to_owned(),
                kind: EdgeKind::FnPtr,
                loc: self.loc_of(at.span),
                guard: Guard::default(),
                barrier: false,
                terminates: false,
                candidate: false,
                sig: Some(format!("{ty}")),
                self_ty: None,
            };
            raw.add_call(at, site);
            return;
        };
        let Some(args) = args.no_bound_vars() else {
            self.generic(raw, at, self.tcx.def_path_str(did));
            return;
        };
        let callee = match Instance::try_resolve(self.tcx, cx.env, did, args) {
            // Not enough is known yet: the target exists only once a caller
            // supplies concrete arguments, which is that caller's choice.
            Ok(None) => {
                self.generic(raw, at, self.tcx.def_path_str(did));
                return;
            }
            Err(_) => {
                self.unresolved(raw, at, self.tcx.def_path_str(did));
                return;
            }
            Ok(Some(callee)) => callee,
        };

        if let Some(sink) = self.sinks.get(self.tcx, callee.def_id()) {
            let origin = written_in(cx, at, mir);
            let sink = self.refine_unwrap(cx, origin, sink);
            let funnel = SinkTable::funnel(self.tcx, origin.def_id())
                .map(Sink::category);
            self.push_sink(raw, cx, (at, funnel), callee, operands, sink);
            return;
        }

        if matches!(
            callee.def,
            ty::InstanceKind::Intrinsic(..)
                | ty::InstanceKind::LlvmIntrinsic(..)
        ) {
            self.push_intrinsic(raw, cx, at, callee, operands, mir);
            return;
        }
        let kind = match callee.def {
            ty::InstanceKind::Virtual(..) => EdgeKind::Vtable,
            _ => EdgeKind::Static,
        };
        let display = self.tcx.def_path_str(callee.def_id());
        let key = self.symbol_of(callee).map(FuncKey);
        self.push_edge(raw, at, key, display, kind, false);
        if kind == EdgeKind::Static {
            raw.successors.push(Work {
                inst: callee,
                env: cx.env,
            });
        } else {
            self.push_dyn_candidates(raw, cx, at, callee);
        }
    }

    /// Resolves a type written in a body against the arguments it was
    /// reached with.
    fn normalize(
        &self,
        cx: Work<'tcx>,
        ty: ty::Ty<'tcx>,
    ) -> Option<ty::Ty<'tcx>> {
        instantiate(self.tcx, cx.inst, cx.env, ty)
    }

    /// Appends an edge to a target the analysis could not pin down.
    fn unresolved(&self, raw: &mut Raw<'tcx>, at: At, display: String) {
        self.push_edge(raw, at, None, display, EdgeKind::Unresolved, false);
    }

    /// Appends an edge that resolves only once a caller chooses arguments.
    fn generic(&self, raw: &mut Raw<'tcx>, at: At, display: String) {
        self.push_edge(raw, at, None, display, EdgeKind::Generic, false);
    }

    /// Appends a call edge and its unwind channel.
    fn push_edge(
        &self,
        raw: &mut Raw<'tcx>,
        at: At,
        callee: Option<FuncKey>,
        callee_display: String,
        kind: EdgeKind,
        barrier: bool,
    ) {
        let site = CallSite {
            callee,
            callee_display,
            kind,
            loc: self.loc_of(at.span),
            guard: Guard::default(),
            barrier,
            terminates: false,
            candidate: false,
            sig: None,
            self_ty: None,
        };
        raw.add_call(at, site);
    }

    /// Marks every cleanup block reachable from each unwind edge.
    fn propagate_origins(
        mir: &mir::Body<'_>,
        edges: &[(UnwindOrigin, BasicBlock)],
    ) -> Map<BasicBlock, Vec<UnwindOrigin>> {
        let mut out: Map<BasicBlock, Vec<UnwindOrigin>> = Map::default();
        // Every edge walks the same body, so the two scratch buffers are
        // reused rather than rebuilt once per edge.
        let mut seen: Set<BasicBlock> = Set::default();
        let mut stack: Vec<BasicBlock> = Vec::new();
        for (origin, start) in edges {
            seen.clear();
            stack.clear();
            stack.push(*start);
            // `seen` admits each block once, so the walk is bounded by the
            // number of basic blocks in the body.
            while let Some(bb) = stack.pop() {
                if !seen.insert(bb) {
                    continue;
                }
                let list = out.entry(bb).or_default();
                if !list.contains(origin) {
                    list.push(*origin);
                }
                let Some(term) = &mir.basic_blocks[bb].terminator else {
                    continue;
                };
                stack.extend(term.successors());
            }
        }
        out
    }

    /// Builds the reachability guard for one basic block.
    fn guard_for(
        mir: &mir::Body<'_>,
        origins: &Map<BasicBlock, Vec<UnwindOrigin>>,
        bb: BasicBlock,
    ) -> Guard {
        Guard {
            normal: !mir.basic_blocks[bb].is_cleanup,
            origins: origins.get(&bb).cloned().unwrap_or_default(),
        }
    }

    /// Whether the compiler guarantees a function cannot unwind.
    fn never_unwinds(&self, did: rustc_hir::def_id::DefId) -> bool {
        self.tcx
            .codegen_fn_attrs(did)
            .flags
            .contains(CodegenFnAttrFlags::NEVER_UNWIND)
    }

    /// Whether the compiler can produce a body for this instance.
    fn has_mir_body(tcx: TyCtxt<'tcx>, inst: Instance<'tcx>) -> bool {
        match inst.def {
            ty::InstanceKind::Item(def) => tcx.is_mir_available(def),
            ty::InstanceKind::Intrinsic(..)
            | ty::InstanceKind::LlvmIntrinsic(..)
            | ty::InstanceKind::Virtual(..) => false,
            ty::InstanceKind::Shim(_) => true,
        }
    }

    /// The globally unique key for an instance.
    fn symbol_of(&self, inst: Instance<'tcx>) -> Option<String> {
        if matches!(inst.def, ty::InstanceKind::Virtual(..)) {
            return None;
        }
        if inst.args.has_param() {
            // A symbol name only exists once the generic arguments are
            // concrete, so a generic body is keyed by its crate, its
            // disambiguated path, and its arguments instead. Two crates may
            // each define `parse<T>`, and `f::<U>` and `f::<Wrapper<T>>`
            // resolve their calls differently.
            let did = inst.def_id();
            let rendered = with_no_trimmed_paths!(inst.to_string());
            return Some(format!(
                "{OPEN_PREFIX}{}[{:016x}]{} {rendered}",
                self.tcx.crate_name(did.krate),
                self.tcx.stable_crate_id(did.krate).as_u64(),
                self.tcx.def_path(did).to_string_no_crate_verbose(),
            ));
        }
        Some(self.tcx.symbol_name(inst).name.to_owned())
    }

    /// Converts a span into a source location.
    fn loc_of(&self, span: rustc_span::Span) -> Option<Loc> {
        if span.is_dummy() {
            return None;
        }
        let map = self.tcx.sess.source_map();
        let pos = map.lookup_char_pos(span.lo());
        Some(Loc {
            file: map.filename_for_diagnostics(&pos.file.name).to_string(),
            line: u32::try_from(pos.line).unwrap_or(0),
            col: pos.col.0.saturating_add(1).try_into().unwrap_or(0),
        })
    }
}