Skip to main content

panproto_schema/
colimit.rs

1//! Schema-level colimit (pushout) computation.
2//!
3//! Given two schemas and a description of their shared elements (the
4//! [`SchemaOverlap`]), [`schema_pushout`] computes the categorical
5//! pushout — a merged schema together with morphisms embedding each
6//! input into the result.
7
8use std::collections::HashMap;
9
10use panproto_gat::Name;
11use smallvec::SmallVec;
12
13use crate::error::SchemaError;
14use crate::morphism::SchemaMorphism;
15use crate::schema::{Edge, Schema, Vertex};
16
17/// Specifies which elements of two schemas are identified (shared).
18///
19/// Each pair `(left_id, right_id)` declares that the left and right
20/// elements represent the same concept and should be merged in the
21/// pushout.
22#[derive(Clone, Debug, Default)]
23pub struct SchemaOverlap {
24    /// Pairs of vertex IDs from `(left, right)` that represent the same vertex.
25    pub vertex_pairs: Vec<(Name, Name)>,
26    /// Pairs of edges from `(left, right)` that represent the same edge.
27    pub edge_pairs: Vec<(Edge, Edge)>,
28}
29
30/// Remap an edge's `src` and `tgt` through a vertex rename map.
31fn remap_edge(edge: &Edge, vmap: &HashMap<Name, Name>) -> Edge {
32    Edge {
33        src: vmap
34            .get(&edge.src)
35            .cloned()
36            .unwrap_or_else(|| edge.src.clone()),
37        tgt: vmap
38            .get(&edge.tgt)
39            .cloned()
40            .unwrap_or_else(|| edge.tgt.clone()),
41        kind: edge.kind.clone(),
42        name: edge.name.clone(),
43    }
44}
45
46/// Compute the pushout (colimit) of two schemas along their overlap.
47///
48/// Returns the pushout `Schema` plus `SchemaMorphism` values from each
49/// input schema into the pushout.
50///
51/// # Errors
52///
53/// Returns [`SchemaError::VertexNotFound`] if an overlap pair references
54/// a vertex ID that does not exist in the corresponding schema.
55pub fn schema_pushout(
56    left: &Schema,
57    right: &Schema,
58    overlap: &SchemaOverlap,
59) -> Result<(Schema, SchemaMorphism, SchemaMorphism), SchemaError> {
60    let right_vertex_rename = build_vertex_rename(left, right, overlap)?;
61
62    let (merged_vertices, left_vertex_map, right_vertex_map) =
63        build_merged_vertices(left, right, &right_vertex_rename);
64
65    let (merged_edges, left_edge_map, right_edge_map) =
66        build_merged_edges(left, right, overlap, &right_vertex_rename);
67
68    let pushout = assemble_pushout(
69        left,
70        right,
71        &right_vertex_rename,
72        merged_vertices,
73        merged_edges,
74    );
75
76    let left_morphism = SchemaMorphism {
77        name: "left→pushout".into(),
78        src_protocol: left.protocol.clone(),
79        tgt_protocol: pushout.protocol.clone(),
80        vertex_map: left_vertex_map,
81        edge_map: left_edge_map,
82        renames: vec![],
83    };
84
85    let right_morphism = SchemaMorphism {
86        name: "right→pushout".into(),
87        src_protocol: right.protocol.clone(),
88        tgt_protocol: pushout.protocol.clone(),
89        vertex_map: right_vertex_map,
90        edge_map: right_edge_map,
91        renames: vec![],
92    };
93
94    Ok((pushout, left_morphism, right_morphism))
95}
96
97/// Build the right→merged vertex ID rename map.
98///
99/// For identified vertices the right ID maps to the left ID.
100/// For non-identified vertices the right ID is kept unless it
101/// conflicts with a left ID, in which case it is prefixed with `"right."`.
102fn build_vertex_rename(
103    left: &Schema,
104    right: &Schema,
105    overlap: &SchemaOverlap,
106) -> Result<HashMap<Name, Name>, SchemaError> {
107    let mut rename: HashMap<Name, Name> = HashMap::new();
108
109    for (left_id, right_id) in &overlap.vertex_pairs {
110        if !left.vertices.contains_key(left_id) {
111            return Err(SchemaError::VertexNotFound(left_id.to_string()));
112        }
113        if !right.vertices.contains_key(right_id) {
114            return Err(SchemaError::VertexNotFound(right_id.to_string()));
115        }
116        rename.insert(right_id.clone(), left_id.clone());
117    }
118
119    for right_id in right.vertices.keys() {
120        if rename.contains_key(right_id) {
121            continue;
122        }
123        if left.vertices.contains_key(right_id) {
124            let merged_id = Name::from(format!("right.{right_id}"));
125            rename.insert(right_id.clone(), merged_id);
126        } else {
127            rename.insert(right_id.clone(), right_id.clone());
128        }
129    }
130
131    Ok(rename)
132}
133
134/// Build merged vertices and the left/right vertex morphism maps.
135fn build_merged_vertices(
136    left: &Schema,
137    right: &Schema,
138    right_rename: &HashMap<Name, Name>,
139) -> (
140    HashMap<Name, Vertex>,
141    HashMap<Name, Name>,
142    HashMap<Name, Name>,
143) {
144    let mut merged: HashMap<Name, Vertex> = HashMap::new();
145
146    for (id, v) in &left.vertices {
147        merged.insert(id.clone(), v.clone());
148    }
149
150    for (right_id, v) in &right.vertices {
151        let merged_id = right_rename
152            .get(right_id)
153            .cloned()
154            .unwrap_or_else(|| right_id.clone());
155        merged.entry(merged_id.clone()).or_insert_with(|| Vertex {
156            id: merged_id,
157            kind: v.kind.clone(),
158            nsid: v.nsid.clone(),
159        });
160    }
161
162    let left_map: HashMap<Name, Name> = left
163        .vertices
164        .keys()
165        .map(|id| (id.clone(), id.clone()))
166        .collect();
167
168    let right_map: HashMap<Name, Name> = right_rename.clone();
169
170    (merged, left_map, right_map)
171}
172
173/// Build merged edges and the left/right edge morphism maps.
174fn build_merged_edges(
175    left: &Schema,
176    right: &Schema,
177    overlap: &SchemaOverlap,
178    right_rename: &HashMap<Name, Name>,
179) -> (
180    HashMap<Edge, Name>,
181    HashMap<Edge, Edge>,
182    HashMap<Edge, Edge>,
183) {
184    let right_edge_to_left: HashMap<Edge, Edge> = overlap
185        .edge_pairs
186        .iter()
187        .map(|(l, r)| (r.clone(), l.clone()))
188        .collect();
189
190    let mut merged: HashMap<Edge, Name> = HashMap::new();
191    let mut left_map: HashMap<Edge, Edge> = HashMap::new();
192    let mut right_map: HashMap<Edge, Edge> = HashMap::new();
193
194    for (edge, kind) in &left.edges {
195        merged.insert(edge.clone(), kind.clone());
196        left_map.insert(edge.clone(), edge.clone());
197    }
198
199    for (edge, kind) in &right.edges {
200        if let Some(left_edge) = right_edge_to_left.get(edge) {
201            right_map.insert(edge.clone(), left_edge.clone());
202        } else {
203            let remapped = remap_edge(edge, right_rename);
204            if !merged.contains_key(&remapped) {
205                merged.insert(remapped.clone(), kind.clone());
206            }
207            right_map.insert(edge.clone(), remapped);
208        }
209    }
210
211    (merged, left_map, right_map)
212}
213
214/// Look up a right vertex ID through the rename map, falling back to identity.
215fn resolve(right_rename: &HashMap<Name, Name>, id: &Name) -> Name {
216    right_rename.get(id).cloned().unwrap_or_else(|| id.clone())
217}
218
219/// Merge vertex-keyed maps (constraints, nsids, variants, etc.) from right into left.
220fn merge_vertex_keyed(
221    left: &Schema,
222    right: &Schema,
223    right_rename: &HashMap<Name, Name>,
224) -> MergedVertexKeyed {
225    // Constraints
226    let mut constraints = left.constraints.clone();
227    for (rid, rcs) in &right.constraints {
228        let mid = resolve(right_rename, rid);
229        let entry = constraints.entry(mid).or_default();
230        for c in rcs {
231            if !entry.contains(c) {
232                entry.push(c.clone());
233            }
234        }
235    }
236
237    // Required edges
238    let mut required = left.required.clone();
239    for (rid, rreqs) in &right.required {
240        let mid = resolve(right_rename, rid);
241        let entry = required.entry(mid).or_default();
242        for req in rreqs {
243            let remapped = remap_edge(req, right_rename);
244            if !entry.contains(&remapped) {
245                entry.push(remapped);
246            }
247        }
248    }
249
250    // NSIDs
251    let mut nsids = left.nsids.clone();
252    for (rid, nsid) in &right.nsids {
253        let mid = resolve(right_rename, rid);
254        nsids.entry(mid).or_insert_with(|| nsid.clone());
255    }
256
257    // Variants
258    let mut variants = left.variants.clone();
259    for (rid, vs) in &right.variants {
260        let mid = resolve(right_rename, rid);
261        let entry = variants.entry(mid).or_default();
262        for v in vs {
263            let mut v2 = v.clone();
264            v2.parent_vertex = resolve(right_rename, &v2.parent_vertex);
265            if !entry.contains(&v2) {
266                entry.push(v2);
267            }
268        }
269    }
270
271    // Nominal
272    let mut nominal = left.nominal.clone();
273    for (rid, &nom) in &right.nominal {
274        let mid = resolve(right_rename, rid);
275        nominal.entry(mid).or_insert(nom);
276    }
277
278    MergedVertexKeyed {
279        constraints,
280        required,
281        nsids,
282        variants,
283        nominal,
284    }
285}
286
287/// Intermediate result for merged vertex-keyed maps.
288struct MergedVertexKeyed {
289    constraints: HashMap<Name, Vec<crate::schema::Constraint>>,
290    required: HashMap<Name, Vec<Edge>>,
291    nsids: HashMap<Name, Name>,
292    variants: HashMap<Name, Vec<crate::schema::Variant>>,
293    nominal: HashMap<Name, bool>,
294}
295
296/// Merge edge-keyed and structural maps from right into left, then
297/// assemble the final `Schema` with rebuilt adjacency indices.
298fn assemble_pushout(
299    left: &Schema,
300    right: &Schema,
301    right_rename: &HashMap<Name, Name>,
302    merged_vertices: HashMap<Name, Vertex>,
303    merged_edges: HashMap<Edge, Name>,
304) -> Schema {
305    let vk = merge_vertex_keyed(left, right, right_rename);
306
307    // Hyper-edges
308    let mut hyper_edges = left.hyper_edges.clone();
309    for (id, he) in &right.hyper_edges {
310        let mid = if hyper_edges.contains_key(id) {
311            Name::from(format!("right.{id}"))
312        } else {
313            id.clone()
314        };
315        let mut he2 = he.clone();
316        he2.id = mid.clone();
317        he2.signature = he2
318            .signature
319            .into_iter()
320            .map(|(label, vid)| {
321                let new_vid = right_rename.get(&vid).cloned().unwrap_or(vid);
322                (label, new_vid)
323            })
324            .collect();
325        hyper_edges.insert(mid, he2);
326    }
327
328    // Orderings
329    let mut orderings = left.orderings.clone();
330    for (edge, pos) in &right.orderings {
331        let remapped = remap_edge(edge, right_rename);
332        orderings.entry(remapped).or_insert(*pos);
333    }
334
335    // Recursion points
336    let mut recursion_points = left.recursion_points.clone();
337    for (id, rp) in &right.recursion_points {
338        let mid = resolve(right_rename, id);
339        recursion_points.entry(mid.clone()).or_insert_with(|| {
340            let mut rp2 = rp.clone();
341            rp2.mu_id = mid;
342            rp2.target_vertex = resolve(right_rename, &rp2.target_vertex);
343            rp2
344        });
345    }
346
347    // Spans
348    let mut spans = left.spans.clone();
349    for (id, sp) in &right.spans {
350        let mid = if spans.contains_key(id) {
351            Name::from(format!("right.{id}"))
352        } else {
353            id.clone()
354        };
355        let mut sp2 = sp.clone();
356        sp2.id = mid.clone();
357        sp2.left = resolve(right_rename, &sp2.left);
358        sp2.right = resolve(right_rename, &sp2.right);
359        spans.insert(mid, sp2);
360    }
361
362    // Usage modes
363    let mut usage_modes = left.usage_modes.clone();
364    for (edge, mode) in &right.usage_modes {
365        let remapped = remap_edge(edge, right_rename);
366        usage_modes.entry(remapped).or_insert_with(|| mode.clone());
367    }
368
369    // Coercions: merge (Name, Name) → CoercionSpec, left wins on overlap
370    let mut coercions = left.coercions.clone();
371    for (key, spec) in &right.coercions {
372        let merged_key = (resolve(right_rename, &key.0), resolve(right_rename, &key.1));
373        coercions.entry(merged_key).or_insert_with(|| spec.clone());
374    }
375
376    // Mergers: merge Name → Expr, left wins on overlap
377    let mut mergers = left.mergers.clone();
378    for (rid, expr) in &right.mergers {
379        let mid = resolve(right_rename, rid);
380        mergers.entry(mid).or_insert_with(|| expr.clone());
381    }
382
383    // Defaults: merge Name → Expr, left wins on overlap
384    let mut defaults = left.defaults.clone();
385    for (rid, expr) in &right.defaults {
386        let mid = resolve(right_rename, rid);
387        defaults.entry(mid).or_insert_with(|| expr.clone());
388    }
389
390    // Policies: merge Name → Expr, left wins on overlap
391    let mut policies = left.policies.clone();
392    for (rid, expr) in &right.policies {
393        let mid = resolve(right_rename, rid);
394        policies.entry(mid).or_insert_with(|| expr.clone());
395    }
396
397    // Rebuild adjacency indices
398    let idx = build_indices(&merged_edges);
399
400    Schema {
401        protocol: left.protocol.clone(),
402        vertices: merged_vertices,
403        edges: merged_edges,
404        hyper_edges,
405        constraints: vk.constraints,
406        required: vk.required,
407        nsids: vk.nsids,
408        variants: vk.variants,
409        orderings,
410        recursion_points,
411        spans,
412        usage_modes,
413        nominal: vk.nominal,
414        coercions,
415        mergers,
416        defaults,
417        policies,
418        outgoing: idx.outgoing,
419        incoming: idx.incoming,
420        between: idx.between,
421    }
422}
423
424/// Precomputed adjacency indices for a schema.
425struct AdjacencyIndices {
426    /// Outgoing edges per vertex ID.
427    outgoing: HashMap<Name, SmallVec<Edge, 4>>,
428    /// Incoming edges per vertex ID.
429    incoming: HashMap<Name, SmallVec<Edge, 4>>,
430    /// Edges between a specific `(src, tgt)` pair.
431    between: HashMap<(Name, Name), SmallVec<Edge, 2>>,
432}
433
434/// Rebuild adjacency indices from an edge map.
435fn build_indices(edges: &HashMap<Edge, Name>) -> AdjacencyIndices {
436    let mut outgoing: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
437    let mut incoming: HashMap<Name, SmallVec<Edge, 4>> = HashMap::new();
438    let mut between: HashMap<(Name, Name), SmallVec<Edge, 2>> = HashMap::new();
439
440    for edge in edges.keys() {
441        outgoing
442            .entry(edge.src.clone())
443            .or_default()
444            .push(edge.clone());
445        incoming
446            .entry(edge.tgt.clone())
447            .or_default()
448            .push(edge.clone());
449        between
450            .entry((edge.src.clone(), edge.tgt.clone()))
451            .or_default()
452            .push(edge.clone());
453    }
454
455    AdjacencyIndices {
456        outgoing,
457        incoming,
458        between,
459    }
460}
461
462#[cfg(test)]
463#[allow(clippy::unwrap_used)]
464mod tests {
465    use super::*;
466    use crate::{Protocol, SchemaBuilder};
467
468    fn test_protocol() -> Protocol {
469        Protocol {
470            name: "test".into(),
471            schema_theory: "ThTest".into(),
472            instance_theory: "ThWType".into(),
473            edge_rules: vec![],
474            obj_kinds: vec!["object".into(), "string".into(), "integer".into()],
475            constraint_sorts: vec![],
476            ..Protocol::default()
477        }
478    }
479
480    fn build_schema(vertices: &[(&str, &str)], edges: &[(&str, &str, &str, &str)]) -> Schema {
481        let proto = test_protocol();
482        let mut builder = SchemaBuilder::new(&proto);
483        for (id, kind) in vertices {
484            builder = builder.vertex(id, kind, None::<&str>).unwrap();
485        }
486        for (src, tgt, kind, name) in edges {
487            builder = builder.edge(src, tgt, kind, Some(*name)).unwrap();
488        }
489        builder.build().unwrap()
490    }
491
492    #[test]
493    fn pushout_of_identical_schemas_is_itself() {
494        let s = build_schema(
495            &[("root", "object"), ("root.x", "string")],
496            &[("root", "root.x", "prop", "x")],
497        );
498        let overlap = SchemaOverlap {
499            vertex_pairs: vec![
500                (Name::from("root"), Name::from("root")),
501                (Name::from("root.x"), Name::from("root.x")),
502            ],
503            edge_pairs: vec![(
504                Edge {
505                    src: Name::from("root"),
506                    tgt: Name::from("root.x"),
507                    kind: Name::from("prop"),
508                    name: Some(Name::from("x")),
509                },
510                Edge {
511                    src: Name::from("root"),
512                    tgt: Name::from("root.x"),
513                    kind: Name::from("prop"),
514                    name: Some(Name::from("x")),
515                },
516            )],
517        };
518
519        let (pushout, left_m, right_m) = schema_pushout(&s, &s, &overlap).unwrap();
520        assert_eq!(pushout.vertex_count(), s.vertex_count());
521        assert_eq!(pushout.edge_count(), s.edge_count());
522
523        for (src, tgt) in &left_m.vertex_map {
524            assert_eq!(src, tgt, "left morphism should be identity");
525        }
526        for (src, tgt) in &right_m.vertex_map {
527            assert_eq!(src, tgt, "right morphism should be identity");
528        }
529    }
530
531    #[test]
532    fn pushout_of_disjoint_schemas_is_union() {
533        let left = build_schema(
534            &[("a", "object"), ("a.x", "string")],
535            &[("a", "a.x", "prop", "x")],
536        );
537        let right = build_schema(
538            &[("b", "object"), ("b.y", "integer")],
539            &[("b", "b.y", "prop", "y")],
540        );
541
542        let overlap = SchemaOverlap::default();
543        let (pushout, _left_m, _right_m) = schema_pushout(&left, &right, &overlap).unwrap();
544
545        assert_eq!(pushout.vertex_count(), 4);
546        assert_eq!(pushout.edge_count(), 2);
547
548        assert!(pushout.has_vertex("a"));
549        assert!(pushout.has_vertex("a.x"));
550        assert!(pushout.has_vertex("b"));
551        assert!(pushout.has_vertex("b.y"));
552    }
553
554    #[test]
555    fn pushout_with_vertex_overlap_merges_vertices() {
556        let left = build_schema(
557            &[("root", "object"), ("root.x", "string")],
558            &[("root", "root.x", "prop", "x")],
559        );
560        let right = build_schema(
561            &[("base", "object"), ("base.y", "integer")],
562            &[("base", "base.y", "prop", "y")],
563        );
564
565        let overlap = SchemaOverlap {
566            vertex_pairs: vec![(Name::from("root"), Name::from("base"))],
567            edge_pairs: vec![],
568        };
569
570        let (pushout, left_m, right_m) = schema_pushout(&left, &right, &overlap).unwrap();
571
572        assert_eq!(pushout.vertex_count(), 3);
573        assert!(pushout.has_vertex("root"));
574        assert!(pushout.has_vertex("root.x"));
575        assert!(pushout.has_vertex("base.y"));
576
577        assert_eq!(
578            left_m.vertex_map.get("root").map(Name::as_str),
579            Some("root")
580        );
581        assert_eq!(
582            right_m.vertex_map.get("base").map(Name::as_str),
583            Some("root")
584        );
585        assert_eq!(
586            right_m.vertex_map.get("base.y").map(Name::as_str),
587            Some("base.y")
588        );
589    }
590
591    #[test]
592    fn pushout_with_edge_overlap_merges_edges() {
593        let left = build_schema(
594            &[("root", "object"), ("root.x", "string")],
595            &[("root", "root.x", "prop", "x")],
596        );
597        let right = build_schema(
598            &[("root", "object"), ("root.x", "string")],
599            &[("root", "root.x", "prop", "x")],
600        );
601
602        let overlap = SchemaOverlap {
603            vertex_pairs: vec![
604                (Name::from("root"), Name::from("root")),
605                (Name::from("root.x"), Name::from("root.x")),
606            ],
607            edge_pairs: vec![(
608                Edge {
609                    src: Name::from("root"),
610                    tgt: Name::from("root.x"),
611                    kind: Name::from("prop"),
612                    name: Some(Name::from("x")),
613                },
614                Edge {
615                    src: Name::from("root"),
616                    tgt: Name::from("root.x"),
617                    kind: Name::from("prop"),
618                    name: Some(Name::from("x")),
619                },
620            )],
621        };
622
623        let (pushout, _left_m, right_m) = schema_pushout(&left, &right, &overlap).unwrap();
624
625        assert_eq!(pushout.edge_count(), 1);
626
627        let right_edge = Edge {
628            src: Name::from("root"),
629            tgt: Name::from("root.x"),
630            kind: Name::from("prop"),
631            name: Some(Name::from("x")),
632        };
633        assert!(
634            right_m.edge_map.contains_key(&right_edge),
635            "right morphism should map the overlapping edge"
636        );
637    }
638
639    #[test]
640    fn morphisms_into_pushout_are_valid() {
641        let left = build_schema(
642            &[("root", "object"), ("root.a", "string")],
643            &[("root", "root.a", "prop", "a")],
644        );
645        let right = build_schema(
646            &[("root", "object"), ("root.b", "integer")],
647            &[("root", "root.b", "prop", "b")],
648        );
649
650        let overlap = SchemaOverlap {
651            vertex_pairs: vec![(Name::from("root"), Name::from("root"))],
652            edge_pairs: vec![],
653        };
654
655        let (pushout, left_m, right_m) = schema_pushout(&left, &right, &overlap).unwrap();
656
657        for (src, tgt) in &left_m.vertex_map {
658            assert!(
659                pushout.has_vertex(tgt),
660                "left morphism target `{tgt}` (from `{src}`) should exist in pushout"
661            );
662        }
663
664        for (src, tgt) in &right_m.vertex_map {
665            assert!(
666                pushout.has_vertex(tgt),
667                "right morphism target `{tgt}` (from `{src}`) should exist in pushout"
668            );
669        }
670
671        for tgt_e in left_m.edge_map.values() {
672            assert!(
673                pushout.edges.contains_key(tgt_e),
674                "left morphism edge target should exist in pushout"
675            );
676        }
677
678        for tgt_e in right_m.edge_map.values() {
679            assert!(
680                pushout.edges.contains_key(tgt_e),
681                "right morphism edge target should exist in pushout"
682            );
683        }
684    }
685
686    #[test]
687    fn pushout_conflicting_vertex_ids_are_prefixed() {
688        let left = build_schema(
689            &[("v", "object"), ("v.x", "string")],
690            &[("v", "v.x", "prop", "x")],
691        );
692        let right = build_schema(
693            &[("v", "object"), ("v.y", "integer")],
694            &[("v", "v.y", "prop", "y")],
695        );
696
697        let overlap = SchemaOverlap::default();
698        let (pushout, _left_m, right_m) = schema_pushout(&left, &right, &overlap).unwrap();
699
700        assert!(pushout.has_vertex("v"));
701        assert!(pushout.has_vertex("right.v"));
702        assert_eq!(
703            right_m.vertex_map.get("v").map(Name::as_str),
704            Some("right.v")
705        );
706    }
707
708    #[test]
709    fn overlap_with_missing_vertex_returns_error() {
710        let s = build_schema(&[("a", "object")], &[]);
711        let overlap = SchemaOverlap {
712            vertex_pairs: vec![(Name::from("nonexistent"), Name::from("a"))],
713            edge_pairs: vec![],
714        };
715        let result = schema_pushout(&s, &s, &overlap);
716        assert!(result.is_err());
717    }
718}