Skip to main content

rustc_borrowck/polonius/
dump.rs

1use std::io;
2
3use rustc_data_structures::fx::{FxHashSet, FxIndexMap, FxIndexSet};
4use rustc_index::IndexVec;
5use rustc_middle::mir::pretty::{MirDumper, PassWhere, PrettyPrintMirOptions};
6use rustc_middle::mir::{Body, Location};
7use rustc_middle::ty::{RegionVid, TyCtxt};
8use rustc_mir_dataflow::points::PointIndex;
9use rustc_session::config::MirIncludeSpans;
10
11use crate::borrow_set::BorrowSet;
12use crate::constraints::OutlivesConstraint;
13use crate::polonius::{LocalizedConstraintGraphVisitor, LocalizedNode, PoloniusContext};
14use crate::region_infer::values::LivenessValues;
15use crate::type_check::Locations;
16use crate::{BorrowckInferCtxt, ClosureRegionRequirements, RegionInferenceContext};
17
18/// `-Zdump-mir=polonius` dumps MIR annotated with NLL and polonius specific information.
19pub(crate) fn dump_polonius_mir<'tcx>(
20    infcx: &BorrowckInferCtxt<'tcx>,
21    body: &Body<'tcx>,
22    regioncx: &RegionInferenceContext<'tcx>,
23    closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
24    borrow_set: &BorrowSet<'tcx>,
25    polonius_context: Option<&PoloniusContext>,
26) {
27    let tcx = infcx.tcx;
28    if !tcx.sess.opts.unstable_opts.polonius.is_next_enabled() {
29        return;
30    }
31
32    let Some(dumper) = MirDumper::new(tcx, "polonius", body) else { return };
33
34    let polonius_context =
35        polonius_context.expect("missing polonius context with `-Zpolonius=next`");
36
37    // If we have a polonius graph to dump along the rest of the MIR and NLL info, we extract its
38    // constraints here.
39    let mut collector = LocalizedOutlivesConstraintCollector { constraints: Vec::new() };
40    if let Some(graph) = &polonius_context.graph {
41        graph.traverse(
42            body,
43            regioncx.liveness_constraints(),
44            &polonius_context.live_region_variances,
45            regioncx.universal_regions(),
46            borrow_set,
47            &mut collector,
48        );
49    }
50
51    let extra_data = &|pass_where, out: &mut dyn io::Write| {
52        emit_polonius_mir(
53            tcx,
54            regioncx,
55            closure_region_requirements,
56            borrow_set,
57            &collector.constraints,
58            pass_where,
59            out,
60        )
61    };
62    // We want the NLL extra comments printed by default in NLL MIR dumps. Specifying `-Z
63    // mir-include-spans` on the CLI still has priority.
64    let options = PrettyPrintMirOptions {
65        include_extra_comments: #[allow(non_exhaustive_omitted_patterns)] match tcx.sess.opts.unstable_opts.mir_include_spans
    {
    MirIncludeSpans::On | MirIncludeSpans::Nll => true,
    _ => false,
}matches!(
66            tcx.sess.opts.unstable_opts.mir_include_spans,
67            MirIncludeSpans::On | MirIncludeSpans::Nll
68        ),
69    };
70
71    let dumper = dumper.set_extra_data(extra_data).set_options(options);
72
73    let _ = try {
74        let mut file = dumper.create_dump_file("html", body)?;
75        emit_polonius_dump(&dumper, body, regioncx, borrow_set, &collector.constraints, &mut file)?;
76    };
77}
78
79/// The constraints we'll dump as text or a mermaid graph.
80struct LocalizedOutlivesConstraint {
81    source: RegionVid,
82    from: PointIndex,
83    target: RegionVid,
84    to: PointIndex,
85}
86
87/// Visitor to record constraints encountered when traversing the localized constraint graph.
88struct LocalizedOutlivesConstraintCollector {
89    constraints: Vec<LocalizedOutlivesConstraint>,
90}
91
92impl LocalizedConstraintGraphVisitor for LocalizedOutlivesConstraintCollector {
93    fn on_successor_discovered(&mut self, current_node: LocalizedNode, successor: LocalizedNode) {
94        self.constraints.push(LocalizedOutlivesConstraint {
95            source: current_node.region,
96            from: current_node.point,
97            target: successor.region,
98            to: successor.point,
99        });
100    }
101}
102
103/// The polonius dump consists of:
104/// - the NLL MIR
105/// - the list of polonius localized constraints
106/// - a mermaid graph of the CFG
107/// - a mermaid graph of the NLL regions and the constraints between them
108/// - a mermaid graph of the NLL SCCs and the constraints between them
109fn emit_polonius_dump<'tcx>(
110    dumper: &MirDumper<'_, '_, 'tcx>,
111    body: &Body<'tcx>,
112    regioncx: &RegionInferenceContext<'tcx>,
113    borrow_set: &BorrowSet<'tcx>,
114    localized_outlives_constraints: &[LocalizedOutlivesConstraint],
115    out: &mut dyn io::Write,
116) -> io::Result<()> {
117    // Prepare the HTML dump file prologue.
118    out.write_fmt(format_args!("<!DOCTYPE html>\n"))writeln!(out, "<!DOCTYPE html>")?;
119    out.write_fmt(format_args!("<html>\n"))writeln!(out, "<html>")?;
120    out.write_fmt(format_args!("<head><title>Polonius MIR dump</title></head>\n"))writeln!(out, "<head><title>Polonius MIR dump</title></head>")?;
121    out.write_fmt(format_args!("<body>\n"))writeln!(out, "<body>")?;
122
123    // Section 1: the NLL + Polonius MIR.
124    out.write_fmt(format_args!("<div>\n"))writeln!(out, "<div>")?;
125    out.write_fmt(format_args!("Raw MIR dump\n"))writeln!(out, "Raw MIR dump")?;
126    out.write_fmt(format_args!("<pre><code>\n"))writeln!(out, "<pre><code>")?;
127    emit_html_mir(dumper, body, out)?;
128    out.write_fmt(format_args!("</code></pre>\n"))writeln!(out, "</code></pre>")?;
129    out.write_fmt(format_args!("</div>\n"))writeln!(out, "</div>")?;
130
131    // Section 2: mermaid visualization of the polonius constraint graph.
132    out.write_fmt(format_args!("<div>\n"))writeln!(out, "<div>")?;
133    out.write_fmt(format_args!("Polonius constraint graph\n"))writeln!(out, "Polonius constraint graph")?;
134    out.write_fmt(format_args!("<pre class=\'mermaid\'>\n"))writeln!(out, "<pre class='mermaid'>")?;
135    let edge_count = emit_mermaid_constraint_graph(
136        borrow_set,
137        regioncx.liveness_constraints(),
138        &localized_outlives_constraints,
139        out,
140    )?;
141    out.write_fmt(format_args!("</pre>\n"))writeln!(out, "</pre>")?;
142    out.write_fmt(format_args!("</div>\n"))writeln!(out, "</div>")?;
143
144    // Section 3: mermaid visualization of the CFG.
145    out.write_fmt(format_args!("<div>\n"))writeln!(out, "<div>")?;
146    out.write_fmt(format_args!("Control-flow graph\n"))writeln!(out, "Control-flow graph")?;
147    out.write_fmt(format_args!("<pre class=\'mermaid\'>\n"))writeln!(out, "<pre class='mermaid'>")?;
148    emit_mermaid_cfg(body, out)?;
149    out.write_fmt(format_args!("</pre>\n"))writeln!(out, "</pre>")?;
150    out.write_fmt(format_args!("</div>\n"))writeln!(out, "</div>")?;
151
152    // Section 4: mermaid visualization of the NLL region graph.
153    out.write_fmt(format_args!("<div>\n"))writeln!(out, "<div>")?;
154    out.write_fmt(format_args!("NLL regions\n"))writeln!(out, "NLL regions")?;
155    out.write_fmt(format_args!("<pre class=\'mermaid\'>\n"))writeln!(out, "<pre class='mermaid'>")?;
156    emit_mermaid_nll_regions(dumper.tcx(), regioncx, out)?;
157    out.write_fmt(format_args!("</pre>\n"))writeln!(out, "</pre>")?;
158    out.write_fmt(format_args!("</div>\n"))writeln!(out, "</div>")?;
159
160    // Section 5: mermaid visualization of the NLL SCC graph.
161    out.write_fmt(format_args!("<div>\n"))writeln!(out, "<div>")?;
162    out.write_fmt(format_args!("NLL SCCs\n"))writeln!(out, "NLL SCCs")?;
163    out.write_fmt(format_args!("<pre class=\'mermaid\'>\n"))writeln!(out, "<pre class='mermaid'>")?;
164    emit_mermaid_nll_sccs(dumper.tcx(), regioncx, out)?;
165    out.write_fmt(format_args!("</pre>\n"))writeln!(out, "</pre>")?;
166    out.write_fmt(format_args!("</div>\n"))writeln!(out, "</div>")?;
167
168    // Finalize the dump with the HTML epilogue.
169    out.write_fmt(format_args!("<script src=\'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js\'></script>\n"))writeln!(
170        out,
171        "<script src='https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js'></script>"
172    )?;
173    out.write_fmt(format_args!("<script>\n"))writeln!(out, "<script>")?;
174    out.write_fmt(format_args!("mermaid.initialize({{ startOnLoad: false, maxEdges: {0} }});\n",
        edge_count.max(100)))writeln!(
175        out,
176        "mermaid.initialize({{ startOnLoad: false, maxEdges: {} }});",
177        edge_count.max(100),
178    )?;
179    out.write_fmt(format_args!("mermaid.run({{ querySelector: \'.mermaid\' }})\n"))writeln!(out, "mermaid.run({{ querySelector: '.mermaid' }})")?;
180    out.write_fmt(format_args!("</script>\n"))writeln!(out, "</script>")?;
181    out.write_fmt(format_args!("</body>\n"))writeln!(out, "</body>")?;
182    out.write_fmt(format_args!("</html>\n"))writeln!(out, "</html>")?;
183
184    Ok(())
185}
186
187/// Emits the polonius MIR, as escaped HTML.
188fn emit_html_mir<'tcx>(
189    dumper: &MirDumper<'_, '_, 'tcx>,
190    body: &Body<'tcx>,
191    out: &mut dyn io::Write,
192) -> io::Result<()> {
193    // Buffer the regular MIR dump to be able to escape it.
194    let mut buffer = Vec::new();
195
196    dumper.dump_mir_to_writer(body, &mut buffer)?;
197
198    // Escape the handful of characters that need it. We don't need to be particularly efficient:
199    // we're actually writing into a buffered writer already. Note that MIR dumps are valid UTF-8.
200    let buffer = String::from_utf8_lossy(&buffer);
201    for ch in buffer.chars() {
202        let escaped = match ch {
203            '>' => "&gt;",
204            '<' => "&lt;",
205            '&' => "&amp;",
206            '\'' => "&#39;",
207            '"' => "&quot;",
208            _ => {
209                // The common case, no escaping needed.
210                out.write_fmt(format_args!("{0}", ch))write!(out, "{}", ch)?;
211                continue;
212            }
213        };
214        out.write_fmt(format_args!("{0}", escaped))write!(out, "{}", escaped)?;
215    }
216    Ok(())
217}
218
219/// Produces the actual NLL + Polonius MIR sections to emit during the dumping process.
220fn emit_polonius_mir<'tcx>(
221    tcx: TyCtxt<'tcx>,
222    regioncx: &RegionInferenceContext<'tcx>,
223    closure_region_requirements: &Option<ClosureRegionRequirements<'tcx>>,
224    borrow_set: &BorrowSet<'tcx>,
225    localized_outlives_constraints: &[LocalizedOutlivesConstraint],
226    pass_where: PassWhere,
227    out: &mut dyn io::Write,
228) -> io::Result<()> {
229    // Emit the regular NLL front-matter
230    crate::nll::emit_nll_mir(
231        tcx,
232        regioncx,
233        closure_region_requirements,
234        borrow_set,
235        pass_where,
236        out,
237    )?;
238
239    let liveness = regioncx.liveness_constraints();
240
241    // Add localized outlives constraints
242    match pass_where {
243        PassWhere::BeforeCFG => {
244            if localized_outlives_constraints.len() > 0 {
245                out.write_fmt(format_args!("| Localized constraints\n"))writeln!(out, "| Localized constraints")?;
246
247                for constraint in localized_outlives_constraints {
248                    let LocalizedOutlivesConstraint { source, from, target, to } = constraint;
249                    let from = liveness.location_from_point(*from);
250                    let to = liveness.location_from_point(*to);
251                    out.write_fmt(format_args!("| {0:?} at {1:?} -> {2:?} at {3:?}\n", source,
        from, target, to))writeln!(out, "| {source:?} at {from:?} -> {target:?} at {to:?}")?;
252                }
253                out.write_fmt(format_args!("|\n"))writeln!(out, "|")?;
254            }
255        }
256        _ => {}
257    }
258
259    Ok(())
260}
261
262/// Emits a mermaid flowchart of the CFG blocks and edges, similar to the graphviz version.
263fn emit_mermaid_cfg(body: &Body<'_>, out: &mut dyn io::Write) -> io::Result<()> {
264    use rustc_middle::mir::{TerminatorEdges, TerminatorKind};
265
266    // The mermaid chart type: a top-down flowchart.
267    out.write_fmt(format_args!("flowchart TD\n"))writeln!(out, "flowchart TD")?;
268
269    // Emit the block nodes.
270    for (block_idx, block) in body.basic_blocks.iter_enumerated() {
271        let block_idx = block_idx.as_usize();
272        let cleanup = if block.is_cleanup { " (cleanup)" } else { "" };
273        out.write_fmt(format_args!("{0}[\"bb{0}{1}\"]\n", block_idx, cleanup))writeln!(out, "{block_idx}[\"bb{block_idx}{cleanup}\"]")?;
274    }
275
276    // Emit the edges between blocks, from the terminator edges.
277    for (block_idx, block) in body.basic_blocks.iter_enumerated() {
278        let block_idx = block_idx.as_usize();
279        let terminator = block.terminator();
280        match terminator.edges() {
281            TerminatorEdges::None => {}
282            TerminatorEdges::Single(bb) => {
283                out.write_fmt(format_args!("{1} --> {0}\n", bb.as_usize(), block_idx))writeln!(out, "{block_idx} --> {}", bb.as_usize())?;
284            }
285            TerminatorEdges::Double(bb1, bb2) => {
286                if #[allow(non_exhaustive_omitted_patterns)] match terminator.kind {
    TerminatorKind::FalseEdge { .. } => true,
    _ => false,
}matches!(terminator.kind, TerminatorKind::FalseEdge { .. }) {
287                    out.write_fmt(format_args!("{1} --> {0}\n", bb1.as_usize(), block_idx))writeln!(out, "{block_idx} --> {}", bb1.as_usize())?;
288                    out.write_fmt(format_args!("{1} -- imaginary --> {0}\n", bb2.as_usize(),
        block_idx))writeln!(out, "{block_idx} -- imaginary --> {}", bb2.as_usize())?;
289                } else {
290                    out.write_fmt(format_args!("{1} --> {0}\n", bb1.as_usize(), block_idx))writeln!(out, "{block_idx} --> {}", bb1.as_usize())?;
291                    out.write_fmt(format_args!("{1} -- unwind --> {0}\n", bb2.as_usize(),
        block_idx))writeln!(out, "{block_idx} -- unwind --> {}", bb2.as_usize())?;
292                }
293            }
294            TerminatorEdges::AssignOnReturn { return_, cleanup, .. } => {
295                for to_idx in return_ {
296                    out.write_fmt(format_args!("{1} --> {0}\n", to_idx.as_usize(), block_idx))writeln!(out, "{block_idx} --> {}", to_idx.as_usize())?;
297                }
298
299                if let Some(to_idx) = cleanup {
300                    out.write_fmt(format_args!("{1} -- unwind --> {0}\n", to_idx.as_usize(),
        block_idx))writeln!(out, "{block_idx} -- unwind --> {}", to_idx.as_usize())?;
301                }
302            }
303            TerminatorEdges::SwitchInt { targets, .. } => {
304                for to_idx in targets.all_targets() {
305                    out.write_fmt(format_args!("{1} --> {0}\n", to_idx.as_usize(), block_idx))writeln!(out, "{block_idx} --> {}", to_idx.as_usize())?;
306                }
307            }
308        }
309    }
310
311    Ok(())
312}
313
314/// Emits a region's label: index, universe, external name.
315fn render_region<'tcx>(
316    tcx: TyCtxt<'tcx>,
317    region: RegionVid,
318    regioncx: &RegionInferenceContext<'tcx>,
319    out: &mut dyn io::Write,
320) -> io::Result<()> {
321    let def = regioncx.region_definition(region);
322    let universe = def.universe;
323
324    out.write_fmt(format_args!("\'{0}", region.as_usize()))write!(out, "'{}", region.as_usize())?;
325    if !universe.is_root() {
326        out.write_fmt(format_args!("/{0:?}", universe))write!(out, "/{universe:?}")?;
327    }
328    if let Some(name) = def.external_name.and_then(|e| e.get_name(tcx)) {
329        out.write_fmt(format_args!(" ({0})", name))write!(out, " ({name})")?;
330    }
331    Ok(())
332}
333
334/// Emits a mermaid flowchart of the NLL regions and the outlives constraints between them, similar
335/// to the graphviz version.
336fn emit_mermaid_nll_regions<'tcx>(
337    tcx: TyCtxt<'tcx>,
338    regioncx: &RegionInferenceContext<'tcx>,
339    out: &mut dyn io::Write,
340) -> io::Result<()> {
341    // The mermaid chart type: a top-down flowchart.
342    out.write_fmt(format_args!("flowchart TD\n"))writeln!(out, "flowchart TD")?;
343
344    // Emit the region nodes.
345    for region in regioncx.definitions.indices() {
346        out.write_fmt(format_args!("{0}[\"", region.as_usize()))write!(out, "{}[\"", region.as_usize())?;
347        render_region(tcx, region, regioncx, out)?;
348        out.write_fmt(format_args!("\"]\n"))writeln!(out, "\"]")?;
349    }
350
351    // Get a set of edges to check for the reverse edge being present.
352    let edges: FxHashSet<_> = regioncx.outlives_constraints().map(|c| (c.sup, c.sub)).collect();
353
354    // Order (and deduplicate) edges for traversal, to display them in a generally increasing order.
355    let constraint_key = |c: &OutlivesConstraint<'_>| {
356        let min = c.sup.min(c.sub);
357        let max = c.sup.max(c.sub);
358        (min, max)
359    };
360    let mut ordered_edges: Vec<_> = regioncx.outlives_constraints().collect();
361    ordered_edges.sort_by_key(|c| constraint_key(c));
362    ordered_edges.dedup_by_key(|c| constraint_key(c));
363
364    for outlives in ordered_edges {
365        // Source node.
366        out.write_fmt(format_args!("{0} ", outlives.sup.as_usize()))write!(out, "{} ", outlives.sup.as_usize())?;
367
368        // The kind of arrow: bidirectional if the opposite edge exists in the set.
369        if edges.contains(&(outlives.sub, outlives.sup)) {
370            out.write_fmt(format_args!("&lt;"))write!(out, "&lt;")?;
371        }
372        out.write_fmt(format_args!("-- "))write!(out, "-- ")?;
373
374        // Edge label from its `Locations`.
375        match outlives.locations {
376            Locations::All(_) => out.write_fmt(format_args!("All"))write!(out, "All")?,
377            Locations::Single(location) => out.write_fmt(format_args!("{0:?}", location))write!(out, "{:?}", location)?,
378        }
379
380        // Target node.
381        out.write_fmt(format_args!(" --> {0}\n", outlives.sub.as_usize()))writeln!(out, " --> {}", outlives.sub.as_usize())?;
382    }
383    Ok(())
384}
385
386/// Emits a mermaid flowchart of the NLL SCCs and the outlives constraints between them, similar
387/// to the graphviz version.
388fn emit_mermaid_nll_sccs<'tcx>(
389    tcx: TyCtxt<'tcx>,
390    regioncx: &RegionInferenceContext<'tcx>,
391    out: &mut dyn io::Write,
392) -> io::Result<()> {
393    // The mermaid chart type: a top-down flowchart.
394    out.write_fmt(format_args!("flowchart TD\n"))writeln!(out, "flowchart TD")?;
395
396    // Gather and emit the SCC nodes.
397    let mut nodes_per_scc: IndexVec<_, _> =
398        regioncx.constraint_sccs().all_sccs().map(|_| Vec::new()).collect();
399    for region in regioncx.definitions.indices() {
400        let scc = regioncx.constraint_sccs().scc(region);
401        nodes_per_scc[scc].push(region);
402    }
403    for (scc, regions) in nodes_per_scc.iter_enumerated() {
404        // The node label: the regions contained in the SCC.
405        out.write_fmt(format_args!("{0}[\"SCC({0}) = {{", scc.as_usize()))write!(out, "{scc}[\"SCC({scc}) = {{", scc = scc.as_usize())?;
406        for (idx, &region) in regions.iter().enumerate() {
407            render_region(tcx, region, regioncx, out)?;
408            if idx < regions.len() - 1 {
409                out.write_fmt(format_args!(","))write!(out, ",")?;
410            }
411        }
412        out.write_fmt(format_args!("}}\"]\n"))writeln!(out, "}}\"]")?;
413    }
414
415    // Emit the edges between SCCs.
416    let edges = regioncx.constraint_sccs().all_sccs().flat_map(|source| {
417        regioncx.constraint_sccs().successors(source).iter().map(move |&target| (source, target))
418    });
419    for (source, target) in edges {
420        out.write_fmt(format_args!("{0} --> {1}\n", source.as_usize(),
        target.as_usize()))writeln!(out, "{} --> {}", source.as_usize(), target.as_usize())?;
421    }
422
423    Ok(())
424}
425
426/// Emits a mermaid flowchart of the polonius localized outlives constraints, with subgraphs per
427/// region, and loan introductions.
428fn emit_mermaid_constraint_graph<'tcx>(
429    borrow_set: &BorrowSet<'tcx>,
430    liveness: &LivenessValues,
431    localized_outlives_constraints: &[LocalizedOutlivesConstraint],
432    out: &mut dyn io::Write,
433) -> io::Result<usize> {
434    let location_name = |location: Location| {
435        // A MIR location looks like `bb5[2]`. As that is not a syntactically valid mermaid node id,
436        // transform it into `BB5_2`.
437        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("BB{0}_{1}", location.block.index(),
                location.statement_index))
    })format!("BB{}_{}", location.block.index(), location.statement_index)
