Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions crates/tracedecay-code-index/src/chunks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2412,10 +2412,11 @@ fn resolve_file_references(
});
}
}
// Java overloads the call's arguments cannot tell apart (equal
// arity, a variadic tail) stay a disclosed caller gap.
candidates if candidates.is_empty() || language == "java" => {
if let Some(candidate) = cross_file_reference_candidate(
// No candidate: the reference is retained for cross-file
// resolution. Several it cannot choose between: a disclosed
// caller gap. Java overloads resolve through Java's own rules.
_ => {
if let Some(mut candidate) = cross_file_reference_candidate(
source,
offsets,
&references_by_site,
Expand All @@ -2434,12 +2435,10 @@ fn resolve_file_references(
candidate.reference_name.clone(),
)))
{
candidate.ambiguous_local = !compatible.is_empty() && language != "java";
retained.push(candidate);
}
}
// Same-file ambiguity: adding cross-file candidates can only make
// it more ambiguous, so the reference stays unresolved.
_ => {}
}
}
// The parser may describe one invocation both as a receiver expression
Expand Down Expand Up @@ -2539,6 +2538,7 @@ fn cross_file_reference_candidate(
.unwrap_or(from.span),
unmodeled_import: reference.unmodeled_import,
argument_count: reference.argument_count,
ambiguous_local: false,
})
}

Expand Down Expand Up @@ -5289,7 +5289,14 @@ pub fn real_symbol() {}
&[],
);
assert!(resolved.is_empty());
assert!(retained.is_empty());
assert_eq!(
retained
.iter()
.map(|reference| reference.reference_name.as_str())
.collect::<Vec<_>>(),
["Base"],
"an ambiguous bare name stays a retained reference, not a silent drop"
);
}

#[test]
Expand Down
4 changes: 4 additions & 0 deletions crates/tracedecay-code-index/src/chunks/artifacts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,10 @@ pub struct CodeIndexUnresolvedReferenceV1 {
/// Arguments the call site passes, where the extractor counts them.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub argument_count: Option<u32>,
/// The file defines more than one candidate the call site cannot choose
/// between, so it is a disclosed caller gap and never binds cross-file.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub ambiguous_local: bool,
}

impl CodeIndexUnresolvedReferenceV1 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ where
evidence_span: interface.span,
unmodeled_import: None,
argument_count: None,
ambiguous_local: false,
};
let methods = match expansion {
Ok(methods) => methods,
Expand Down
90 changes: 61 additions & 29 deletions crates/tracedecay-code-index/src/production/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ type EdgeEvidenceV1 = (
u64,
);

/// `files`' edge evidence resolved whole, with the interfaces whose
/// implementors the seal cannot decide.
/// `files`' edge evidence resolved whole, with the references the seal
/// cannot decide.
pub(crate) fn collect_edge_evidence<T>(
files: &[T],
) -> Result<EdgeEvidenceV1, CodeIndexProductionErrorV1>
Expand All @@ -327,11 +327,11 @@ where
// the edges this returns plus one sort buffer.
let CrossFileResolutionV1 {
edges,
implementor_gaps,
gaps,
ambiguous_name_drops,
} = resolve_cross_file_references(files)?;
let (edges, abstentions) = edge_evidence(files, edges);
Ok((edges, abstentions, implementor_gaps, ambiguous_name_drops))
Ok((edges, abstentions, gaps, ambiguous_name_drops))
}

/// `files`' edge evidence: each file's own edges and `cross_file`, the
Expand Down Expand Up @@ -398,7 +398,8 @@ pub(crate) type ReferenceSelectionV1 = [(usize, Vec<usize>)];

