689 lines
27 KiB
Rust
689 lines
27 KiB
Rust
|
|
//! Patch types for version control of geolog structures
|
||
|
|
//!
|
||
|
|
//! A Patch represents the changes between two versions of a Structure.
|
||
|
|
//! Patches are the fundamental unit of version history - each commit
|
||
|
|
//! creates a new patch that can be applied to recreate the structure.
|
||
|
|
|
||
|
|
use crate::core::SortId;
|
||
|
|
use crate::id::{NumericId, Slid, Uuid};
|
||
|
|
use rkyv::{Archive, Deserialize, Serialize};
|
||
|
|
use std::collections::{BTreeMap, BTreeSet};
|
||
|
|
|
||
|
|
/// Changes to the element universe (additions and deletions)
|
||
|
|
///
|
||
|
|
/// Note: Element names are tracked separately in NamingPatch.
|
||
|
|
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||
|
|
#[archive(check_bytes)]
|
||
|
|
pub struct ElementPatch {
|
||
|
|
/// Elements removed from structure (by UUID)
|
||
|
|
pub deletions: BTreeSet<Uuid>,
|
||
|
|
/// Elements added: Uuid → sort_id
|
||
|
|
pub additions: BTreeMap<Uuid, SortId>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl ElementPatch {
|
||
|
|
pub fn is_empty(&self) -> bool {
|
||
|
|
self.deletions.is_empty() && self.additions.is_empty()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Changes to element names (separate from structural changes)
|
||
|
|
///
|
||
|
|
/// Names can change independently of structure (renames), and new elements
|
||
|
|
/// need names. This keeps patches self-contained for version control.
|
||
|
|
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||
|
|
#[archive(check_bytes)]
|
||
|
|
pub struct NamingPatch {
|
||
|
|
/// Names removed (by UUID) - typically when element is deleted
|
||
|
|
pub deletions: BTreeSet<Uuid>,
|
||
|
|
/// Names added or changed: UUID → qualified_name path
|
||
|
|
pub additions: BTreeMap<Uuid, Vec<String>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl NamingPatch {
|
||
|
|
pub fn is_empty(&self) -> bool {
|
||
|
|
self.deletions.is_empty() && self.additions.is_empty()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Changes to function definitions
|
||
|
|
///
|
||
|
|
/// We track both old and new values to support inversion (for undo).
|
||
|
|
/// The structure uses UUIDs rather than Slids since Slids are unstable
|
||
|
|
/// across different structure versions.
|
||
|
|
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||
|
|
#[archive(check_bytes)]
|
||
|
|
pub struct FunctionPatch {
|
||
|
|
/// func_id → (domain_uuid → old_codomain_uuid)
|
||
|
|
/// None means was undefined before
|
||
|
|
pub old_values: BTreeMap<usize, BTreeMap<Uuid, Option<Uuid>>>,
|
||
|
|
/// func_id → (domain_uuid → new_codomain_uuid)
|
||
|
|
pub new_values: BTreeMap<usize, BTreeMap<Uuid, Uuid>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl FunctionPatch {
|
||
|
|
pub fn is_empty(&self) -> bool {
|
||
|
|
self.new_values.is_empty()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Changes to relation assertions (tuples added/removed)
|
||
|
|
///
|
||
|
|
/// Tuples are stored as `Vec<Uuid>` since element Slids are unstable across versions.
|
||
|
|
/// We track both assertions and retractions to support inversion.
|
||
|
|
#[derive(Default, Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||
|
|
#[archive(check_bytes)]
|
||
|
|
pub struct RelationPatch {
|
||
|
|
/// rel_id → set of tuples retracted (as UUID vectors)
|
||
|
|
pub retractions: BTreeMap<usize, BTreeSet<Vec<Uuid>>>,
|
||
|
|
/// rel_id → set of tuples asserted (as UUID vectors)
|
||
|
|
pub assertions: BTreeMap<usize, BTreeSet<Vec<Uuid>>>,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl RelationPatch {
|
||
|
|
pub fn is_empty(&self) -> bool {
|
||
|
|
self.assertions.is_empty() && self.retractions.is_empty()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// A complete patch between two structure versions
|
||
|
|
///
|
||
|
|
/// Patches form a linked list via source_commit → target_commit.
|
||
|
|
/// The initial commit has source_commit = None.
|
||
|
|
///
|
||
|
|
/// Note: Theory reference is stored as a Luid in the Structure, not here.
|
||
|
|
#[derive(Clone, Debug, PartialEq, Eq, Archive, Deserialize, Serialize)]
|
||
|
|
#[archive(check_bytes)]
|
||
|
|
pub struct Patch {
|
||
|
|
/// The commit this patch is based on (None for initial commit)
|
||
|
|
pub source_commit: Option<Uuid>,
|
||
|
|
/// The commit this patch creates
|
||
|
|
pub target_commit: Uuid,
|
||
|
|
/// Number of sorts in the theory (needed to rebuild structure)
|
||
|
|
pub num_sorts: usize,
|
||
|
|
/// Number of functions in the theory (needed to rebuild structure)
|
||
|
|
pub num_functions: usize,
|
||
|
|
/// Number of relations in the theory (needed to rebuild structure)
|
||
|
|
pub num_relations: usize,
|
||
|
|
/// Element changes (additions/deletions)
|
||
|
|
pub elements: ElementPatch,
|
||
|
|
/// Function value changes
|
||
|
|
pub functions: FunctionPatch,
|
||
|
|
/// Relation tuple changes (assertions/retractions)
|
||
|
|
pub relations: RelationPatch,
|
||
|
|
/// Name changes (for self-contained patches)
|
||
|
|
pub names: NamingPatch,
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Patch {
|
||
|
|
/// Create a new patch
|
||
|
|
pub fn new(
|
||
|
|
source_commit: Option<Uuid>,
|
||
|
|
num_sorts: usize,
|
||
|
|
num_functions: usize,
|
||
|
|
num_relations: usize,
|
||
|
|
) -> Self {
|
||
|
|
Self {
|
||
|
|
source_commit,
|
||
|
|
target_commit: Uuid::now_v7(),
|
||
|
|
num_sorts,
|
||
|
|
num_functions,
|
||
|
|
num_relations,
|
||
|
|
elements: ElementPatch::default(),
|
||
|
|
functions: FunctionPatch::default(),
|
||
|
|
relations: RelationPatch::default(),
|
||
|
|
names: NamingPatch::default(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Check if this patch makes any changes
|
||
|
|
pub fn is_empty(&self) -> bool {
|
||
|
|
self.elements.is_empty()
|
||
|
|
&& self.functions.is_empty()
|
||
|
|
&& self.relations.is_empty()
|
||
|
|
&& self.names.is_empty()
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Invert this patch (swap old/new, additions/deletions)
|
||
|
|
///
|
||
|
|
/// Note: Inversion of element additions requires knowing the sort_id of deleted elements,
|
||
|
|
/// which we don't track in deletions. This is a known limitation - sort info is lost on invert.
|
||
|
|
/// Names are fully invertible since we track the full qualified name.
|
||
|
|
/// Relations are fully invertible (assertions ↔ retractions).
|
||
|
|
pub fn invert(&self) -> Patch {
|
||
|
|
Patch {
|
||
|
|
source_commit: Some(self.target_commit),
|
||
|
|
target_commit: self.source_commit.unwrap_or_else(Uuid::now_v7),
|
||
|
|
num_sorts: self.num_sorts,
|
||
|
|
num_functions: self.num_functions,
|
||
|
|
num_relations: self.num_relations,
|
||
|
|
elements: ElementPatch {
|
||
|
|
deletions: self.elements.additions.keys().copied().collect(),
|
||
|
|
additions: self
|
||
|
|
.elements
|
||
|
|
.deletions
|
||
|
|
.iter()
|
||
|
|
.map(|uuid| (*uuid, 0)) // Note: loses sort info on invert
|
||
|
|
.collect(),
|
||
|
|
},
|
||
|
|
functions: FunctionPatch {
|
||
|
|
old_values: self
|
||
|
|
.functions
|
||
|
|
.new_values
|
||
|
|
.iter()
|
||
|
|
.map(|(func_id, changes)| {
|
||
|
|
(
|
||
|
|
*func_id,
|
||
|
|
changes.iter().map(|(k, v)| (*k, Some(*v))).collect(),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
.collect(),
|
||
|
|
new_values: self
|
||
|
|
.functions
|
||
|
|
.old_values
|
||
|
|
.iter()
|
||
|
|
.filter_map(|(func_id, changes)| {
|
||
|
|
let filtered: BTreeMap<_, _> = changes
|
||
|
|
.iter()
|
||
|
|
.filter_map(|(k, v)| v.map(|v| (*k, v)))
|
||
|
|
.collect();
|
||
|
|
if filtered.is_empty() {
|
||
|
|
None
|
||
|
|
} else {
|
||
|
|
Some((*func_id, filtered))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect(),
|
||
|
|
},
|
||
|
|
relations: RelationPatch {
|
||
|
|
// Swap assertions ↔ retractions
|
||
|
|
retractions: self.relations.assertions.clone(),
|
||
|
|
assertions: self.relations.retractions.clone(),
|
||
|
|
},
|
||
|
|
names: NamingPatch {
|
||
|
|
deletions: self.names.additions.keys().copied().collect(),
|
||
|
|
additions: self
|
||
|
|
.names
|
||
|
|
.deletions
|
||
|
|
.iter()
|
||
|
|
.map(|uuid| (*uuid, vec![])) // Note: loses name on invert (would need old_names tracking)
|
||
|
|
.collect(),
|
||
|
|
},
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============ Diff and Apply operations ============
|
||
|
|
|
||
|
|
use crate::core::{RelationStorage, Structure};
|
||
|
|
use crate::id::{Luid, get_slid, some_slid};
|
||
|
|
use crate::naming::NamingIndex;
|
||
|
|
use crate::universe::Universe;
|
||
|
|
|
||
|
|
/// Create a patch representing the difference from `old` to `new`.
|
||
|
|
///
|
||
|
|
/// The resulting patch, when applied to `old`, produces `new`.
|
||
|
|
/// Requires Universe for UUID lookup and NamingIndex for name changes.
|
||
|
|
pub fn diff(
|
||
|
|
old: &Structure,
|
||
|
|
new: &Structure,
|
||
|
|
universe: &Universe,
|
||
|
|
old_naming: &NamingIndex,
|
||
|
|
new_naming: &NamingIndex,
|
||
|
|
) -> Patch {
|
||
|
|
let mut patch = Patch::new(
|
||
|
|
None, // Will be set by caller if needed
|
||
|
|
new.num_sorts(),
|
||
|
|
new.num_functions(),
|
||
|
|
new.relations.len(),
|
||
|
|
);
|
||
|
|
|
||
|
|
// Find element deletions: elements in old but not in new
|
||
|
|
for &luid in old.luids.iter() {
|
||
|
|
if !new.luid_to_slid.contains_key(&luid)
|
||
|
|
&& let Some(uuid) = universe.get(luid) {
|
||
|
|
patch.elements.deletions.insert(uuid);
|
||
|
|
// Also mark name as deleted
|
||
|
|
patch.names.deletions.insert(uuid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find element additions: elements in new but not in old
|
||
|
|
for (slid, &luid) in new.luids.iter().enumerate() {
|
||
|
|
if !old.luid_to_slid.contains_key(&luid)
|
||
|
|
&& let Some(uuid) = universe.get(luid) {
|
||
|
|
patch.elements.additions.insert(uuid, new.sorts[slid]);
|
||
|
|
// Also add name from new_naming
|
||
|
|
if let Some(name) = new_naming.get(&uuid) {
|
||
|
|
patch.names.additions.insert(uuid, name.clone());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find name changes for elements that exist in both
|
||
|
|
for &luid in new.luids.iter() {
|
||
|
|
if old.luid_to_slid.contains_key(&luid) {
|
||
|
|
// Element exists in both - check for name change
|
||
|
|
if let Some(uuid) = universe.get(luid) {
|
||
|
|
let old_name = old_naming.get(&uuid);
|
||
|
|
let new_name = new_naming.get(&uuid);
|
||
|
|
if old_name != new_name
|
||
|
|
&& let Some(name) = new_name {
|
||
|
|
patch.names.additions.insert(uuid, name.clone());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find function value changes
|
||
|
|
// We need to compare function values for elements that exist in both
|
||
|
|
for func_id in 0..new.num_functions() {
|
||
|
|
if func_id >= old.num_functions() {
|
||
|
|
// New function added to schema - all its values are additions
|
||
|
|
// Record each defined value with old_value = None
|
||
|
|
let Some(new_func_col) = new.functions[func_id].as_local() else { continue };
|
||
|
|
for (sort_slid, opt_codomain) in new_func_col.iter().enumerate() {
|
||
|
|
if let Some(new_codomain_slid) = get_slid(*opt_codomain) {
|
||
|
|
// Find UUIDs for domain and codomain
|
||
|
|
let domain_uuid = find_uuid_by_sort_slid(new, universe, func_id, sort_slid);
|
||
|
|
if let Some(domain_uuid) = domain_uuid {
|
||
|
|
let new_codomain_luid = new.luids[new_codomain_slid.index()];
|
||
|
|
if let Some(new_codomain_uuid) = universe.get(new_codomain_luid) {
|
||
|
|
// Record: this domain element now maps to this codomain element
|
||
|
|
// (was undefined before since function didn't exist)
|
||
|
|
patch.functions.old_values
|
||
|
|
.entry(func_id)
|
||
|
|
.or_default()
|
||
|
|
.insert(domain_uuid, None);
|
||
|
|
patch.functions.new_values
|
||
|
|
.entry(func_id)
|
||
|
|
.or_default()
|
||
|
|
.insert(domain_uuid, new_codomain_uuid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
let mut old_vals: BTreeMap<Uuid, Option<Uuid>> = BTreeMap::new();
|
||
|
|
let mut new_vals: BTreeMap<Uuid, Uuid> = BTreeMap::new();
|
||
|
|
|
||
|
|
// Iterate over elements in the new structure's function domain
|
||
|
|
// Note: patches only work with local functions currently
|
||
|
|
let Some(new_func_col) = new.functions[func_id].as_local() else { continue };
|
||
|
|
let Some(old_func_col) = old.functions[func_id].as_local() else { continue };
|
||
|
|
|
||
|
|
for (sort_slid, opt_codomain) in new_func_col.iter().enumerate() {
|
||
|
|
// Find the UUID for this domain element
|
||
|
|
if let Some(new_codomain_slid) = get_slid(*opt_codomain) {
|
||
|
|
let domain_uuid = find_uuid_by_sort_slid(new, universe, func_id, sort_slid);
|
||
|
|
if let Some(domain_uuid) = domain_uuid {
|
||
|
|
let new_codomain_luid = new.luids[new_codomain_slid.index()];
|
||
|
|
let new_codomain_uuid = universe.get(new_codomain_luid);
|
||
|
|
|
||
|
|
if let Some(new_codomain_uuid) = new_codomain_uuid {
|
||
|
|
// Check if this element existed in old (by looking up its luid)
|
||
|
|
let domain_luid = find_luid_by_sort_slid(new, func_id, sort_slid);
|
||
|
|
if let Some(domain_luid) = domain_luid {
|
||
|
|
if let Some(&old_domain_slid) = old.luid_to_slid.get(&domain_luid) {
|
||
|
|
let old_sort_slid = old.sort_local_id(old_domain_slid);
|
||
|
|
let old_codomain = get_slid(old_func_col[old_sort_slid.index()]);
|
||
|
|
|
||
|
|
match old_codomain {
|
||
|
|
Some(old_codomain_slid) => {
|
||
|
|
let old_codomain_luid = old.luids[old_codomain_slid.index()];
|
||
|
|
if let Some(old_codomain_uuid) =
|
||
|
|
universe.get(old_codomain_luid)
|
||
|
|
&& old_codomain_uuid != new_codomain_uuid {
|
||
|
|
// Value changed
|
||
|
|
old_vals
|
||
|
|
.insert(domain_uuid, Some(old_codomain_uuid));
|
||
|
|
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None => {
|
||
|
|
// Was undefined, now defined
|
||
|
|
old_vals.insert(domain_uuid, None);
|
||
|
|
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// Domain element is new - function value is part of the addition
|
||
|
|
new_vals.insert(domain_uuid, new_codomain_uuid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !new_vals.is_empty() {
|
||
|
|
patch.functions.old_values.insert(func_id, old_vals);
|
||
|
|
patch.functions.new_values.insert(func_id, new_vals);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find relation changes
|
||
|
|
// Compare tuples in each relation between old and new
|
||
|
|
let num_relations = new.relations.len().min(old.relations.len());
|
||
|
|
for rel_id in 0..num_relations {
|
||
|
|
let old_rel = &old.relations[rel_id];
|
||
|
|
let new_rel = &new.relations[rel_id];
|
||
|
|
|
||
|
|
// Helper: convert a Slid tuple to UUID tuple
|
||
|
|
let slid_tuple_to_uuids = |tuple: &[Slid], structure: &Structure| -> Option<Vec<Uuid>> {
|
||
|
|
tuple
|
||
|
|
.iter()
|
||
|
|
.map(|&slid| {
|
||
|
|
let luid = structure.luids[slid.index()];
|
||
|
|
universe.get(luid)
|
||
|
|
})
|
||
|
|
.collect()
|
||
|
|
};
|
||
|
|
|
||
|
|
// Find tuples in old but not in new (retractions)
|
||
|
|
let mut retractions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||
|
|
for tuple in old_rel.iter() {
|
||
|
|
// Check if this tuple (by UUID) exists in new
|
||
|
|
if let Some(uuid_tuple) = slid_tuple_to_uuids(tuple, old) {
|
||
|
|
// See if we can find the same UUID tuple in new
|
||
|
|
let exists_in_new = new_rel.iter().any(|new_tuple| {
|
||
|
|
slid_tuple_to_uuids(new_tuple, new)
|
||
|
|
.map(|new_uuids| new_uuids == uuid_tuple)
|
||
|
|
.unwrap_or(false)
|
||
|
|
});
|
||
|
|
if !exists_in_new {
|
||
|
|
retractions.insert(uuid_tuple);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find tuples in new but not in old (assertions)
|
||
|
|
let mut assertions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||
|
|
for tuple in new_rel.iter() {
|
||
|
|
if let Some(uuid_tuple) = slid_tuple_to_uuids(tuple, new) {
|
||
|
|
let exists_in_old = old_rel.iter().any(|old_tuple| {
|
||
|
|
slid_tuple_to_uuids(old_tuple, old)
|
||
|
|
.map(|old_uuids| old_uuids == uuid_tuple)
|
||
|
|
.unwrap_or(false)
|
||
|
|
});
|
||
|
|
if !exists_in_old {
|
||
|
|
assertions.insert(uuid_tuple);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !retractions.is_empty() {
|
||
|
|
patch.relations.retractions.insert(rel_id, retractions);
|
||
|
|
}
|
||
|
|
if !assertions.is_empty() {
|
||
|
|
patch.relations.assertions.insert(rel_id, assertions);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle new relations in new that don't exist in old
|
||
|
|
for rel_id in num_relations..new.relations.len() {
|
||
|
|
let new_rel = &new.relations[rel_id];
|
||
|
|
let mut assertions: BTreeSet<Vec<Uuid>> = BTreeSet::new();
|
||
|
|
|
||
|
|
for tuple in new_rel.iter() {
|
||
|
|
let uuid_tuple: Option<Vec<Uuid>> = tuple
|
||
|
|
.iter()
|
||
|
|
.map(|&slid| {
|
||
|
|
let luid = new.luids[slid.index()];
|
||
|
|
universe.get(luid)
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
if let Some(uuids) = uuid_tuple {
|
||
|
|
assertions.insert(uuids);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if !assertions.is_empty() {
|
||
|
|
patch.relations.assertions.insert(rel_id, assertions);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
patch
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper to find the Luid of an element given its func_id and sort_slid in a structure
|
||
|
|
fn find_luid_by_sort_slid(structure: &Structure, func_id: usize, sort_slid: usize) -> Option<Luid> {
|
||
|
|
let func_col_len = structure.functions[func_id].len();
|
||
|
|
for (slid_idx, &_sort_id) in structure.sorts.iter().enumerate() {
|
||
|
|
let slid = Slid::from_usize(slid_idx);
|
||
|
|
let elem_sort_slid = structure.sort_local_id(slid);
|
||
|
|
if elem_sort_slid.index() == sort_slid && func_col_len > sort_slid {
|
||
|
|
return Some(structure.luids[slid_idx]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Helper to find the UUID of an element given its func_id and sort_slid in a structure
|
||
|
|
fn find_uuid_by_sort_slid(
|
||
|
|
structure: &Structure,
|
||
|
|
universe: &Universe,
|
||
|
|
func_id: usize,
|
||
|
|
sort_slid: usize,
|
||
|
|
) -> Option<Uuid> {
|
||
|
|
find_luid_by_sort_slid(structure, func_id, sort_slid).and_then(|luid| universe.get(luid))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Apply a patch to create a new structure and update naming index.
|
||
|
|
///
|
||
|
|
/// Returns Ok(new_structure) on success, or Err with a description of what went wrong.
|
||
|
|
/// Requires a Universe to convert UUIDs from the patch to Luids.
|
||
|
|
/// The naming parameter is updated with name changes from the patch.
|
||
|
|
pub fn apply_patch(
|
||
|
|
base: &Structure,
|
||
|
|
patch: &Patch,
|
||
|
|
universe: &mut Universe,
|
||
|
|
naming: &mut NamingIndex,
|
||
|
|
) -> Result<Structure, String> {
|
||
|
|
// Create a new structure
|
||
|
|
let mut result = Structure::new(patch.num_sorts);
|
||
|
|
|
||
|
|
// Build a set of deleted UUIDs for quick lookup
|
||
|
|
let deleted_uuids: std::collections::HashSet<Uuid> =
|
||
|
|
patch.elements.deletions.iter().copied().collect();
|
||
|
|
|
||
|
|
// Copy elements from base that weren't deleted
|
||
|
|
for (slid, &luid) in base.luids.iter().enumerate() {
|
||
|
|
let uuid = universe.get(luid).ok_or("Unknown luid in base structure")?;
|
||
|
|
if !deleted_uuids.contains(&uuid) {
|
||
|
|
result.add_element_with_luid(luid, base.sorts[slid]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add new elements from the patch (register UUIDs in universe)
|
||
|
|
for (uuid, sort_id) in &patch.elements.additions {
|
||
|
|
result.add_element_with_uuid(universe, *uuid, *sort_id);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply naming changes
|
||
|
|
for uuid in &patch.names.deletions {
|
||
|
|
// Note: NamingIndex doesn't have a remove method yet, skip for now
|
||
|
|
let _ = uuid;
|
||
|
|
}
|
||
|
|
for (uuid, name) in &patch.names.additions {
|
||
|
|
naming.insert(*uuid, name.clone());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize function storage
|
||
|
|
let domain_sort_ids: Vec<Option<SortId>> = (0..patch.num_functions)
|
||
|
|
.map(|func_id| {
|
||
|
|
if func_id < base.functions.len() && !base.functions[func_id].is_empty() {
|
||
|
|
let func_len = base.functions[func_id].len();
|
||
|
|
for (sort_id, carrier) in base.carriers.iter().enumerate() {
|
||
|
|
if carrier.len() as usize == func_len {
|
||
|
|
return Some(sort_id);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
None
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
result.init_functions(&domain_sort_ids);
|
||
|
|
|
||
|
|
// Copy function values from base (for non-deleted elements)
|
||
|
|
// Note: patches only work with local functions currently
|
||
|
|
for func_id in 0..base.num_functions().min(result.num_functions()) {
|
||
|
|
let Some(base_func_col) = base.functions[func_id].as_local() else { continue };
|
||
|
|
if !result.functions[func_id].is_local() { continue };
|
||
|
|
|
||
|
|
// Collect all the updates we need to make (to avoid borrow checker issues)
|
||
|
|
let mut updates: Vec<(usize, Slid)> = Vec::new();
|
||
|
|
|
||
|
|
for (old_sort_slid, opt_codomain) in base_func_col.iter().enumerate() {
|
||
|
|
if let Some(old_codomain_slid) = get_slid(*opt_codomain) {
|
||
|
|
// Find the domain element's Luid
|
||
|
|
let domain_luid = find_luid_by_sort_slid(base, func_id, old_sort_slid);
|
||
|
|
if let Some(domain_luid) = domain_luid {
|
||
|
|
// Check if domain element still exists in result
|
||
|
|
if let Some(&new_domain_slid) = result.luid_to_slid.get(&domain_luid) {
|
||
|
|
// Check if codomain element still exists
|
||
|
|
let codomain_luid = base.luids[old_codomain_slid.index()];
|
||
|
|
if let Some(&new_codomain_slid) = result.luid_to_slid.get(&codomain_luid) {
|
||
|
|
let new_sort_slid = result.sort_local_id(new_domain_slid);
|
||
|
|
updates.push((new_sort_slid.index(), new_codomain_slid));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply updates
|
||
|
|
if let Some(result_func_col) = result.functions[func_id].as_local_mut() {
|
||
|
|
for (idx, codomain_slid) in updates {
|
||
|
|
if idx < result_func_col.len() {
|
||
|
|
result_func_col[idx] = some_slid(codomain_slid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply function value changes from patch (using UUIDs → Luids)
|
||
|
|
// Note: patches only work with local functions currently
|
||
|
|
for (func_id, changes) in &patch.functions.new_values {
|
||
|
|
if *func_id < result.num_functions() && result.functions[*func_id].is_local() {
|
||
|
|
// Collect updates first to avoid borrow checker issues
|
||
|
|
let mut updates: Vec<(usize, Slid)> = Vec::new();
|
||
|
|
for (domain_uuid, codomain_uuid) in changes {
|
||
|
|
let domain_luid = universe.lookup(domain_uuid);
|
||
|
|
let codomain_luid = universe.lookup(codomain_uuid);
|
||
|
|
if let (Some(domain_luid), Some(codomain_luid)) = (domain_luid, codomain_luid)
|
||
|
|
&& let (Some(&domain_slid), Some(&codomain_slid)) = (
|
||
|
|
result.luid_to_slid.get(&domain_luid),
|
||
|
|
result.luid_to_slid.get(&codomain_luid),
|
||
|
|
)
|
||
|
|
{
|
||
|
|
let sort_slid = result.sort_local_id(domain_slid);
|
||
|
|
updates.push((sort_slid.index(), codomain_slid));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply updates
|
||
|
|
if let Some(result_func_col) = result.functions[*func_id].as_local_mut() {
|
||
|
|
for (idx, codomain_slid) in updates {
|
||
|
|
if idx < result_func_col.len() {
|
||
|
|
result_func_col[idx] = some_slid(codomain_slid);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize relation storage
|
||
|
|
// Infer arities from base if available, otherwise from patch assertions
|
||
|
|
let relation_arities: Vec<usize> = (0..patch.num_relations)
|
||
|
|
.map(|rel_id| {
|
||
|
|
// Try base first
|
||
|
|
if rel_id < base.relations.len() {
|
||
|
|
base.relations[rel_id].arity()
|
||
|
|
} else if let Some(assertions) = patch.relations.assertions.get(&rel_id) {
|
||
|
|
// Infer from first assertion
|
||
|
|
assertions.iter().next().map(|t| t.len()).unwrap_or(0)
|
||
|
|
} else {
|
||
|
|
0
|
||
|
|
}
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
result.init_relations(&relation_arities);
|
||
|
|
|
||
|
|
// Copy relation tuples from base (for non-deleted elements)
|
||
|
|
for rel_id in 0..base.relations.len().min(patch.num_relations) {
|
||
|
|
let base_rel = &base.relations[rel_id];
|
||
|
|
|
||
|
|
for tuple in base_rel.iter() {
|
||
|
|
// Convert Slid tuple to UUID tuple to check if still valid
|
||
|
|
let uuid_tuple: Option<Vec<Uuid>> = tuple
|
||
|
|
.iter()
|
||
|
|
.map(|&slid| {
|
||
|
|
let luid = base.luids[slid.index()];
|
||
|
|
universe.get(luid)
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
if let Some(uuid_tuple) = uuid_tuple {
|
||
|
|
// Check if this tuple should be retracted
|
||
|
|
let should_retract = patch
|
||
|
|
.relations
|
||
|
|
.retractions
|
||
|
|
.get(&rel_id)
|
||
|
|
.map(|r| r.contains(&uuid_tuple))
|
||
|
|
.unwrap_or(false);
|
||
|
|
|
||
|
|
if !should_retract {
|
||
|
|
// Check all elements still exist and convert to new Slids
|
||
|
|
let new_tuple: Option<Vec<Slid>> = uuid_tuple
|
||
|
|
.iter()
|
||
|
|
.map(|uuid| {
|
||
|
|
universe
|
||
|
|
.lookup(uuid)
|
||
|
|
.and_then(|luid| result.luid_to_slid.get(&luid).copied())
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
if let Some(new_tuple) = new_tuple {
|
||
|
|
result.assert_relation(rel_id, new_tuple);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Apply relation assertions from patch
|
||
|
|
for (rel_id, assertions) in &patch.relations.assertions {
|
||
|
|
if *rel_id < patch.num_relations {
|
||
|
|
for uuid_tuple in assertions {
|
||
|
|
let slid_tuple: Option<Vec<Slid>> = uuid_tuple
|
||
|
|
.iter()
|
||
|
|
.map(|uuid| {
|
||
|
|
universe
|
||
|
|
.lookup(uuid)
|
||
|
|
.and_then(|luid| result.luid_to_slid.get(&luid).copied())
|
||
|
|
})
|
||
|
|
.collect();
|
||
|
|
|
||
|
|
if let Some(slid_tuple) = slid_tuple {
|
||
|
|
result.assert_relation(*rel_id, slid_tuple);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Ok(result)
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a patch representing a structure from empty (initial commit)
|
||
|
|
pub fn to_initial_patch(structure: &Structure, universe: &Universe, naming: &NamingIndex) -> Patch {
|
||
|
|
let empty = Structure::new(structure.num_sorts());
|
||
|
|
let empty_naming = NamingIndex::new();
|
||
|
|
diff(&empty, structure, universe, &empty_naming, naming)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Unit tests moved to tests/proptest_patch.rs
|