438    };
439    let region_name = |region: RegionVid| ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("\'{0}", region.index()))
    })format!("'{}", region.index());
440    let node_name = |region: RegionVid, point: PointIndex| {
441        let location = liveness.location_from_point(point);
442        ::alloc::__export::must_use({
        ::alloc::fmt::format(format_args!("{0}_{1}", region_name(region),
                location_name(location)))
    })format!("{}_{}", region_name(region), location_name(location))
443    };
444
445    // The mermaid chart type: a top-down flowchart, which supports subgraphs.
446    out.write_fmt(format_args!("flowchart TD\n"))writeln!(out, "flowchart TD")?;
447
448    // The loans subgraph: a node per loan.
449    out.write_fmt(format_args!("    subgraph \"Loans\"\n"))writeln!(out, "    subgraph \"Loans\"")?;
450    for loan_idx in 0..borrow_set.len() {
451        out.write_fmt(format_args!("        L{0}\n", loan_idx))writeln!(out, "        L{loan_idx}")?;
452    }
453    out.write_fmt(format_args!("    end\n\n"))writeln!(out, "    end\n")?;
454
455    // And an edge from that loan node to where it enters the constraint graph.
456    for (loan_idx, loan) in borrow_set.iter_enumerated() {
457        out.write_fmt(format_args!("    L{0} --> {1}_{2}\n", loan_idx.index(),
        region_name(loan.region), location_name(loan.reserve_location)))writeln!(
458            out,
459            "    L{} --> {}_{}",
460            loan_idx.index(),
461            region_name(loan.region),
462            location_name(loan.reserve_location),
463        )?;
464    }
465    out.write_fmt(format_args!("\n"))writeln!(out, "")?;
466
467    // The regions subgraphs containing the region/point nodes.
468    let mut points_per_region: FxIndexMap<RegionVid, FxIndexSet<PointIndex>> =
469        FxIndexMap::default();
470    for constraint in localized_outlives_constraints {
471        points_per_region.entry(constraint.source).or_default().insert(constraint.from);
472        points_per_region.entry(constraint.target).or_default().insert(constraint.to);
473    }
474    for (region, points) in points_per_region {
475        out.write_fmt(format_args!("    subgraph \"{0}\"\n", region_name(region)))writeln!(out, "    subgraph \"{}\"", region_name(region))?;
476        for point in points {
477            out.write_fmt(format_args!("        {0}\n", node_name(region, point)))writeln!(out, "        {}", node_name(region, point))?;
478        }
479        out.write_fmt(format_args!("    end\n\n"))writeln!(out, "    end\n")?;
480    }
481
482    // The constraint graph edges.
483    for constraint in localized_outlives_constraints {
484        // FIXME: add killed loans and constraint kind as edge labels.
485        out.write_fmt(format_args!("    {0} --> {1}\n",
        node_name(constraint.source, constraint.from),
        node_name(constraint.target, constraint.to)))writeln!(
486            out,
487            "    {} --> {}",
488            node_name(constraint.source, constraint.from),
489            node_name(constraint.target, constraint.to),
490        )?;
491    }
492
493    // Return the number of edges: this is the biggest graph in the dump and its edge count will be
494    // mermaid's max edge count to support.
495    let edge_count = borrow_set.len() + localized_outlives_constraints.len();
496    Ok(edge_count)
497}