From b0d5fa0e3151598d87ded5229a1b859440de2fd8 Mon Sep 17 00:00:00 2001 From: Nadrieril Date: Wed, 1 Jan 2025 23:22:16 +0100 Subject: [PATCH] Improve diff precision with a generic tree differ --- Cargo.lock | 32 +++- Cargo.toml | 1 + src/analyses/compute_rules.rs | 95 +++++------ src/ast/printer.rs | 90 ++++++---- src/ast/printer/display_tree.rs | 281 ++++++++++++++++++++++++++++++++ src/wasm.rs | 49 ++---- 6 files changed, 422 insertions(+), 126 deletions(-) create mode 100644 src/ast/printer/display_tree.rs diff --git a/Cargo.lock b/Cargo.lock index 955a90b..405c13b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "adler" @@ -735,6 +735,15 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "strip-ansi-escapes" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff8ef943b384c414f54aefa961dd2bd853add74ec75e7ac74cf91dba62bcfa" +dependencies = [ + "vte", +] + [[package]] name = "syn" version = "2.0.74" @@ -784,6 +793,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "strip-ansi-escapes", "wasm-bindgen", "wasm-rs-dbg", "web-sys", @@ -831,6 +841,26 @@ version = "0.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9dcc60c0624df774c82a0ef104151231d37da4962957d691c011c852b2473314" +[[package]] +name = "vte" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197" +dependencies = [ + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 4728949..06fd8e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ nom-supreme = "0.8.0" serde_json = "1.0.125" serde = { version = "1.0.208", features = ["derive"] } serde_yaml = "0.9.34" +strip-ansi-escapes = "0.2.0" wasm-bindgen = "0.2" wasm-rs-dbg = "0.1.2" web-sys = { version = "0.3.70", features = ["console"] } diff --git a/src/analyses/compute_rules.rs b/src/analyses/compute_rules.rs index 246312a..567aaa0 100644 --- a/src/analyses/compute_rules.rs +++ b/src/analyses/compute_rules.rs @@ -2,7 +2,6 @@ use bincode::{Decode, Encode}; use serde::{Deserialize, Serialize}; use std::cmp::max; use std::collections::HashSet; -use std::fmt::Write; use itertools::{EitherOrBoth, Itertools}; @@ -315,17 +314,23 @@ enum RenderablePredicate<'a> { } impl RenderablePredicate<'_> { - fn display(&self, style: PredicateStyle) -> String { + fn display_to_tree<'d>(&self, a: &'d Arenas<'d>, style: PredicateStyle) -> DisplayTree<'d> { match self { - RenderablePredicate::Pred(p) => p.display(style), - RenderablePredicate::ExprNotRef(e) => format!("{e} is not a reference"), - RenderablePredicate::TyNotRef(ty) => format!("{ty} is not a reference"), + RenderablePredicate::Pred(p) => p.display_to_tree(a, style), + RenderablePredicate::ExprNotRef(e) => e + .to_string() + .to_display_tree(a) + .then(a, " is not a reference"), + RenderablePredicate::TyNotRef(ty) => ty + .to_string() + .to_display_tree(a) + .then(a, " is not a reference"), RenderablePredicate::Mutability(e, mtbl) => { let mtbl = match mtbl { Mutability::Shared => "read-only", Mutability::Mutable => "mutable", }; - format!("{e} {mtbl}") + e.to_string().to_display_tree(a).sep_then(a, " ", mtbl) } } } @@ -448,67 +453,47 @@ impl<'a> TypingRule<'a> { impl<'a> RenderableTypingRule<'a> { pub fn display(&self, style: PredicateStyle) -> String { - Self::assemble_pieces(self.display_piecewise(style)) - } - - pub fn assemble_pieces( - [preconditions_str, bar, name, postconditions_str]: [String; 4], - ) -> String { - let mut out = String::new(); - let _ = write!(&mut out, "{preconditions_str}\n"); - let _ = write!(&mut out, "{bar} \"{name}\"\n"); - let _ = write!(&mut out, "{postconditions_str}"); - out + let a = &Arenas::default(); + self.display_to_tree(a, style).to_string() } - pub fn display_piecewise(&self, style: PredicateStyle) -> [String; 4] { - let postconditions_str = self - .postconditions - .iter() - .map(|x| x.display(style)) - .join(", "); - let preconditions_str = self - .preconditions - .iter() - .map(|x| x.display(style)) - .join(", "); - - let display_len = if cfg!(target_arch = "wasm32") { - // Compute string length skipping html tags. - |s: &str| { - let mut in_tag = false; - s.chars() - .filter(|&c| { - if c == '<' { - in_tag = true; - false - } else if c == '>' { - in_tag = false; - false - } else { - !in_tag - } - }) - .count() - } - } else { - // Compute string length skipping ansi escape codes. - ansi_width::ansi_width - }; - + pub fn display_to_tree<'d>(&self, a: &'d Arenas<'d>, style: PredicateStyle) -> DisplayTree<'d> { + let preconditions = DisplayTree::sep_by( + a, + ", ", + self.preconditions + .iter() + .map(|x| x.display_to_tree(a, style)), + ); + let postconditions = DisplayTree::sep_by( + a, + ", ", + self.postconditions + .iter() + .map(|x| x.display_to_tree(a, style)), + ); let len = max( - display_len(&preconditions_str), - display_len(&postconditions_str), + preconditions.len_ignoring_markup(), + postconditions.len_ignoring_markup(), ); let bar = "-".repeat(len); let name = self.name.display(self.options); - [preconditions_str, bar, name, postconditions_str] + DisplayTree::sep_by( + a, + "\n", + [ + preconditions, + DisplayTree::sep2_by(a, bar, " ", format!("\"{name}\"")).ignore_for_diff(), + postconditions, + ], + ) } } #[test] /// Compute the rulesets for each known bundle. fn bundle_rules() -> anyhow::Result<()> { + use std::fmt::Write; colored::control::set_override(false); #[derive(Serialize)] diff --git a/src/ast/printer.rs b/src/ast/printer.rs index 62f8a9a..b309032 100644 --- a/src/ast/printer.rs +++ b/src/ast/printer.rs @@ -3,6 +3,9 @@ use std::fmt::{Debug, Display, Write}; use crate::*; +pub mod display_tree; +pub use display_tree::*; + pub trait Style { fn green(&self) -> String; fn red(&self) -> String; @@ -101,6 +104,23 @@ impl BindingMode { } } +impl<'d> ToDisplayTree<'d> for Type<'_> { + fn to_display_tree(&self, a: &'d Arenas<'d>) -> DisplayTree<'d> { + match self { + Self::Tuple(tys) => DisplayTree::sep_by(a, ", ", tys.iter()) + .surrounded(a, "[", "]") + .tag("ty_list"), + Self::Ref(mutable, ty) => format!("&{mutable}") + .to_display_tree(a) + .then(a, ty) + .tag("ty_ref"), + Self::OtherNonRef(name) | Self::AbstractNonRef(name) | Self::Abstract(name) => { + name.to_display_tree(a) + } + } + } +} + impl<'a> TypingPredicate<'a> { /// Display as `let ...`. pub fn display_as_let(&self) -> String { @@ -108,10 +128,18 @@ impl<'a> TypingPredicate<'a> { } pub fn display(&self, style: PredicateStyle) -> String { + let a = &Arenas::default(); + self.display_to_tree(a, style).to_string() + } + + pub fn display_to_tree<'d>(&self, a: &'d Arenas<'d>, style: PredicateStyle) -> DisplayTree<'d> { match style { - PredicateStyle::Expression => { - format!("{} @ {}: {}", self.pat, self.expr, self.expr.ty) - } + PredicateStyle::Expression => self + .pat + .to_string() + .to_display_tree(a) + .sep_then(a, " @ ", self.expr.to_string()) + .sep_then(a, ": ", self.expr.ty), PredicateStyle::Sequent { ty: toi, show_reference_state, @@ -148,45 +176,50 @@ impl<'a> TypingPredicate<'a> { }; pre_turnstile.push(scrut_access.to_string()); } + let pre_turnstile = DisplayTree::sep_by(a, ", ", pre_turnstile); // Type to display. let ty = match toi { TypeOfInterest::UserVisible => { - let mut ty = self.expr.ty.to_string(); + let ty = self.expr.ty; if show_reference_state && let Some(BindingMode::ByRef(_)) = self.expr.binding_mode().ok() + && let Type::Ref(mtbl, sub_ty) = ty { - if let Some(rest) = ty.strip_prefix("&mut") { - ty = format!("{}{rest}", "&mut".inherited_ref()); - } else if let Some(rest) = ty.strip_prefix("&") { - ty = format!("{}{rest}", "&".inherited_ref()); - } + // Highlight the inherited reference. + format!("&{mtbl}") + .inherited_ref() + .to_display_tree(a) + .then(a, sub_ty) + .tag("ty_ref") + } else { + ty.to_display_tree(a) } - ty } TypeOfInterest::InMemory => match self.expr.binding_mode().ok() { - Some(BindingMode::ByMove) => self.expr.ty.to_string(), - Some(BindingMode::ByRef(_)) => { - self.expr.reset_binding_mode().unwrap().ty.to_string() - } + Some(BindingMode::ByMove) => self.expr.ty.to_display_tree(a), + Some(BindingMode::ByRef(_)) => self + .expr + .reset_binding_mode() + .unwrap() + .ty + .to_display_tree(a), None => match self.expr.ty { Type::Ref(_, inner_ty) => { - format!("{} or {}", self.expr.ty, inner_ty) + format!("{} or {}", self.expr.ty, inner_ty).to_display_tree(a) } - Type::Abstract(_) => self.expr.ty.to_string(), + Type::Abstract(_) => self.expr.ty.to_display_tree(a), _ => unreachable!(), }, }, }; + let post_turnstile = self + .pat + .to_string() + .to_display_tree(a) + .sep_then(a, ": ", ty); - let turnstile = if pre_turnstile.is_empty() { - "" - } else { - " ⊢ " - }; - let pre_turnstile = pre_turnstile.join(", "); - let pat = self.pat; - format!("{pre_turnstile}{turnstile}{pat}: {ty}") + pre_turnstile.sep_then(a, " ⊢ ", post_turnstile) } } } @@ -268,13 +301,8 @@ impl Display for Pattern<'_> { impl Display for Type<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Tuple(tys) => write!(f, "[{}]", tys.iter().format(", ")), - Self::Ref(mutable, ty) => write!(f, "&{mutable}{ty}"), - Self::OtherNonRef(name) | Self::AbstractNonRef(name) | Self::Abstract(name) => { - write!(f, "{name}") - } - } + let a = &Arenas::default(); + write!(f, "{}", self.to_display_tree(a)) } } diff --git a/src/ast/printer/display_tree.rs b/src/ast/printer/display_tree.rs new file mode 100644 index 0000000..6f6221a --- /dev/null +++ b/src/ast/printer/display_tree.rs @@ -0,0 +1,281 @@ +use std::fmt::Display; + +use itertools::Itertools; + +use crate::*; +use DisplayTreeKind::*; + +/// A structured representation of a string to be displayed. Used to compute structured diffs. +#[derive(Clone, Copy)] +enum DisplayTreeKind<'a> { + /// Leaf node. + Leaf(&'a str), + /// Separated node. Considered different if the separator is different or the lengths are + /// different. + Separated { + /// The separator, intercalated between the children. + sep: &'a str, + /// The children. + children: &'a [DisplayTree<'a>], + }, +} + +#[derive(Clone, Copy)] +pub struct DisplayTree<'a> { + kind: DisplayTreeKind<'a>, + /// Identifies the kind of node. Two nodes with different tags are always considered different. + tag: &'static str, + /// Whether to this sub-tree unchanged for the purposes of diffing. + ignore_for_diff: bool, +} + +/// Compute the length of this string as displayed on the screen (i.e. ignoring html tags or ansi +/// escapes, depending on context). +fn len_ignoring_markup(s: &str) -> usize { + if cfg!(target_arch = "wasm32") { + // Compute string length skipping html tags. + let mut in_tag = false; + s.chars() + .filter(|&c| { + if c == '<' { + in_tag = true; + false + } else if c == '>' { + in_tag = false; + false + } else { + !in_tag + } + }) + .count() + } else { + // Compute string length skipping ansi escape codes. + ansi_width::ansi_width(s) + } +} + +/// Strip html tags or ansi escapes, depending on context. +fn strip_markup(s: &str) -> String { + if cfg!(target_arch = "wasm32") { + let mut in_tag = false; + s.chars() + .filter(|&c| { + if c == '<' { + in_tag = true; + false + } else if c == '>' { + in_tag = false; + false + } else { + !in_tag + } + }) + .collect() + } else { + strip_ansi_escapes::strip_str(s) + } +} + +impl<'a> DisplayTree<'a> { + pub fn len_ignoring_markup(&self) -> usize { + match self.kind { + Leaf(s) => len_ignoring_markup(s), + Separated { sep, children } => { + let sep_len = if children.len() <= 1 { + 0 + } else { + len_ignoring_markup(sep) * (children.len() - 1) + }; + let children_len: usize = children.iter().map(|t| t.len_ignoring_markup()).sum(); + sep_len + children_len + } + } + } + + pub fn leaf_noalloc(s: &'a str) -> Self { + Self { + kind: Leaf(s), + tag: "", + ignore_for_diff: false, + } + } + + pub fn leaf(a: &'a Arenas<'a>, s: &str) -> Self { + Self::leaf_noalloc(a.alloc_str(s)) + } + + pub fn sep_by( + a: &'a Arenas<'a>, + sep: &str, + it: impl IntoIterator>, + ) -> Self { + let children = it.into_iter().map(|x| x.to_display_tree(a)).collect_vec(); + Self { + kind: Separated { + sep: a.alloc_str(sep), + children: a.bump.alloc_slice_copy(&children), + }, + tag: "", + ignore_for_diff: false, + } + } + + /// Constructs `self` followed by `after`. + pub fn then(&self, a: &'a Arenas<'a>, x: impl ToDisplayTree<'a>) -> Self { + self.sep_then(a, "", x) + } + + /// Constructs `self` surrounded by `before` and `after`. + pub fn surrounded(&self, a: &'a Arenas<'a>, before: &'static str, after: &'static str) -> Self { + Self::sep_by( + a, + "", + [Self::leaf_noalloc(before), *self, Self::leaf_noalloc(after)], + ) + } + + /// Concatenates `self` and `x`, separated by `sep`. + pub fn sep_then( + &self, + a: &'a Arenas<'a>, + sep: &'static str, + x: impl ToDisplayTree<'a>, + ) -> Self { + Self::sep2_by(a, self, sep, x) + } + + /// Concatenates `x` and `y`, separated by `sep`. + pub fn sep2_by( + a: &'a Arenas<'a>, + x: impl ToDisplayTree<'a>, + sep: &str, + y: impl ToDisplayTree<'a>, + ) -> Self { + Self::sep_by(a, sep, [x.to_display_tree(a), y.to_display_tree(a)]) + } + + pub fn ignore_for_diff(mut self) -> Self { + self.ignore_for_diff = true; + self + } + + pub fn tag(mut self, tag: &'static str) -> Self { + self.tag = tag; + self + } + + /// Display `self` and `other`, highlighting differences. + pub fn diff_display(&self, other: &Self) -> (String, String) { + let mut left = String::new(); + let mut right = String::new(); + let _ = self.diff_display_inner(other, &mut left, &mut right); + (left, right) + } + + fn diff_display_inner( + &self, + other: &Self, + left: &mut String, + right: &mut String, + ) -> std::fmt::Result { + use std::fmt::Write; + // The trivial cases: the trees are either fully identical or fully different. + let all_same = |left: &mut String, right: &mut String| { + write!(left, "{self}")?; + write!(right, "{other}")?; + Ok(()) + }; + let all_different = |left: &mut String, right: &mut String| { + write!(left, "{}", self.to_string().red())?; + write!(right, "{}", other.to_string().green())?; + Ok(()) + }; + match (self.kind, other.kind) { + _ if self.tag != other.tag => { + all_different(left, right)?; + } + _ if self.ignore_for_diff && other.ignore_for_diff => { + all_same(left, right)?; + } + (Leaf(l), Leaf(r)) if strip_markup(l) == strip_markup(r) => { + all_same(left, right)?; + } + // The non-trivial case: the trees differ partially. + ( + Separated { sep, children: c1 }, + Separated { + sep: sep2, + children: c2, + }, + ) if strip_markup(sep) == strip_markup(sep2) && c1.len() == c2.len() => { + let mut is_first = true; + for (c1, c2) in c1.iter().zip(c2) { + if !is_first { + write!(left, "{sep}")?; + write!(right, "{sep}")?; + } + c1.diff_display_inner(c2, left, right)?; + is_first = false; + } + } + _ => { + all_different(left, right)?; + } + } + Ok(()) + } +} + +impl Default for DisplayTree<'_> { + fn default() -> Self { + Self { + kind: Leaf(""), + tag: "", + ignore_for_diff: false, + } + } +} + +impl<'a> Display for DisplayTree<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.kind { + Leaf(s) => write!(f, "{s}")?, + Separated { sep, children } => { + let mut is_first = true; + for child in children { + if !is_first { + write!(f, "{sep}")?; + } + write!(f, "{child}")?; + is_first = false; + } + } + } + Ok(()) + } +} + +pub trait ToDisplayTree<'a> { + fn to_display_tree(&self, a: &'a Arenas<'a>) -> DisplayTree<'a>; +} + +impl<'a, T: ToDisplayTree<'a>> ToDisplayTree<'a> for &T { + fn to_display_tree(&self, a: &'a Arenas<'a>) -> DisplayTree<'a> { + (*self).to_display_tree(a) + } +} +impl<'a> ToDisplayTree<'a> for &str { + fn to_display_tree(&self, a: &'a Arenas<'a>) -> DisplayTree<'a> { + DisplayTree::leaf(a, self) + } +} +impl<'a> ToDisplayTree<'a> for String { + fn to_display_tree(&self, a: &'a Arenas<'a>) -> DisplayTree<'a> { + self.as_str().to_display_tree(a) + } +} +impl<'a> ToDisplayTree<'a> for DisplayTree<'a> { + fn to_display_tree(&self, _: &'a Arenas<'a>) -> DisplayTree<'a> { + *self + } +} diff --git a/src/wasm.rs b/src/wasm.rs index 68d026c..5128b92 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,7 +1,6 @@ use crate::*; use bincode::{Decode, Encode}; use gloo_utils::format::JsValueSerdeExt; -use itertools::EitherOrBoth; use serde::Serialize; use std::cmp::Ordering; use std::fmt::Write; @@ -277,44 +276,16 @@ pub fn display_joint_rules_js( compute_joint_rules(a, type_of_interest, left.ty_based, right.ty_based) .into_iter() .map(|joint_rule| { - let joint_rule = joint_rule - .as_ref() - .map_left(|r| r.make_renderable(a, style).unwrap()) - .map_right(|r| r.make_renderable(a, style).unwrap()); - let (left, right) = match joint_rule { - EitherOrBoth::Left(left) => { - let left = left.display(style).red(); - (left, String::new()) - } - EitherOrBoth::Right(right) => { - let right = right.display(style).green(); - (String::new(), right) - } - EitherOrBoth::Both(left, right) => { - let [mut lpreconditions_str, lbar, lname, lpostconditions_str] = - left.display_piecewise(style); - let [mut rpreconditions_str, rbar, rname, rpostconditions_str] = - right.display_piecewise(style); - if lpreconditions_str != rpreconditions_str { - lpreconditions_str = lpreconditions_str.red(); - rpreconditions_str = rpreconditions_str.green(); - } - assert_eq!(lpostconditions_str, rpostconditions_str); - let left = RenderableTypingRule::assemble_pieces([ - lpreconditions_str, - lbar, - lname, - lpostconditions_str, - ]); - let right = RenderableTypingRule::assemble_pieces([ - rpreconditions_str, - rbar, - rname, - rpostconditions_str, - ]); - (left, right) - } - }; + let (left, right) = joint_rule.as_ref().left_and_right(); + let left = left + .map(|r| r.make_renderable(a, style).unwrap()) + .map(|r| r.display_to_tree(a, style)) + .unwrap_or_default(); + let right = right + .map(|r| r.make_renderable(a, style).unwrap()) + .map(|r| r.display_to_tree(a, style)) + .unwrap_or_default(); + let (left, right) = left.diff_display(&right); JointDisplayOutput { left, right } }) .map(|out| JsValue::from_serde(&out).unwrap())