Skip to main content

sim_lib_topology/
run.rs

1//! Topology run-state and sequential core scheduler.
2
3use 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/// Packet emitted from one node output port.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct TopologyPacket {
23    /// Source node index in the compiled graph.
24    pub node_index: usize,
25    /// Source output port.
26    pub port: Symbol,
27    /// Packet payload.
28    pub expr: Expr,
29}
30
31/// Scheduled delivery to one node input port.
32#[derive(Clone, Debug, PartialEq, Eq)]
33pub struct WorkItem {
34    /// Destination node index in the compiled graph.
35    pub node_index: usize,
36    /// Destination input port.
37    pub port: Symbol,
38    /// Delivered payload.
39    pub expr: Expr,
40}
41
42/// Basic event kind emitted by the sequential topology scheduler.
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum TopologyEventKind {
45    /// A work item was queued.
46    Enqueued,
47    /// A node began executing.
48    NodeStarted,
49    /// A node emitted an output port packet.
50    PortEmitted,
51    /// An edge routed a packet.
52    EdgeRouted,
53    /// A public output was produced.
54    OutputEmitted,
55}
56
57/// Compact execution event for inspection and later replay support.
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct TopologyEvent {
60    /// Event kind.
61    pub kind: TopologyEventKind,
62    /// Node index associated with the event.
63    pub node_index: usize,
64    /// Optional port associated with the event.
65    pub port: Option<Symbol>,
66    /// Optional edge index associated with the event.
67    pub edge_index: Option<usize>,
68    /// Optional event payload.
69    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/// Structured budget exhaustion details for a topology run.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct TopologyBudgetError {
117    /// Exhausted budget resource.
118    pub resource: Symbol,
119    /// Configured resource limit.
120    pub limit: u32,
121    /// Observed resource count.
122    pub actual: u32,
123}
124
125impl TopologyBudgetError {
126    /// Converts the budget error into a stable expression value.
127    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/// Runtime budget counters for one topology run.
158#[derive(Clone, Debug, PartialEq, Eq)]
159pub struct BudgetLedger {
160    limits: Budget,
161    /// Scheduler steps consumed.
162    pub steps: u32,
163    /// Per-node visit counts.
164    pub node_visits: Vec<u32>,
165    /// Per-edge route counts.
166    pub edge_visits: Vec<u32>,
167    /// Public outputs emitted.
168    pub outputs: u32,
169    /// Nested eval-fabric calls.
170    pub child_runs: u32,
171}
172
173impl BudgetLedger {
174    /// Creates a zeroed budget ledger for a compiled graph.
175    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    /// Records one scheduler step.
187    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    /// Records one node visit.
193    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    /// Records one edge traversal.
203    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    /// Records one public output.
217    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    /// Records one nested eval-fabric call.
223    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    /// Checks one buffered merge node against the run budget.
233    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
252/// Live state for a sequential topology run.
253pub 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    /// Budget counters for this run.
261    pub budget: BudgetLedger,
262    events: Vec<TopologyEvent>,
263}
264
265impl<'a> TopologyRun<'a> {
266    /// Creates a run with one boundary input expression.
267    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    /// Runs until the queue is exhausted.
290    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    /// Returns public outputs in emission order.
332    pub fn outputs(&self) -> &[Expr] {
333        &self.outputs
334    }
335
336    /// Returns emitted execution events.
337    pub fn events(&self) -> &[TopologyEvent] {
338        &self.events
339    }
340
341    /// Returns run-local cell state.
342    pub fn cells(&self) -> &TopologyCells {
343        &self.cells
344    }
345
346    /// Converts outputs to the graph return expression.
347    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
482/// Runs a compiled graph with one input expression.
483pub 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}