spirt/print/
multiversion.rs

1//! Multi-version pretty-printing support (e.g. for comparing the IR between passes).
2
3use crate::FxIndexMap;
4use crate::print::pretty::{self, TextOp};
5use internal_iterator::{
6    FromInternalIterator, InternalIterator, IntoInternalIterator, IteratorExt,
7};
8use itertools::{Either, Itertools};
9use smallvec::SmallVec;
10use std::fmt::Write;
11use std::{fmt, iter, mem};
12
13#[allow(rustdoc::private_intra_doc_links)]
14/// Wrapper for handling the difference between single-version and multi-version
15/// output, which aren't expressible in [`pretty::Fragment`].
16//
17// FIXME(eddyb) introduce a `pretty::Node` variant capable of handling this,
18// but that's complicated wrt non-HTML output, if they're to also be 2D tables.
19pub enum Versions<PF> {
20    Single(PF),
21    Multiple {
22        // FIXME(eddyb) avoid allocating this if possible.
23        version_names: Vec<String>,
24
25        /// Each row consists of *deduplicated* (or "run-length encoded")
26        /// versions, with "repeat count"s larger than `1` indicating that
27        /// multiple versions (columns) have the exact same content.
28        ///
29        /// For HTML output, "repeat count"s map to `colspan` attributes.
30        //
31        // FIXME(eddyb) remove the "repeat count" mechanism.
32        per_row_versions_with_repeat_count: Vec<SmallVec<[(PF, usize); 1]>>,
33    },
34}
35
36impl fmt::Display for Versions<pretty::FragmentPostLayout> {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        match self {
39            Self::Single(fragment) => fragment.fmt(f),
40            Self::Multiple { version_names, per_row_versions_with_repeat_count } => {
41                let mut first = true;
42
43                // HACK(eddyb) this is not the nicest output, but multi-version
44                // is intended for HTML input primarily anyway.
45                for versions_with_repeat_count in per_row_versions_with_repeat_count {
46                    if !first {
47                        writeln!(f)?;
48                    }
49                    first = false;
50
51                    let mut next_version_idx = 0;
52                    let mut any_headings = false;
53                    for (fragment, repeat_count) in versions_with_repeat_count {
54                        // No headings for anything uniform across versions.
55                        if (next_version_idx, *repeat_count) != (0, version_names.len()) {
56                            any_headings = true;
57
58                            if next_version_idx == 0 {
59                                write!(f, "//#IF ")?;
60                            } else {
61                                write!(f, "//#ELSEIF ")?;
62                            }
63                            let mut first_name = true;
64                            for name in &version_names[next_version_idx..][..*repeat_count] {
65                                if !first_name {
66                                    write!(f, " | ")?;
67                                }
68                                first_name = false;
69
70                                write!(f, "`{name}`")?;
71                            }
72                            writeln!(f)?;
73                        }
74
75                        writeln!(f, "{fragment}")?;
76
77                        next_version_idx += repeat_count;
78                    }
79                    if any_headings {
80                        writeln!(f, "//#ENDIF")?;
81                    }
82                }
83
84                Ok(())
85            }
86        }
87    }
88}
89
90impl Versions<pretty::FragmentPostLayout> {
91    // FIXME(eddyb) provide a non-allocating version.
92    pub fn render_to_html(&self) -> pretty::HtmlSnippet {
93        match self {
94            Self::Single(fragment) => fragment.render_to_html(),
95            Self::Multiple { version_names, per_row_versions_with_repeat_count } => {
96                // HACK(eddyb) using an UUID as a class name in lieu of "scoped <style>".
97                const TABLE_CLASS_NAME: &str = "spirt-table-90c2056d-5b38-4644-824a-b4be1c82f14d";
98
99                let mut html = pretty::HtmlSnippet::default();
100                html.head_deduplicatable_elements.insert(
101                    "
102<style>
103    SCOPE {
104        border-collapse: collapse;
105    }
106    SCOPE>tbody>tr>*:not(:only-child) {
107        border: solid 1px;
108    }
109    SCOPE>tbody>tr>th {
110        /* HACK(eddyb) these are relative to `pretty`'s own HTML styles. */
111        font-size: 19px;
112        font-weight: 700;
113
114        font-style: italic;
115    }
116    SCOPE>tbody>tr>td {
117        vertical-align: top;
118    }
119    SCOPE>tbody>tr>td>pre {
120        max-width: MAX_LINE_WIDTHch;
121
122        overflow-x: auto;
123
124        /* HACK(eddyb) this shouldn't be needed but `visible` will turn into
125           `auto` because `overflow-x` is `auto` (for cursed browser reasons),
126           and some table cells are always assumed to need vertical scroll,
127           e.g. because they have `<sub>` elements on the last line (which don't
128           contribute to the surrounding bounding box, due to `line-height: 0`)
129         */
130        overflow-y: hidden;
131    }
132</style>
133        "
134                    .replace("SCOPE", &format!("table.{TABLE_CLASS_NAME}"))
135                    .replace("MAX_LINE_WIDTH", &super::MAX_LINE_WIDTH.to_string()),
136                );
137
138                let headings = {
139                    let mut h = "<tr>".to_string();
140                    for name in version_names {
141                        write!(h, "<th><code>{name}</code></th>").unwrap();
142                    }
143                    h + "</tr>\n"
144                };
145
146                html.body = format!("<table class=\"{TABLE_CLASS_NAME}\">\n");
147                let mut last_was_uniform = true;
148                for versions_with_repeat_count in per_row_versions_with_repeat_count {
149                    // FIXME(eddyb) remove the "repeat count" mechanism.
150                    let is_uniform = match versions_with_repeat_count[..] {
151                        [(_, repeat_count)] => repeat_count == version_names.len(),
152                        _ => false,
153                    };
154
155                    if last_was_uniform && is_uniform {
156                        // Headings unnecessary, they would be between uniform
157                        // rows (or at the very start, before an uniform row).
158                    } else {
159                        // Repeat the headings often, where necessary.
160                        html.body += &headings;
161                    }
162                    last_was_uniform = is_uniform;
163
164                    // Attempt to align as many anchors as possible between the
165                    // columns, to improve legibility (see also `AnchorAligner`).
166                    let mut anchor_aligner = AnchorAligner::default();
167                    for (fragment, _) in versions_with_repeat_count {
168                        anchor_aligner
169                            .add_column_and_align_anchors(fragment.render_to_text_ops().collect());
170                    }
171
172                    html.body += "<tr>\n";
173                    if is_uniform {
174                        // FIXME(eddyb) avoid duplication with the non-uniform case.
175                        let pretty::HtmlSnippet {
176                            head_deduplicatable_elements: fragment_head,
177                            body: fragment_body,
178                        } = anchor_aligner
179                            .merged_columns()
180                            .next()
181                            .unwrap()
182                            .lines()
183                            .intersperse(&[TextOp::Text("\n")])
184                            .flatten()
185                            .copied()
186                            .into_internal()
187                            .collect();
188
189                        html.head_deduplicatable_elements.extend(fragment_head);
190
191                        writeln!(html.body, "<td colspan=\"{}\">", version_names.len()).unwrap();
192                        html.body += &fragment_body;
193                        html.body += "</td>\n";
194                    } else {
195                        let mut merged_columns = versions_with_repeat_count
196                            .iter()
197                            .zip(anchor_aligner.merged_columns())
198                            .flat_map(|(&(_, repeat_count), column)| {
199                                iter::repeat(column).take(repeat_count)
200                            })
201                            .peekable();
202
203                        let mut prev_column = None;
204                        while let Some(column) = merged_columns.next() {
205                            let prev_column = prev_column.replace(column);
206                            let next_column = merged_columns.peek().copied();
207
208                            let unchanged_line_style = pretty::Styles {
209                                desaturate_and_dim_for_unchanged_multiversion_line: true,
210                                ..Default::default()
211                            };
212
213                            // NOTE(eddyb) infinite (but limited by `zip` below),
214                            // and `Some([])`/`None` distinguishes empty/missing.
215                            let prev_lines = prev_column
216                                .iter()
217                                .flat_map(|prev| prev.lines().map(Some))
218                                .chain(iter::repeat(prev_column.map(|_| &[][..])));
219                            let next_lines = next_column
220                                .iter()
221                                .flat_map(|next| next.lines().map(Some))
222                                .chain(iter::repeat(next_column.map(|_| &[][..])));
223
224                            let lines = column.lines().zip(prev_lines).zip(next_lines).map(
225                                |((line, prev_line), next_line)| {
226                                    // FIXME(eddyb) apply a `class` instead of an inline `style`,
227                                    // and allow `:hover` to disable the desaturation/dimming.
228                                    // FIXME(eddyb) maybe indicate when lines
229                                    // were removed (red "hashed" background?).
230                                    let diff = |other: Option<_>| {
231                                        // Ignore indendation-only changes.
232                                        fn strip_indents<'a, 'b>(
233                                            mut line: &'b [TextOp<'a>],
234                                        ) -> &'b [TextOp<'a>]
235                                        {
236                                            // HACK(eddyb) also ignore helper anchors,
237                                            // which can go before indents.
238                                            while let [TextOp::Text(pretty::INDENT), rest @ ..]
239                                            | [
240                                                TextOp::PushAnchor { .. },
241                                                TextOp::PopAnchor { .. },
242                                                rest @ ..,
243                                            ] = line
244                                            {
245                                                line = rest;
246                                            }
247                                            line
248                                        }
249                                        other.map_or(false, |other| {
250                                            strip_indents(line) != strip_indents(other)
251                                        })
252                                    };
253                                    let line_style = if !diff(prev_line) && !diff(next_line) {
254                                        Some(&unchanged_line_style)
255                                    } else {
256                                        None
257                                    };
258                                    line_style
259                                        .map(TextOp::PushStyles)
260                                        .into_iter()
261                                        .chain(line.iter().copied())
262                                        .chain(line_style.map(TextOp::PopStyles))
263                                },
264                            );
265
266                            let pretty::HtmlSnippet {
267                                head_deduplicatable_elements: fragment_head,
268                                body: fragment_body,
269                            } = lines
270                                .map(Either::Left)
271                                .intersperse(Either::Right([TextOp::Text("\n")].into_iter()))
272                                .flatten()
273                                .into_internal()
274                                .collect();
275
276                            html.head_deduplicatable_elements.extend(fragment_head);
277
278                            html.body += "<td>\n";
279                            html.body += &fragment_body;
280                            html.body += "</td>\n";
281                        }
282                    }
283                    html.body += "</tr>\n";
284                }
285                html.body += "</table>";
286
287                html
288            }
289        }
290    }
291}
292
293impl<PF> Versions<PF> {
294    pub fn map_pretty_fragments<PF2>(self, f: impl Fn(PF) -> PF2) -> Versions<PF2> {
295        match self {
296            Versions::Single(fragment) => Versions::Single(f(fragment)),
297            Versions::Multiple { version_names, per_row_versions_with_repeat_count } => {
298                Versions::Multiple {
299                    version_names,
300                    per_row_versions_with_repeat_count: per_row_versions_with_repeat_count
301                        .into_iter()
302                        .map(|versions_with_repeat_count| {
303                            versions_with_repeat_count
304                                .into_iter()
305                                .map(|(fragment, repeat_count)| (f(fragment), repeat_count))
306                                .collect()
307                        })
308                        .collect(),
309                }
310            }
311        }
312    }
313}
314
315/// Tool for adjusting pretty-printed columns, so that their anchors line up
316/// (by adding empty lines to whichever side "is behind").
317#[derive(Default)]
318struct AnchorAligner<'a> {
319    merged_lines: Vec<AAMergedLine>,
320
321    /// Current ("rightmost") column's anchor definitions (with indices pointing
322    /// into `merged_lines`), which the next column will align to.
323    //
324    // FIXME(eddyb) does this need additional interning?
325    anchor_def_to_merged_line_idx: FxIndexMap<&'a str, usize>,
326
327    // FIXME(eddyb) fine-tune this inline size.
328    // FIXME(eddyb) maybe don't keep most of this data around anyway?
329    original_columns: SmallVec<[AAColumn<'a>; 4]>,
330}
331
332/// Abstraction for one "physical" line spanning all columns, after alignment.
333struct AAMergedLine {
334    // FIXME(eddyb) fine-tune this inline size.
335    // FIXME(eddyb) consider using `u32` here?
336    per_column_line_lengths: SmallVec<[usize; 4]>,
337}
338
339struct AAColumn<'a> {
340    /// All `TextOp`s in all lines from this column, concatenated together.
341    text_ops: Vec<TextOp<'a>>,
342
343    /// The length, in `TextOp`s (from `text_ops`), of each line.
344    //
345    // FIXME(eddyb) consider using `u32` here?
346    line_lengths: Vec<usize>,
347}
348
349impl<'a> AAColumn<'a> {
350    /// Reconstruct lines (made of `TextOp`s) from line lengths.
351    fn lines(
352        &self,
353        line_lengths: impl Iterator<Item = usize>,
354    ) -> impl Iterator<Item = &[TextOp<'a>]> {
355        let mut next_start = 0;
356        line_lengths.map(move |len| {
357            let start = next_start;
358            let end = start + len;
359            next_start = end;
360            &self.text_ops[start..end]
361        })
362    }
363}
364
365// FIXME(eddyb) is this impl the best way? (maybe it should be a inherent method)
366impl<'a> FromInternalIterator<TextOp<'a>> for AAColumn<'a> {
367    fn from_iter<T>(text_ops: T) -> Self
368    where
369        T: IntoInternalIterator<Item = TextOp<'a>>,
370    {
371        let mut column = AAColumn { text_ops: vec![], line_lengths: vec![0] };
372        text_ops.into_internal_iter().for_each(|op| {
373            if let TextOp::Text("\n") = op {
374                column.line_lengths.push(0);
375            } else {
376                // FIXME(eddyb) this *happens* to be true,
377                // but the `LineOp`/`TextOp` split could be
378                // improved to avoid such sanity checks.
379                if let TextOp::Text(text) = op {
380                    assert!(!text.contains('\n'));
381                }
382                column.text_ops.push(op);
383                *column.line_lengths.last_mut().unwrap() += 1;
384            }
385        });
386        column
387    }
388}
389
390#[derive(Copy, Clone)]
391struct AAMergedColumn<'a, 'b> {
392    original_column: &'b AAColumn<'a>,
393    column_idx: usize,
394    merged_lines: &'b [AAMergedLine],
395}
396
397impl<'a, 'b> AAMergedColumn<'a, 'b> {
398    fn lines(&self) -> impl Iterator<Item = &'b [TextOp<'a>]> + '_ {
399        let column_idx = self.column_idx;
400        let line_lengths =
401            self.merged_lines.iter().map(move |line| line.per_column_line_lengths[column_idx]);
402        self.original_column.lines(line_lengths)
403    }
404}
405
406impl<'a> AnchorAligner<'a> {
407    /// Flatten all columns to `TextOp`s (including line separators).
408    fn merged_columns(&self) -> impl Iterator<Item = AAMergedColumn<'a, '_>> {
409        self.original_columns.iter().enumerate().map(|(column_idx, original_column)| {
410            let mut merged_lines = &self.merged_lines[..];
411
412            // Trim all trailing lines that are empty in this column.
413            while let Some((last, before_last)) = merged_lines.split_last() {
414                if last.per_column_line_lengths[column_idx] > 0 {
415                    break;
416                }
417                merged_lines = before_last;
418            }
419
420            AAMergedColumn { original_column, column_idx, merged_lines }
421        })
422    }
423
424    /// Merge `new_column` into the current set of columns, aligning as many
425    /// anchors as possible, between it, and the most recent column.
426    fn add_column_and_align_anchors(&mut self, new_column: AAColumn<'a>) {
427        // NOTE(eddyb) "old" and "new" are used to refer to the two columns being
428        // aligned, but "old" maps to the *merged* lines, not its original ones.
429
430        let old_lines = mem::take(&mut self.merged_lines);
431        let old_anchor_def_to_line_idx = mem::take(&mut self.anchor_def_to_merged_line_idx);
432
433        // Index all the anchor definitions in the new column.
434        let mut new_anchor_def_to_line_idx = FxIndexMap::default();
435        for (new_line_idx, new_line_text_ops) in
436            new_column.lines(new_column.line_lengths.iter().copied()).enumerate()
437        {
438            for op in new_line_text_ops {
439                if let TextOp::PushAnchor { is_def: true, anchor } = *op {
440                    new_anchor_def_to_line_idx.entry(anchor).or_insert(new_line_idx);
441                }
442            }
443        }
444
445        // Find all the possible anchor alignments (i.e. anchors defined in both
446        // "old" and "new") as pairs of line indices in "old" and "new".
447        //
448        // HACK(eddyb) the order is given by the "new" line index, implicitly.
449        // FIXME(eddyb) fine-tune this inline size.
450        let common_anchors: SmallVec<[_; 8]> = new_anchor_def_to_line_idx
451            .iter()
452            .filter_map(|(anchor, &new_line_idx)| {
453                Some((*old_anchor_def_to_line_idx.get(anchor)?, new_line_idx))
454            })
455            .collect();
456
457        // Fast-path: if all the "old" line indices are already in (increasing)
458        // order (i.e. "monotonic"), they can all be used directly for alignment.
459        let is_already_monotonic = {
460            // FIXME(eddyb) should be `.is_sorted_by_key(|&(old_line_idx, _)| old_line_idx)`
461            // but that slice method is still unstable.
462            common_anchors.windows(2).all(|w| w[0].0 <= w[1].0)
463        };
464        let monotonic_common_anchors = if is_already_monotonic {
465            common_anchors
466        } else {
467            // FIXME(eddyb) this could maybe avoid all the unnecessary allocations.
468            longest_increasing_subsequence::lis(&common_anchors)
469                .into_iter()
470                .map(|i| common_anchors[i])
471                .collect()
472        };
473
474        // Allocate space for the merge of "old" and "new".
475        let mut merged_lines = Vec::with_capacity({
476            // Cheap conservative estimate, based on the last anchor (i.e. the
477            // final position of the last anchor is *at least* `min_before_last`).
478            let &(old_last, new_last) = monotonic_common_anchors.last().unwrap_or(&(0, 0));
479            let min_before_last = old_last.max(new_last);
480            let after_last =
481                (old_lines.len() - old_last).max(new_column.line_lengths.len() - new_last);
482            (min_before_last + after_last).next_power_of_two()
483        });
484
485        // Build the merged lines using (partially) lockstep iteration to pull
486        // the relevant data out of either side, and update "new" line indices.
487        let mut old_lines = old_lines.into_iter().enumerate().peekable();
488        let mut new_lines = new_column.line_lengths.iter().copied().enumerate().peekable();
489        let mut monotonic_common_anchors = monotonic_common_anchors.into_iter().peekable();
490        let mut fixup_new_to_merged = new_anchor_def_to_line_idx.values_mut().peekable();
491        while old_lines.len() > 0 || new_lines.len() > 0 {
492            let old_line_idx = old_lines.peek().map(|&(i, _)| i);
493            let new_line_idx = new_lines.peek().map(|&(i, _)| i);
494            let mut next_anchor = monotonic_common_anchors.peek().copied();
495
496            // Discard anchor alignments that have been used already, and also
497            // any others that cannot be relevant anymore - this can occur when
498            // multiple anchors coincide on the same line.
499            while let Some((anchor_old, anchor_new)) = next_anchor {
500                // NOTE(eddyb) noop anchors (i.e. those describing an alignment
501                // between "old" and "new", which has already beeing reached)
502                // are not considered "relevant" here, and "misalignments" are
503                // preferred instead - the outcome is mostly identical to always
504                // eagerly processing noop anchors, except when another anchor
505                // is overlapping (in only one of "old" or "new"), as it will
506                // only get get processed if the noop one is skipped first.
507                let relevant = match (old_line_idx, new_line_idx) {
508                    (Some(old), Some(new)) => old < anchor_old || new < anchor_new,
509                    _ => false,
510                };
511                if relevant {
512                    break;
513                }
514                monotonic_common_anchors.next().unwrap();
515                next_anchor = monotonic_common_anchors.peek().copied();
516            }
517
518            // Figure out which side has to wait, to align an upcoming anchor.
519            let (old_at_anchor, new_at_anchor) =
520                next_anchor.map_or((false, false), |(anchor_old, anchor_new)| {
521                    (
522                        old_line_idx.map_or(false, |old| old == anchor_old),
523                        new_line_idx.map_or(false, |new| new == anchor_new),
524                    )
525                });
526            let old_line = if old_at_anchor && !new_at_anchor {
527                // Pausing "old", waiting for "new".
528                None
529            } else {
530                old_lines.next().map(|(_, old_line)| old_line)
531            };
532            let new_line_len = if !old_at_anchor && new_at_anchor {
533                // Pausing "new", waiting for "old".
534                None
535            } else {
536                new_lines.next().map(|(_, new_line_len)| new_line_len)
537            };
538
539            // When the "new" side is advanced, that "sets" the merged line index
540            // of the consumed line, which can then be used for fixing up indices.
541            if new_line_len.is_some() {
542                let new_line_idx = new_line_idx.unwrap();
543                let merged_line_idx = merged_lines.len();
544                while fixup_new_to_merged.peek().map(|i| **i) == Some(new_line_idx) {
545                    *fixup_new_to_merged.next().unwrap() = merged_line_idx;
546                }
547            }
548
549            let new_line_len = new_line_len.unwrap_or(0);
550            let merged_line = match old_line {
551                Some(mut line) => {
552                    line.per_column_line_lengths.push(new_line_len);
553                    line
554                }
555                None => AAMergedLine {
556                    per_column_line_lengths: (0..self.original_columns.len())
557                        .map(|_| 0)
558                        .chain([new_line_len])
559                        .collect(),
560                },
561            };
562            merged_lines.push(merged_line);
563        }
564
565        self.merged_lines = merged_lines;
566        self.anchor_def_to_merged_line_idx = new_anchor_def_to_line_idx;
567        self.original_columns.push(new_column);
568    }
569}