Skip to main content

sim_lib_control/
exception.rs

1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use sim_lib_mutation::{
4    ArenaError, EdgeId, EdgeLimits, EdgeSnapshot, EdgeVisitor, ManagedArena, ManagedHandle,
5    ManagedId, ManagedNode, ManagedObject, StrongEdgeMutationError,
6};
7
8/// A guest exception payload with caller-defined relation roles.
9///
10/// The payload is the open role carried by the shared [`ManagedNode`]. Relation
11/// roles are deliberately caller data: this adapter does not prescribe cause,
12/// context, group, or suppression semantics.
13#[derive(Clone, Debug, Eq, PartialEq)]
14pub struct ManagedException<P, R> {
15    node: ManagedNode<P>,
16    relation_roles: BTreeMap<EdgeId, R>,
17}
18
19impl<P, R> ManagedException<P, R> {
20    /// Creates an exception payload with the managed node's standard edge limits.
21    pub const fn new(payload: P) -> Self {
22        Self::with_edge_limits(payload, EdgeLimits::DEFAULT)
23    }
24
25    /// Creates an exception payload with explicit managed edge limits.
26    pub const fn with_edge_limits(payload: P, limits: EdgeLimits) -> Self {
27        Self {
28            node: ManagedNode::with_edge_limits(payload, limits),
29            relation_roles: BTreeMap::new(),
30        }
31    }
32
33    /// Borrows the guest payload.
34    pub const fn payload(&self) -> &P {
35        self.node.role()
36    }
37
38    /// Replaces the guest payload without changing relation identity.
39    pub fn replace_payload(&mut self, payload: P) -> P {
40        self.node.replace_role(payload)
41    }
42
43    /// Adds a retaining relation with caller-owned role evidence.
44    pub fn insert_relation(
45        &mut self,
46        role: R,
47        target: ManagedId,
48    ) -> Result<EdgeId, StrongEdgeMutationError> {
49        let edge = self.node.insert_strong(target)?;
50        let previous = self.relation_roles.insert(edge, role);
51        debug_assert!(
52            previous.is_none(),
53            "fresh managed edge must not have a role"
54        );
55        Ok(edge)
56    }
57
58    /// Removes exactly the expected relation and returns its role and target.
59    pub fn remove_relation(
60        &mut self,
61        edge: EdgeId,
62        expected: ManagedId,
63    ) -> Result<(R, ManagedId), StrongEdgeMutationError> {
64        let target = self.node.remove_strong(edge, expected)?;
65        let role = self
66            .relation_roles
67            .remove(&edge)
68            .expect("every adapter relation has role evidence");
69        Ok((role, target))
70    }
71
72    /// Returns relation edges in stable edge-id order.
73    pub fn relations(&self) -> impl Iterator<Item = (EdgeId, &R, ManagedId)> {
74        self.node
75            .edge_snapshot()
76            .into_iter()
77            .filter_map(|snapshot| {
78                let EdgeSnapshot::Strong { edge, target } = snapshot else {
79                    return None;
80                };
81                Some((edge, &self.relation_roles[&edge], target))
82            })
83    }
84}
85
86impl<P, R> ManagedObject for ManagedException<P, R> {
87    fn trace_edges(&self, visitor: &mut dyn EdgeVisitor) {
88        self.node.trace_edges(visitor);
89    }
90
91    fn clear_weak_edge(&mut self, edge: EdgeId, expected: ManagedId) -> bool {
92        self.node.clear_weak_edge(edge, expected)
93    }
94
95    fn clear_ephemeron_edge(
96        &mut self,
97        edge: EdgeId,
98        expected_key: ManagedId,
99        expected_value: ManagedId,
100    ) -> bool {
101        self.node
102            .clear_ephemeron_edge(edge, expected_key, expected_value)
103    }
104}
105
106/// Maximum number of relation edges admitted to one graph projection.
107#[derive(Clone, Copy, Debug, Eq, PartialEq)]
108pub struct ExceptionGraphBudget {
109    max_edges: usize,
110}
111
112impl ExceptionGraphBudget {
113    /// Creates an edge budget. Zero is useful for a presence-only probe.
114    pub const fn new(max_edges: usize) -> Self {
115        Self { max_edges }
116    }
117
118    /// Returns the admitted edge count.
119    pub const fn max_edges(self) -> usize {
120        self.max_edges
121    }
122}
123
124/// One caller-typed relation in a bounded graph view.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct ExceptionGraphEdge<R> {
127    /// Parent object that owns the stable edge identity.
128    pub parent: ManagedId,
129    /// Stable identity local to `parent`.
130    pub edge: EdgeId,
131    /// Caller-owned relation role.
132    pub role: R,
133    /// Related exception object.
134    pub target: ManagedId,
135}
136
137/// Terminating graph projection with explicit loss reporting.
138#[derive(Clone, Debug, Eq, PartialEq)]
139pub struct ExceptionGraphView<R> {
140    /// Relations in deterministic parent-discovery and edge-id order.
141    pub edges: Vec<ExceptionGraphEdge<R>>,
142    /// True when at least one relation was omitted by the edge budget.
143    pub truncated: bool,
144}
145
146impl<R: Clone> ExceptionGraphView<R> {
147    /// Projects reachable relations iteratively, expanding each object once.
148    ///
149    /// Every parent edge remains a row even when multiple parents target the
150    /// same object. The expanded set only prevents cycles from recurring.
151    pub fn project<P>(
152        arena: &ManagedArena<ManagedException<P, R>>,
153        root: ManagedHandle,
154        budget: ExceptionGraphBudget,
155    ) -> Result<Self, ArenaError> {
156        arena.get(root)?;
157        let mut queue = VecDeque::from([root.id()]);
158        let mut expanded = BTreeSet::new();
159        let mut edges = Vec::new();
160
161        while let Some(parent) = queue.pop_front() {
162            if !expanded.insert(parent) {
163                continue;
164            }
165            let handle = arena.handle(parent)?;
166            for (edge, role, target) in arena.get(handle)?.relations() {
167                if edges.len() == budget.max_edges {
168                    return Ok(Self {
169                        edges,
170                        truncated: true,
171                    });
172                }
173                edges.push(ExceptionGraphEdge {
174                    parent,
175                    edge,
176                    role: role.clone(),
177                    target,
178                });
179                if !expanded.contains(&target) {
180                    queue.push_back(target);
181                }
182            }
183        }
184        Ok(Self {
185            edges,
186            truncated: false,
187        })
188    }
189}