use super::var_id::{VarId, VarSymbolMapping};
use crate::define_index;
use crate::symbol::{SymbolId, SymbolRegistry};
use serde::{Deserialize, Serialize};
use slotmap::SecondaryMap;
use smallvec::SmallVec;
use std::collections::{HashMap, HashSet};
use super::borrow_v2::{
ActiveBorrowV2, BorrowAnalysis, BorrowConflict, BorrowKind, BorrowTrackerV2, MoveError,
};
use super::lock_v2::LockTrackerV2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum FlowKind {
Assign,
Derive,
Read,
Write,
Return,
Argument,
Reference,
Dereference,
Conditional,
SharedBorrow,
MutBorrow,
Move,
Clone,
Copy,
Drop,
Reborrow,
Index,
}
impl FlowKind {
pub fn transfers_ownership(&self) -> bool {
matches!(self, FlowKind::Move | FlowKind::Return)
}
pub fn is_borrow(&self) -> bool {
matches!(
self,
FlowKind::SharedBorrow | FlowKind::MutBorrow | FlowKind::Reborrow
)
}
pub fn is_mutable(&self) -> bool {
matches!(
self,
FlowKind::MutBorrow | FlowKind::Write | FlowKind::Assign
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VarKind {
Local,
Parameter,
Field,
Static,
Return,
Temp,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct FlowEdge {
pub kind: FlowKind,
pub line: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VarNode {
pub name: String,
pub kind: VarKind,
pub line: u32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DataFlowStats {
pub var_count: usize,
pub flow_count: usize,
pub vars_by_kind: HashMap<String, usize>,
pub flows_by_kind: HashMap<String, usize>,
}
#[derive(Debug, Clone)]
pub struct FlowChain {
pub steps: Vec<FlowStep>,
}
#[derive(Debug, Clone)]
pub struct FlowStep {
pub var_id: VarId,
pub flow_kind: Option<FlowKind>,
}
define_index! {
pub struct FlowId;
}
define_index! {
pub struct ScopeId;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ScopeKind {
Function,
IfThen,
IfElse,
WhileBody,
ForBody,
LoopBody,
MatchArm,
Block,
Closure,
}
#[derive(Debug, Clone)]
pub struct ScopeData {
pub parent: Option<ScopeId>,
pub kind: ScopeKind,
pub guard: Option<Guard>,
}
#[derive(Debug, Clone)]
pub struct Guard {
pub kind: GuardKind,
pub var_names: SmallVec<[String; 2]>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum GuardKind {
LessThan,
LessEqual,
GreaterThan,
GreaterEqual,
LetSome,
LetOk,
IsSome,
IsOk,
NonEmpty,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct VarData {
pub parent: SymbolId,
pub kind: VarKind,
pub line: u32,
pub is_mut: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FlowEdgeData {
pub from: VarId,
pub to: VarId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct FlowData {
pub kind: FlowKind,
pub line: u32,
pub scope: ScopeId,
}
#[derive(Debug, Clone, Default)]
pub struct DataFlowGraphV2 {
vars: SecondaryMap<VarId, VarData>,
var_names: SecondaryMap<VarId, String>,
flows: Vec<FlowData>,
edges: Vec<FlowEdgeData>,
scopes: Vec<ScopeData>,
outgoing: SecondaryMap<VarId, SmallVec<[FlowId; 4]>>,
incoming: SecondaryMap<VarId, SmallVec<[FlowId; 4]>>,
var_mapping: VarSymbolMapping,
symbol_to_vars: HashMap<SymbolId, SmallVec<[VarId; 8]>>,
borrow_tracker: BorrowTrackerV2,
lock_tracker: LockTrackerV2,
}
impl DataFlowGraphV2 {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(var_capacity: usize, flow_capacity: usize) -> Self {
Self {
vars: SecondaryMap::new(),
var_names: SecondaryMap::new(),
flows: Vec::with_capacity(flow_capacity),
edges: Vec::with_capacity(flow_capacity),
scopes: Vec::new(),
outgoing: SecondaryMap::new(),
incoming: SecondaryMap::new(),
var_mapping: VarSymbolMapping::with_capacity(var_capacity),
symbol_to_vars: HashMap::with_capacity(var_capacity / 4),
borrow_tracker: BorrowTrackerV2::new(),
lock_tracker: LockTrackerV2::new(),
}
}
pub fn add_var(
&mut self,
data: VarData,
name: String,
var_symbol_id: Option<SymbolId>,
) -> VarId {
let var_id = match var_symbol_id {
Some(sid) => self.var_mapping.register(sid),
None => self.var_mapping.allocate(),
};
self.vars.insert(var_id, data);
self.var_names.insert(var_id, name);
self.outgoing.insert(var_id, SmallVec::new());
self.incoming.insert(var_id, SmallVec::new());
self.symbol_to_vars
.entry(data.parent)
.or_default()
.push(var_id);
var_id
}
#[inline]
pub fn var(&self, id: VarId) -> Option<&VarData> {
self.vars.get(id)
}
#[inline]
pub fn var_name(&self, id: VarId) -> Option<&str> {
self.var_names.get(id).map(|s| s.as_str())
}
#[inline]
pub fn var_mut(&mut self, id: VarId) -> Option<&mut VarData> {
self.vars.get_mut(id)
}
#[inline]
pub fn vars_in_symbol(&self, symbol: SymbolId) -> &[VarId] {
self.symbol_to_vars
.get(&symbol)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
#[inline]
pub fn var_count(&self) -> usize {
self.vars.len()
}
pub fn add_flow(&mut self, from: VarId, to: VarId, data: FlowData) -> FlowId {
let flow_id = FlowId::from_raw(self.flows.len() as u32);
self.flows.push(data);
self.edges.push(FlowEdgeData { from, to });
if let Some(out) = self.outgoing.get_mut(from) {
out.push(flow_id);
}
if let Some(inc) = self.incoming.get_mut(to) {
inc.push(flow_id);
}
flow_id
}
#[inline]
pub fn flow(&self, id: FlowId) -> Option<&FlowData> {
self.flows.get(id.as_usize())
}
#[inline]
pub fn edge(&self, id: FlowId) -> Option<&FlowEdgeData> {
self.edges.get(id.as_usize())
}
#[inline]
pub fn flow_count(&self) -> usize {
self.flows.len()
}
#[inline]
pub fn outgoing(&self, var: VarId) -> &[FlowId] {
self.outgoing.get(var).map(|v| v.as_slice()).unwrap_or(&[])
}
#[inline]
pub fn incoming(&self, var: VarId) -> &[FlowId] {
self.incoming.get(var).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn successors(&self, var: VarId) -> impl Iterator<Item = VarId> + '_ {
self.outgoing(var)
.iter()
.filter_map(|&flow_id| self.edges.get(flow_id.as_usize()).map(|e| e.to))
}
pub fn predecessors(&self, var: VarId) -> impl Iterator<Item = VarId> + '_ {
self.incoming(var)
.iter()
.filter_map(|&flow_id| self.edges.get(flow_id.as_usize()).map(|e| e.from))
}
pub fn impact(&self, start: VarId) -> Vec<VarId> {
let mut visited: HashSet<VarId> = HashSet::new();
let mut result = Vec::new();
let mut stack = vec![start];
while let Some(var) = stack.pop() {
if visited.contains(&var) {
continue;
}
visited.insert(var);
result.push(var);
for next in self.successors(var) {
if !visited.contains(&next) {
stack.push(next);
}
}
}
result
}
pub fn provenance(&self, start: VarId) -> Vec<VarId> {
let mut visited: HashSet<VarId> = HashSet::new();
let mut result = Vec::new();
let mut stack = vec![start];
while let Some(var) = stack.pop() {
if visited.contains(&var) {
continue;
}
visited.insert(var);
result.push(var);
for prev in self.predecessors(var) {
if !visited.contains(&prev) {
stack.push(prev);
}
}
}
result
}
#[inline]
pub fn var_to_symbol(&self, var: VarId) -> Option<SymbolId> {
self.var_mapping.to_symbol(var)
}
#[inline]
pub fn symbol_to_var(&self, symbol: SymbolId) -> Option<VarId> {
self.var_mapping.to_var(symbol)
}
pub fn iter_vars(&self) -> impl Iterator<Item = (VarId, &VarData)> {
self.vars.iter()
}
pub fn resolve_vars(
&self,
symbol_id: Option<SymbolId>,
name: Option<&str>,
registry: &SymbolRegistry,
) -> Option<(Vec<VarId>, String)> {
if let Some(sid) = symbol_id {
let vars = self.vars_in_symbol(sid).to_vec();
let display = registry
.resolve(sid)
.map(|p| p.name().to_string())
.unwrap_or_else(|| format!("{:?}", sid));
return Some((vars, display));
}
let name = name?;
let symbol_ids = registry.find_by_name(name);
let mut vars = Vec::new();
for &sid in &symbol_ids {
vars.extend_from_slice(self.vars_in_symbol(sid));
}
if !vars.is_empty() {
return Some((vars, name.to_string()));
}
let vars: Vec<VarId> = self
.iter_vars()
.filter_map(|(var_id, _)| {
self.var_name(var_id).and_then(|n| {
if n == name || n.contains(name) {
Some(var_id)
} else {
None
}
})
})
.collect();
Some((vars, name.to_string()))
}
pub fn iter_flows(&self) -> impl Iterator<Item = (FlowId, &FlowData, &FlowEdgeData)> {
self.flows
.iter()
.zip(self.edges.iter())
.enumerate()
.map(|(i, (data, edge))| (FlowId::from_raw(i as u32), data, edge))
}
pub fn clear(&mut self) {
self.vars.clear();
self.var_names.clear();
self.flows.clear();
self.edges.clear();
self.scopes.clear();
self.outgoing.clear();
self.incoming.clear();
self.var_mapping.clear();
self.symbol_to_vars.clear();
self.borrow_tracker.clear();
self.lock_tracker.clear();
}
pub fn clear_for_symbols(&mut self, symbols: &[SymbolId]) {
for &symbol_id in symbols {
if let Some(var_ids) = self.symbol_to_vars.remove(&symbol_id) {
for var_id in var_ids {
self.vars.remove(var_id);
self.var_names.remove(var_id);
self.outgoing.remove(var_id);
self.incoming.remove(var_id);
self.var_mapping.remove(var_id);
}
}
}
}
pub fn add_scope(&mut self, data: ScopeData) -> ScopeId {
let id = ScopeId::from_raw(self.scopes.len() as u32);
self.scopes.push(data);
id
}
#[inline]
pub fn scope(&self, id: ScopeId) -> Option<&ScopeData> {
self.scopes.get(id.as_usize())
}
#[inline]
pub fn scope_count(&self) -> usize {
self.scopes.len()
}
pub fn is_guarded(&self, var_name: &str, scope: ScopeId, kinds: &[GuardKind]) -> bool {
let mut current = Some(scope);
while let Some(sid) = current {
if let Some(scope_data) = self.scopes.get(sid.as_usize()) {
if let Some(guard) = &scope_data.guard {
if kinds.contains(&guard.kind) && guard.var_names.iter().any(|n| n == var_name)
{
return true;
}
}
current = scope_data.parent;
} else {
break;
}
}
false
}
pub fn active_guards(&self, scope: ScopeId) -> Vec<&Guard> {
let mut guards = Vec::new();
let mut current = Some(scope);
while let Some(sid) = current {
if let Some(scope_data) = self.scopes.get(sid.as_usize()) {
if let Some(guard) = &scope_data.guard {
guards.push(guard);
}
current = scope_data.parent;
} else {
break;
}
}
guards
}
pub fn flows_of_kind_in_symbol(
&self,
symbol_id: SymbolId,
kind: FlowKind,
) -> Vec<(FlowId, &FlowData, &FlowEdgeData)> {
let var_ids = self.vars_in_symbol(symbol_id);
let mut result = Vec::new();
for &var_id in var_ids {
for &flow_id in self.outgoing(var_id) {
if let (Some(flow), Some(edge)) = (self.flow(flow_id), self.edge(flow_id)) {
if flow.kind == kind {
result.push((flow_id, flow, edge));
}
}
}
}
result
}
#[inline]
pub fn borrow_tracker(&self) -> &BorrowTrackerV2 {
&self.borrow_tracker
}
#[inline]
pub fn borrow_tracker_mut(&mut self) -> &mut BorrowTrackerV2 {
&mut self.borrow_tracker
}
#[inline]
pub fn lock_tracker(&self) -> &LockTrackerV2 {
&self.lock_tracker
}
#[inline]
pub fn lock_tracker_mut(&mut self) -> &mut LockTrackerV2 {
&mut self.lock_tracker
}
pub fn add_borrow(&mut self, source: VarId, borrow: VarId, kind: BorrowKind, line: u32) {
self.borrow_tracker.add_borrow(source, borrow, kind, line);
}
pub fn end_borrow(&mut self, borrow: VarId, line: u32) {
self.borrow_tracker.end_borrow(borrow, line);
}
pub fn borrow_conflicts(
&self,
source: VarId,
kind: BorrowKind,
at_line: u32,
) -> Vec<BorrowConflict> {
self.borrow_tracker.conflicts(source, kind, at_line)
}
pub fn borrow_analysis(&self, symbol_id: SymbolId) -> BorrowAnalysis {
let mut conflicts = Vec::new();
let mut move_errors = Vec::new();
let var_ids = self.vars_in_symbol(symbol_id);
let mut active_borrows: HashMap<VarId, Vec<(VarId, BorrowKind, u32)>> = HashMap::new();
let mut moved_vars: HashMap<VarId, u32> = HashMap::new();
for &var_id in var_ids {
for &flow_id in self.outgoing(var_id) {
if let (Some(flow), Some(edge)) = (self.flow(flow_id), self.edge(flow_id)) {
match flow.kind {
FlowKind::SharedBorrow => {
let source = edge.from;
let borrower = edge.to;
if let Some(borrows) = active_borrows.get(&source) {
for &(existing_borrower, existing_kind, existing_line) in borrows {
if existing_kind.conflicts_with(BorrowKind::Shared) {
conflicts.push(BorrowConflict::new(
source,
ActiveBorrowV2::new(
existing_borrower,
existing_kind,
existing_line,
),
BorrowKind::Shared,
flow.line,
));
}
}
}
active_borrows.entry(source).or_default().push((
borrower,
BorrowKind::Shared,
flow.line,
));
}
FlowKind::MutBorrow => {
let source = edge.from;
let borrower = edge.to;
if let Some(borrows) = active_borrows.get(&source) {
for &(existing_borrower, existing_kind, existing_line) in borrows {
conflicts.push(BorrowConflict::new(
source,
ActiveBorrowV2::new(
existing_borrower,
existing_kind,
existing_line,
),
BorrowKind::Mutable,
flow.line,
));
}
}
active_borrows.entry(source).or_default().push((
borrower,
BorrowKind::Mutable,
flow.line,
));
}
FlowKind::Move => {
let source = edge.from;
moved_vars.insert(source, flow.line);
}
FlowKind::Read | FlowKind::Derive => {
let source = edge.from;
if let Some(&move_line) = moved_vars.get(&source) {
if flow.line > move_line {
move_errors.push(MoveError {
variable: source,
moved_at: move_line,
used_at: flow.line,
});
}
}
}
FlowKind::Drop => {
let source = edge.from;
active_borrows.remove(&source);
}
_ => {}
}
}
}
}
BorrowAnalysis {
conflicts,
move_errors,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use slotmap::SlotMap;
fn setup_symbols() -> (
SlotMap<SymbolId, &'static str>,
SymbolId,
SymbolId,
SymbolId,
) {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let x_sym = symbols.insert("x");
let y_sym = symbols.insert("y");
(symbols, fn_sym, x_sym, y_sym)
}
#[test]
fn test_add_var() {
let (_symbols, fn_sym, x_sym, _y_sym) = setup_symbols();
let mut graph = DataFlowGraphV2::new();
let var_id = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
Some(x_sym),
);
assert_eq!(graph.var_count(), 1);
let data = graph.var(var_id).unwrap();
assert_eq!(data.parent, fn_sym);
assert_eq!(data.kind, VarKind::Local);
assert_eq!(data.line, 10);
assert_eq!(graph.var_name(var_id), Some("x"));
assert_eq!(graph.var_to_symbol(var_id), Some(x_sym));
}
#[test]
fn test_add_flow() {
let (_symbols, fn_sym, x_sym, y_sym) = setup_symbols();
let mut graph = DataFlowGraphV2::new();
let x = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
Some(x_sym),
);
let y = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 11,
is_mut: false,
},
"y".to_string(),
Some(y_sym),
);
let flow_id = graph.add_flow(
x,
y,
FlowData {
kind: FlowKind::Assign,
line: 11,
scope: ScopeId::from_raw(0),
},
);
assert_eq!(graph.flow_count(), 1);
let edge = graph.edge(flow_id).unwrap();
assert_eq!(edge.from, x);
assert_eq!(edge.to, y);
let flow = graph.flow(flow_id).unwrap();
assert_eq!(flow.kind, FlowKind::Assign);
}
#[test]
fn test_successors_predecessors() {
let (_symbols, fn_sym, x_sym, y_sym) = setup_symbols();
let mut graph = DataFlowGraphV2::new();
let x = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
Some(x_sym),
);
let y = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 11,
is_mut: false,
},
"y".to_string(),
Some(y_sym),
);
graph.add_flow(
x,
y,
FlowData {
kind: FlowKind::Assign,
line: 11,
scope: ScopeId::from_raw(0),
},
);
let succs: Vec<_> = graph.successors(x).collect();
assert_eq!(succs, vec![y]);
let preds: Vec<_> = graph.predecessors(y).collect();
assert_eq!(preds, vec![x]);
}
#[test]
fn test_vars_in_symbol() {
let (_symbols, fn_sym, x_sym, y_sym) = setup_symbols();
let mut graph = DataFlowGraphV2::new();
let x = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
Some(x_sym),
);
let y = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 11,
is_mut: false,
},
"y".to_string(),
Some(y_sym),
);
let vars = graph.vars_in_symbol(fn_sym);
assert_eq!(vars.len(), 2);
assert!(vars.contains(&x));
assert!(vars.contains(&y));
}
#[test]
fn test_impact_analysis() {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let a_sym = symbols.insert("a");
let b_sym = symbols.insert("b");
let c_sym = symbols.insert("c");
let mut graph = DataFlowGraphV2::new();
let a = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 1,
is_mut: false,
},
"a".to_string(),
Some(a_sym),
);
let b = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 2,
is_mut: false,
},
"b".to_string(),
Some(b_sym),
);
let c = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 3,
is_mut: false,
},
"c".to_string(),
Some(c_sym),
);
graph.add_flow(
a,
b,
FlowData {
kind: FlowKind::Assign,
line: 2,
scope: ScopeId::from_raw(0),
},
);
graph.add_flow(
b,
c,
FlowData {
kind: FlowKind::Assign,
line: 3,
scope: ScopeId::from_raw(0),
},
);
let impact = graph.impact(a);
assert_eq!(impact.len(), 3);
assert!(impact.contains(&a));
assert!(impact.contains(&b));
assert!(impact.contains(&c));
}
#[test]
fn test_provenance_analysis() {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let a_sym = symbols.insert("a");
let b_sym = symbols.insert("b");
let c_sym = symbols.insert("c");
let mut graph = DataFlowGraphV2::new();
let a = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 1,
is_mut: false,
},
"a".to_string(),
Some(a_sym),
);
let b = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 2,
is_mut: false,
},
"b".to_string(),
Some(b_sym),
);
let c = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 3,
is_mut: false,
},
"c".to_string(),
Some(c_sym),
);
graph.add_flow(
a,
b,
FlowData {
kind: FlowKind::Assign,
line: 2,
scope: ScopeId::from_raw(0),
},
);
graph.add_flow(
b,
c,
FlowData {
kind: FlowKind::Assign,
line: 3,
scope: ScopeId::from_raw(0),
},
);
let provenance = graph.provenance(c);
assert_eq!(provenance.len(), 3);
assert!(provenance.contains(&a));
assert!(provenance.contains(&b));
assert!(provenance.contains(&c));
}
#[test]
fn test_add_var_without_symbol_id_produces_distinct_vars() {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let mut graph = DataFlowGraphV2::new();
let x = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
None,
);
let y = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 11,
is_mut: false,
},
"y".to_string(),
None,
);
assert_ne!(x, y, "Different variables must get different VarIds");
assert_eq!(graph.var_name(x), Some("x"));
assert_eq!(graph.var_name(y), Some("y"));
assert_eq!(graph.var_count(), 2);
assert_eq!(graph.var_to_symbol(x), None);
assert_eq!(graph.var_to_symbol(y), None);
}
#[test]
fn test_add_var_with_symbol_id_still_works() {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let x_sym = symbols.insert("x");
let mut graph = DataFlowGraphV2::new();
let var_id = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
Some(x_sym),
);
assert_eq!(graph.var_count(), 1);
assert_eq!(graph.var_name(var_id), Some("x"));
assert_eq!(graph.var_to_symbol(var_id), Some(x_sym));
}
#[test]
fn test_vars_for_symbol_with_anonymous_vars() {
let mut symbols: SlotMap<SymbolId, &str> = SlotMap::with_key();
let fn_sym = symbols.insert("my_fn");
let mut graph = DataFlowGraphV2::new();
let x = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 10,
is_mut: false,
},
"x".to_string(),
None,
);
let y = graph.add_var(
VarData {
parent: fn_sym,
kind: VarKind::Local,
line: 11,
is_mut: false,
},
"y".to_string(),
None,
);
let vars = graph.vars_in_symbol(fn_sym);
assert_eq!(vars.len(), 2);
assert!(vars.contains(&x));
assert!(vars.contains(&y));
}
}