Skip to main content

graphrefly_graph/
mount.rs

1//! Mount / unmount + `ancestors()` (canonical spec §3.4).
2//!
3//! v1 limitation: shared-Core only. Mounting a child graph requires
4//! `Arc::ptr_eq` of the inner `Arc<Core>` — cross-Core (multi-binding)
5//! mount is post-M6 per Open Question 1 in
6//! `archive/docs/SESSION-rust-port-architecture.md` Part 6.
7
8use std::sync::Arc;
9
10use crate::graph::{Graph, GraphInner, NameError};
11
12/// Errors from `mount` / `mount_new` / `unmount`.
13#[derive(Debug, thiserror::Error)]
14pub enum MountError {
15    #[error("Graph::mount: name `{0}` already mounted in this graph")]
16    NameCollision(String),
17    #[error("Graph::mount: name `{0}` collides with an existing local node name")]
18    NodeNameCollision(String),
19    #[error("Graph::mount: name `{0}` may not contain the `::` path separator")]
20    InvalidName(String),
21    #[error(
22        "Graph::mount: child graph has a different Core (cross-Core mount is post-M6); \
23         clone-and-rebuild against this graph's Core, or use `mount_new` + builder"
24    )]
25    CoreMismatch,
26    #[error("Graph::mount: child graph already has a parent; unmount it first")]
27    AlreadyMounted,
28    #[error("Graph::unmount: no subgraph named `{0}`")]
29    NotMounted(String),
30    #[error("Graph::mount: graph has been destroyed")]
31    Destroyed,
32}
33
34/// Result of [`Graph::unmount`] (and exposed for future
35/// [`Graph::remove`] parity, canonical spec §3.2.1 `GraphRemoveAudit`).
36#[derive(Debug, Clone, PartialEq, Eq)]
37pub struct GraphRemoveAudit {
38    /// Number of nodes torn down (own + recursive into mounts).
39    pub node_count: usize,
40    /// Number of mounts torn down (recursive count).
41    pub mount_count: usize,
42}
43
44impl From<NameError> for MountError {
45    fn from(err: NameError) -> Self {
46        match err {
47            NameError::Collision(n) => Self::NodeNameCollision(n),
48            NameError::InvalidName(n) | NameError::ReservedPrefix(n) => Self::InvalidName(n),
49            NameError::Destroyed => Self::Destroyed,
50        }
51    }
52}
53
54pub(crate) fn mount(parent: &Graph, name: String, child: Graph) -> Result<Graph, MountError> {
55    if name.contains("::") {
56        return Err(MountError::InvalidName(name));
57    }
58    // Same-Core check — child must share the parent's Core. Cross-Core
59    // mount is post-M6 per session-doc Open Question 1.
60    if !parent.core.same_dispatcher(&child.core) {
61        return Err(MountError::CoreMismatch);
62    }
63    // Validate + claim the namespace slot under the parent lock so
64    // concurrent mount(name, ...) calls cannot both pass validation
65    // and overwrite each other's insert (TOCTOU fix from /qa Slice E+).
66    // We acquire the child lock under the parent lock (lock ordering:
67    // parent → child, never child → parent — there is no Graph code
68    // that acquires a parent lock from inside a child lock). Both are
69    // `parking_lot::Mutex` so the hold is short and contention-bound.
70    {
71        let mut parent_inner = parent.inner.lock();
72        if parent_inner.destroyed {
73            return Err(MountError::Destroyed);
74        }
75        if parent_inner.children.contains_key(&name) {
76            return Err(MountError::NameCollision(name));
77        }
78        if parent_inner.names.contains_key(&name) {
79            return Err(MountError::NodeNameCollision(name));
80        }
81        {
82            let mut child_inner = child.inner.lock();
83            if child_inner.parent.is_some() {
84                return Err(MountError::AlreadyMounted);
85            }
86            child_inner.parent = Some(Arc::downgrade(&parent.inner));
87        }
88        parent_inner.children.insert(name, child.clone());
89    }
90    // Fire AFTER lock drops (P3 — reactive describe / observe_all
91    // must see mounts as namespace changes).
92    parent.fire_namespace_change();
93    Ok(child)
94}
95
96pub(crate) fn mount_new(parent: &Graph, name: String) -> Result<Graph, MountError> {
97    if name.contains("::") {
98        return Err(MountError::InvalidName(name));
99    }
100    let parent_weak = Arc::downgrade(&parent.inner);
101    // Hold the parent lock across validation, child construction, and
102    // insert (TOCTOU fix from /qa Slice E+). `Graph::with_core` does
103    // not take any lock that conflicts — it allocates `Arc<Mutex<...>>`
104    // for the new child's GraphInner.
105    let child = {
106        let mut parent_inner = parent.inner.lock();
107        if parent_inner.destroyed {
108            return Err(MountError::Destroyed);
109        }
110        if parent_inner.children.contains_key(&name) {
111            return Err(MountError::NameCollision(name));
112        }
113        if parent_inner.names.contains_key(&name) {
114            return Err(MountError::NodeNameCollision(name));
115        }
116        let child = Graph::with_core(name.clone(), parent.core.clone(), Some(parent_weak));
117        parent_inner.children.insert(name, child.clone());
118        child
119    };
120    // Fire AFTER lock drops (P3).
121    parent.fire_namespace_change();
122    Ok(child)
123}
124
125pub(crate) fn unmount(parent: &Graph, name: &str) -> Result<GraphRemoveAudit, MountError> {
126    let child = {
127        let mut parent_inner = parent.inner.lock();
128        if parent_inner.destroyed {
129            return Err(MountError::Destroyed);
130        }
131        parent_inner
132            .children
133            .shift_remove(name)
134            .ok_or_else(|| MountError::NotMounted(name.to_owned()))?
135    };
136    let audit = audit_of(&child);
137    // Detach + destroy.
138    child.inner.lock().parent = None;
139    child.destroy();
140    // Fire on the parent AFTER the child's destroy completes (P3).
141    parent.fire_namespace_change();
142    Ok(audit)
143}
144
145pub(crate) fn ancestors(graph: &Graph, include_self: bool) -> Vec<Graph> {
146    let mut chain: Vec<Graph> = Vec::new();
147    if include_self {
148        chain.push(graph.clone());
149    }
150    // Walk up via Weak parent pointers. We need to reconstruct a `Graph`
151    // from the `Arc<Mutex<GraphInner>>` — but `Graph` also needs a
152    // `Core`. Because mount is shared-Core only, walking up never
153    // changes the Core; reuse `graph.core.clone()`.
154    //
155    // Slice V3: visited-set cycle insurance per porting-deferred.md.
156    // Mount validation prevents cycles, but this belt-and-braces check
157    // guards against future bugs in mount/unmount.
158    let mut visited = std::collections::HashSet::new();
159    visited.insert(Arc::as_ptr(&graph.inner) as usize);
160    let mut cursor: Option<Arc<parking_lot::Mutex<GraphInner>>> = graph
161        .inner
162        .lock()
163        .parent
164        .as_ref()
165        .and_then(std::sync::Weak::upgrade);
166    while let Some(inner) = cursor {
167        let ptr = Arc::as_ptr(&inner) as usize;
168        if !visited.insert(ptr) {
169            break; // Cycle detected — break rather than infinite-loop.
170        }
171        let next_parent = inner
172            .lock()
173            .parent
174            .as_ref()
175            .and_then(std::sync::Weak::upgrade);
176        chain.push(Graph {
177            core: graph.core.clone(),
178            inner,
179        });
180        cursor = next_parent;
181    }
182    chain
183}
184
185fn audit_of(graph: &Graph) -> GraphRemoveAudit {
186    let inner = graph.inner.lock();
187    let own = inner.names.len();
188    let mount_count_self = inner.children.len();
189    let mut node_count = own;
190    let mut mount_count = mount_count_self;
191    let kids: Vec<Graph> = inner.children.values().cloned().collect();
192    drop(inner);
193    for kid in kids {
194        let sub = audit_of(&kid);
195        node_count += sub.node_count;
196        mount_count += sub.mount_count;
197    }
198    GraphRemoveAudit {
199        node_count,
200        mount_count,
201    }
202}