immigrant_schema/
changelist.rs

1//! Given the old and new list of named items, classify differences between them as renames (see `crate::renamelist`),
2//! creates, drops, and updates.
3//!
4//! If values are not compatible by the decision of `IsCompatible` trait, then they will not be updated, and instead
5//! old version will be dropped, and the new version will be created.
6
7use std::{collections::HashSet, fmt::Debug};
8
9use itertools::Itertools;
10
11use crate::{
12	diagnostics::Report, renamelist::{reorder_renames, RenameMoveaway, RenameOp, RenameTempAllocator}, uid::{RenameExt, RenameMap}, Diff
13};
14
15#[derive(Debug, PartialEq)]
16pub struct ChangeList<T: RenameExt> {
17	pub dropped: Vec<T>,
18	pub renamed: Vec<RenameOp<T>>,
19	pub moved_away: Vec<(T, RenameMoveaway)>,
20	pub updated: Vec<Diff<T>>,
21	pub created: Vec<T>,
22}
23impl<T: RenameExt> ChangeList<T> {
24	fn new() -> Self {
25		Self {
26			renamed: vec![],
27			created: vec![],
28			updated: vec![],
29			dropped: vec![],
30			moved_away: vec![],
31		}
32	}
33}
34
35pub trait IsCompatible {
36	fn is_compatible(&self, new: &Self, rn: &RenameMap, report_self: &mut Report, report_new: &mut Report) -> bool;
37}
38impl<T> IsCompatible for &T
39where
40	T: IsCompatible,
41{
42	fn is_compatible(&self, new: &Self, rn: &RenameMap, report_self: &mut Report, report_new: &mut Report) -> bool {
43		(**self).is_compatible(*new, rn, report_self, report_new)
44	}
45}
46
47pub trait IsIsomorph {
48	fn is_isomorph(&self, other: &Self, rn: &RenameMap, report_self: &mut Report, report_other: &mut Report) -> bool;
49}
50impl<T> IsIsomorph for &T
51where
52	T: IsIsomorph,
53{
54	fn is_isomorph(&self, new: &Self, rn: &RenameMap, report_self: &mut Report, report_other: &mut Report) -> bool {
55		(**self).is_isomorph(*new, rn, report_self, report_other)
56	}
57}
58#[macro_export]
59macro_rules! derive_is_isomorph_by_id_name {
60	($t:ty) => {
61		impl $crate::IsIsomorph for $t {
62			fn is_isomorph(&self, other: &Self, _rn: &$crate::RenameMap, _report_self: &mut $crate::diagnostics::Report, _report_other: &mut $crate::diagnostics::Report) -> bool {
63				use $crate::HasIdent;
64				HasIdent::id(self).name() == HasIdent::id(other).name()
65			}
66		}
67	};
68}
69
70pub fn mk_change_list<T: RenameExt + Clone + Copy + Debug, V: IsCompatible + IsIsomorph>(
71	rn: &RenameMap,
72	old: &[T],
73	new: &[T],
74	mapper: impl Fn(T) -> V,
75	report_old: &mut Report,
76	report_new: &mut Report,
77) -> ChangeList<T> {
78	let mut out = <ChangeList<T>>::new();
79
80	let mut old_listed = HashSet::new();
81	let mut new_listed = HashSet::new();
82
83	// Final list of occupied items
84	let occupancy = new.iter().map(|n| n.db(rn)).collect::<HashSet<_>>();
85
86	for (oid, old) in old.iter().cloned().enumerate() {
87		let mut new_by_exact = new.iter().cloned().enumerate().filter(|(i, f)| {
88			!new_listed.contains(i)
89				&& mapper(*f).is_isomorph(&mapper(old), rn, report_new, report_old)
90				&& f.db(rn) == old.db(rn)
91		});
92		if let Some((nid, new)) = new_by_exact.next() {
93			{
94				let other = new_by_exact.next();
95				assert!(
96					other.is_none(),
97					"second exact match shouldn't be possible: {nid} {new:?} {other:?}"
98				);
99			}
100			old_listed.insert(oid);
101			new_listed.insert(nid);
102			out.updated.push(Diff { old, new });
103			continue;
104		}
105	}
106	for (oid, old) in old.iter().cloned().enumerate() {
107		if old_listed.contains(&oid) {
108			continue;
109		}
110		let mut new_by_code =
111			new.iter().cloned().enumerate().filter(|(i, f)| {
112				mapper(*f).is_isomorph(&mapper(old), rn, report_new, report_old) && !new_listed.contains(i)
113			});
114		if let Some((nid, new)) = new_by_code.next() {
115			assert!(new_by_code.next().is_none());
116			old_listed.insert(oid);
117			new_listed.insert(nid);
118			out.updated.push(Diff { old, new });
119			continue;
120		}
121	}
122
123	let mut out_dropped = Vec::new();
124
125	let mut allocator = RenameTempAllocator::default();
126	for (oid, old) in old.iter().cloned().enumerate() {
127		if old_listed.contains(&oid) {
128			continue;
129		}
130		let mut new_by_db = new
131			.iter()
132			.cloned()
133			.enumerate()
134			.filter(|(i, f)| f.db(rn) == old.db(rn) && !new_listed.contains(i));
135		if let Some((nid, new)) = new_by_db.next() {
136			assert!(new_by_db.next().is_none());
137			old_listed.insert(oid);
138			new_listed.insert(nid);
139			out.updated.push(Diff { old, new });
140			continue;
141		}
142		let tmp = if occupancy.contains(&old.db(rn)) {
143			Some(allocator.next_moveaway())
144		} else {
145			None
146		};
147		out_dropped.push((old, tmp));
148	}
149
150	for recreated in out
151		.updated
152		.iter()
153		.filter(|diff| !mapper(diff.old).is_compatible(&mapper(diff.new), rn, report_old, report_new))
154		.collect::<Vec<_>>()
155	{
156		out.created.push(recreated.new);
157		out_dropped.push((recreated.old, Some(allocator.next_moveaway())));
158	}
159	out.updated
160		.retain(|diff| mapper(diff.old).is_compatible(&mapper(diff.new), rn, report_old, report_new));
161
162	for (_, new) in new
163		.iter()
164		.cloned()
165		.enumerate()
166		.filter(|(i, _)| !new_listed.contains(i))
167	{
168		out.created.push(new);
169	}
170
171	let mut to_rename = vec![];
172	for updated in out.updated.iter() {
173		to_rename.push((updated.old, updated.new.db(rn), updated.old));
174	}
175	let mut moveaways = vec![];
176	for old_dropped in out_dropped.iter() {
177		if let Some(tmp) = old_dropped.1 {
178			moveaways.push((old_dropped.0, tmp));
179		} else if new.iter().any(|n| n.db(rn) == old_dropped.0.db(rn)) {
180			moveaways.push((old_dropped.0, allocator.next_moveaway()));
181		}
182	}
183	out.renamed = reorder_renames(rn, to_rename, moveaways.clone(), &mut allocator);
184	out.moved_away = moveaways;
185	out.dropped = out_dropped.into_iter().map(|(v, _)| v).collect_vec();
186
187	out
188}
189
190// Always disabled, any() == false
191#[cfg(all(test, any()))]
192mod tests {
193	use crate::{
194		changelist::{ChangeList, IsCompatible},
195		ids::{in_allocator, DbIdent, Ident},
196		mk_change_list,
197		names::{DefName, TypeKind},
198		renamelist::{RenameOp, RenameTempAllocator},
199		span::{register_source, SimpleSpan},
200		uid::{next_uid, HasUid, OwnUid, RenameMap, Uid},
201		HasDefaultDbName, HasIdent,
202	};
203	#[test]
204	fn changelist_conflict() {
205		tracing_subscriber::fmt().init();
206		in_allocator(|| {
207			#[derive(Debug, PartialEq)]
208			struct P(OwnUid, DefName<TypeKind>);
209			derive_is_isomorph_by_id_name!(P);
210			impl IsCompatible for P {
211				fn is_compatible(&self, _new: &Self, _rn: &RenameMap) -> bool {
212					true
213				}
214			}
215			impl HasDefaultDbName for P {
216				type Kind = TypeKind;
217				fn default_db(&self) -> Option<DbIdent<Self::Kind>> {
218					self.1.default_db()
219				}
220			}
221			impl HasIdent for P {
222				type Kind = TypeKind;
223				fn id(&self) -> Ident<Self::Kind> {
224					self.1.id()
225				}
226			}
227			impl HasUid for P {
228				fn uid(&self) -> Uid {
229					self.0.uid()
230				}
231			}
232			fn p(a: &'static str, b: &'static str) -> P {
233				let s = a.to_string();
234				let s = register_source(s);
235				P(
236					next_uid(),
237					DefName::alloc((SimpleSpan::new(s, 0, a.len() as u32), a, Some(b))),
238				)
239			}
240			fn i(n: &'static str) -> DbIdent<TypeKind> {
241				DbIdent::new(n)
242			}
243			macro_rules! diff {
244				($a:expr, $b:expr) => {
245					crate::Diff { old: $a, new: $b }
246				};
247			}
248			let mut ren = RenameTempAllocator::default();
249			let ren1 = ren.next_temp();
250
251			assert_eq!(
252				mk_change_list(
253					&RenameMap::default(),
254					&[p("A", "D"), p("C", "B")],
255					&[p("C", "D"), p("A", "B")]
256				),
257				ChangeList {
258					renamed: vec![
259						RenameOp::Store(p("A", "D"), ren1),
260						RenameOp::Rename(p("C", "B"), i("D")),
261						RenameOp::Restore(ren1, i("B"))
262					],
263					updated: vec![
264						diff!(p("A", "D"), p("A", "B")),
265						diff!(p("C", "B"), p("C", "D"))
266					],
267
268					created: vec![],
269					dropped: vec![],
270					moved_away: vec![],
271				}
272			);
273			assert_eq!(
274				mk_change_list(
275					&RenameMap::default(),
276					&[p("A", "B")],
277					&[p("A", "C"), p("D", "B")]
278				),
279				ChangeList {
280					renamed: vec![RenameOp::Rename(p("A", "B"), i("C"))],
281					updated: vec![diff!(p("A", "B"), p("A", "C"))],
282					created: vec![p("D", "B")],
283					moved_away: vec![],
284
285					dropped: vec![],
286				}
287			);
288			assert_eq!(
289				mk_change_list(
290					&RenameMap::default(),
291					&[p("D", "B"), p("A", "C")],
292					&[p("A", "B")]
293				),
294				ChangeList {
295					renamed: vec![
296						RenameOp::Store(p("D", "B"), ren1),
297						RenameOp::Rename(p("A", "C"), i("B"))
298					],
299					updated: vec![diff!(p("A", "C"), p("A", "B")),],
300					dropped: vec![p("D", "B")],
301					moved_away: vec![(p("D", "B"), ren1)],
302
303					created: vec![],
304				}
305			);
306		});
307	}
308}