Skip to main content

sim_lib_topology/
reflect.rs

1//! Topology graph and run reflection.
2
3use std::collections::VecDeque;
4
5use sim_kernel::{Cx, Expr, Result, Symbol};
6
7use crate::{
8    CompiledGraph, Graph,
9    capability::{
10        require_graph_capabilities, topology_reflect_capability, topology_run_capability,
11    },
12    run::{TopologyEvent, TopologyEventKind, TopologyRun},
13    text::graph_to_expr,
14};
15
16/// One node visit count in a reflected topology run.
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct TopologyVisit {
19    /// The visited node.
20    pub node: Symbol,
21    /// Number of times the node was visited.
22    pub visits: u32,
23}
24
25/// One edge route count in a reflected topology run.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct TopologyEdgeVisit {
28    /// The routed edge.
29    pub edge: crate::EdgeId,
30    /// Number of times the edge was routed.
31    pub visits: u32,
32}
33
34/// Reflected node metadata with target redaction applied.
35#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct ReflectedNode {
37    /// The node id.
38    pub id: Symbol,
39    /// The node verb.
40    pub verb: Symbol,
41    /// The node target, present unless redacted.
42    pub target: Option<Expr>,
43    /// Whether the target was redacted.
44    pub redacted: bool,
45}
46
47/// Reflected cell state with privacy redaction applied.
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ReflectedCell {
50    /// The cell name.
51    pub name: Symbol,
52    /// The cell value, redacted if the cell is private.
53    pub value: Expr,
54    /// Whether the cell is marked private.
55    pub private: bool,
56    /// Whether the value was redacted.
57    pub redacted: bool,
58}
59
60/// Recorded target output used by replay-oriented reports.
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct TopologyRecordedReply {
63    /// The call node that produced the output.
64    pub node: Symbol,
65    /// The recorded output value.
66    pub output: Expr,
67}
68
69/// Compact reflection of one topology run.
70#[derive(Clone, Debug)]
71pub struct TopologyRunReport {
72    /// Identifier of the run this report reflects.
73    pub run_id: u64,
74    /// The graph that was run.
75    pub graph: Symbol,
76    /// The run input.
77    pub input: Expr,
78    /// The run output.
79    pub output: Expr,
80    /// The recorded run events in order.
81    pub events: Vec<TopologyEvent>,
82    /// Per-node visit counts.
83    pub node_visits: Vec<TopologyVisit>,
84    /// Per-edge route counts.
85    pub edge_visits: Vec<TopologyEdgeVisit>,
86    /// Reflected node metadata.
87    pub nodes: Vec<ReflectedNode>,
88    /// Reflected final cell state.
89    pub cells: Vec<ReflectedCell>,
90    /// Recorded call-node outputs.
91    pub recorded_replies: Vec<TopologyRecordedReply>,
92    /// Whether any field in the report was redacted.
93    pub redacted: bool,
94    pub(crate) source_graph: Graph,
95}
96
97impl TopologyRunReport {
98    /// Converts the run report into a stable expression map.
99    pub fn as_expr(&self) -> Expr {
100        Expr::Map(vec![
101            entry("kind", Expr::Symbol(Symbol::new("topology-run"))),
102            entry("run-id", number_expr(self.run_id)),
103            entry("graph", Expr::Symbol(self.graph.clone())),
104            entry("input", self.input.clone()),
105            entry("output", self.output.clone()),
106            entry("redacted", Expr::Bool(self.redacted)),
107            entry("nodes", Expr::List(node_exprs(&self.nodes))),
108            entry("cells", Expr::List(cell_exprs(&self.cells))),
109            entry(
110                "node-visits",
111                Expr::List(node_visit_exprs(&self.node_visits)),
112            ),
113            entry(
114                "edge-visits",
115                Expr::List(edge_visit_exprs(&self.edge_visits)),
116            ),
117            entry("events", Expr::List(event_exprs(self))),
118            entry(
119                "recorded-replies",
120                Expr::List(reply_exprs(&self.recorded_replies)),
121            ),
122        ])
123    }
124
125    /// Converts this report into a compact explanation expression.
126    pub fn explanation_expr(&self) -> Expr {
127        Expr::Map(vec![
128            entry("kind", Expr::Symbol(Symbol::new("topology-explanation"))),
129            entry("run-id", number_expr(self.run_id)),
130            entry("graph", Expr::Symbol(self.graph.clone())),
131            entry("summary", Expr::String(summary(self))),
132            entry(
133                "node-visits",
134                Expr::List(node_visit_exprs(&self.node_visits)),
135            ),
136            entry("edge-choices", Expr::List(edge_choice_exprs(self))),
137            entry("outputs", Expr::List(output_exprs(&self.events))),
138        ])
139    }
140
141    /// Returns the unredacted graph retained for replay inside this crate.
142    pub(crate) fn source_graph(&self) -> &Graph {
143        &self.source_graph
144    }
145}
146
147/// Bounded in-memory topology run history.
148#[derive(Clone, Debug)]
149pub struct TopologyHistory {
150    capacity: usize,
151    next_id: u64,
152    entries: VecDeque<TopologyRunReport>,
153}
154
155impl TopologyHistory {
156    /// Creates a history with the requested maximum number of retained runs.
157    pub fn new(capacity: usize) -> Self {
158        Self {
159            capacity,
160            next_id: 1,
161            entries: VecDeque::new(),
162        }
163    }
164
165    /// Records a run and returns its assigned run id.
166    pub fn record(&mut self, mut report: TopologyRunReport) -> u64 {
167        let id = self.next_id;
168        self.next_id = self.next_id.saturating_add(1);
169        report.run_id = id;
170        if self.capacity > 0 {
171            self.entries.push_back(report);
172            while self.entries.len() > self.capacity {
173                self.entries.pop_front();
174            }
175        }
176        id
177    }
178
179    /// Returns a recorded run by id.
180    pub fn get(&self, run_id: u64) -> Option<&TopologyRunReport> {
181        self.entries.iter().find(|report| report.run_id == run_id)
182    }
183
184    /// Returns retained run ids in oldest-to-newest order.
185    pub fn run_ids(&self) -> Vec<u64> {
186        self.entries.iter().map(|report| report.run_id).collect()
187    }
188}
189
190/// Reflects a graph as canonical topology data.
191pub fn topology_reflect_graph(cx: &Cx, graph: &Graph) -> Expr {
192    if cx.capabilities().contains(&topology_reflect_capability()) {
193        return graph_to_expr(graph);
194    }
195    let mut reflected = graph.clone();
196    for node in &mut reflected.nodes {
197        if node.target.is_some() {
198            node.target = Some(redacted_expr());
199        }
200    }
201    for cell in &mut reflected.cells {
202        if cell.private {
203            cell.initial = redacted_expr();
204        }
205    }
206    graph_to_expr(&reflected)
207}
208
209/// Runs a compiled topology and returns a reflected run report.
210pub fn topology_reflect(
211    cx: &mut Cx,
212    graph: &Graph,
213    plan: &CompiledGraph,
214    input: Expr,
215) -> Result<TopologyRunReport> {
216    cx.require(&topology_run_capability())?;
217    require_graph_capabilities(cx, graph)?;
218    let input_for_report = input.clone();
219    let mut run = TopologyRun::new(graph, plan, input)?;
220    run.run(cx)?;
221    let output = run.output_expr();
222    let reveal = cx.capabilities().contains(&topology_reflect_capability());
223
224    Ok(TopologyRunReport {
225        run_id: 0,
226        graph: graph.name.clone(),
227        input: input_for_report,
228        output,
229        events: run.events().to_vec(),
230        node_visits: reflect_node_visits(graph, &run.budget.node_visits),
231        edge_visits: reflect_edge_visits(graph, &run.budget.edge_visits),
232        nodes: reflect_nodes(graph, reveal),
233        cells: reflect_cells(graph, &run, reveal)?,
234        recorded_replies: recorded_replies(graph, run.events()),
235        redacted: !reveal && has_sensitive_reflection(graph),
236        source_graph: graph.clone(),
237    })
238}
239
240/// Compiles, runs, and reflects a topology graph.
241pub fn topology_reflect_run(cx: &mut Cx, graph: &Graph, input: Expr) -> Result<TopologyRunReport> {
242    let plan = crate::compile_graph(cx, graph)?;
243    topology_reflect(cx, graph, &plan, input)
244}
245
246/// Returns a run explanation expression.
247pub fn topology_explain(report: &TopologyRunReport) -> Expr {
248    report.explanation_expr()
249}
250
251fn reflect_node_visits(graph: &Graph, visits: &[u32]) -> Vec<TopologyVisit> {
252    graph
253        .nodes
254        .iter()
255        .zip(visits.iter())
256        .map(|(node, visits)| TopologyVisit {
257            node: node.id.as_symbol().clone(),
258            visits: *visits,
259        })
260        .collect()
261}
262
263fn reflect_edge_visits(graph: &Graph, visits: &[u32]) -> Vec<TopologyEdgeVisit> {
264    graph
265        .edges
266        .iter()
267        .zip(visits.iter())
268        .map(|(edge, visits)| TopologyEdgeVisit {
269            edge: edge.id,
270            visits: *visits,
271        })
272        .collect()
273}
274
275fn reflect_nodes(graph: &Graph, reveal: bool) -> Vec<ReflectedNode> {
276    graph
277        .nodes
278        .iter()
279        .map(|node| {
280            let redacted = node.target.is_some() && !reveal;
281            ReflectedNode {
282                id: node.id.as_symbol().clone(),
283                verb: node.verb.clone(),
284                target: node.target.as_ref().map(|target| {
285                    if redacted {
286                        redacted_expr()
287                    } else {
288                        target.clone()
289                    }
290                }),
291                redacted,
292            }
293        })
294        .collect()
295}
296
297fn reflect_cells(graph: &Graph, run: &TopologyRun<'_>, reveal: bool) -> Result<Vec<ReflectedCell>> {
298    graph
299        .cells
300        .iter()
301        .map(|cell| {
302            let redacted = cell.private && !reveal;
303            Ok(ReflectedCell {
304                name: cell.name.clone(),
305                value: if redacted {
306                    redacted_expr()
307                } else {
308                    run.cells().read(&cell.name)?
309                },
310                private: cell.private,
311                redacted,
312            })
313        })
314        .collect()
315}
316
317fn recorded_replies(graph: &Graph, events: &[TopologyEvent]) -> Vec<TopologyRecordedReply> {
318    events
319        .iter()
320        .filter(|event| event.kind == TopologyEventKind::PortEmitted)
321        .filter_map(|event| {
322            let node = graph.nodes.get(event.node_index)?;
323            (node.verb.name.as_ref() == "call").then(|| TopologyRecordedReply {
324                node: node.id.as_symbol().clone(),
325                output: event.expr.clone().unwrap_or(Expr::Nil),
326            })
327        })
328        .collect()
329}
330
331fn has_sensitive_reflection(graph: &Graph) -> bool {
332    graph.nodes.iter().any(|node| node.target.is_some())
333        || graph.cells.iter().any(|cell| cell.private)
334}
335
336fn event_exprs(report: &TopologyRunReport) -> Vec<Expr> {
337    report
338        .events
339        .iter()
340        .map(|event| {
341            let mut entries = vec![
342                entry("kind", Expr::Symbol(event_kind_symbol(&event.kind))),
343                entry("node", event_node_expr(report, event.node_index)),
344            ];
345            if let Some(port) = &event.port {
346                entries.push(entry("port", Expr::Symbol(port.clone())));
347            }
348            if let Some(edge_index) = event.edge_index {
349                entries.push(entry("edge", event_edge_expr(report, edge_index)));
350            }
351            if let Some(expr) = &event.expr {
352                entries.push(entry("expr", expr.clone()));
353            }
354            Expr::Map(entries)
355        })
356        .collect()
357}
358
359fn node_exprs(nodes: &[ReflectedNode]) -> Vec<Expr> {
360    nodes
361        .iter()
362        .map(|node| {
363            let mut entries = vec![
364                entry("id", Expr::Symbol(node.id.clone())),
365                entry("verb", Expr::Symbol(node.verb.clone())),
366                entry("redacted", Expr::Bool(node.redacted)),
367            ];
368            if let Some(target) = &node.target {
369                entries.push(entry("target", target.clone()));
370            }
371            Expr::Map(entries)
372        })
373        .collect()
374}
375
376fn cell_exprs(cells: &[ReflectedCell]) -> Vec<Expr> {
377    cells
378        .iter()
379        .map(|cell| {
380            Expr::Map(vec![
381                entry("name", Expr::Symbol(cell.name.clone())),
382                entry("value", cell.value.clone()),
383                entry("private", Expr::Bool(cell.private)),
384                entry("redacted", Expr::Bool(cell.redacted)),
385            ])
386        })
387        .collect()
388}
389
390fn node_visit_exprs(visits: &[TopologyVisit]) -> Vec<Expr> {
391    visits
392        .iter()
393        .map(|visit| {
394            Expr::Map(vec![
395                entry("node", Expr::Symbol(visit.node.clone())),
396                entry("visits", number_expr(u64::from(visit.visits))),
397            ])
398        })
399        .collect()
400}
401
402fn edge_visit_exprs(visits: &[TopologyEdgeVisit]) -> Vec<Expr> {
403    visits
404        .iter()
405        .map(|visit| {
406            Expr::Map(vec![
407                entry("edge", number_expr(u64::from(visit.edge.0))),
408                entry("visits", number_expr(u64::from(visit.visits))),
409            ])
410        })
411        .collect()
412}
413
414fn edge_choice_exprs(report: &TopologyRunReport) -> Vec<Expr> {
415    report
416        .events
417        .iter()
418        .filter(|event| event.kind == TopologyEventKind::EdgeRouted)
419        .map(|event| {
420            Expr::Map(vec![
421                entry("from", event_node_expr(report, event.node_index)),
422                entry(
423                    "edge",
424                    event
425                        .edge_index
426                        .map_or(Expr::Nil, |index| event_edge_expr(report, index)),
427                ),
428                entry(
429                    "port",
430                    event.port.clone().map(Expr::Symbol).unwrap_or(Expr::Nil),
431                ),
432            ])
433        })
434        .collect()
435}
436
437fn output_exprs(events: &[TopologyEvent]) -> Vec<Expr> {
438    events
439        .iter()
440        .filter(|event| event.kind == TopologyEventKind::OutputEmitted)
441        .filter_map(|event| event.expr.clone())
442        .collect()
443}
444
445fn reply_exprs(replies: &[TopologyRecordedReply]) -> Vec<Expr> {
446    replies
447        .iter()
448        .map(|reply| {
449            Expr::Map(vec![
450                entry("node", Expr::Symbol(reply.node.clone())),
451                entry("output", reply.output.clone()),
452            ])
453        })
454        .collect()
455}
456
457fn summary(report: &TopologyRunReport) -> String {
458    format!(
459        "run {} visited {} nodes, routed {} edges, emitted {} output(s)",
460        report.graph,
461        report
462            .node_visits
463            .iter()
464            .filter(|visit| visit.visits > 0)
465            .count(),
466        report
467            .edge_visits
468            .iter()
469            .filter(|visit| visit.visits > 0)
470            .count(),
471        output_exprs(&report.events).len()
472    )
473}
474
475fn event_node_expr(report: &TopologyRunReport, node_index: usize) -> Expr {
476    report
477        .source_graph
478        .nodes
479        .get(node_index)
480        .map(|node| Expr::Symbol(node.id.as_symbol().clone()))
481        .unwrap_or(Expr::Nil)
482}
483
484fn event_edge_expr(report: &TopologyRunReport, edge_index: usize) -> Expr {
485    report
486        .source_graph
487        .edges
488        .get(edge_index)
489        .map(|edge| number_expr(u64::from(edge.id.0)))
490        .unwrap_or(Expr::Nil)
491}
492
493fn event_kind_symbol(kind: &TopologyEventKind) -> Symbol {
494    Symbol::new(match kind {
495        TopologyEventKind::Enqueued => "enqueued",
496        TopologyEventKind::NodeStarted => "node-started",
497        TopologyEventKind::PortEmitted => "port-emitted",
498        TopologyEventKind::EdgeRouted => "edge-routed",
499        TopologyEventKind::OutputEmitted => "output-emitted",
500    })
501}
502
503fn redacted_expr() -> Expr {
504    Expr::Symbol(Symbol::qualified("topology", "redacted"))
505}
506
507use sim_value::build::{entry, uint as number_expr};