use std::collections::HashMap;
use crate::graph::node::Span;
use crate::graph::unified::concurrent::CodeGraph;
use crate::graph::unified::edge::{EdgeKind, ResolvedVia};
use crate::graph::unified::node::id::NodeId;
use crate::graph::unified::storage::c_indirect::{IndirectCallsite, IndirectShape};
use crate::graph::unified::string::StringId;
const CAP: usize = 4;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Pass5bStats {
pub binding_resolved: u64,
pub typematch_resolved: u64,
pub cap_exceeded: u64,
pub stub_fallback: u64,
}
impl Pass5bStats {
#[must_use]
pub fn total_resolved(&self) -> u64 {
self.binding_resolved + self.typematch_resolved
}
}
pub fn resolve_c_indirect_calls(graph: &mut CodeGraph) -> Pass5bStats {
let mut stats = Pass5bStats::default();
if graph.c_indirect_tables().is_none() {
return stats;
}
seed_fn_signature_from_bindings(graph);
let callsites: Vec<IndirectCallsite> = graph
.c_indirect_tables()
.map(|t| t.pending_callsites.clone())
.unwrap_or_default();
log::info!(
target: "sqry_core::build",
"Pass 5b start: {} indirect callsite(s) pending",
callsites.len(),
);
let signature_index = build_address_taken_signature_index(graph);
for callsite in &callsites {
match resolve_one(graph, callsite, &signature_index) {
ResolutionOutcome::BindingPlane(targets) => {
rewrite_synthetic_edge(graph, callsite, &targets, ResolvedVia::BindingPlane);
stats.binding_resolved += 1;
}
ResolutionOutcome::TypeMatch(targets) => {
rewrite_synthetic_edge(graph, callsite, &targets, ResolvedVia::TypeMatch);
stats.typematch_resolved += 1;
}
ResolutionOutcome::CapExceeded => {
graph
.macro_metadata_mut()
.mark_callsite_promiscuous(callsite.caller);
stats.cap_exceeded += 1;
}
ResolutionOutcome::FallbackToStub => {
stats.stub_fallback += 1;
}
}
}
log::info!(
target: "sqry_core::build",
"Pass 5b end: binding={}, typematch={}, cap_exceeded={}, fallback={}",
stats.binding_resolved,
stats.typematch_resolved,
stats.cap_exceeded,
stats.stub_fallback,
);
stats
}
enum ResolutionOutcome {
BindingPlane(Vec<NodeId>),
TypeMatch(Vec<NodeId>),
CapExceeded,
FallbackToStub,
}
fn resolve_one(
graph: &CodeGraph,
callsite: &IndirectCallsite,
signature_index: &HashMap<StringId, Vec<NodeId>>,
) -> ResolutionOutcome {
let Some(tables) = graph.c_indirect_tables() else {
return ResolutionOutcome::FallbackToStub;
};
let (binding_targets, expected_sig): (Vec<NodeId>, Option<StringId>) = match &callsite.shape {
IndirectShape::FieldExpr {
receiver_name,
field_name,
} => {
let Some(scope) = tables.scope_index_for(callsite.file_id) else {
return ResolutionOutcome::FallbackToStub;
};
let Some(receiver_type) = scope.resolve_type(receiver_name, callsite.use_span.0) else {
return ResolutionOutcome::FallbackToStub;
};
let struct_tag = strip_struct_keyword_and_pointer(receiver_type);
let strings = graph.strings();
let Some(struct_id) = strings.get(struct_tag) else {
return ResolutionOutcome::FallbackToStub;
};
let Some(field_id) = strings.get(field_name) else {
return ResolutionOutcome::FallbackToStub;
};
let key = (struct_id, field_id);
let binding_targets: Vec<NodeId> = tables
.bindings_by_field
.get(&key)
.map(|entries| entries.iter().map(|e| e.target_fn).collect())
.unwrap_or_default();
let expected_sig = tables.struct_field_fnptr.get(&key).copied();
(binding_targets, expected_sig)
}
IndirectShape::PointerExpr { var_name } => {
let Some(scope) = tables.scope_index_for(callsite.file_id) else {
return ResolutionOutcome::FallbackToStub;
};
let Some(type_token) = scope.resolve_type(var_name, callsite.use_span.0) else {
return ResolutionOutcome::FallbackToStub;
};
let expected_sig = graph.strings().get(type_token);
(Vec::new(), expected_sig)
}
};
if !binding_targets.is_empty() {
let mut seen: std::collections::HashSet<(u32, u64)> =
std::collections::HashSet::with_capacity(binding_targets.len());
let deduped: Vec<NodeId> = binding_targets
.into_iter()
.filter(|nid| seen.insert((nid.index(), nid.generation())))
.collect();
if !deduped.is_empty() && deduped.len() <= CAP {
return ResolutionOutcome::BindingPlane(deduped);
}
}
let Some(expected) = expected_sig else {
return ResolutionOutcome::FallbackToStub;
};
let typematch_targets: Vec<NodeId> =
signature_index.get(&expected).cloned().unwrap_or_default();
if typematch_targets.is_empty() {
return ResolutionOutcome::FallbackToStub;
}
if typematch_targets.len() > CAP {
return ResolutionOutcome::CapExceeded;
}
ResolutionOutcome::TypeMatch(typematch_targets)
}
fn rewrite_synthetic_edge(
graph: &mut CodeGraph,
callsite: &IndirectCallsite,
candidates: &[NodeId],
resolved_via: ResolvedVia,
) {
let stub_target_name: &str = match &callsite.shape {
IndirectShape::FieldExpr { field_name, .. } => field_name.as_str(),
IndirectShape::PointerExpr { var_name } => var_name.as_str(),
};
let staged_argc_u8 = u8::try_from(callsite.argument_count).unwrap_or(u8::MAX);
let outgoing = graph.edges().edges_from(callsite.caller);
let stub = outgoing.into_iter().find(|e| {
let EdgeKind::Calls {
argument_count,
is_async,
resolved_via,
} = &e.kind
else {
return false;
};
if *resolved_via != ResolvedVia::Direct {
return false;
}
if *argument_count != staged_argc_u8 || *is_async != callsite.is_async {
return false;
}
let Some(entry) = graph.nodes().get(e.target) else {
return false;
};
let Some(name) = graph.strings().resolve(entry.name) else {
return false;
};
name.as_ref() == stub_target_name
});
let Some(stub) = stub else {
log::debug!(
target: "sqry_core::build",
"Pass 5b: no synthetic stub found for callsite caller={:?} \
shape={:?} use_span={:?} — skipping rewrite",
callsite.caller,
callsite.shape,
callsite.use_span,
);
return;
};
let stub_file = stub.file;
let stub_target = stub.target;
let stub_spans = stub.spans.clone();
let stub_kind = stub.kind.clone();
graph
.edges_mut()
.remove_edge(callsite.caller, stub_target, stub_kind, stub_file);
let new_kind = EdgeKind::Calls {
argument_count: staged_argc_u8,
is_async: callsite.is_async,
resolved_via,
};
let spans_template: Vec<Span> = stub_spans;
for &candidate in candidates {
graph.edges_mut().add_edge_with_spans(
callsite.caller,
candidate,
new_kind.clone(),
stub_file,
spans_template.clone(),
);
}
}
fn seed_fn_signature_from_bindings(graph: &mut CodeGraph) {
let pairs: Vec<(NodeId, StringId)> = {
let Some(tables) = graph.c_indirect_tables() else {
return;
};
let mut out = Vec::new();
for (key, entries) in &tables.bindings_by_field {
let Some(&sig) = tables.struct_field_fnptr.get(key) else {
continue;
};
for entry in entries {
out.push((entry.target_fn, sig));
}
}
out
};
if pairs.is_empty() {
return;
}
let Some(tables_mut) = graph.c_indirect_tables_mut().as_mut() else {
return;
};
for (node_id, sig) in pairs {
tables_mut.fn_signature.entry(node_id).or_insert(sig);
}
}
fn build_address_taken_signature_index(graph: &CodeGraph) -> HashMap<StringId, Vec<NodeId>> {
let mut out: HashMap<StringId, Vec<NodeId>> = HashMap::new();
let Some(tables) = graph.c_indirect_tables() else {
return out;
};
let metadata = graph.macro_metadata();
for (&node_id, &sig) in &tables.fn_signature {
if metadata.is_address_taken(node_id) {
out.entry(sig).or_default().push(node_id);
}
}
out
}
fn strip_struct_keyword_and_pointer(type_token: &str) -> &str {
let trimmed = type_token.trim();
let after_keyword = trimmed
.strip_prefix("struct ")
.or_else(|| trimmed.strip_prefix("union "))
.or_else(|| trimmed.strip_prefix("enum "))
.unwrap_or(trimmed);
after_keyword
.trim()
.trim_end_matches(|c: char| c == '*' || c.is_whitespace())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_struct_keyword_strips_struct_prefix() {
assert_eq!(strip_struct_keyword_and_pointer("struct ops"), "ops");
assert_eq!(
strip_struct_keyword_and_pointer("struct file_operations"),
"file_operations"
);
}
#[test]
fn strip_struct_keyword_strips_union_and_enum() {
assert_eq!(strip_struct_keyword_and_pointer("union variant"), "variant");
assert_eq!(strip_struct_keyword_and_pointer("enum color"), "color");
}
#[test]
fn strip_struct_keyword_no_op_on_bare_tag() {
assert_eq!(strip_struct_keyword_and_pointer("ops"), "ops");
}
#[test]
fn strip_struct_keyword_trims_trailing_pointer() {
assert_eq!(strip_struct_keyword_and_pointer("struct ops *"), "ops");
assert_eq!(strip_struct_keyword_and_pointer("struct ops **"), "ops");
}
#[test]
fn pass5b_stats_total_resolved_sums_binding_and_typematch() {
let s = Pass5bStats {
binding_resolved: 3,
typematch_resolved: 5,
cap_exceeded: 1,
stub_fallback: 2,
};
assert_eq!(s.total_resolved(), 8);
}
#[test]
fn resolve_c_indirect_calls_short_circuits_when_no_c() {
let mut graph = CodeGraph::new();
let stats = resolve_c_indirect_calls(&mut graph);
assert_eq!(stats, Pass5bStats::default());
}
}