1use std::sync::Arc;
2
3use rustc_hash::FxHashSet;
4
5use crate::error::GatError;
6use crate::morphism::TheoryMorphism;
7use crate::schema_functor::{TheoryConstraint, TheoryEndofunctor, TheoryTransform};
8use crate::theory::Theory;
9
10#[derive(Debug, Clone)]
12pub struct Factorization {
13 pub steps: Vec<TheoryEndofunctor>,
15 pub domain: Arc<str>,
17 pub codomain: Arc<str>,
19}
20
21fn emit_drops(
23 steps: &mut Vec<TheoryEndofunctor>,
24 morphism: &TheoryMorphism,
25 domain: &Theory,
26 codomain: &Theory,
27) {
28 let codomain_sort_names: FxHashSet<&str> = codomain.sorts.iter().map(|s| &*s.name).collect();
29 let codomain_op_names: FxHashSet<&str> = codomain.ops.iter().map(|o| &*o.name).collect();
30 let codomain_eq_names: FxHashSet<&str> = codomain.eqs.iter().map(|e| &*e.name).collect();
31
32 for eq in &domain.eqs {
34 if !codomain_eq_names.contains(&*eq.name) {
35 steps.push(TheoryEndofunctor {
36 name: Arc::from(format!("drop_eq_{}", eq.name)),
37 precondition: TheoryConstraint::HasEquation(Arc::clone(&eq.name)),
38 transform: TheoryTransform::DropEquation(Arc::clone(&eq.name)),
39 });
40 }
41 }
42
43 for op in &domain.ops {
45 let effective_name = morphism.op_map.get(&op.name).unwrap_or(&op.name);
46 if !codomain_op_names.contains(&**effective_name) {
47 steps.push(TheoryEndofunctor {
48 name: Arc::from(format!("drop_op_{}", op.name)),
49 precondition: TheoryConstraint::HasOp(Arc::clone(&op.name)),
50 transform: TheoryTransform::DropOp(Arc::clone(&op.name)),
51 });
52 }
53 }
54
55 for sort in &domain.sorts {
57 let effective_name = morphism.sort_map.get(&sort.name).unwrap_or(&sort.name);
58 if !codomain_sort_names.contains(&**effective_name) {
59 steps.push(TheoryEndofunctor {
60 name: Arc::from(format!("drop_sort_{}", sort.name)),
61 precondition: TheoryConstraint::HasSort(Arc::clone(&sort.name)),
62 transform: TheoryTransform::DropSort(Arc::clone(&sort.name)),
63 });
64 }
65 }
66}
67
68fn emit_renames(
70 steps: &mut Vec<TheoryEndofunctor>,
71 sort_renames: &[(Arc<str>, Arc<str>)],
72 op_renames: &[(Arc<str>, Arc<str>)],
73) {
74 for (old, new) in sort_renames {
75 steps.push(TheoryEndofunctor {
76 name: Arc::from(format!("rename_sort_{old}_{new}")),
77 precondition: TheoryConstraint::HasSort(Arc::clone(old)),
78 transform: TheoryTransform::RenameSort {
79 old: Arc::clone(old),
80 new: Arc::clone(new),
81 },
82 });
83 }
84
85 for (old, new) in op_renames {
86 steps.push(TheoryEndofunctor {
87 name: Arc::from(format!("rename_op_{old}_{new}")),
88 precondition: TheoryConstraint::HasOp(Arc::clone(old)),
89 transform: TheoryTransform::RenameOp {
90 old: Arc::clone(old),
91 new: Arc::clone(new),
92 },
93 });
94 }
95}
96
97fn emit_adds(
100 steps: &mut Vec<TheoryEndofunctor>,
101 morphism: &TheoryMorphism,
102 domain: &Theory,
103 codomain: &Theory,
104) {
105 let domain_sort_names_after_renames: FxHashSet<Arc<str>> = domain
106 .sorts
107 .iter()
108 .map(|s| morphism.sort_map.get(&s.name).unwrap_or(&s.name).clone())
109 .collect();
110 let domain_op_names_after_renames: FxHashSet<Arc<str>> = domain
111 .ops
112 .iter()
113 .map(|o| morphism.op_map.get(&o.name).unwrap_or(&o.name).clone())
114 .collect();
115
116 let sorts_to_add: Vec<_> = codomain
118 .sorts
119 .iter()
120 .filter(|s| !domain_sort_names_after_renames.contains(&s.name))
121 .collect();
122 let mut added_sorts = domain_sort_names_after_renames;
123 let mut sorted_adds = Vec::new();
124 let mut remaining = sorts_to_add;
125 let max_iterations = remaining.len() + 1;
126 for _ in 0..max_iterations {
127 if remaining.is_empty() {
128 break;
129 }
130 let (ready, not_ready): (Vec<_>, Vec<_>) = remaining
131 .into_iter()
132 .partition(|s| s.params.iter().all(|p| added_sorts.contains(p.sort.head())));
133 for sort in &ready {
134 added_sorts.insert(Arc::clone(&sort.name));
135 sorted_adds.push((*sort).clone());
136 }
137 remaining = not_ready;
138 }
139 for sort in sorted_adds {
140 steps.push(TheoryEndofunctor {
141 name: Arc::from(format!("add_sort_{}", sort.name)),
142 precondition: TheoryConstraint::Unconstrained,
143 transform: TheoryTransform::AddSort {
144 sort,
145 vertex_kind: None,
146 },
147 });
148 }
149
150 for op in &codomain.ops {
152 if !domain_op_names_after_renames.contains(&op.name) {
153 steps.push(TheoryEndofunctor {
154 name: Arc::from(format!("add_op_{}", op.name)),
155 precondition: TheoryConstraint::Unconstrained,
156 transform: TheoryTransform::AddOp(op.clone()),
157 });
158 }
159 }
160
161 for eq in &codomain.eqs {
163 let domain_has_eq = domain.eqs.iter().any(|deq| {
164 let mapped = deq.rename_ops(
165 &morphism
166 .op_map
167 .iter()
168 .map(|(k, v)| (Arc::clone(k), Arc::clone(v)))
169 .collect(),
170 );
171 mapped.lhs == eq.lhs && mapped.rhs == eq.rhs
172 });
173 if !domain_has_eq && domain.find_eq(&eq.name).is_none() {
174 steps.push(TheoryEndofunctor {
175 name: Arc::from(format!("add_eq_{}", eq.name)),
176 precondition: TheoryConstraint::Unconstrained,
177 transform: TheoryTransform::AddEquation(eq.clone()),
178 });
179 }
180 }
181}
182
183pub fn factorize(
198 morphism: &TheoryMorphism,
199 domain: &Theory,
200 codomain: &Theory,
201) -> Result<Factorization, GatError> {
202 let mut steps = Vec::new();
203
204 let mut sort_renames: Vec<(Arc<str>, Arc<str>)> = morphism
211 .sort_map
212 .iter()
213 .filter(|(old, new)| old != new && domain.has_sort(old) && codomain.has_sort(new))
214 .map(|(old, new)| (Arc::clone(old), Arc::clone(new)))
215 .collect();
216 sort_renames.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
217
218 let mut op_renames: Vec<(Arc<str>, Arc<str>)> = morphism
219 .op_map
220 .iter()
221 .filter(|(old, new)| old != new && domain.has_op(old) && codomain.has_op(new))
222 .map(|(old, new)| (Arc::clone(old), Arc::clone(new)))
223 .collect();
224 op_renames.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1)));
225
226 emit_drops(&mut steps, morphism, domain, codomain);
228
229 emit_renames(&mut steps, &sort_renames, &op_renames);
231
232 emit_adds(&mut steps, morphism, domain, codomain);
234
235 Ok(Factorization {
236 steps,
237 domain: Arc::clone(&morphism.domain),
238 codomain: Arc::clone(&morphism.codomain),
239 })
240}
241
242pub fn validate_factorization(
250 factorization: &Factorization,
251 domain: &Theory,
252 codomain: &Theory,
253) -> Result<(), GatError> {
254 let mut current = domain.clone();
255 for step in &factorization.steps {
256 current = step.transform.apply(¤t)?;
257 }
258 for sort in &codomain.sorts {
259 if !current.has_sort(&sort.name) {
260 return Err(GatError::FactorizationError(format!(
261 "factorized theory missing sort '{}' from codomain",
262 sort.name
263 )));
264 }
265 }
266 for op in &codomain.ops {
267 if !current.has_op(&op.name) {
268 return Err(GatError::FactorizationError(format!(
269 "factorized theory missing op '{}' from codomain",
270 op.name
271 )));
272 }
273 }
274 Ok(())
275}
276
277#[cfg(test)]
278#[allow(clippy::unwrap_used)]
279mod tests {
280 use super::*;
281 use std::collections::HashMap;
282
283 use crate::eq::{Equation, Term};
284 use crate::op::Operation;
285 use crate::sort::{Sort, SortParam};
286
287 fn graph_theory() -> Theory {
288 Theory::new(
289 "ThGraph",
290 vec![Sort::simple("Vertex"), Sort::simple("Edge")],
291 vec![
292 Operation::unary("src", "e", "Edge", "Vertex"),
293 Operation::unary("tgt", "e", "Edge", "Vertex"),
294 ],
295 Vec::new(),
296 )
297 }
298
299 fn renamed_graph_theory() -> Theory {
300 Theory::new(
301 "ThRenamedGraph",
302 vec![Sort::simple("Node"), Sort::simple("Arrow")],
303 vec![
304 Operation::unary("source", "e", "Arrow", "Node"),
305 Operation::unary("target", "e", "Arrow", "Node"),
306 ],
307 Vec::new(),
308 )
309 }
310
311 #[test]
312 fn identity_morphism_empty_factorization() {
313 let t = graph_theory();
314 let morph = TheoryMorphism::new(
315 "id",
316 "ThGraph",
317 "ThGraph",
318 HashMap::from([
319 (Arc::from("Vertex"), Arc::from("Vertex")),
320 (Arc::from("Edge"), Arc::from("Edge")),
321 ]),
322 HashMap::from([
323 (Arc::from("src"), Arc::from("src")),
324 (Arc::from("tgt"), Arc::from("tgt")),
325 ]),
326 );
327 let result = factorize(&morph, &t, &t).unwrap();
328 assert!(
329 result.steps.is_empty(),
330 "identity morphism should produce no steps"
331 );
332 }
333
334 #[test]
335 fn pure_rename_morphism() {
336 let domain = graph_theory();
337 let codomain = renamed_graph_theory();
338 let morph = TheoryMorphism::new(
339 "rename",
340 "ThGraph",
341 "ThRenamedGraph",
342 HashMap::from([
343 (Arc::from("Vertex"), Arc::from("Node")),
344 (Arc::from("Edge"), Arc::from("Arrow")),
345 ]),
346 HashMap::from([
347 (Arc::from("src"), Arc::from("source")),
348 (Arc::from("tgt"), Arc::from("target")),
349 ]),
350 );
351 let result = factorize(&morph, &domain, &codomain).unwrap();
352 assert_eq!(result.steps.len(), 4);
354 validate_factorization(&result, &domain, &codomain).unwrap();
356 }
357
358 #[test]
359 fn adding_sort_produces_add_step() {
360 let domain = Theory::new("T1", vec![Sort::simple("A")], Vec::new(), Vec::new());
361 let codomain = Theory::new(
362 "T2",
363 vec![Sort::simple("A"), Sort::simple("B")],
364 Vec::new(),
365 Vec::new(),
366 );
367 let morph = TheoryMorphism::new(
368 "add_b",
369 "T1",
370 "T2",
371 HashMap::from([(Arc::from("A"), Arc::from("A"))]),
372 HashMap::new(),
373 );
374 let result = factorize(&morph, &domain, &codomain).unwrap();
375 assert_eq!(result.steps.len(), 1);
376 assert!(matches!(
377 result.steps[0].transform,
378 TheoryTransform::AddSort { .. }
379 ));
380 validate_factorization(&result, &domain, &codomain).unwrap();
381 }
382
383 #[test]
384 fn dropping_sort_produces_correct_sequence() {
385 let domain = Theory::new(
386 "T1",
387 vec![Sort::simple("A"), Sort::simple("B")],
388 vec![Operation::unary("f", "x", "A", "B")],
389 Vec::new(),
390 );
391 let codomain = Theory::new("T2", vec![Sort::simple("A")], Vec::new(), Vec::new());
392 let morph = TheoryMorphism::new(
393 "drop_b",
394 "T1",
395 "T2",
396 HashMap::from([(Arc::from("A"), Arc::from("A"))]),
397 HashMap::new(),
398 );
399 let result = factorize(&morph, &domain, &codomain).unwrap();
400 assert!(result.steps.len() >= 2);
402 validate_factorization(&result, &domain, &codomain).unwrap();
403 }
404
405 #[test]
406 fn dependent_sort_ordering() {
407 let domain = Theory::new("T1", vec![Sort::simple("A")], Vec::new(), Vec::new());
408 let codomain = Theory::new(
409 "T2",
410 vec![
411 Sort::simple("A"),
412 Sort::simple("B"),
413 Sort::dependent("C", vec![SortParam::new("x", "B")]),
414 ],
415 Vec::new(),
416 Vec::new(),
417 );
418 let morph = TheoryMorphism::new(
419 "add",
420 "T1",
421 "T2",
422 HashMap::from([(Arc::from("A"), Arc::from("A"))]),
423 HashMap::new(),
424 );
425 let result = factorize(&morph, &domain, &codomain).unwrap();
426 let b_idx = result.steps.iter().position(
428 |s| matches!(&s.transform, TheoryTransform::AddSort { sort, .. } if &*sort.name == "B"),
429 );
430 let c_idx = result.steps.iter().position(
431 |s| matches!(&s.transform, TheoryTransform::AddSort { sort, .. } if &*sort.name == "C"),
432 );
433 assert!(b_idx.is_some() && c_idx.is_some());
434 assert!(b_idx.unwrap() < c_idx.unwrap(), "B must come before C");
435 validate_factorization(&result, &domain, &codomain).unwrap();
436 }
437
438 #[test]
439 fn mixed_add_drop_rename() {
440 let domain = Theory::new(
441 "T1",
442 vec![Sort::simple("A"), Sort::simple("B")],
443 vec![Operation::unary("f", "x", "A", "B")],
444 Vec::new(),
445 );
446 let codomain = Theory::new(
447 "T2",
448 vec![Sort::simple("Alpha"), Sort::simple("C")],
449 vec![Operation::unary("g", "x", "Alpha", "C")],
450 Vec::new(),
451 );
452 let morph = TheoryMorphism::new(
453 "mixed",
454 "T1",
455 "T2",
456 HashMap::from([
457 (Arc::from("A"), Arc::from("Alpha")),
458 ]),
460 HashMap::from([
461 ]),
463 );
464 let result = factorize(&morph, &domain, &codomain).unwrap();
465 assert!(!result.steps.is_empty());
467 validate_factorization(&result, &domain, &codomain).unwrap();
468 }
469
470 #[test]
471 fn equation_changes() {
472 let domain = Theory::new(
473 "T1",
474 vec![Sort::simple("A")],
475 vec![Operation::nullary("a", "A"), Operation::nullary("b", "A")],
476 vec![Equation::new(
477 "old_eq",
478 Term::constant("a"),
479 Term::constant("b"),
480 )],
481 );
482 let codomain = Theory::new(
483 "T2",
484 vec![Sort::simple("A")],
485 vec![Operation::nullary("a", "A"), Operation::nullary("b", "A")],
486 vec![Equation::new(
487 "new_eq",
488 Term::constant("b"),
489 Term::constant("a"),
490 )],
491 );
492 let morph = TheoryMorphism::new(
493 "eq_change",
494 "T1",
495 "T2",
496 HashMap::from([(Arc::from("A"), Arc::from("A"))]),
497 HashMap::from([
498 (Arc::from("a"), Arc::from("a")),
499 (Arc::from("b"), Arc::from("b")),
500 ]),
501 );
502 let result = factorize(&morph, &domain, &codomain).unwrap();
503 let has_drop = result
505 .steps
506 .iter()
507 .any(|s| matches!(&s.transform, TheoryTransform::DropEquation(n) if &**n == "old_eq"));
508 let has_add = result.steps.iter().any(
509 |s| matches!(&s.transform, TheoryTransform::AddEquation(eq) if &*eq.name == "new_eq"),
510 );
511 assert!(has_drop, "should drop old equation");
512 assert!(has_add, "should add new equation");
513 validate_factorization(&result, &domain, &codomain).unwrap();
514 }
515
516 #[test]
517 fn validate_factorization_catches_missing_sort() {
518 let domain = graph_theory();
519 let bad_factorization = Factorization {
520 steps: vec![],
521 domain: Arc::from("ThGraph"),
522 codomain: Arc::from("ThRenamedGraph"),
523 };
524 let codomain = renamed_graph_theory();
525 let result = validate_factorization(&bad_factorization, &domain, &codomain);
526 assert!(result.is_err());
527 }
528}