sealmap_model/flow.rs
1//! Ordered call/control flow of a function body: the raw material for
2//! sequence diagrams.
3//!
4//! A [`Flow`] is a tree of [`Step`]s in **source order**. Calls are leaves;
5//! branches, loops and optional blocks are interior nodes that map one-to-one
6//! onto Mermaid `alt` / `else`, `loop` and `opt` fragments. Language adapters drop
7//! anything that contains no calls, so a flow is already the minimal skeleton
8//! of "who talks to whom, in what order, under which condition".
9//!
10//! Every [`Call::target`] is a canonical [`SymbolId`]. When the target is
11//! itself a callable in the codebase, its own flow is a separate fragment
12//! with the same id, which is what lets an orchestrator *inline* one sequence
13//! into another, or stitch fragments across files, without guessing.
14//!
15//! ```
16//! use sealmap_model::*;
17//!
18//! let open = SymbolId::parse("sym:cargo app . Db#open().").unwrap();
19//! let insert = SymbolId::parse("sym:cargo app . Db#insert().").unwrap();
20//! let flow = Flow::new(vec![
21//! Step::Call(Call::new(open, "open", Confidence::Exact)),
22//! Step::Loop {
23//! label: "for row in rows".into(),
24//! body: vec![Step::Call(Call::new(insert, "insert", Confidence::Inferred))],
25//! },
26//! ]);
27//! assert_eq!(flow.call_count(), 2);
28//! assert_eq!(flow.calls().map(|c| c.target.name()).collect::<Vec<_>>(), ["open", "insert"]);
29//! ```
30
31use serde::{Deserialize, Serialize};
32
33use crate::sym::SymbolId;
34use crate::symbol::Confidence;
35
36/// The ordered call/control skeleton of one callable body.
37#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
38pub struct Flow {
39 /// Top-level steps in source order.
40 pub steps: Vec<Step>,
41}
42
43impl Flow {
44 /// Wrap a list of steps.
45 pub fn new(steps: Vec<Step>) -> Self {
46 Self { steps }
47 }
48
49 /// `true` when the body makes no calls at all.
50 pub fn is_empty(&self) -> bool {
51 self.steps.is_empty()
52 }
53
54 /// Every call in depth-first source order.
55 pub fn calls(&self) -> impl Iterator<Item = &Call> {
56 let mut out = Vec::new();
57 collect_calls(&self.steps, &mut out);
58 out.into_iter()
59 }
60
61 /// Number of calls in the whole tree.
62 pub fn call_count(&self) -> usize {
63 self.calls().count()
64 }
65
66 /// Maximum nesting depth of control fragments (0 for a flat sequence).
67 pub fn depth(&self) -> usize {
68 fn depth(steps: &[Step]) -> usize {
69 steps
70 .iter()
71 .map(|s| match s {
72 Step::Call(_) | Step::Return(_) => 0,
73 Step::Branch { arms, .. } => 1 + arms.iter().map(|a| depth(&a.steps)).max().unwrap_or(0),
74 Step::Loop { body, .. } | Step::Optional { body, .. } => 1 + depth(body),
75 Step::Parallel { arms } => 1 + arms.iter().map(|a| depth(&a.steps)).max().unwrap_or(0),
76 })
77 .max()
78 .unwrap_or(0)
79 }
80 depth(&self.steps)
81 }
82}
83
84fn collect_calls<'a>(steps: &'a [Step], out: &mut Vec<&'a Call>) {
85 for step in steps {
86 match step {
87 Step::Call(c) => out.push(c),
88 Step::Branch { arms, .. } | Step::Parallel { arms } => {
89 for arm in arms {
90 collect_calls(&arm.steps, out);
91 }
92 }
93 Step::Loop { body, .. } | Step::Optional { body, .. } => collect_calls(body, out),
94 Step::Return(_) => {}
95 }
96 }
97}
98
99/// One node of a [`Flow`].
100#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(tag = "step", rename_all = "snake_case")]
102pub enum Step {
103 /// A call. Rendered as a message arrow. Calls nested in receivers and
104 /// arguments appear as separate, earlier steps (evaluation order).
105 Call(Call),
106 /// Mutually exclusive arms (`if`/`else if`/`else`, `match`).
107 /// Rendered as `alt` / `else`.
108 Branch {
109 /// Arms in source order. Arms without calls are kept (with empty
110 /// steps) only when they matter for reading the branch, e.g. an
111 /// `else` that returns early.
112 arms: Vec<Arm>,
113 },
114 /// A loop (`for`, `while`, `loop`, iterator adaptors with closures).
115 /// Rendered as `loop`.
116 Loop {
117 /// Loop header, e.g. `for item in items`.
118 label: String,
119 /// Body.
120 body: Vec<Step>,
121 },
122 /// A block that may not run (`if` without `else`, `if let`, closures
123 /// passed to `map`/`and_then`). Rendered as `opt`.
124 Optional {
125 /// Condition, e.g. `if let Some(x) = cache.get(k)`.
126 label: String,
127 /// Body.
128 body: Vec<Step>,
129 },
130 /// Concurrent arms (`join!`, `select!`, spawned tasks).
131 /// Rendered as `par` / `and`.
132 Parallel {
133 /// Arms.
134 arms: Vec<Arm>,
135 },
136 /// An explicit early exit (`return`, `?` propagation, `break` out of a
137 /// labelled loop). Rendered as a note or a reply arrow.
138 Return(Exit),
139}
140
141/// One arm of a [`Step::Branch`] or [`Step::Parallel`].
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143pub struct Arm {
144 /// Condition or pattern, e.g. `Ok(conn)`, `x > 3`, `else`.
145 pub label: String,
146 /// Steps in the arm.
147 pub steps: Vec<Step>,
148}
149
150/// How a call is spelled at the call site.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
152#[serde(rename_all = "snake_case")]
153pub enum CallKind {
154 /// Path call: `foo()`, `Type::new()`, `module::f()`.
155 Function,
156 /// Method call: `x.foo()`.
157 Method,
158 /// Macro invocation that the language adapter chose to keep (`println!` is
159 /// dropped, `tokio::spawn`-like or user macros are kept).
160 Macro,
161}
162
163/// A single call site.
164#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct Call {
166 /// Canonical id of the callee.
167 pub target: SymbolId,
168 /// Message text shown on the arrow (callee name plus a compact argument
169 /// sketch, e.g. `insert(key, value)`).
170 pub label: String,
171 /// How the call is spelled.
172 pub kind: CallKind,
173 /// How sure the language adapter is about `target`.
174 pub confidence: Confidence,
175 /// `.await`ed at the call site.
176 pub awaited: bool,
177 /// Followed by `?` (errors propagate to the caller).
178 pub fallible: bool,
179 /// 1-based line of the call site.
180 pub line: u32,
181}
182
183impl Call {
184 /// A synchronous, infallible function call at line 0. Language adapters set the remaining fields.
185 pub fn new(target: SymbolId, label: impl Into<String>, confidence: Confidence) -> Self {
186 Self {
187 target,
188 label: label.into(),
189 kind: CallKind::Function,
190 confidence,
191 awaited: false,
192 fallible: false,
193 line: 0,
194 }
195 }
196}
197
198/// An early exit.
199#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
200pub struct Exit {
201 /// What is returned / propagated, e.g. `Err(e)`, `?`, `None`.
202 pub label: String,
203 /// 1-based line.
204 pub line: u32,
205}