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. 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}