1use std::collections::{BTreeSet, VecDeque};
5use std::fmt::Write as _;
6
7use super::*;
8
9#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct DataEdge {
17 pub from: u64,
20 pub to: u64,
23 pub label: String,
26 pub source: Option<SourceLocation>,
29}
30
31#[derive(Clone, Copy, Debug)]
34pub struct DataHop<'a> {
35 pub edge: &'a DataEdge,
38 pub value: Option<&'a LocalValue>,
41}
42
43impl CallTrace {
44 #[track_caller]
51 pub fn transform(
52 &mut self,
53 input: LocalId,
54 name: impl Into<String>,
55 type_name: impl Into<String>,
56 value: impl std::fmt::Display,
57 ) -> LocalId {
58 let caller = std::panic::Location::caller();
59 self.transform_at_callsite(
60 input,
61 name,
62 type_name,
63 value.to_string(),
64 caller.file(),
65 caller.line(),
66 caller.column(),
67 )
68 }
69
70 #[allow(clippy::too_many_arguments)]
73 pub fn transform_at_callsite(
74 &mut self,
75 input: LocalId,
76 name: impl Into<String>,
77 type_name: impl Into<String>,
78 value: impl Into<String>,
79 file: &'static str,
80 line: u32,
81 column: u32,
82 ) -> LocalId {
83 if matches!(self.mode, TraceMode::Off) {
84 return LocalId(0);
85 }
86 let source = self.callsite_source(file, line, column);
87 let output = self.local_at(name, type_name, value, LocalKind::Binding, source);
88 self.push_edge(DataEdge {
89 from: input.0,
90 to: output.0,
91 label: "transform".to_owned(),
92 source: Some(source),
93 });
94 output
95 }
96
97 #[track_caller]
100 pub fn return_value(
101 &mut self,
102 input: LocalId,
103 name: impl Into<String>,
104 type_name: impl Into<String>,
105 value: impl std::fmt::Display,
106 ) -> LocalId {
107 let caller = std::panic::Location::caller();
108 self.return_at_callsite(
109 input,
110 name,
111 type_name,
112 value.to_string(),
113 caller.file(),
114 caller.line(),
115 caller.column(),
116 )
117 }
118
119 #[allow(clippy::too_many_arguments)]
122 pub fn return_at_callsite(
123 &mut self,
124 input: LocalId,
125 name: impl Into<String>,
126 type_name: impl Into<String>,
127 value: impl Into<String>,
128 file: &'static str,
129 line: u32,
130 column: u32,
131 ) -> LocalId {
132 if matches!(self.mode, TraceMode::Off) {
133 return LocalId(0);
134 }
135 let source = self.callsite_source(file, line, column);
136 let output = self.local_at(name, type_name, value, LocalKind::Output, source);
137 self.push_edge(DataEdge {
138 from: input.0,
139 to: output.0,
140 label: "return".to_owned(),
141 source: Some(source),
142 });
143 output
144 }
145
146 #[track_caller]
153 pub fn consume(
154 &mut self,
155 input: LocalId,
156 function: impl Into<String>,
157 parameter: impl Into<String>,
158 ) -> LocalId {
159 let caller = std::panic::Location::caller();
160 self.consume_at_callsite(
161 function,
162 parameter,
163 input,
164 caller.file(),
165 caller.line(),
166 caller.column(),
167 )
168 }
169
170 pub fn consume_at_callsite(
173 &mut self,
174 function: impl Into<String>,
175 parameter: impl Into<String>,
176 input: LocalId,
177 file: &'static str,
178 line: u32,
179 column: u32,
180 ) -> LocalId {
181 if matches!(self.mode, TraceMode::Off) {
182 return LocalId(0);
183 }
184 let source = self.callsite_source(file, line, column);
185 let function = function.into();
186 let parameter = parameter.into();
187 let output = self.local_at(
188 parameter.clone(),
189 "consumer",
190 format!("{function}::{parameter}"),
191 LocalKind::Consumer,
192 source,
193 );
194 self.push_edge(DataEdge {
195 from: input.0,
196 to: output.0,
197 label: format!("used by {function}::{parameter}"),
198 source: Some(source),
199 });
200 output
201 }
202
203 pub fn data_edges(&self) -> &[DataEdge] {
206 &self.edges
207 }
208
209 fn push_edge(&mut self, edge: DataEdge) {
210 let index = self.edges.len();
211 self.outgoing_index
212 .entry(edge.from)
213 .or_default()
214 .push(index);
215 self.incoming_index.entry(edge.to).or_default().push(index);
216 self.edges.push(edge);
217 }
218
219 pub fn provenance(&self, id: LocalId) -> Vec<&LocalValue> {
222 let mut result = Vec::new();
223 self.collect_upstream(id.0, &mut BTreeSet::new(), &mut result);
224 result
225 }
226
227 pub fn consumers(&self, id: LocalId) -> Vec<&DataEdge> {
230 self.outgoing_index
231 .get(&id.0)
232 .into_iter()
233 .flatten()
234 .filter_map(|index| self.edges.get(*index))
235 .collect()
236 }
237
238 pub fn outgoing(&self, id: LocalId) -> Vec<DataHop<'_>> {
241 self.consumers(id)
242 .into_iter()
243 .map(|edge| DataHop {
244 edge,
245 value: self.find_local(LocalId(edge.to)),
246 })
247 .collect()
248 }
249
250 pub fn incoming(&self, id: LocalId) -> Vec<DataHop<'_>> {
253 self.incoming_index
254 .get(&id.0)
255 .into_iter()
256 .flatten()
257 .filter_map(|index| self.edges.get(*index))
258 .map(|edge| DataHop {
259 edge,
260 value: self.find_local(LocalId(edge.from)),
261 })
262 .collect()
263 }
264
265 pub fn downstream(&self, id: LocalId) -> Vec<&LocalValue> {
268 let mut result = Vec::new();
269 let mut queue = VecDeque::from([id.0]);
270 let mut visited = BTreeSet::from([id.0]);
271 while let Some(current) = queue.pop_front() {
272 for index in self.outgoing_index.get(¤t).into_iter().flatten() {
273 let Some(edge) = self.edges.get(*index) else {
274 continue;
275 };
276 if !visited.insert(edge.to) {
277 continue;
278 }
279 if let Some(local) = self.find_local(LocalId(edge.to)) {
280 result.push(local);
281 }
282 queue.push_back(edge.to);
283 }
284 }
285 result
286 }
287
288 pub fn render_provenance(&self, id: LocalId) -> String {
295 let mut output = String::new();
296 let Some(selected) = self.find_local(id) else {
297 return format!("local {} not found", id.0);
298 };
299 writeln!(
300 output,
301 "value {} {} = {} ({}) [{}] @ {}",
302 selected.id,
303 selected.name,
304 selected.value,
305 selected.type_name,
306 selected.observation.label(),
307 selected.source
308 )
309 .unwrap();
310 output.push_str("upstream:\n");
311 self.render_upstream(id.0, 1, &mut BTreeSet::new(), &mut output);
312 output.push_str("downstream:\n");
313 self.render_downstream(id.0, 1, &mut BTreeSet::new(), &mut output);
314 output
315 }
316
317 fn render_upstream(
318 &self,
319 id: u64,
320 depth: usize,
321 visited: &mut BTreeSet<u64>,
322 output: &mut String,
323 ) {
324 if !visited.insert(id) {
325 return;
326 }
327 for hop in self.incoming(LocalId(id)) {
328 let source = hop
329 .value
330 .map(|local| format!("{} {} = {}", local.id, local.name, local.value))
331 .unwrap_or_else(|| format!("local {}", hop.edge.from));
332 writeln!(
333 output,
334 "{} `-- {source} [{}]",
335 " ".repeat(depth),
336 hop.edge.label
337 )
338 .unwrap();
339 self.render_upstream(hop.edge.from, depth + 1, visited, output);
340 }
341 }
342
343 fn render_downstream(
344 &self,
345 id: u64,
346 depth: usize,
347 visited: &mut BTreeSet<u64>,
348 output: &mut String,
349 ) {
350 if !visited.insert(id) {
351 return;
352 }
353 for hop in self.outgoing(LocalId(id)) {
354 let target = hop
355 .value
356 .map(|local| format!("{} {} = {}", local.id, local.name, local.value))
357 .unwrap_or_else(|| format!("local {}", hop.edge.to));
358 writeln!(
359 output,
360 "{} `-- {target} [{}]",
361 " ".repeat(depth),
362 hop.edge.label
363 )
364 .unwrap();
365 self.render_downstream(hop.edge.to, depth + 1, visited, output);
366 }
367 }
368
369 fn collect_upstream<'a>(
370 &'a self,
371 id: u64,
372 visited: &mut BTreeSet<u64>,
373 result: &mut Vec<&'a LocalValue>,
374 ) {
375 if !visited.insert(id) {
376 return;
377 }
378 for index in self.incoming_index.get(&id).into_iter().flatten() {
379 if let Some(edge) = self.edges.get(*index) {
380 self.collect_upstream(edge.from, visited, result);
381 }
382 }
383 if let Some(local) = self.find_local(LocalId(id)) {
384 result.push(local);
385 }
386 }
387
388 pub fn render_data_flow(&self) -> String {
391 let mut output = String::new();
392 for local in &self.locals {
393 let frame = local
394 .frame_id
395 .and_then(|id| self.frame(id))
396 .map(|frame| frame.call.function)
397 .unwrap_or("<outside-call>");
398 writeln!(
399 output,
400 "{} [{}|{}] {}#{} depth={}: {} = {} @ {} ({})",
401 local.id,
402 local.kind.label(),
403 local.observation.label(),
404 frame,
405 local.frame_id.unwrap_or(0),
406 local.frame_id.map_or(0, |id| self.frame_depth(id)),
407 local.name,
408 local.value,
409 local.source,
410 local.type_name
411 )
412 .unwrap();
413 }
414 if !self.edges.is_empty() {
415 output.push_str("edges:\n");
416 for edge in &self.edges {
417 let source = edge
418 .source
419 .map_or_else(String::new, |location| format!(" @ {location}"));
420 writeln!(
421 output,
422 " {} -> {} ({}){}",
423 edge.from, edge.to, edge.label, source
424 )
425 .unwrap();
426 }
427 }
428 output
429 }
430}