Skip to main content

sim_lib_topology/
parse.rs

1//! Parsing of canonical expression and list forms into topology graph data.
2
3mod common;
4mod fields;
5mod util;
6
7use sim_kernel::{Cx, Expr, Result, Symbol, Value};
8
9use crate::{
10    Budget, BudgetExhausted, Cell, Edge, EdgeId, Graph, Node, Port, PortMode, Scheduler,
11    SchedulerMode, model::TOPOLOGY_API,
12};
13
14use self::{common::*, fields::Fields, util::*};
15
16/// Parses a runtime value into canonical topology graph data.
17pub fn parse_graph(cx: &mut Cx, value: Value) -> Result<Graph> {
18    let expr = value.object().as_expr(cx)?;
19    parse_graph_expr(&expr)
20}
21
22/// Parses a runtime value into a [`Graph`]; alias for [`parse_graph`].
23pub fn graph_from_value(cx: &mut Cx, value: Value) -> Result<Graph> {
24    parse_graph(cx, value)
25}
26
27pub(crate) fn parse_graph_expr(expr: &Expr) -> Result<Graph> {
28    match data_expr(expr) {
29        Expr::Map(entries) => parse_graph_fields(Fields::from_map(entries, "graph")?),
30        Expr::List(items) | Expr::Vector(items) => parse_graph_list(items),
31        other => Err(parse_error(
32            "graph",
33            format!(
34                "expected graph map or keyword list, found {}",
35                expr_kind(other)
36            ),
37        )),
38    }
39}
40
41fn parse_graph_list(items: &[Expr]) -> Result<Graph> {
42    let body = match items.first().map(data_expr) {
43        Some(Expr::Symbol(symbol)) if symbol.as_qualified_str() == "topology/graph" => &items[1..],
44        _ => items,
45    };
46    parse_graph_fields(Fields::from_keywords(body, "graph")?)
47}
48
49fn parse_graph_fields(fields: Fields<'_>) -> Result<Graph> {
50    if let Some(kind) = fields.get("kind") {
51        expect_named(kind, "topology", "graph.kind")?;
52    }
53
54    let name = parse_symbol(fields.required("name", "graph.name")?, "graph.name")?;
55    let mut graph = Graph::new(name);
56
57    if let Some(version) = fields.get("version") {
58        graph.version = parse_string(version, "graph.version")?;
59    }
60    if let Some(api) = fields.get("api") {
61        let api = parse_string(api, "graph.api")?;
62        if api != TOPOLOGY_API {
63            return Err(parse_error(
64                "graph.api",
65                format!("expected {TOPOLOGY_API}, found {api}"),
66            ));
67        }
68        graph.api = api;
69    }
70    if let Some(input) = fields.get("input") {
71        graph.input = parse_optional_expr(input);
72    }
73    if let Some(output) = fields.get("output") {
74        graph.output = parse_optional_expr(output);
75    }
76    if let Some(nodes) = fields.get("nodes") {
77        graph.nodes = parse_nodes(nodes, "graph.nodes")?;
78    }
79    if let Some(edges) = fields.get("edges") {
80        graph.edges = parse_edges(edges, "graph.edges")?;
81    }
82    if let Some(cells) = fields.get("cells") {
83        graph.cells = parse_cells(cells, "graph.cells")?;
84    }
85    if let Some(scheduler) = fields.get("scheduler") {
86        graph.scheduler = parse_scheduler(scheduler, "graph.scheduler")?;
87    }
88    if let Some(budget) = fields.get("budget") {
89        graph.budget = parse_budget(budget, "graph.budget")?;
90    }
91    if let Some(capabilities) = fields.get("capabilities") {
92        graph.capabilities = parse_symbol_list(capabilities, "graph.capabilities")?;
93    }
94    if let Some(metadata) = fields.get("metadata") {
95        graph.metadata = parse_symbol_expr_map(metadata, "graph.metadata")?;
96    }
97    if let Some(tests) = fields.get("tests") {
98        graph.tests = parse_tests(tests, "graph.tests")?;
99    }
100
101    fields.reject_unknown(&[
102        "kind",
103        "name",
104        "version",
105        "api",
106        "input",
107        "output",
108        "nodes",
109        "edges",
110        "cells",
111        "scheduler",
112        "budget",
113        "capabilities",
114        "metadata",
115        "tests",
116    ])?;
117
118    Ok(graph)
119}
120
121fn parse_nodes(expr: &Expr, path: &str) -> Result<Vec<Node>> {
122    sequence_items(expr, path)?
123        .iter()
124        .enumerate()
125        .map(|(index, item)| parse_node(item, &format!("{path}[{index}]")))
126        .collect()
127}
128
129fn parse_node(expr: &Expr, path: &str) -> Result<Node> {
130    match data_expr(expr) {
131        Expr::Map(entries) => parse_node_fields(Fields::from_map(entries, path)?, path),
132        Expr::List(items) | Expr::Vector(items) => {
133            let Some((first, rest)) = items.split_first() else {
134                return Err(parse_error(path, "expected node id"));
135            };
136            let id = parse_symbol(first, &format!("{path}.id"))?;
137            let fields = Fields::from_keywords(rest, path)?;
138            parse_node_with_id(id, fields, path)
139        }
140        Expr::Symbol(symbol) => {
141            let id = symbol.clone();
142            Ok(Node::new(id.clone(), id))
143        }
144        other => Err(parse_error(
145            path,
146            format!(
147                "expected node map, list, or symbol, found {}",
148                expr_kind(other)
149            ),
150        )),
151    }
152}
153
154fn parse_node_fields(fields: Fields<'_>, path: &str) -> Result<Node> {
155    let id = parse_symbol(
156        fields.required("id", &format!("{path}.id"))?,
157        &format!("{path}.id"),
158    )?;
159    parse_node_with_id(id, fields, path)
160}
161
162fn parse_node_with_id(id: Symbol, fields: Fields<'_>, path: &str) -> Result<Node> {
163    let verb = match fields.get("verb") {
164        Some(value) => parse_symbol(value, &format!("{path}.verb"))?,
165        None => id.clone(),
166    };
167    let mut node = Node::new(id, verb);
168
169    if let Some(inputs) = fields.get("in") {
170        node.inputs = parse_ports(inputs, &format!("{path}.in"))?;
171    }
172    if let Some(outputs) = fields.get("out") {
173        node.outputs = parse_ports(outputs, &format!("{path}.out"))?;
174    }
175    if let Some(target) = fields.get("target") {
176        node.target = parse_optional_expr(target);
177    }
178    if let Some(role) = fields.get("role") {
179        node.role = parse_optional_symbol(role, &format!("{path}.role"))?;
180    }
181    if let Some(input) = fields.get("input") {
182        node.input = parse_optional_expr(input);
183    }
184    if let Some(output) = fields.get("output") {
185        node.output = parse_optional_expr(output);
186    }
187    if let Some(options) = fields.get("options") {
188        node.options = parse_symbol_expr_map(options, &format!("{path}.options"))?;
189    }
190    node.options.extend(fields.unknown_pairs(&[
191        "id", "verb", "in", "out", "target", "role", "input", "output", "options",
192    ]));
193
194    Ok(node)
195}
196
197fn parse_ports(expr: &Expr, path: &str) -> Result<Vec<Port>> {
198    sequence_items(expr, path)?
199        .iter()
200        .enumerate()
201        .map(|(index, item)| parse_port(item, &format!("{path}[{index}]")))
202        .collect()
203}
204
205fn parse_port(expr: &Expr, path: &str) -> Result<Port> {
206    match data_expr(expr) {
207        Expr::Map(entries) => parse_port_fields(Fields::from_map(entries, path)?, path),
208        Expr::List(items) | Expr::Vector(items) => {
209            let Some((first, rest)) = items.split_first() else {
210                return Err(parse_error(path, "expected port name"));
211            };
212            let name = parse_symbol(first, &format!("{path}.name"))?;
213            let fields = Fields::from_keywords(rest, path)?;
214            parse_port_with_name(name, fields, path)
215        }
216        Expr::Symbol(symbol) => Ok(Port::new(symbol.clone(), PortMode::Value, true)),
217        other => Err(parse_error(
218            path,
219            format!(
220                "expected port map, list, or symbol, found {}",
221                expr_kind(other)
222            ),
223        )),
224    }
225}
226
227fn parse_port_fields(fields: Fields<'_>, path: &str) -> Result<Port> {
228    let name = parse_symbol(
229        fields.required("name", &format!("{path}.name"))?,
230        &format!("{path}.name"),
231    )?;
232    parse_port_with_name(name, fields, path)
233}
234
235fn parse_port_with_name(name: Symbol, fields: Fields<'_>, path: &str) -> Result<Port> {
236    let mode = match fields.get("mode") {
237        Some(value) => parse_port_mode(value, &format!("{path}.mode"))?,
238        None => PortMode::Value,
239    };
240    let required = match fields.get("required") {
241        Some(value) => parse_bool(value, &format!("{path}.required"))?,
242        None => true,
243    };
244    let mut port = Port::new(name, mode, required);
245    if let Some(shape) = fields.get("shape") {
246        port.shape = parse_optional_expr(shape);
247    }
248    fields.reject_unknown(&["name", "shape", "mode", "required"])?;
249    Ok(port)
250}
251
252fn parse_edges(expr: &Expr, path: &str) -> Result<Vec<Edge>> {
253    sequence_items(expr, path)?
254        .iter()
255        .enumerate()
256        .map(|(index, item)| {
257            parse_edge(item, EdgeId::new(index as u32), &format!("{path}[{index}]"))
258        })
259        .collect()
260}
261
262fn parse_edge(expr: &Expr, fallback_id: EdgeId, path: &str) -> Result<Edge> {
263    match data_expr(expr) {
264        Expr::Map(entries) => {
265            parse_edge_fields(Fields::from_map(entries, path)?, fallback_id, path)
266        }
267        Expr::List(items) | Expr::Vector(items) => parse_edge_list(items, fallback_id, path),
268        other => Err(parse_error(
269            path,
270            format!("expected edge map or list, found {}", expr_kind(other)),
271        )),
272    }
273}
274
275fn parse_edge_list(items: &[Expr], fallback_id: EdgeId, path: &str) -> Result<Edge> {
276    if items.len() < 3 || !is_arrow(&items[1]) {
277        return Err(parse_error(path, "expected edge form: from -> to"));
278    }
279    let mut fields = Fields::from_keywords(&items[3..], path)?;
280    fields.push("from", &items[0]);
281    fields.push("to", &items[2]);
282    parse_edge_fields(fields, fallback_id, path)
283}
284
285fn parse_edge_fields(fields: Fields<'_>, fallback_id: EdgeId, path: &str) -> Result<Edge> {
286    let id = match fields.get("id") {
287        Some(value) => EdgeId::new(parse_u32(value, &format!("{path}.id"))?),
288        None => fallback_id,
289    };
290    let from = parse_port_ref(
291        fields.required("from", &format!("{path}.from"))?,
292        "out",
293        &format!("{path}.from"),
294    )?;
295    let to = parse_port_ref(
296        fields.required("to", &format!("{path}.to"))?,
297        "in",
298        &format!("{path}.to"),
299    )?;
300    let mut edge = Edge::new(id, from, to);
301
302    if let Some(when) = fields.get("when") {
303        edge.when = parse_optional_expr(when);
304    }
305    if let Some(transform) = fields.get("transform") {
306        edge.transform = parse_optional_expr(transform);
307    }
308    if let Some(as_name) = fields.get("as") {
309        edge.as_name = parse_optional_symbol(as_name, &format!("{path}.as"))?;
310    }
311    if let Some(priority) = fields.get("priority") {
312        edge.priority = parse_i64(priority, &format!("{path}.priority"))?;
313    }
314    if let Some(max_visits) = fields.get("max_visits") {
315        edge.max_visits = parse_optional_u32(max_visits, &format!("{path}.max_visits"))?;
316    }
317    if let Some(buffer) = fields.get("buffer") {
318        edge.buffer = parse_optional_expr(buffer);
319    }
320    if let Some(metadata) = fields.get("metadata") {
321        edge.metadata = parse_symbol_expr_map(metadata, &format!("{path}.metadata"))?;
322    }
323    edge.metadata.extend(fields.unknown_pairs(&[
324        "id",
325        "from",
326        "to",
327        "when",
328        "transform",
329        "as",
330        "priority",
331        "max_visits",
332        "buffer",
333        "metadata",
334    ]));
335
336    Ok(edge)
337}
338
339fn parse_cells(expr: &Expr, path: &str) -> Result<Vec<Cell>> {
340    sequence_items(expr, path)?
341        .iter()
342        .enumerate()
343        .map(|(index, item)| parse_cell(item, &format!("{path}[{index}]")))
344        .collect()
345}
346
347fn parse_cell(expr: &Expr, path: &str) -> Result<Cell> {
348    match data_expr(expr) {
349        Expr::Map(entries) => parse_cell_fields(Fields::from_map(entries, path)?, path),
350        Expr::List(items) | Expr::Vector(items) => {
351            let Some((first, rest)) = items.split_first() else {
352                return Err(parse_error(path, "expected cell name"));
353            };
354            let name = parse_symbol(first, &format!("{path}.name"))?;
355            parse_cell_with_name(name, Fields::from_keywords(rest, path)?, path)
356        }
357        other => Err(parse_error(
358            path,
359            format!("expected cell map or list, found {}", expr_kind(other)),
360        )),
361    }
362}
363
364fn parse_cell_fields(fields: Fields<'_>, path: &str) -> Result<Cell> {
365    let name = parse_symbol(
366        fields.required("name", &format!("{path}.name"))?,
367        &format!("{path}.name"),
368    )?;
369    parse_cell_with_name(name, fields, path)
370}
371
372fn parse_cell_with_name(name: Symbol, fields: Fields<'_>, path: &str) -> Result<Cell> {
373    let initial = fields
374        .required("initial", &format!("{path}.initial"))?
375        .clone();
376    let mut cell = Cell::new(name, initial);
377    if let Some(shape) = fields.get("shape") {
378        cell.shape = parse_optional_expr(shape);
379    }
380    if let Some(merge) = fields.get("merge") {
381        cell.merge = parse_optional_symbol(merge, &format!("{path}.merge"))?;
382    }
383    if let Some(private) = fields.get("private") {
384        cell.private = parse_bool(private, &format!("{path}.private"))?;
385    }
386    fields.reject_unknown(&["name", "shape", "initial", "merge", "private"])?;
387    Ok(cell)
388}
389
390fn parse_scheduler(expr: &Expr, path: &str) -> Result<Scheduler> {
391    let fields = parse_field_set(expr, path)?;
392    let mut scheduler = Scheduler::default();
393    if let Some(mode) = fields.get("mode") {
394        expect_named(mode, "sequential", &format!("{path}.mode"))?;
395        scheduler.mode = SchedulerMode::Sequential;
396    }
397    if let Some(seed) = fields.get("seed") {
398        scheduler.seed = parse_optional_u64(seed, &format!("{path}.seed"))?;
399    }
400    if let Some(max_concurrency) = fields.get("max_concurrency") {
401        scheduler.max_concurrency = parse_u32(max_concurrency, &format!("{path}.max_concurrency"))?;
402    }
403    if let Some(deterministic) = fields.get("deterministic") {
404        scheduler.deterministic = parse_bool(deterministic, &format!("{path}.deterministic"))?;
405    }
406    fields.reject_unknown(&["mode", "seed", "max_concurrency", "deterministic"])?;
407    Ok(scheduler)
408}
409
410fn parse_budget(expr: &Expr, path: &str) -> Result<Budget> {
411    let fields = parse_field_set(expr, path)?;
412    let mut budget = Budget::default();
413    if let Some(max_steps) = fields.get("max_steps") {
414        budget.max_steps = parse_u32(max_steps, &format!("{path}.max_steps"))?;
415    }
416    if let Some(max_node_visits) = fields.get("max_node_visits") {
417        budget.max_node_visits = parse_u32(max_node_visits, &format!("{path}.max_node_visits"))?;
418    }
419    if let Some(max_edge_visits) = fields.get("max_edge_visits") {
420        budget.max_edge_visits = parse_u32(max_edge_visits, &format!("{path}.max_edge_visits"))?;
421    }
422    if let Some(max_outputs) = fields.get("max_outputs") {
423        budget.max_outputs = parse_u32(max_outputs, &format!("{path}.max_outputs"))?;
424    }
425    if let Some(max_child_runs) = fields.get("max_child_runs") {
426        budget.max_child_runs = parse_u32(max_child_runs, &format!("{path}.max_child_runs"))?;
427    }
428    if let Some(deadline_ms) = fields.get("deadline_ms") {
429        budget.deadline_ms = parse_optional_u64(deadline_ms, &format!("{path}.deadline_ms"))?;
430    }
431    if let Some(on_exhausted) = fields.get("on_exhausted") {
432        budget.on_exhausted =
433            match symbolish_name(on_exhausted, &format!("{path}.on_exhausted"))?.as_str() {
434                "fail" => BudgetExhausted::Fail,
435                "partial" => BudgetExhausted::Partial,
436                other => {
437                    return Err(parse_error(
438                        format!("{path}.on_exhausted"),
439                        format!("expected fail or partial, found {other}"),
440                    ));
441                }
442            };
443    }
444    fields.reject_unknown(&[
445        "max_steps",
446        "max_node_visits",
447        "max_edge_visits",
448        "max_outputs",
449        "max_child_runs",
450        "deadline_ms",
451        "on_exhausted",
452    ])?;
453    Ok(budget)
454}