Skip to main content

graphrefly_graph/
mount.rs

1//! Mount / unmount + `ancestors()` (canonical spec §3.4).
2//!
3//! D246: subgraphs are just child levels of the **Core-free** namespace
4//! tree (`Rc<RefCell<GraphInner>>`); the embedder owns the one `Core`.
5//! `mount` / `mount_new` / `ancestors` are pure-namespace (no `&Core`);
6//! `unmount` takes the embedder's `&Core` because it runs the TEARDOWN
7//! cascade. Returns plain [`Graph`] handles (a cheap `Arc` clone). The
8//! old `SubgraphRef`/`same_dispatcher`/`CoreMismatch` machinery is
9//! deleted — there is only ever the one embedder `Core` (no Core lives
10//! on a `Graph` to mismatch).
11
12use std::cell::RefCell;
13use std::rc::{Rc, Weak};
14
15use graphrefly_core::Core;
16
17use crate::graph::{destroy_subtree, fire_ns, Graph, GraphInner, NameError};
18
19/// Errors from `mount` / `mount_new` / `unmount`.
20#[derive(Debug, thiserror::Error)]
21pub enum MountError {
22    #[error("Graph::mount: name `{0}` already mounted in this graph")]
23    NameCollision(String),
24    #[error("Graph::mount: name `{0}` collides with an existing local node name")]
25    NodeNameCollision(String),
26    #[error("Graph::mount: name `{0}` may not contain the `::` path separator")]
27    InvalidName(String),
28    #[error("Graph::mount: child graph already has a parent; unmount it first")]
29    AlreadyMounted,
30    #[error("Graph::unmount: no subgraph named `{0}`")]
31    NotMounted(String),
32    #[error("Graph::mount: graph has been destroyed")]
33    Destroyed,
34}
35
36/// Result of [`Graph::unmount`] / [`Graph::remove`].
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct GraphRemoveAudit {
39    /// Number of nodes torn down (own + recursive into mounts).
40    pub node_count: usize,
41    /// Number of mounts torn down (recursive count).
42    pub mount_count: usize,
43}
44
45impl From<NameError> for MountError {
46    fn from(err: NameError) -> Self {
47        match err {
48            NameError::Collision(n) => Self::NodeNameCollision(n),
49            NameError::InvalidName(n) | NameError::ReservedPrefix(n) => Self::InvalidName(n),
50            NameError::Destroyed => Self::Destroyed,
51        }
52    }
53}
54
55/// Construct a fresh empty child `GraphInner` handle with `parent` set.
56fn new_child_inner(name: String, parent: Weak<RefCell<GraphInner>>) -> Rc<RefCell<GraphInner>> {
57    Rc::new(RefCell::new(GraphInner {
58        name,
59        names: indexmap::IndexMap::new(),
60        names_inverse: indexmap::IndexMap::new(),
61        children: indexmap::IndexMap::new(),
62        parent: Some(parent),
63        destroyed: false,
64        namespace_sinks: indexmap::IndexMap::new(),
65        next_ns_sink_id: 0,
66    }))
67}
68
69pub(crate) fn mount(
70    core: &Core,
71    parent_inner: &Rc<RefCell<GraphInner>>,
72    name: String,
73    child: &Graph,
74) -> Result<Graph, MountError> {
75    if name.contains("::") {
76        return Err(MountError::InvalidName(name));
77    }
78    let child_inner = child.inner_arc().clone();
79    // Validate + claim the namespace slot under the parent lock so
80    // concurrent mount(name, ...) calls cannot both pass validation
81    // (TOCTOU fix from /qa Slice E+). Lock order: parent → child.
82    {
83        let mut p = parent_inner.borrow_mut();
84        if p.destroyed {
85            return Err(MountError::Destroyed);
86        }
87        if p.children.contains_key(&name) {
88            return Err(MountError::NameCollision(name));
89        }
90        if p.names.contains_key(&name) {
91            return Err(MountError::NodeNameCollision(name));
92        }
93        {
94            let mut c = child_inner.borrow_mut();
95            if c.parent.is_some() {
96                return Err(MountError::AlreadyMounted);
97            }
98            c.parent = Some(Rc::downgrade(parent_inner));
99        }
100        p.children.insert(name, child_inner.clone());
101    }
102    // Fire AFTER lock drops (P3 — reactive describe / observe_all must
103    // see mounts as namespace changes). Owner-side `&Core` (D246 r2:
104    // firing ns sinks hands them `&Core`, so it IS a Core-touching op).
105    fire_ns(core, parent_inner);
106    Ok(Graph::from_inner(child_inner))
107}
108
109pub(crate) fn mount_new(
110    core: &Core,
111    parent_inner: &Rc<RefCell<GraphInner>>,
112    name: String,
113) -> Result<Graph, MountError> {
114    if name.contains("::") {
115        return Err(MountError::InvalidName(name));
116    }
117    let parent_weak = Rc::downgrade(parent_inner);
118    // Hold the parent lock across validation + child construction +
119    // insert (TOCTOU fix from /qa Slice E+).
120    let child_inner = {
121        let mut p = parent_inner.borrow_mut();
122        if p.destroyed {
123            return Err(MountError::Destroyed);
124        }
125        if p.children.contains_key(&name) {
126            return Err(MountError::NameCollision(name));
127        }
128        if p.names.contains_key(&name) {
129            return Err(MountError::NodeNameCollision(name));
130        }
131        let child_inner = new_child_inner(name.clone(), parent_weak);
132        p.children.insert(name, child_inner.clone());
133        child_inner
134    };
135    // Fire AFTER lock drops (P3). Owner-side `&Core` (D246 r2).
136    fire_ns(core, parent_inner);
137    Ok(Graph::from_inner(child_inner))
138}
139
140pub(crate) fn unmount(
141    core: &Core,
142    parent_inner: &Rc<RefCell<GraphInner>>,
143    name: &str,
144) -> Result<GraphRemoveAudit, MountError> {
145    let child = {
146        let mut p = parent_inner.borrow_mut();
147        if p.destroyed {
148            return Err(MountError::Destroyed);
149        }
150        p.children
151            .shift_remove(name)
152            .ok_or_else(|| MountError::NotMounted(name.to_owned()))?
153    };
154    let audit = audit_of(&child);
155    // Detach + destroy.
156    child.borrow_mut().parent = None;
157    destroy_subtree(core, &child);
158    // Fire on the parent AFTER the child's destroy completes (P3).
159    fire_ns(core, parent_inner);
160    Ok(audit)
161}
162
163pub(crate) fn ancestors(inner: &Rc<RefCell<GraphInner>>, include_self: bool) -> Vec<Graph> {
164    let mut chain: Vec<Graph> = Vec::new();
165    if include_self {
166        chain.push(Graph::from_inner(inner.clone()));
167    }
168    // Walk up via Weak parent pointers.
169    //
170    // Slice V3: visited-set cycle insurance per porting-deferred.md.
171    let mut visited = std::collections::HashSet::new();
172    visited.insert(Rc::as_ptr(inner) as usize);
173    let mut cursor: Option<Rc<RefCell<GraphInner>>> =
174        inner.borrow_mut().parent.as_ref().and_then(Weak::upgrade);
175    while let Some(cur) = cursor {
176        let ptr = Rc::as_ptr(&cur) as usize;
177        if !visited.insert(ptr) {
178            break; // Cycle detected — break rather than infinite-loop.
179        }
180        let next_parent = cur.borrow_mut().parent.as_ref().and_then(Weak::upgrade);
181        chain.push(Graph::from_inner(cur));
182        cursor = next_parent;
183    }
184    chain
185}
186
187fn audit_of(inner_arc: &Rc<RefCell<GraphInner>>) -> GraphRemoveAudit {
188    let inner = inner_arc.borrow_mut();
189    let own = inner.names.len();
190    let mount_count_self = inner.children.len();
191    let mut node_count = own;
192    let mut mount_count = mount_count_self;
193    let kids: Vec<Rc<RefCell<GraphInner>>> = inner.children.values().cloned().collect();
194    drop(inner);
195    for kid in kids {
196        let sub = audit_of(&kid);
197        node_count += sub.node_count;
198        mount_count += sub.mount_count;
199    }
200    GraphRemoveAudit {
201        node_count,
202        mount_count,
203    }
204}