1use std::collections::VecDeque;
4
5use sim_kernel::{Cx, Error, Expr, Result, Symbol};
6
7use crate::{
8 Budget, BudgetExhausted, CompiledGraph, Edge, Graph, Node, Port,
9 adapter::{call_target_expr, resolve_target},
10 capability::{require_graph_capabilities, topology_run_capability},
11 run_contract::check_expr_shape,
12 verb::{VerbAction, run_core_node},
13};
14
15pub use crate::{
16 run_cells::TopologyCells, run_nonlinear::TopologyNonlinearState,
17 run_predicate::predicate_accepts,
18};
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct TopologyPacket {
23 pub node_index: usize,
25 pub port: Symbol,
27 pub expr: Expr,
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct WorkItem {
34 pub node_index: usize,
36 pub port: Symbol,
38 pub expr: Expr,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum TopologyEventKind {
45 Enqueued,
47 NodeStarted,
49 PortEmitted,
51 EdgeRouted,
53 OutputEmitted,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct TopologyEvent {
60 pub kind: TopologyEventKind,
62 pub node_index: usize,
64 pub port: Option<Symbol>,
66 pub edge_index: Option<usize>,
68 pub expr: Option<Expr>,
70}
71
72impl TopologyEvent {
73 fn node(kind: TopologyEventKind, node_index: usize) -> Self {
74 Self {
75 kind,
76 node_index,
77 port: None,
78 edge_index: None,
79 expr: None,
80 }
81 }
82
83 fn node_expr(kind: TopologyEventKind, node_index: usize, expr: Expr) -> Self {
84 Self {
85 kind,
86 node_index,
87 port: None,
88 edge_index: None,
89 expr: Some(expr),
90 }
91 }
92
93 fn port(kind: TopologyEventKind, node_index: usize, port: Symbol, expr: Expr) -> Self {
94 Self {
95 kind,
96 node_index,
97 port: Some(port),
98 edge_index: None,
99 expr: Some(expr),
100 }
101 }
102
103 fn edge(node_index: usize, port: Symbol, edge_index: usize, expr: Expr) -> Self {
104 Self {
105 kind: TopologyEventKind::EdgeRouted,
106 node_index,
107 port: Some(port),
108 edge_index: Some(edge_index),
109 expr: Some(expr),
110 }
111 }
112}
113
114#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct TopologyBudgetError {
117 pub resource: Symbol,
119 pub limit: u32,
121 pub actual: u32,
123}
124
125impl TopologyBudgetError {
126 pub fn as_expr(&self) -> Expr {
128 Expr::Map(vec![
129 (
130 Expr::Symbol(Symbol::new("kind")),
131 Expr::Symbol(Symbol::new("topology-budget-exhausted")),
132 ),
133 (
134 Expr::Symbol(Symbol::new("resource")),
135 Expr::Symbol(self.resource.clone()),
136 ),
137 (
138 Expr::Symbol(Symbol::new("limit")),
139 Expr::String(self.limit.to_string()),
140 ),
141 (
142 Expr::Symbol(Symbol::new("actual")),
143 Expr::String(self.actual.to_string()),
144 ),
145 ])
146 }
147
148 fn into_error(self) -> Error {
149 let value = self.as_expr();
150 Error::Eval(format!(
151 "topology budget exhausted: resource={} limit={} actual={} value={value:?}",
152 self.resource, self.limit, self.actual,
153 ))
154 }
155}
156
157#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct BudgetLedger {
160 limits: Budget,
161 pub steps: u32,
163 pub node_visits: Vec<u32>,
165 pub edge_visits: Vec<u32>,
167 pub outputs: u32,
169 pub child_runs: u32,
171}
172
173impl BudgetLedger {
174 pub fn new(limits: Budget, node_count: usize, edge_count: usize) -> Self {
176 Self {
177 limits,
178 steps: 0,
179 node_visits: vec![0; node_count],
180 edge_visits: vec![0; edge_count],
181 outputs: 0,
182 child_runs: 0,
183 }
184 }
185
186 pub fn record_step(&mut self) -> Result<()> {
188 self.steps = self.steps.saturating_add(1);
189 self.require_at_most(self.steps, self.limits.max_steps, "max-steps")
190 }
191
192 pub fn record_node_visit(&mut self, node_index: usize) -> Result<()> {
194 self.node_visits[node_index] = self.node_visits[node_index].saturating_add(1);
195 self.require_at_most(
196 self.node_visits[node_index],
197 self.limits.max_node_visits,
198 "max-node-visits",
199 )
200 }
201
202 pub fn record_edge_visit(&mut self, edge_index: usize, edge_limit: Option<u32>) -> Result<()> {
204 self.edge_visits[edge_index] = self.edge_visits[edge_index].saturating_add(1);
205 let limit = edge_limit
206 .unwrap_or(self.limits.max_edge_visits)
207 .min(self.limits.max_edge_visits);
208 let resource = if edge_limit.is_some() {
209 "edge-max-visits"
210 } else {
211 "max-edge-visits"
212 };
213 self.require_at_most(self.edge_visits[edge_index], limit, resource)
214 }
215
216 pub fn record_output(&mut self) -> Result<()> {
218 self.outputs = self.outputs.saturating_add(1);
219 self.require_at_most(self.outputs, self.limits.max_outputs, "max-outputs")
220 }
221
222 pub fn record_child_run(&mut self) -> Result<()> {
224 self.child_runs = self.child_runs.saturating_add(1);
225 self.require_at_most(
226 self.child_runs,
227 self.limits.max_child_runs,
228 "max-child-runs",
229 )
230 }
231
232 pub fn check_merge_buffer(&self, buffered: usize) -> Result<()> {
234 let actual = u32::try_from(buffered).unwrap_or(u32::MAX);
235 self.require_at_most(actual, self.limits.max_steps, "merge-buffer")
236 }
237
238 fn require_at_most(&self, actual: u32, limit: u32, resource: &str) -> Result<()> {
239 if actual <= limit {
240 Ok(())
241 } else {
242 Err(TopologyBudgetError {
243 resource: Symbol::new(resource),
244 limit,
245 actual,
246 }
247 .into_error())
248 }
249 }
250}
251
252pub struct TopologyRun<'a> {
254 graph: &'a Graph,
255 plan: &'a CompiledGraph,
256 queue: VecDeque<WorkItem>,
257 outputs: Vec<Expr>,
258 cells: TopologyCells,
259 nonlinear: TopologyNonlinearState,
260 pub budget: BudgetLedger,
262 events: Vec<TopologyEvent>,
263}
264
265impl<'a> TopologyRun<'a> {
266 pub fn new(graph: &'a Graph, plan: &'a CompiledGraph, input: Expr) -> Result<Self> {
268 validate_budget_policy(&graph.budget)?;
269 let mut run = Self {
270 graph,
271 plan,
272 queue: VecDeque::new(),
273 outputs: Vec::new(),
274 cells: TopologyCells::new(graph)?,
275 nonlinear: TopologyNonlinearState::new(plan.nodes.len()),
276 budget: BudgetLedger::new(graph.budget.clone(), plan.nodes.len(), plan.edges.len()),
277 events: Vec::new(),
278 };
279 for input_node in &plan.input_nodes {
280 run.enqueue(WorkItem {
281 node_index: *input_node,
282 port: Symbol::new("in"),
283 expr: input.clone(),
284 });
285 }
286 Ok(run)
287 }
288
289 pub fn run(&mut self, cx: &mut Cx) -> Result<()> {
291 while let Some(item) = self.queue.pop_front() {
292 self.budget.record_step()?;
293 self.budget.record_node_visit(item.node_index)?;
294 self.check_graph_input(cx, &item)?;
295 self.check_node_input(cx, &item)?;
296 self.events.push(TopologyEvent::node(
297 TopologyEventKind::NodeStarted,
298 item.node_index,
299 ));
300
301 let actions = run_core_node(
302 cx,
303 self.graph,
304 self.plan,
305 &mut self.budget,
306 &mut self.cells,
307 &mut self.nonlinear,
308 &item,
309 )?;
310 for action in actions {
311 match action {
312 VerbAction::Emit(packet) => {
313 self.check_node_output(cx, &packet)?;
314 self.events.push(TopologyEvent::port(
315 TopologyEventKind::PortEmitted,
316 packet.node_index,
317 packet.port.clone(),
318 packet.expr.clone(),
319 ));
320 self.route_packet(cx, packet)?;
321 }
322 VerbAction::Complete { node_index, expr } => {
323 self.push_output(cx, node_index, expr)?
324 }
325 }
326 }
327 }
328 Ok(())
329 }
330
331 pub fn outputs(&self) -> &[Expr] {
333 &self.outputs
334 }
335
336 pub fn events(&self) -> &[TopologyEvent] {
338 &self.events
339 }
340
341 pub fn cells(&self) -> &TopologyCells {
343 &self.cells
344 }
345
346 pub fn output_expr(&self) -> Expr {
348 match self.outputs.as_slice() {
349 [] => Expr::Nil,
350 [single] => single.clone(),
351 many => Expr::List(many.to_vec()),
352 }
353 }
354
355 fn enqueue(&mut self, item: WorkItem) {
356 self.events.push(TopologyEvent::port(
357 TopologyEventKind::Enqueued,
358 item.node_index,
359 item.port.clone(),
360 item.expr.clone(),
361 ));
362 self.queue.push_back(item);
363 }
364
365 fn push_output(&mut self, cx: &mut Cx, node_index: usize, expr: Expr) -> Result<()> {
366 check_expr_shape(cx, "graph output", self.graph.output.as_ref(), &expr)?;
367 self.budget.record_output()?;
368 self.events.push(TopologyEvent::node_expr(
369 TopologyEventKind::OutputEmitted,
370 node_index,
371 expr.clone(),
372 ));
373 self.outputs.push(expr);
374 Ok(())
375 }
376
377 fn route_packet(&mut self, cx: &mut Cx, packet: TopologyPacket) -> Result<()> {
378 let edge_indices = self.plan.outgoing_edges[packet.node_index].clone();
379 for edge_index in edge_indices {
380 let compiled = &self.plan.edges[edge_index];
381 if compiled.from.port != packet.port {
382 continue;
383 }
384 let edge = &self.graph.edges[compiled.source_index];
385 if !edge_allows(cx, edge, &packet.expr)? {
386 continue;
387 }
388 self.budget.record_edge_visit(edge_index, edge.max_visits)?;
389 let routed = route_edge_expr(cx, edge, packet.expr.clone())?;
390 self.check_edge_value(cx, compiled.to_node, &compiled.to.port, &routed)?;
391 self.events.push(TopologyEvent::edge(
392 packet.node_index,
393 packet.port.clone(),
394 edge_index,
395 routed.clone(),
396 ));
397 self.enqueue(WorkItem {
398 node_index: compiled.to_node,
399 port: compiled.to.port.clone(),
400 expr: routed,
401 });
402 }
403 Ok(())
404 }
405
406 fn check_graph_input(&self, cx: &mut Cx, item: &WorkItem) -> Result<()> {
407 if item.port == Symbol::new("in") && self.plan.input_nodes.contains(&item.node_index) {
408 check_expr_shape(cx, "graph input", self.graph.input.as_ref(), &item.expr)?;
409 }
410 Ok(())
411 }
412
413 fn check_node_input(&self, cx: &mut Cx, item: &WorkItem) -> Result<()> {
414 let node = self.node(item.node_index)?;
415 check_expr_shape(
416 cx,
417 format!("node {} input", node.id.as_symbol()),
418 node.input.as_ref(),
419 &item.expr,
420 )?;
421 if let Some(port) = input_port(node, &item.port) {
422 check_expr_shape(
423 cx,
424 format!("node {} input port {}", node.id.as_symbol(), port.name),
425 port.shape.as_ref(),
426 &item.expr,
427 )?;
428 }
429 Ok(())
430 }
431
432 fn check_node_output(&self, cx: &mut Cx, packet: &TopologyPacket) -> Result<()> {
433 let node = self.node(packet.node_index)?;
434 check_expr_shape(
435 cx,
436 format!("node {} output", node.id.as_symbol()),
437 node.output.as_ref(),
438 &packet.expr,
439 )?;
440 if let Some(port) = output_port(node, &packet.port) {
441 check_expr_shape(
442 cx,
443 format!("node {} output port {}", node.id.as_symbol(), port.name),
444 port.shape.as_ref(),
445 &packet.expr,
446 )?;
447 }
448 Ok(())
449 }
450
451 fn check_edge_value(
452 &self,
453 cx: &mut Cx,
454 node_index: usize,
455 port_name: &Symbol,
456 routed: &Expr,
457 ) -> Result<()> {
458 let node = self.node(node_index)?;
459 if let Some(port) = input_port(node, port_name) {
460 check_expr_shape(
461 cx,
462 format!(
463 "edge into node {} input port {}",
464 node.id.as_symbol(),
465 port.name
466 ),
467 port.shape.as_ref(),
468 routed,
469 )?;
470 }
471 Ok(())
472 }
473
474 fn node(&self, node_index: usize) -> Result<&'a Node> {
475 self.graph
476 .nodes
477 .get(node_index)
478 .ok_or_else(|| Error::Eval(format!("topology run: unknown node index {node_index}")))
479 }
480}
481
482pub fn run_graph(cx: &mut Cx, graph: &Graph, plan: &CompiledGraph, input: Expr) -> Result<Expr> {
484 cx.require(&topology_run_capability())?;
485 require_graph_capabilities(cx, graph)?;
486 let mut run = TopologyRun::new(graph, plan, input)?;
487 run.run(cx)?;
488 Ok(run.output_expr())
489}
490
491fn edge_allows(cx: &mut Cx, edge: &Edge, input: &Expr) -> Result<bool> {
492 match &edge.when {
493 Some(predicate) => predicate_accepts(cx, predicate, input),
494 None => Ok(true),
495 }
496}
497
498fn route_edge_expr(cx: &mut Cx, edge: &crate::Edge, input: Expr) -> Result<Expr> {
499 let transformed = match &edge.transform {
500 Some(transform) => {
501 let target = resolve_target(cx, transform)?;
502 call_target_expr(cx, target, input)?
503 }
504 None => input,
505 };
506
507 if let Some(name) = &edge.as_name {
508 Ok(Expr::Map(vec![(Expr::Symbol(name.clone()), transformed)]))
509 } else {
510 Ok(transformed)
511 }
512}
513
514fn input_port<'a>(node: &'a Node, name: &Symbol) -> Option<&'a Port> {
515 node.inputs.iter().find(|port| port.name == *name)
516}
517
518fn output_port<'a>(node: &'a Node, name: &Symbol) -> Option<&'a Port> {
519 node.outputs.iter().find(|port| port.name == *name)
520}
521
522fn validate_budget_policy(budget: &Budget) -> Result<()> {
523 if budget.deadline_ms.is_some() {
524 return Err(Error::Eval(
525 "topology run: deadline_ms budget policy is unsupported".to_owned(),
526 ));
527 }
528 if budget.on_exhausted == BudgetExhausted::Partial {
529 return Err(Error::Eval(
530 "topology run: partial exhaustion policy is unsupported".to_owned(),
531 ));
532 }
533 Ok(())
534}