Skip to main content

panproto_gat/
factorize.rs

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/// The result of factorizing a theory morphism.
11#[derive(Debug, Clone)]
12pub struct Factorization {
13    /// Ordered sequence of elementary endofunctors.
14    pub steps: Vec<TheoryEndofunctor>,
15    /// Domain theory name.
16    pub domain: Arc<str>,
17    /// Codomain theory name.
18    pub codomain: Arc<str>,
19}
20
21/// Emit drop steps for elements in domain not present in codomain.
22fn 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    // Equations first (they depend on ops/sorts)
33    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    // Ops
44    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    // Sorts
56    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
68/// Emit rename steps from the identified renames.
69fn 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
97/// Emit add steps for elements in codomain not present in domain (after renames).
98/// Sorts are topologically sorted by parameter dependencies.
99fn 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    // Sorts to add (topologically sorted by parameter deps)
117    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    // Ops to add
151    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    // Equations to add
162    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
183/// Factorize a theory morphism into elementary endofunctors.
184///
185/// Given a morphism F: T1 → T2, decompose it into a sequence of
186/// elementary endofunctors (add/drop/rename sort/op/equation) such
187/// that applying them in order to T1 produces T2.
188///
189/// Ordering: drops first (equations → ops → sorts), then renames,
190/// then adds (sorts → ops → equations). Within adds, sorts are
191/// topologically sorted by parameter dependencies.
192///
193/// # Errors
194///
195/// Returns [`GatError::FactorizationError`] if the morphism cannot be
196/// factorized into a valid sequence of transforms.
197pub fn factorize(
198    morphism: &TheoryMorphism,
199    domain: &Theory,
200    codomain: &Theory,
201) -> Result<Factorization, GatError> {
202    let mut steps = Vec::new();
203
204    // Phase 1: Identify renames. `sort_map` / `op_map` are `HashMap`s
205    // with a process-randomized hasher; iterating them directly drifts
206    // the emitted rename-step order across runs, which is observable in
207    // the factorization's `steps` vector and therefore in every
208    // downstream protolens chain. Sort by the `(old, new)` pair so the
209    // ordering is a pure function of the morphism contents.
210    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    // Phase 2: Drops
227    emit_drops(&mut steps, morphism, domain, codomain);
228
229    // Phase 3: Renames
230    emit_renames(&mut steps, &sort_renames, &op_renames);
231
232    // Phase 4: Adds
233    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
242/// Validate that applying a factorization to the domain yields a theory
243/// compatible with the codomain.
244///
245/// # Errors
246///
247/// Returns [`GatError::FactorizationError`] if the factorized theory is
248/// missing sorts or operations from the codomain.
249pub 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(&current)?;
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        // Should have 4 renames (2 sorts + 2 ops)
353        assert_eq!(result.steps.len(), 4);
354        // Validate
355        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        // Should drop op f first (depends on B), then sort B
401        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        // B must be added before C (since C depends on B)
427        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                // B is dropped, not mapped
459            ]),
460            HashMap::from([
461                // f is dropped, not mapped
462            ]),
463        );
464        let result = factorize(&morph, &domain, &codomain).unwrap();
465        // Should have: drop f, drop B, rename A→Alpha, add C, add g
466        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        // Should drop old_eq and add new_eq
504        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}