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///
38/// # Best-effort under concurrent mutation
39///
40/// Counts are a **point-in-time best-effort snapshot** of the detached
41/// subtree, NOT a transaction-isolated tally. The unmount flow detaches
42/// the child from the parent BEFORE auditing, so the only writers that
43/// can race the count are threads holding a [`Graph`] clone of the
44/// detached child (e.g., calling `state(...)` / `mount_new(...)` on
45/// the child between detach and audit). [`audit_of`] recursively walks
46/// the subtree, dropping each level's inner lock before descending —
47/// concurrent mutation within that window may shift the tally by the
48/// number of nodes/mounts added or removed.
49///
50/// **Why deliberate v1:** the single-owner [`graphrefly_core::OwnedCore`]
51/// model (D248/D255 actor-thread) makes the "Graph clone on another
52/// thread" scenario possible but narrow; a single-locked walk would
53/// require a cross-graph lock-ordering pass (parent + every descendant)
54/// that composes against `state()` / `mount_new()` re-entry — overkill
55/// for diagnostic data the spec frames as best-effort. If a real
56/// consumer needs transaction-isolated audit counts, lift via either a
57/// cross-graph multi-level lock-ordering scheme OR a copy-on-write
58/// epoch on the subtree (gated on D196 consumer pressure).
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct GraphRemoveAudit {
61    /// Number of nodes torn down (own + recursive into mounts).
62    pub node_count: usize,
63    /// Number of mounts torn down (recursive count).
64    pub mount_count: usize,
65}
66
67impl From<NameError> for MountError {
68    fn from(err: NameError) -> Self {
69        match err {
70            NameError::Collision(n) => Self::NodeNameCollision(n),
71            NameError::InvalidName(n) => Self::InvalidName(n),
72            NameError::Destroyed => Self::Destroyed,
73        }
74    }
75}
76
77/// Construct a fresh empty child `GraphInner` handle with `parent` set.
78fn new_child_inner(name: String, parent: Weak<RefCell<GraphInner>>) -> Rc<RefCell<GraphInner>> {
79    Rc::new(RefCell::new(GraphInner {
80        name,
81        names: indexmap::IndexMap::new(),
82        names_inverse: indexmap::IndexMap::new(),
83        children: indexmap::IndexMap::new(),
84        parent: Some(parent),
85        destroyed: false,
86        namespace_sinks: indexmap::IndexMap::new(),
87        next_ns_sink_id: 0,
88        factory: None,
89        factory_args: None,
90    }))
91}
92
93pub(crate) fn mount(
94    core: &Core,
95    parent_inner: &Rc<RefCell<GraphInner>>,
96    name: String,
97    child: &Graph,
98) -> Result<Graph, MountError> {
99    if name.contains("::") {
100        return Err(MountError::InvalidName(name));
101    }
102    let child_inner = child.inner_arc().clone();
103    // Validate + claim the namespace slot under the parent lock so
104    // concurrent mount(name, ...) calls cannot both pass validation
105    // (TOCTOU fix from /qa Slice E+). Lock order: parent → child.
106    {
107        let mut p = parent_inner.borrow_mut();
108        if p.destroyed {
109            return Err(MountError::Destroyed);
110        }
111        if p.children.contains_key(&name) {
112            return Err(MountError::NameCollision(name));
113        }
114        if p.names.contains_key(&name) {
115            return Err(MountError::NodeNameCollision(name));
116        }
117        {
118            let mut c = child_inner.borrow_mut();
119            if c.parent.is_some() {
120                return Err(MountError::AlreadyMounted);
121            }
122            c.parent = Some(Rc::downgrade(parent_inner));
123        }
124        p.children.insert(name, child_inner.clone());
125    }
126    // Fire AFTER lock drops (P3 — reactive describe / observe_all must
127    // see mounts as namespace changes). Owner-side `&Core` (D246 r2:
128    // firing ns sinks hands them `&Core`, so it IS a Core-touching op).
129    fire_ns(core, parent_inner);
130    Ok(Graph::from_inner(child_inner))
131}
132
133pub(crate) fn mount_new(
134    core: &Core,
135    parent_inner: &Rc<RefCell<GraphInner>>,
136    name: String,
137) -> Result<Graph, MountError> {
138    if name.contains("::") {
139        return Err(MountError::InvalidName(name));
140    }
141    let parent_weak = Rc::downgrade(parent_inner);
142    // Hold the parent lock across validation + child construction +
143    // insert (TOCTOU fix from /qa Slice E+).
144    let child_inner = {
145        let mut p = parent_inner.borrow_mut();
146        if p.destroyed {
147            return Err(MountError::Destroyed);
148        }
149        if p.children.contains_key(&name) {
150            return Err(MountError::NameCollision(name));
151        }
152        if p.names.contains_key(&name) {
153            return Err(MountError::NodeNameCollision(name));
154        }
155        let child_inner = new_child_inner(name.clone(), parent_weak);
156        p.children.insert(name, child_inner.clone());
157        child_inner
158    };
159    // Fire AFTER lock drops (P3). Owner-side `&Core` (D246 r2).
160    fire_ns(core, parent_inner);
161    Ok(Graph::from_inner(child_inner))
162}
163
164pub(crate) fn unmount(
165    core: &Core,
166    parent_inner: &Rc<RefCell<GraphInner>>,
167    name: &str,
168) -> Result<GraphRemoveAudit, MountError> {
169    let child = {
170        let mut p = parent_inner.borrow_mut();
171        if p.destroyed {
172            return Err(MountError::Destroyed);
173        }
174        p.children
175            .shift_remove(name)
176            .ok_or_else(|| MountError::NotMounted(name.to_owned()))?
177    };
178    let audit = audit_of(&child);
179    // Detach + destroy.
180    child.borrow_mut().parent = None;
181    destroy_subtree(core, &child);
182    // Fire on the parent AFTER the child's destroy completes (P3).
183    fire_ns(core, parent_inner);
184    Ok(audit)
185}
186
187pub(crate) fn ancestors(inner: &Rc<RefCell<GraphInner>>, include_self: bool) -> Vec<Graph> {
188    let mut chain: Vec<Graph> = Vec::new();
189    if include_self {
190        chain.push(Graph::from_inner(inner.clone()));
191    }
192    // Walk up via Weak parent pointers.
193    //
194    // Slice V3: visited-set cycle insurance per porting-deferred.md.
195    let mut visited = std::collections::HashSet::new();
196    visited.insert(Rc::as_ptr(inner) as usize);
197    let mut cursor: Option<Rc<RefCell<GraphInner>>> =
198        inner.borrow_mut().parent.as_ref().and_then(Weak::upgrade);
199    while let Some(cur) = cursor {
200        let ptr = Rc::as_ptr(&cur) as usize;
201        if !visited.insert(ptr) {
202            break; // Cycle detected — break rather than infinite-loop.
203        }
204        let next_parent = cur.borrow_mut().parent.as_ref().and_then(Weak::upgrade);
205        chain.push(Graph::from_inner(cur));
206        cursor = next_parent;
207    }
208    chain
209}
210
211/// Recursive subtree audit. Best-effort under concurrent mutation — see
212/// [`GraphRemoveAudit`] doc for the race-window framing. Each recursion
213/// level drops its inner lock before descending into children; writers
214/// holding a [`Graph`] clone of any descendant can shift the tally
215/// during that window. The single-owner D248/D255 actor-thread model
216/// makes this narrow but not impossible.
217fn audit_of(inner_arc: &Rc<RefCell<GraphInner>>) -> GraphRemoveAudit {
218    let inner = inner_arc.borrow_mut();
219    let own = inner.names.len();
220    let mount_count_self = inner.children.len();
221    let mut node_count = own;
222    let mut mount_count = mount_count_self;
223    let kids: Vec<Rc<RefCell<GraphInner>>> = inner.children.values().cloned().collect();
224    drop(inner);
225    for kid in kids {
226        let sub = audit_of(&kid);
227        node_count += sub.node_count;
228        mount_count += sub.mount_count;
229    }
230    GraphRemoveAudit {
231        node_count,
232        mount_count,
233    }
234}