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