Skip to main content

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}