/// Resolves only `selection`'s references against the whole file set whose
/// symbols `by_simple_name` indexes. Each reference binds exactly as
/// [`resolve_cross_file_references`] binds it.
/// [`resolve_cross_file_references`] binds it; the result is the edges those
/// references contribute and the ambiguous calls among them.
#[tracing::instrument(name = "code_index.seal.resolve_selected", level = "trace", skip_all)]
pub(crate) fn resolve_selected_cross_file_references<T>(
files: &[T],
Expand All @@ -411,11 +412,13 @@ where
resolve_references(files, by_simple_name, Some(selection))
}

/// A whole-set resolution: the cross-file edges, and one row per interface
/// whose implementors the seal cannot decide.
/// A resolution: the cross-file edges, and the references the seal cannot
/// decide, each a disclosed gap: calls with more than one candidate, and, for
/// a whole-set pass, one row per interface whose implementors it cannot
/// decide.
pub(crate) struct CrossFileResolutionV1 {
pub(crate) edges: Vec<CanonicalRelationEdgeV1>,
pub(crate) implementor_gaps: Vec<CodeIndexUnresolvedReferenceV1>,
pub(crate) gaps: Vec<CodeIndexUnresolvedReferenceV1>,
pub(crate) ambiguous_name_drops: u64,
}

Expand Down Expand Up @@ -505,7 +508,7 @@ where
})?;
let ambiguous_name_drops = per_file
.iter()
.fold(0_u64, |total, (_, drops)| total.saturating_add(*drops));
.fold(0_u64, |total, (_, _, drops)| total.saturating_add(*drops));
// Satisfaction needs every Go method set, so only a whole-set pass
// decides it, and only a file set with Go types builds the module index.
let satisfaction = if selection.is_none()
Expand All @@ -521,12 +524,14 @@ where
let mut edges = Vec::with_capacity(
per_file
.iter()
.map(|(file_edges, _)| file_edges.len())
.map(|(edges, _, _)| edges.len())
.sum::<usize>()
.saturating_add(satisfaction.edges.len()),
);
for (file_edges, _) in per_file {
let mut gaps = satisfaction.gaps;
for (file_edges, file_gaps, _) in per_file {
edges.extend(file_edges);
gaps.extend(file_gaps);
}
edges.extend(satisfaction.edges);
{
Expand All @@ -538,7 +543,7 @@ where
};
Ok(CrossFileResolutionV1 {
edges,
implementor_gaps: satisfaction.gaps,
gaps,
ambiguous_name_drops,
})
}
Expand Down Expand Up @@ -677,7 +682,7 @@ where
}

/// Resolve one file's retained unresolved references against the whole staged
/// file set.
/// file set: the edges they bind, and the calls left ambiguous.
///
/// The memo is file-local on purpose. `ResolvedReferenceCacheV1` is keyed by
/// source-file index, so a shared map could never serve another file's entry.
Expand All @@ -689,7 +694,11 @@ fn resolve_one_file_cross_file_references<T>(
modules: &ResolutionModulesV1<'_, T>,
index: usize,
picks: Option<&[usize]>,
) -> (Vec<CanonicalRelationEdgeV1>, u64)
) -> (
Vec<CanonicalRelationEdgeV1>,
Vec<CodeIndexUnresolvedReferenceV1>,
u64,
)
where
T: ResolutionFileV1,
{
Expand All @@ -698,6 +707,7 @@ where
let same_file_binds = is_module_import_language(files[index].language());
let mut resolved_references = ResolvedReferenceCacheV1::new();
let mut edges = Vec::new();
let mut gaps = Vec::new();
let mut ambiguous_name_drops = 0_u64;
let references = &files[index].as_ref().artifacts.unresolved_references;
let every = picks.is_none().then(|| references.iter());
Expand All @@ -708,6 +718,7 @@ where
reference.reference_name.as_str(),
reference.kind,
reference.argument_count,
reference.ambiguous_local,
);
let (resolved, ambiguous) = if let Some(resolved) = resolved_references.get(&cache_key) {
resolved.clone()
Expand All @@ -732,8 +743,15 @@ where
if ambiguous {
ambiguous_name_drops = ambiguous_name_drops.saturating_add(1);
}
let Some((target_index, targets)) = resolved else {
continue;
let (target_index, targets) = match resolved {
Some(ReferenceResolutionV1::Bound(target_index, targets)) => (target_index, targets),
Some(ReferenceResolutionV1::Ambiguous) => {
if reference.kind == RelationEdgeKindV1::Calls {
gaps.push(reference.clone());
}
continue;
}
None => continue,
};
if target_index == index && !same_file_binds {
continue;
Expand All @@ -746,7 +764,7 @@ where
evidence_span: reference.evidence_span,
}));
}
(edges, ambiguous_name_drops)
(edges, gaps, ambiguous_name_drops)
}

