use crate::ast_impl::{Decl, Located};
use crate::prettyprint::print_decl;
fn fnv1a_hash(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &b in data {
hash ^= b as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum DeclKind {
Definition,
Theorem,
Axiom,
Inductive,
Structure,
Class,
Instance,
Namespace,
Section,
Import,
Variable,
Open,
Attribute,
HashCmd,
Mutual,
Derive,
Notation,
Universe,
Other,
}
impl DeclKind {
pub fn of(decl: &Decl) -> Self {
match decl {
Decl::Definition { .. } => DeclKind::Definition,
Decl::Theorem { .. } => DeclKind::Theorem,
Decl::Axiom { .. } => DeclKind::Axiom,
Decl::Inductive { .. } => DeclKind::Inductive,
Decl::Structure { .. } => DeclKind::Structure,
Decl::ClassDecl { .. } => DeclKind::Class,
Decl::InstanceDecl { .. } => DeclKind::Instance,
Decl::Namespace { .. } => DeclKind::Namespace,
Decl::SectionDecl { .. } => DeclKind::Section,
Decl::Import { .. } => DeclKind::Import,
Decl::Variable { .. } => DeclKind::Variable,
Decl::Open { .. } => DeclKind::Open,
Decl::Attribute { .. } => DeclKind::Attribute,
Decl::HashCmd { .. } => DeclKind::HashCmd,
Decl::Mutual { .. } => DeclKind::Mutual,
Decl::Derive { .. } => DeclKind::Derive,
Decl::NotationDecl { .. } => DeclKind::Notation,
Decl::Universe { .. } => DeclKind::Universe,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeclFingerprint {
pub name: String,
pub kind: DeclKind,
pub body_hash: u64,
}
impl DeclFingerprint {
pub fn of(decl: &Located<Decl>) -> Self {
let kind = DeclKind::of(&decl.value);
let name = decl.value.name().unwrap_or("").to_owned();
let repr = print_decl(&decl.value);
let body_hash = fnv1a_hash(repr.as_bytes());
DeclFingerprint {
name,
kind,
body_hash,
}
}
pub fn is_modified_version_of(&self, other: &DeclFingerprint) -> bool {
self.name == other.name && self.kind == other.kind && self.body_hash != other.body_hash
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EditKind {
Unchanged,
Inserted,
Deleted,
Modified,
}
#[derive(Debug, Clone)]
pub struct DeclEdit {
pub kind: EditKind,
pub old_idx: Option<usize>,
pub new_idx: Option<usize>,
pub fingerprint: DeclFingerprint,
}
pub fn diff_modules(old: &[Located<Decl>], new: &[Located<Decl>]) -> Vec<DeclEdit> {
let old_fps: Vec<DeclFingerprint> = old.iter().map(DeclFingerprint::of).collect();
let new_fps: Vec<DeclFingerprint> = new.iter().map(DeclFingerprint::of).collect();
let raw = myers_diff(&old_fps, &new_fps);
merge_modified(raw)
}
fn myers_diff(old: &[DeclFingerprint], new: &[DeclFingerprint]) -> Vec<DeclEdit> {
let n = old.len();
let m = new.len();
if n == 0 && m == 0 {
return Vec::new();
}
if n == 0 {
return new
.iter()
.enumerate()
.map(|(j, fp)| DeclEdit {
kind: EditKind::Inserted,
old_idx: None,
new_idx: Some(j),
fingerprint: fp.clone(),
})
.collect();
}
if m == 0 {
return old
.iter()
.enumerate()
.map(|(i, fp)| DeclEdit {
kind: EditKind::Deleted,
old_idx: Some(i),
new_idx: None,
fingerprint: fp.clone(),
})
.collect();
}
let max_d = n + m;
let offset = max_d as isize; let size = 2 * max_d + 2;
let mut v: Vec<isize> = vec![0_isize; size];
let mut trace: Vec<Vec<isize>> = Vec::with_capacity(max_d + 1);
let mut found = false;
'outer: for d in 0..=(max_d as isize) {
let mut k = -d;
while k <= d {
let ki = (k + offset) as usize;
let mut x: isize = if k == -d || (k != d && v[ki + 1] > v[ki - 1]) {
v[ki + 1]
} else {
v[ki - 1] + 1
};
let mut y: isize = x - k;
while x < n as isize && y < m as isize && old[x as usize] == new[y as usize] {
x += 1;
y += 1;
}
v[ki] = x;
if x >= n as isize && y >= m as isize {
trace.push(v.clone());
found = true;
break 'outer;
}
k += 2;
}
trace.push(v.clone());
}
if !found {
return Vec::new();
}
backtrack_myers(old, new, &trace, offset)
}
fn backtrack_myers(
old: &[DeclFingerprint],
new: &[DeclFingerprint],
trace: &[Vec<isize>],
offset: isize,
) -> Vec<DeclEdit> {
let n = old.len() as isize;
let m = new.len() as isize;
let mut x = n;
let mut y = m;
let mut edits: Vec<DeclEdit> = Vec::new();
for d in (1..trace.len()).rev() {
let v_prev = &trace[d - 1];
let k = x - y;
let ki = (k + offset) as usize;
let came_from_insert = if k == -(d as isize) {
true } else if k == d as isize {
false } else {
v_prev[ki + 1] > v_prev[ki - 1]
};
let (mid_x, mid_y) = if came_from_insert {
let px = v_prev[ki + 1];
let py = px - (k + 1);
(px, py)
} else {
let px = v_prev[ki - 1];
let py = px - (k - 1);
(px, py)
};
let (after_x, after_y) = if came_from_insert {
(mid_x, mid_y + 1) } else {
(mid_x + 1, mid_y) };
let mut sx = x - 1;
let mut sy = y - 1;
while sx >= after_x && sy >= after_y {
edits.push(DeclEdit {
kind: EditKind::Unchanged,
old_idx: Some(sx as usize),
new_idx: Some(sy as usize),
fingerprint: new[sy as usize].clone(),
});
sx -= 1;
sy -= 1;
}
if came_from_insert {
if mid_y >= 0 && mid_y < m {
edits.push(DeclEdit {
kind: EditKind::Inserted,
old_idx: None,
new_idx: Some(mid_y as usize),
fingerprint: new[mid_y as usize].clone(),
});
}
} else {
if mid_x >= 0 && mid_x < n {
edits.push(DeclEdit {
kind: EditKind::Deleted,
old_idx: Some(mid_x as usize),
new_idx: None,
fingerprint: old[mid_x as usize].clone(),
});
}
}
x = mid_x;
y = mid_y;
}
let mut sx = x - 1;
let mut sy = y - 1;
while sx >= 0 && sy >= 0 {
edits.push(DeclEdit {
kind: EditKind::Unchanged,
old_idx: Some(sx as usize),
new_idx: Some(sy as usize),
fingerprint: new[sy as usize].clone(),
});
sx -= 1;
sy -= 1;
}
edits.reverse();
edits
}
fn merge_modified(raw: Vec<DeclEdit>) -> Vec<DeclEdit> {
use std::collections::HashMap;
let mut deleted_by_key: HashMap<
(String, String),
std::collections::VecDeque<(usize, DeclEdit)>,
> = HashMap::new();
for (pos, edit) in raw.iter().enumerate() {
if edit.kind == EditKind::Deleted {
let key = (
edit.fingerprint.name.clone(),
format!("{:?}", edit.fingerprint.kind),
);
deleted_by_key
.entry(key)
.or_default()
.push_back((pos, edit.clone()));
}
}
let mut inserted_by_key: HashMap<
(String, String),
std::collections::VecDeque<(usize, DeclEdit)>,
> = HashMap::new();
for (pos, edit) in raw.iter().enumerate() {
if edit.kind == EditKind::Inserted {
let key = (
edit.fingerprint.name.clone(),
format!("{:?}", edit.fingerprint.kind),
);
inserted_by_key
.entry(key)
.or_default()
.push_back((pos, edit.clone()));
}
}
let mut suppressed: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut modified_inserts: HashMap<usize, DeclEdit> = HashMap::new();
for ((name, kind_s), del_queue) in deleted_by_key.iter_mut() {
let key = (name.clone(), kind_s.clone());
if let Some(ins_queue) = inserted_by_key.get_mut(&key) {
while !del_queue.is_empty() && !ins_queue.is_empty() {
let (del_pos, del_edit) = del_queue.pop_front().expect("non-empty");
let (ins_pos, ins_edit) = ins_queue.pop_front().expect("non-empty");
if del_edit.fingerprint.body_hash != ins_edit.fingerprint.body_hash {
suppressed.insert(del_pos);
suppressed.insert(ins_pos);
modified_inserts.insert(
del_pos,
DeclEdit {
kind: EditKind::Modified,
old_idx: del_edit.old_idx,
new_idx: ins_edit.new_idx,
fingerprint: ins_edit.fingerprint.clone(),
},
);
}
}
}
}
let mut result: Vec<DeclEdit> = Vec::with_capacity(raw.len());
for (pos, edit) in raw.into_iter().enumerate() {
if suppressed.contains(&pos) {
if let Some(modified) = modified_inserts.remove(&pos) {
result.push(modified);
}
} else {
result.push(edit);
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::functions::parse_decls;
fn parse_lean(src: &str) -> Vec<Located<Decl>> {
use crate::lexer::Lexer;
use crate::parser_impl::Parser;
use crate::tokens::TokenKind;
let tokens = Lexer::new(src).tokenize();
let mut parser = Parser::new(tokens);
let mut decls = Vec::new();
loop {
if parser.is_eof() {
break;
}
match parser.parse_decl() {
Ok(d) => decls.push(d),
Err(e) => {
let is_eof_err = e.is_eof()
|| matches!(
e.message().as_str(),
s if s.contains("EOF") || s.contains("Eof") || s.contains("end of file")
);
if is_eof_err || parser.is_eof() {
break;
}
parser.advance();
}
}
}
decls
}
#[test]
fn test_diff_identical() {
let src = "def foo : Nat := 0\ndef bar : Nat := 1";
let old = parse_lean(src);
let new = parse_lean(src);
let edits = diff_modules(&old, &new);
assert!(
edits.iter().all(|e| e.kind == EditKind::Unchanged),
"identical sources should produce only Unchanged edits, got: {edits:?}"
);
assert_eq!(edits.len(), old.len());
}
#[test]
fn test_diff_insert_decl() {
let old_src = "def foo : Nat := 0";
let new_src = "def foo : Nat := 0\ndef bar : Nat := 1";
let old = parse_lean(old_src);
let new = parse_lean(new_src);
let edits = diff_modules(&old, &new);
let unchanged: Vec<_> = edits
.iter()
.filter(|e| e.kind == EditKind::Unchanged)
.collect();
let inserted: Vec<_> = edits
.iter()
.filter(|e| e.kind == EditKind::Inserted)
.collect();
assert_eq!(unchanged.len(), 1, "foo should be Unchanged");
assert_eq!(inserted.len(), 1, "bar should be Inserted");
assert_eq!(inserted[0].fingerprint.name, "bar");
}
#[test]
fn test_diff_delete_decl() {
let old_src = "def a : Nat := 0\ndef b : Nat := 1\ndef c : Nat := 2";
let new_src = "def a : Nat := 0\ndef c : Nat := 2";
let old = parse_lean(old_src);
let new = parse_lean(new_src);
let edits = diff_modules(&old, &new);
let deleted: Vec<_> = edits
.iter()
.filter(|e| e.kind == EditKind::Deleted)
.collect();
let unchanged: Vec<_> = edits
.iter()
.filter(|e| e.kind == EditKind::Unchanged)
.collect();
assert_eq!(deleted.len(), 1, "b should be Deleted");
assert_eq!(deleted[0].fingerprint.name, "b");
assert_eq!(unchanged.len(), 2, "a and c should be Unchanged");
let mut seen_old: Vec<bool> = vec![false; old.len()];
for edit in &edits {
if let Some(i) = edit.old_idx {
assert!(!seen_old[i], "old index {i} appears more than once");
seen_old[i] = true;
}
}
assert!(seen_old.iter().all(|&b| b), "not all old indices covered");
}
#[test]
fn test_diff_modified_decl() {
let old_src = "def foo : Nat := 0";
let new_src = "def foo : Nat := 99";
let old = parse_lean(old_src);
let new = parse_lean(new_src);
let edits = diff_modules(&old, &new);
let modified: Vec<_> = edits
.iter()
.filter(|e| e.kind == EditKind::Modified)
.collect();
assert_eq!(modified.len(), 1, "foo body change should be Modified");
assert_eq!(modified[0].fingerprint.name, "foo");
assert_eq!(modified[0].old_idx, Some(0));
assert_eq!(modified[0].new_idx, Some(0));
}
#[test]
fn test_diff_reorder() {
let old_src = "def a : Nat := 0\ndef b : Nat := 1";
let new_src = "def b : Nat := 1\ndef a : Nat := 0";
let old = parse_lean(old_src);
let new = parse_lean(new_src);
let edits = diff_modules(&old, &new);
let total_old_coverage: usize = edits.iter().filter(|e| e.old_idx.is_some()).count();
let total_new_coverage: usize = edits.iter().filter(|e| e.new_idx.is_some()).count();
assert_eq!(
total_old_coverage,
old.len(),
"all old indices must be covered"
);
assert_eq!(
total_new_coverage,
new.len(),
"all new indices must be covered"
);
let reconstructed = apply_edit_script(&old, &new, &edits);
assert_eq!(reconstructed.len(), new.len());
}
#[test]
fn test_diff_empty() {
let edits = diff_modules(&[], &[]);
assert!(
edits.is_empty(),
"diffing empty sequences should produce no edits"
);
}
#[test]
fn test_index_coverage_invariant() {
let old_src = "def a : Nat := 0\ntheorem t : True := trivial\ndef b : Nat := 2";
let new_src = "def a : Nat := 0\ndef b : Nat := 99\ndef c : Nat := 3";
let old = parse_lean(old_src);
let new = parse_lean(new_src);
let edits = diff_modules(&old, &new);
let mut old_seen = vec![false; old.len()];
for edit in &edits {
if let Some(i) = edit.old_idx {
assert!(!old_seen[i], "old_idx {i} appears twice");
old_seen[i] = true;
}
}
assert!(
old_seen.iter().all(|&b| b),
"some old indices not covered: {:?}",
old_seen
);
let mut new_seen = vec![false; new.len()];
for edit in &edits {
if let Some(j) = edit.new_idx {
assert!(!new_seen[j], "new_idx {j} appears twice");
new_seen[j] = true;
}
}
assert!(
new_seen.iter().all(|&b| b),
"some new indices not covered: {:?}",
new_seen
);
}
fn apply_edit_script<'a>(
old: &'a [Located<Decl>],
new: &'a [Located<Decl>],
edits: &[DeclEdit],
) -> Vec<&'a Located<Decl>> {
let mut result = Vec::new();
for edit in edits {
match edit.kind {
EditKind::Unchanged | EditKind::Modified => {
if let Some(j) = edit.new_idx {
result.push(&new[j]);
}
}
EditKind::Inserted => {
if let Some(j) = edit.new_idx {
result.push(&new[j]);
}
}
EditKind::Deleted => {
if let Some(i) = edit.old_idx {
let _ = &old[i]; }
}
}
}
result
}
}