1use 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#[derive(Clone, Debug, PartialEq, Eq)]
18pub struct TopologyVisit {
19 pub node: Symbol,
21 pub visits: u32,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct TopologyEdgeVisit {
28 pub edge: crate::EdgeId,
30 pub visits: u32,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
36pub struct ReflectedNode {
37 pub id: Symbol,
39 pub verb: Symbol,
41 pub target: Option<Expr>,
43 pub redacted: bool,
45}
46
47#[derive(Clone, Debug, PartialEq, Eq)]
49pub struct ReflectedCell {
50 pub name: Symbol,
52 pub value: Expr,
54 pub private: bool,
56 pub redacted: bool,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct TopologyRecordedReply {
63 pub node: Symbol,
65 pub output: Expr,
67}
68
69#[derive(Clone, Debug)]
71pub struct TopologyRunReport {
72 pub run_id: u64,
74 pub graph: Symbol,
76 pub input: Expr,
78 pub output: Expr,
80 pub events: Vec<TopologyEvent>,
82 pub node_visits: Vec<TopologyVisit>,
84 pub edge_visits: Vec<TopologyEdgeVisit>,
86 pub nodes: Vec<ReflectedNode>,
88 pub cells: Vec<ReflectedCell>,
90 pub recorded_replies: Vec<TopologyRecordedReply>,
92 pub redacted: bool,
94 pub(crate) source_graph: Graph,
95}
96
97impl TopologyRunReport {
98 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 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 pub(crate) fn source_graph(&self) -> &Graph {
143 &self.source_graph
144 }
145}
146
147#[derive(Clone, Debug)]
149pub struct TopologyHistory {
150 capacity: usize,
151 next_id: u64,
152 entries: VecDeque<TopologyRunReport>,
153}
154
155impl TopologyHistory {
156 pub fn new(capacity: usize) -> Self {
158 Self {
159 capacity,
160 next_id: 1,
161 entries: VecDeque::new(),
162 }
163 }
164
165 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 pub fn get(&self, run_id: u64) -> Option<&TopologyRunReport> {
181 self.entries.iter().find(|report| report.run_id == run_id)
182 }
183
184 pub fn run_ids(&self) -> Vec<u64> {
186 self.entries.iter().map(|report| report.run_id).collect()
187 }
188}
189
190pub 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
209pub 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
240pub 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
246pub 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};