#[cfg(test)]
Expand All @@ -760,30 +778,43 @@ pub(super) fn take_seal_reference_resolutions() -> usize {
}

type ResolvedReferenceCacheV1<'a> = HashMap<
(usize, &'a str, RelationEdgeKindV1, Option<u32>),
(Option<(usize, Vec<SymbolOccurrenceId>)>, bool),
(usize, &'a str, RelationEdgeKindV1, Option<u32>, bool),
(Option<ReferenceResolutionV1>, bool),
>;

/// What one retained reference resolves to; `None` beside it is a reference
/// with no cross-file binding.
#[derive(Clone)]
enum ReferenceResolutionV1 {
/// The targets, all in one file.
Bound(usize, Vec<SymbolOccurrenceId>),
/// More than one candidate the reference cannot choose between.
Ambiguous,
}

fn resolve_cross_file_reference<T>(
files: &[T],
by_simple_name: &dyn SymbolsByNameV1,
modules: &ResolutionModulesV1<'_, T>,
index: usize,
reference: &CodeIndexUnresolvedReferenceV1,
ambiguous: &mut bool,
) -> Option<(usize, Vec<SymbolOccurrenceId>)>
) -> Option<ReferenceResolutionV1>
where
T: ResolutionFileV1,
{
if reference.ambiguous_local {
return Some(ReferenceResolutionV1::Ambiguous);
}
let rust = &modules.rust;
let file = files[index].as_ref();
// These languages bind one exact module member through their own import
// and package rules, never by name matching.
if is_module_import_language(file.extraction.language.as_str()) {
return match modules.modules().call_outcome(index, reference)? {
ImportBindingOutcomeV1::Bound(target_index, symbol) => {
Some((target_index, vec![symbol.occurrence.clone()]))
}
ImportBindingOutcomeV1::Bound(target_index, symbol) => Some(
ReferenceResolutionV1::Bound(target_index, vec![symbol.occurrence.clone()]),
),
ImportBindingOutcomeV1::External
| ImportBindingOutcomeV1::Unresolved
| ImportBindingOutcomeV1::ValueMember => None,
Expand All @@ -810,9 +841,9 @@ where
)
{
return match outcome {
ImportBindingOutcomeV1::Bound(target_index, symbol) if target_index != index => {
Some((target_index, vec![symbol.occurrence.clone()]))
}
ImportBindingOutcomeV1::Bound(target_index, symbol) if target_index != index => Some(
ReferenceResolutionV1::Bound(target_index, vec![symbol.occurrence.clone()]),
),
ImportBindingOutcomeV1::Bound(..)
| ImportBindingOutcomeV1::External
| ImportBindingOutcomeV1::Unresolved
Expand Down Expand Up @@ -937,7 +968,8 @@ where
.collect::<Vec<_>>();
// A type-path call may match both an inherent `Type::method` and one or
// more `<Type as Trait>::method` aliases; Rust prefers the inherent, so
// keep a unique non-UFCS hit when aliases also matched.
// keep a unique non-UFCS hit when aliases also matched. Any other choice
// between candidates is ambiguous.
let compatible = match compatible.as_slice() {
[] => return None,
[_] => compatible,
Expand Down Expand Up @@ -965,7 +997,7 @@ where
[_] => inherent,
_ => {
*ambiguous = true;
return None;
return Some(ReferenceResolutionV1::Ambiguous);
}
}
}
Expand All @@ -974,7 +1006,7 @@ where
if *target_index == index {
return None;
}
Some((
Some(ReferenceResolutionV1::Bound(
*target_index,
compatible
.iter()
Expand Down
4 changes: 2 additions & 2 deletions crates/tracedecay-code-index/src/production/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2155,14 +2155,14 @@ where
let _span =
tracing::trace_span!("code_index.build.assemble.graph_outputs").entered();
{
let (edges, abstentions, implementor_gaps, ambiguous_name_drops) =
let (edges, abstentions, gaps, ambiguous_name_drops) =
collect_edge_evidence(&staged.files)?;
let mut unresolved = resolution_outputs::unresolved_calls_for_edges(
&staged.files,
&edges,
&|| Ok(()),
)?;
unresolved.extend(implementor_gaps);
unresolved.extend(gaps);
unresolved.sort();
unresolved.dedup();
Ok::<_, CodeIndexProductionErrorV1>((
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -556,3 +556,55 @@ fn ambiguity_census_preserves_unknown_parent_count() {
);
assert_eq!(answers(&store).statistics.ambiguous_name_drops, None);
}

#[test]
fn sparse_local_shadowing_preserves_gaps_and_the_generic_ambiguity_census() {
let clear =
"import { scale } from './lib';\nexport function clear(): number { return scale(1); }\n";
let shadowed = "import { scale } from './lib';\ndescribe('scope', () => {\n function scale(x: number) { return x; }\n function scale(x: number) { return x + 1; }\n it('shadowed', () => { scale(1); });\n});\nexport function clear(): number { return scale(1); }\n";
let tree = |app| {
vec![
("web/app.ts", "typescript", app),
(
"web/lib.ts",
"typescript",
"export function scale(x: number) { return x * 2; }\n",
),
("web/a.ts", "typescript", "export const a = 1;\n"),
("web/b.ts", "typescript", "export const b = 1;\n"),
("web/c.ts", "typescript", "export const c = 1;\n"),
("web/d.ts", "typescript", "export const d = 1;\n"),
("web/e.ts", "typescript", "export const e = 1;\n"),
("web/f.ts", "typescript", "export const f = 1;\n"),
("web/g.ts", "typescript", "export const g = 1;\n"),
]
};
assert_sparse_matches_cold(&tree(clear), &tree(shadowed));
assert_sparse_matches_cold(&tree(shadowed), &tree(clear));

let store = MemorySealedPublicationStoreV1::default();
let mut incremental = owner(&store);
publish(&mut incremental, &tree(clear), 1_100_000);
for (app, expected_gaps, sealed_at) in [(shadowed, 1, 1_200_000), (clear, 0, 1_300_000)] {
let successor = publish(&mut incremental, &tree(app), sealed_at);
assert_eq!(successor.cold_reason(), None);
let restored = answers(&store);
assert_eq!(restored.statistics.ambiguous_name_drops, Some(0));
assert_eq!(
restored
.unresolved_calls
.iter()
.filter(|call| call.ambiguous_local && call.reference_name == "scale")
.count(),
expected_gaps
);
assert_eq!(
restored
.edges
.iter()
.filter(|edge| edge.kind == RelationEdgeKindV1::Calls)
.count(),
1
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -640,7 +640,7 @@ pub(super) fn resolve_edit(
&|| Ok(()),
)
.map_err(|error| CodeIndexProductionErrorV1::Contract(error.to_string()))?;
for call in rederived {
for call in rederived.into_iter().chain(resolved.gaps) {
let owner = owner_of
.get(&call.from_occurrence)
.ok_or_else(|| contract("a re-derived call limitation leaves the selection"))?;
Expand Down
Loading
Loading