1use super::super::cargo_roots::{CargoRootCohort, repository_join};
3use super::{
4 BindingResolver, BindingSource, CallBindingResolution, CallBindings, CanonicalResolutionKey,
5 CauseWorkspace, CliError, EntitySelector, GraphEntity, IndexWorkStage, MAX_FORWARDING_DEPTH,
6 MAX_INCREMENTAL_RESOLUTION_ITEMS, ResolverScratch, STAGED_GRAPH_ROW_BYTES, SourceCallBinding,
7 SourceGraph, SourceSemanticFacts, document_file_resolution_key, invalid_graph_contract,
8 resolution_key_limit_failure,
9};
10use projectatlas_core::graph::{ExternalSelector, RepositoryFilePath};
11use projectatlas_symbols::{
12 SourceCargoTargetKind as TargetKind, SourceRustAnchor as Anchor, SourceRustBinding,
13 SourceRustPath, SourceRustUseRole as UseRole, SourceRustVisibility as Visibility,
14 cargo_target_inventory_key,
15};
16use std::collections::{BTreeMap, BTreeSet};
17use std::sync::Arc;
18
19#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
21struct Module {
22 root: String,
24 file: String,
26 inline: Arc<[String]>,
28 logical: Vec<String>,
30 directory: String,
32 visibility: Visibility,
34}
35
36fn module_bytes(module: &Module) -> u64 {
38 [
39 module.root.capacity(),
40 module.file.capacity(),
41 module.directory.capacity(),
42 ]
43 .into_iter()
44 .fold(STAGED_GRAPH_ROW_BYTES, |bytes, capacity| {
45 bytes.saturating_add(capacity as u64)
46 })
47 .saturating_add((module.logical.capacity() * std::mem::size_of::<String>()) as u64)
48 .saturating_add(
49 module
50 .logical
51 .iter()
52 .map(|part| part.capacity() as u64)
53 .sum::<u64>(),
54 )
55 .saturating_add(std::mem::size_of_val(module.inline.as_ref()) as u64)
56 .saturating_add(
57 module
58 .inline
59 .iter()
60 .map(|part| part.capacity() as u64)
61 .sum::<u64>(),
62 )
63}
64
65#[derive(Default)]
67struct Membership {
68 owner: Option<Module>,
70 causes: BTreeSet<CanonicalResolutionKey>,
72}
73
74#[derive(Default)]
76struct Slots {
77 values: BTreeMap<Arc<[String]>, BTreeMap<String, Vec<usize>>>,
79 modules: BTreeMap<Arc<[String]>, BTreeMap<String, Vec<usize>>>,
81 toolchain: BTreeMap<*const projectatlas_symbols::SourceRustUse, String>,
83 associated: BTreeMap<(Arc<[String]>, String, String), Vec<usize>>,
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
89enum Namespace {
90 Value,
92 Module,
94}
95
96enum Selection {
98 Absent,
100 Value(GraphEntity, Visibility, Vec<String>),
102 Module(Module),
104 Type {
106 owner: Module,
108 name: String,
110 visibility: Visibility,
112 },
113}
114
115#[derive(Clone, Eq, PartialEq, Ord, PartialOrd)]
117struct Request {
118 source: String,
120 module: Arc<[String]>,
122 path: SourceRustPath,
124 namespace: bool,
126 import: bool,
128 toolchain: bool,
130}
131
132struct RootCandidate {
134 path: String,
136 certain: bool,
138 cohort: Option<Arc<CargoRootCohort>>,
140}
141
142#[derive(Default)]
144struct RootDirectoryCandidates {
145 causes: BTreeSet<CanonicalResolutionKey>,
147 roots: Vec<usize>,
149}
150
151type RootModuleCandidates = BTreeMap<String, Vec<usize>>;
153
154struct RootMembershipIndex {
156 roots: Arc<[RootCandidate]>,
158 directories: BTreeMap<String, RootDirectoryCandidates>,
160}
161
162struct RustResolver<'r, 'a, 'g, 'n> {
164 shared: &'r mut BindingResolver<'a, 'g, 'n>,
166 memberships: BTreeMap<String, Arc<Membership>>,
168 roots: BTreeMap<String, Arc<[RootCandidate]>>,
170 root_memberships: BTreeMap<String, Arc<RootMembershipIndex>>,
172 root_modules: BTreeMap<String, BTreeMap<String, Arc<RootModuleCandidates>>>,
174 slots: BTreeMap<String, Slots>,
176 queries: BTreeMap<Request, Arc<CallBindingResolution>>,
178 ambiguous: u32,
180 candidate_queries: BTreeMap<
182 (
183 String,
184 *const [(usize, usize)],
185 *const [usize],
186 Option<String>,
187 ),
188 Arc<CallBindingResolution>,
189 >,
190}
191
192pub(super) fn resolve_rust_bindings<'g>(
194 shared: &mut BindingResolver<'_, 'g, '_>,
195 graphs: &[&SourceGraph<'g>],
196 bindings: &mut CallBindings,
197) -> Result<(), CliError> {
198 let mut resolver = RustResolver {
199 shared,
200 memberships: BTreeMap::new(),
201 roots: BTreeMap::new(),
202 root_memberships: BTreeMap::new(),
203 root_modules: BTreeMap::new(),
204 slots: BTreeMap::new(),
205 queries: BTreeMap::new(),
206 ambiguous: 0,
207 candidate_queries: BTreeMap::new(),
208 };
209 for graph in graphs {
210 resolver.shared.check_budget()?;
211 let raw = graph.graph();
212 let Some(facts) = graph
213 .semantics()
214 .filter(|facts| facts.rust().is_some_and(|rust| rust.complete))
215 else {
216 continue;
217 };
218 if resolver.shared.packages.package_owner(&raw.path).is_none() {
219 continue;
220 }
221 resolver.with_scratch(|resolver, scratch| {
222 let candidate_index_bytes = (facts.rust().map_or(0, |rust| rust.candidate_calls.len())
223 as u64)
224 .saturating_mul(STAGED_GRAPH_ROW_BYTES);
225 scratch.reserve(resolver.shared, candidate_index_bytes)?;
226 let candidate_calls = facts
227 .rust()
228 .into_iter()
229 .flat_map(|rust| &rust.candidate_calls)
230 .map(|call| (call.relation, call))
231 .collect::<BTreeMap<_, _>>();
232 for call in facts.calls() {
233 resolver.shared.check_budget()?;
234 if let Some(candidates) = candidate_calls.get(&call.relation) {
235 let outcome = resolver.resolve_candidates(&raw.path, facts, candidates)?;
236 resolver
237 .charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
238 bindings
239 .entry(raw.path.clone())
240 .or_default()
241 .insert(call.relation, outcome);
242 continue;
243 }
244 let (module, path, namespace) = match &call.binding {
245 SourceCallBinding::Imported {
246 import,
247 member,
248 rust_toolchain: None,
249 ..
250 } => {
251 let Some(binding) = facts
252 .imports()
253 .get(*import)
254 .and_then(|import| import.rust.as_deref())
255 else {
256 continue;
257 };
258 if !binding.supported || binding.role == UseRole::Wildcard {
259 continue;
260 }
261 let mut path = binding.path.clone();
262 if let Some(member) = member {
263 path.segments
264 .extend(member.split("::").map(ToString::to_string));
265 }
266 (
267 Arc::clone(&binding.module),
268 path,
269 binding.role == UseRole::SelfNamespace && member.is_none(),
270 )
271 }
272 SourceCallBinding::RustPath(call) => {
273 (Arc::clone(&call.module), call.path.clone(), false)
274 }
275 _ => continue,
276 };
277 let outcome = resolver.resolve(Request {
279 source: raw.path.clone(),
280 module,
281 path,
282 namespace,
283 import: false,
284 toolchain: false,
285 })?;
286 resolver.charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
287 bindings
288 .entry(raw.path.clone())
289 .or_default()
290 .insert(call.relation, outcome);
291 }
292 for import in facts.imports() {
293 resolver.shared.check_budget()?;
294 let Some(binding) = import.rust.as_deref() else {
295 continue;
296 };
297 if !binding.supported || import.rust_toolchain.is_some() {
298 continue;
299 }
300 let outcome = resolver.resolve(Request {
301 source: raw.path.clone(),
302 module: Arc::clone(&binding.module),
303 path: binding.path.clone(),
304 namespace: binding.role != UseRole::Item,
305 import: true,
306 toolchain: false,
307 })?;
308 resolver.charge(STAGED_GRAPH_ROW_BYTES.saturating_add(raw.path.len() as u64))?;
309 bindings
310 .entry(raw.path.clone())
311 .or_default()
312 .insert(import.relation, outcome);
313 }
314 Ok(((), 0))
315 })?;
316 }
317 Ok(())
318}
319
320impl<'g> RustResolver<'_, '_, 'g, '_> {
321 fn charge(&mut self, bytes: u64) -> Result<(), CliError> {
323 self.shared.reserve_scratch(bytes)
324 }
325
326 fn with_scratch<T>(
328 &mut self,
329 operation: impl FnOnce(&mut Self, &mut ResolverScratch) -> Result<(T, u64), CliError>,
330 ) -> Result<T, CliError> {
331 let mut scratch = ResolverScratch::default();
332 let result = operation(self, &mut scratch);
333 self.shared.release_scratch(scratch.retained_bytes)?;
334 let (value, retained_bytes) = result?;
335 self.shared.reserve_scratch(retained_bytes)?;
336 Ok(value)
337 }
338
339 fn insert_cause(
341 &mut self,
342 causes: &mut CauseWorkspace<'_>,
343 key: &CanonicalResolutionKey,
344 ) -> Result<(), CliError> {
345 self.shared.insert_cause(causes, key)
346 }
347
348 fn cause(&mut self, path: &str, causes: &mut CauseWorkspace<'_>) -> Result<bool, CliError> {
350 let key = document_file_resolution_key(self.shared.project, path)?;
351 self.insert_cause(causes, &key)?;
352 Ok(self.shared.sources.nodes.contains_key(path))
353 }
354
355 fn insert_root(
357 &mut self,
358 scratch: &mut ResolverScratch,
359 roots: &mut BTreeMap<String, bool>,
360 path: String,
361 certain: bool,
362 ) -> Result<(), CliError> {
363 self.shared.check_budget()?;
364 if let Some(existing) = roots.get_mut(&path) {
365 *existing &= certain;
366 return Ok(());
367 }
368 if roots.len() >= MAX_INCREMENTAL_RESOLUTION_ITEMS as usize {
369 return Err(resolution_key_limit_failure(roots.len().saturating_add(1)));
370 }
371 scratch.reserve(
372 self.shared,
373 STAGED_GRAPH_ROW_BYTES.saturating_add(path.capacity() as u64),
374 )?;
375 roots.insert(path, certain);
376 Ok(())
377 }
378
379 fn source(&mut self, path: &str) -> Result<Option<Arc<BindingSource<'g>>>, CliError> {
381 let source = self.shared.source(path)?;
382 let Some(facts) = source
383 .source
384 .semantics()
385 .and_then(SourceSemanticFacts::rust)
386 .filter(|facts| facts.complete)
387 else {
388 return Ok(None);
389 };
390 if !self.slots.contains_key(path) {
391 let slots = self.with_scratch(|this, scratch| {
392 let mut slots = Slots::default();
393 let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(path.len() as u64);
394 scratch.reserve(this.shared, bytes)?;
395 for (ordinal, declaration) in facts.declarations.iter().enumerate() {
396 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
397 scratch.reserve(
398 this.shared,
399 STAGED_GRAPH_ROW_BYTES
400 .saturating_mul(4)
401 .saturating_add(declaration.name.len() as u64),
402 )?;
403 slots
404 .values
405 .entry(Arc::clone(&declaration.module))
406 .or_default()
407 .entry(declaration.name.clone())
408 .or_default()
409 .push(ordinal);
410 bytes = bytes
411 .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
412 .saturating_add(declaration.name.len() as u64);
413 }
414 for (ordinal, module) in facts.modules.iter().enumerate() {
415 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
416 scratch.reserve(
417 this.shared,
418 STAGED_GRAPH_ROW_BYTES
419 .saturating_mul(4)
420 .saturating_add(module.name.len() as u64),
421 )?;
422 slots
423 .modules
424 .entry(Arc::clone(&module.parent))
425 .or_default()
426 .entry(module.name.clone())
427 .or_default()
428 .push(ordinal);
429 bytes = bytes
430 .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
431 .saturating_add(module.name.len() as u64);
432 }
433 for (ordinal, method) in facts.associated.iter().enumerate() {
434 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
435 scratch.reserve(
436 this.shared,
437 STAGED_GRAPH_ROW_BYTES
438 .saturating_mul(4)
439 .saturating_add(method.owner.len() as u64)
440 .saturating_add(method.name.len() as u64),
441 )?;
442 slots
443 .associated
444 .entry((
445 Arc::clone(&method.module),
446 method.owner.clone(),
447 method.name.clone(),
448 ))
449 .or_default()
450 .push(ordinal);
451 bytes = bytes
452 .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
453 .saturating_add(method.owner.len() as u64)
454 .saturating_add(method.name.len() as u64);
455 }
456 if let Some(facts) = source.source.semantics() {
457 for import in facts.imports() {
458 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
459 if let (Some(binding), Some(reference)) =
460 (&import.rust, &import.rust_toolchain)
461 && let Some(identity) = super::super::rust_import_identity(reference)
462 {
463 scratch.reserve(
464 this.shared,
465 STAGED_GRAPH_ROW_BYTES
466 .saturating_mul(4)
467 .saturating_add(identity.capacity() as u64),
468 )?;
469 bytes = bytes
470 .saturating_add(STAGED_GRAPH_ROW_BYTES * 4)
471 .saturating_add(identity.capacity() as u64);
472 slots.toolchain.insert(Arc::as_ptr(binding), identity);
473 }
474 }
475 }
476 Ok((slots, bytes))
477 })?;
478 self.slots.insert(path.to_string(), slots);
479 }
480 Ok(Some(source))
481 }
482
483 fn roots(
485 &mut self,
486 path: &str,
487 causes: &mut CauseWorkspace<'_>,
488 ) -> Result<Arc<[RootCandidate]>, CliError> {
489 let Some(package) = self.shared.packages.package_owner(path) else {
490 return Ok(Arc::from([]));
491 };
492 let manifest = package.manifest.clone();
493 let directory = package.root.clone();
494 let package_name = package.name.clone();
495 let cohort_index = package.cohorts.as_ref().map(Arc::clone);
496 if !self.cause(&manifest, causes)? {
497 return Ok(Arc::from([]));
498 }
499 let source = self.shared.source(&manifest)?;
500 let Some(targets) = source
501 .source
502 .semantics()
503 .and_then(SourceSemanticFacts::cargo)
504 .filter(|targets| targets.complete)
505 else {
506 return Ok(Arc::from([]));
507 };
508 for auto in &targets.automatic {
509 if let Some(relative) = auto.kind.conventional_root() {
510 self.cause(&join(&directory, relative), causes)?;
511 }
512 if let Some(relative) = auto.kind.automatic_directory() {
513 let target_directory = join(&directory, relative);
514 let prefix = format!("{target_directory}/");
515 if let Some(relative) = path.strip_prefix(&prefix) {
516 let key = cargo_target_inventory_key(
517 self.shared.project,
518 &package_name,
519 &manifest,
520 &target_directory,
521 )
522 .map_err(invalid_graph_contract)?;
523 self.insert_cause(causes, &key)?;
524 if let Some(component) = relative
525 .split('/')
526 .next()
527 .filter(|part| part.strip_suffix(".rs").is_none())
528 {
529 self.cause(&format!("{target_directory}/{component}/main.rs"), causes)?;
530 }
531 }
532 }
533 }
534 if let Some(roots) = self.roots.get(&manifest) {
535 return Ok(Arc::clone(roots));
536 }
537 let roots = self.with_scratch(|this, scratch| {
538 scratch.reserve(
539 this.shared,
540 (targets.targets.len() as u64).saturating_mul(STAGED_GRAPH_ROW_BYTES),
541 )?;
542 let overrides = targets
543 .targets
544 .iter()
545 .filter_map(|target| {
546 target
547 .name
548 .as_ref()
549 .map(|name| (target.kind, name.as_str()))
550 })
551 .collect::<BTreeSet<_>>();
552 let mut roots = BTreeMap::<String, bool>::new();
553 for target in &targets.targets {
554 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
555 if let Some(path) = repository_join(&directory, &target.path) {
556 this.insert_root(scratch, &mut roots, path, true)?;
557 }
558 }
559 for auto in &targets.automatic {
560 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
561 if let Some(path) = auto.kind.conventional_root() {
562 let overridden = auto.kind == TargetKind::Binary
563 && overrides.contains(&(auto.kind, package_name.as_str()));
564 if !overridden {
565 this.insert_root(
566 scratch,
567 &mut roots,
568 join(&directory, path),
569 auto.certain,
570 )?;
571 }
572 }
573 let Some(relative) = auto.kind.automatic_directory() else {
574 continue;
575 };
576 let prefix = format!("{}/", join(&directory, relative));
577 let nodes = Arc::clone(&this.shared.sources.nodes);
578 for (path, _) in nodes.range(prefix.as_str()..) {
579 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
580 let Some(relative) = path.strip_prefix(&prefix) else {
581 break;
582 };
583 let name = relative
584 .strip_suffix(".rs")
585 .filter(|name| !name.contains('/'))
586 .or_else(|| {
587 relative
588 .strip_suffix("/main.rs")
589 .filter(|name| !name.contains('/'))
590 });
591 let Some(name) = name.filter(|name| !name.is_empty()) else {
592 continue;
593 };
594 if overrides.contains(&(auto.kind, name)) {
595 continue;
596 }
597 this.insert_root(scratch, &mut roots, (*path).to_string(), auto.certain)?;
598 }
599 }
600 let mut parent_counts = BTreeMap::<String, usize>::new();
601 for path in roots.keys() {
602 this.shared.control.check(IndexWorkStage::SymbolParsing)?;
603 let directory = parent(path);
604 if let Some(count) = parent_counts.get_mut(directory) {
605 *count += 1;
606 } else {
607 scratch.reserve(
608 this.shared,
609 STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64),
610 )?;
611 parent_counts.insert(directory.to_string(), 1);
612 }
613 }
614 let roots: Arc<[RootCandidate]> = roots
615 .into_iter()
616 .map(|(path, certain)| {
617 let cohort = if parent_counts
618 .get(parent(&path))
619 .is_some_and(|count| *count > 1)
620 {
621 cohort_index
622 .as_ref()
623 .and_then(|index| index.candidate(&path, &directory))
624 } else {
625 None
626 };
627 RootCandidate {
628 path,
629 certain,
630 cohort,
631 }
632 })
633 .collect::<Vec<_>>()
634 .into();
635 let retained_bytes = roots.iter().fold(
636 STAGED_GRAPH_ROW_BYTES.saturating_add(manifest.capacity() as u64),
637 |bytes, root| {
638 bytes
639 .saturating_add(STAGED_GRAPH_ROW_BYTES)
640 .saturating_add(root.path.capacity() as u64)
641 },
642 );
643 Ok((roots, retained_bytes))
644 })?;
645 self.roots.insert(manifest, Arc::clone(&roots));
646 Ok(roots)
647 }
648
649 fn root_membership_index(
651 &mut self,
652 path: &str,
653 roots: Arc<[RootCandidate]>,
654 ) -> Result<Arc<RootMembershipIndex>, CliError> {
655 let manifest = self
656 .shared
657 .packages
658 .package_owner(path)
659 .map(|package| package.manifest.as_str())
660 .unwrap_or_default();
661 if let Some(index) = self.root_memberships.get(manifest) {
662 return Ok(Arc::clone(index));
663 }
664 let manifest = manifest.to_string();
665 let index = self.with_scratch(|this, scratch| {
666 let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(manifest.len() as u64);
667 scratch.reserve(this.shared, bytes)?;
668 let mut directories = BTreeMap::<String, RootDirectoryCandidates>::new();
669 for (ordinal, candidate) in roots.iter().enumerate() {
670 this.shared.check_budget()?;
671 let directory = parent(&candidate.path);
672 if !directories.contains_key(directory) {
673 let extra = STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64);
674 scratch.reserve(this.shared, extra)?;
675 bytes = bytes.saturating_add(extra);
676 directories.insert(directory.to_string(), RootDirectoryCandidates::default());
677 }
678 let Some(entry) = directories.get_mut(directory) else {
679 return Err(CliError::InvalidInput(
680 "root directory index disappeared".to_string(),
681 ));
682 };
683 let key = if let Some(cohort) = &candidate.cohort {
684 &cohort.key
685 } else {
686 &document_file_resolution_key(this.shared.project, &candidate.path)?
687 };
688 if !entry.causes.contains(key) {
689 let extra = STAGED_GRAPH_ROW_BYTES
690 .saturating_add(key.canonical_identity().len() as u64);
691 scratch.reserve(this.shared, extra)?;
692 bytes = bytes.saturating_add(extra);
693 entry.causes.insert(key.clone());
694 }
695 scratch.reserve(this.shared, STAGED_GRAPH_ROW_BYTES)?;
697 bytes = bytes.saturating_add(STAGED_GRAPH_ROW_BYTES);
698 entry.roots.push(ordinal);
699 }
700 Ok((RootMembershipIndex { roots, directories }, bytes))
701 })?;
702 let index = Arc::new(index);
703 self.root_memberships.insert(manifest, Arc::clone(&index));
704 Ok(index)
705 }
706
707 fn root_directory_modules(
709 &mut self,
710 path: &str,
711 index: &RootMembershipIndex,
712 directory: &str,
713 candidates: &RootDirectoryCandidates,
714 ) -> Result<Arc<RootModuleCandidates>, CliError> {
715 let manifest = self
716 .shared
717 .packages
718 .package_owner(path)
719 .map(|package| package.manifest.as_str())
720 .unwrap_or_default();
721 if let Some(modules) = self
722 .root_modules
723 .get(manifest)
724 .and_then(|rows| rows.get(directory))
725 {
726 return Ok(Arc::clone(modules));
727 }
728 let manifest = manifest.to_string();
729 let modules = self.with_scratch(|this, scratch| {
730 let mut bytes = STAGED_GRAPH_ROW_BYTES.saturating_add(directory.len() as u64);
731 if !this.root_modules.contains_key(&manifest) {
732 bytes = bytes
733 .saturating_add(STAGED_GRAPH_ROW_BYTES)
734 .saturating_add(manifest.len() as u64);
735 }
736 scratch.reserve(this.shared, bytes)?;
737 let mut selected = RootModuleCandidates::new();
738 for ordinal in &candidates.roots {
739 this.shared.check_budget()?;
740 let root = &index.roots[*ordinal].path;
741 if !this.shared.sources.nodes.contains_key(root.as_str())
742 || this.source(root)?.is_none()
743 {
744 continue;
745 }
746 let Some(modules) = this
747 .slots
748 .get(root)
749 .and_then(|slots| slots.modules.get::<[String]>(&[]))
750 else {
751 continue;
752 };
753 for name in modules.keys() {
754 this.shared.check_budget()?;
755 let extra =
757 STAGED_GRAPH_ROW_BYTES.saturating_add(if selected.contains_key(name) {
758 0
759 } else {
760 STAGED_GRAPH_ROW_BYTES.saturating_add(name.len() as u64)
761 });
762 scratch.reserve(this.shared, extra)?;
763 bytes = bytes.saturating_add(extra);
764 selected.entry(name.clone()).or_default().push(*ordinal);
765 }
766 }
767 Ok((selected, bytes))
768 })?;
769 let modules = Arc::new(modules);
770 self.root_modules
771 .entry(manifest)
772 .or_default()
773 .insert(directory.to_string(), Arc::clone(&modules));
774 Ok(modules)
775 }
776
777 fn membership(&mut self, path: &str) -> Result<Arc<Membership>, CliError> {
779 if let Some(result) = self.memberships.get(path) {
780 return Ok(Arc::clone(result));
781 }
782 let result = self.with_scratch(|this, scratch| {
783 let mut causes = CauseWorkspace::new(scratch);
784 let mut selected_owner = None;
785 let roots = this.roots(path, &mut causes)?;
786 let index = this.root_membership_index(path, roots)?;
787 let mut owner = None;
788 let mut count = 0;
789 let direct = index
790 .roots
791 .binary_search_by(|candidate| candidate.path.as_str().cmp(path))
792 .ok();
793 let mut directory = parent(path);
794 loop {
795 this.shared.check_budget()?;
796 if let Some(entry) = index.directories.get(directory)
797 && let Some(parts) = candidate_path(directory, path)
798 {
799 for key in &entry.causes {
800 this.insert_cause(&mut causes, key)?;
801 }
802 let direct =
803 direct.filter(|ordinal| parent(&index.roots[*ordinal].path) == directory);
804 let modules = this.root_directory_modules(path, &index, directory, entry)?;
805 let declared = parts.first().and_then(|name| modules.get(name));
806 for ordinal in direct.into_iter().chain(
807 declared
808 .into_iter()
809 .flatten()
810 .copied()
811 .filter(|ordinal| Some(*ordinal) != direct),
812 ) {
813 this.shared.check_budget()?;
814 let candidate = &index.roots[ordinal];
815 let root = &candidate.path;
816 if !this.cause(root, &mut causes)? {
817 continue;
818 }
819 let mut module = Module {
820 root: root.clone(),
821 file: root.clone(),
822 inline: Arc::from([]),
823 logical: Vec::new(),
824 directory: directory.to_string(),
825 visibility: Visibility::Public,
826 };
827 if module.file != path {
828 for part in &parts {
829 let Some(child) =
830 this.declared_module(&module, part, &mut causes)?
831 else {
832 break;
833 };
834 module = child;
835 if module.file == path {
836 break;
837 }
838 }
839 }
840 if module.file == path && module.inline.is_empty() {
841 count += 1;
842 if candidate.certain {
843 owner = Some(module);
844 }
845 }
846 }
847 }
848 if directory.is_empty() {
849 break;
850 }
851 directory = parent(directory);
852 }
853 if count == 1 {
854 selected_owner = owner;
855 }
856 let result = Membership {
857 owner: selected_owner,
858 causes: causes.keys,
859 };
860 let cause_bytes = result.causes.iter().fold(0_u64, |bytes, key| {
861 bytes
862 .saturating_add(STAGED_GRAPH_ROW_BYTES)
863 .saturating_add(key.canonical_identity().len() as u64)
864 });
865 let retained_bytes = STAGED_GRAPH_ROW_BYTES
866 .saturating_add(path.len() as u64)
867 .saturating_add(cause_bytes)
868 .saturating_add(result.owner.as_ref().map_or(0, module_bytes));
869 Ok((result, retained_bytes))
870 })?;
871 let result = Arc::new(result);
872 self.memberships
873 .insert(path.to_string(), Arc::clone(&result));
874 Ok(result)
875 }
876
877 fn declared_module(
879 &mut self,
880 owner: &Module,
881 name: &str,
882 causes: &mut CauseWorkspace<'_>,
883 ) -> Result<Option<Module>, CliError> {
884 if !self.cause(&owner.file, causes)? {
885 return Ok(None);
886 }
887 let Some(source) = self.source(&owner.file)? else {
888 return Ok(None);
889 };
890 let Some(indices) = self
891 .slots
892 .get(&owner.file)
893 .and_then(|slots| slots.modules.get(&owner.inline))
894 .and_then(|slots| slots.get(name))
895 else {
896 return Ok(None);
897 };
898 if indices.len() != 1 {
899 return Ok(None);
900 }
901 let Some(module) = source
902 .source
903 .semantics()
904 .and_then(SourceSemanticFacts::rust)
905 .and_then(|facts| facts.modules.get(indices[0]))
906 else {
907 return Ok(None);
908 };
909 if !module.supported {
910 return Ok(None);
911 }
912 let mut child = owner.clone();
913 child.visibility = module.visibility;
914 child.logical.push(name.to_string());
915 child.directory = join(&owner.directory, name);
916 if module.body.is_some() {
917 let mut inline = owner.inline.to_vec();
918 inline.push(name.to_string());
919 child.inline = inline.into();
920 } else {
921 let direct = join(&owner.directory, &format!("{name}.rs"));
922 let nested = join(&owner.directory, &format!("{name}/mod.rs"));
923 child.file = match (self.cause(&direct, causes)?, self.cause(&nested, causes)?) {
924 (true, false) => direct,
925 (false, true) => nested,
926 _ => return Ok(None),
927 };
928 child.inline = Arc::from([]);
929 }
930 Ok(Some(child))
931 }
932
933 fn address(
935 &mut self,
936 owner: &Module,
937 logical: &[String],
938 causes: &mut CauseWorkspace<'_>,
939 ) -> Result<Option<Module>, CliError> {
940 let mut module = Module {
941 root: owner.root.clone(),
942 file: owner.root.clone(),
943 inline: Arc::from([]),
944 logical: Vec::new(),
945 directory: parent(&owner.root).to_string(),
946 visibility: Visibility::Public,
947 };
948 for name in logical {
949 let Some(child) = self.declared_module(&module, name, causes)? else {
950 return Ok(None);
951 };
952 module = child;
953 }
954 Ok(Some(module))
955 }
956
957 fn resolve_candidates(
959 &mut self,
960 source: &str,
961 facts: &SourceSemanticFacts,
962 call: &projectatlas_symbols::SourceRustCallImports,
963 ) -> Result<Arc<CallBindingResolution>, CliError> {
964 let key = (
965 source.to_string(),
966 Arc::as_ptr(&call.bindings),
967 Arc::as_ptr(&call.declarations),
968 call.member.clone(),
969 );
970 if let Some(result) = self.candidate_queries.get(&key) {
971 return Ok(Arc::clone(result));
972 }
973 let result = self.with_scratch(|this, scratch| {
974 let mut causes = CauseWorkspace::new(scratch);
975 let mut result = CallBindingResolution {
976 followed_export: true,
977 ..CallBindingResolution::default()
978 };
979 let mut count = 0_u32;
980 if call.member.is_none() && !call.declarations.is_empty() {
981 let owner = this.shared.source(source)?;
982 for ordinal in call.declarations.iter() {
983 this.shared.check_budget()?;
984 count += 1;
985 result.target = this.shared.target(&owner, *ordinal)?;
986 }
987 }
988 for (import, reference) in call.bindings.iter() {
989 this.shared.check_budget()?;
990 let Some(import) = facts
991 .imports()
992 .get(*import)
993 .filter(|import| import.references.get(*reference).is_some())
994 else {
995 count += 1;
996 continue;
997 };
998 let Some(binding) = import
999 .rust
1000 .as_deref()
1001 .filter(|binding| binding.supported && binding.role != UseRole::Wildcard)
1002 else {
1003 count += 1;
1004 continue;
1005 };
1006 let mut path = binding.path.clone();
1007 if let Some(member) = &call.member {
1008 path.segments
1009 .extend(member.split("::").map(ToString::to_string));
1010 }
1011 let outcome = this.resolve(Request {
1012 source: source.to_string(),
1013 module: Arc::clone(&binding.module),
1014 path,
1015 namespace: binding.role == UseRole::SelfNamespace && call.member.is_none(),
1016 import: false,
1017 toolchain: import.rust_toolchain.is_some(),
1018 })?;
1019 for key in &outcome.dependencies {
1020 this.insert_cause(&mut causes, key)?;
1021 }
1022 if outcome.absent {
1023 continue;
1024 }
1025 count += 1;
1026 result.target.clone_from(&outcome.target);
1027 result.ambiguous = result.ambiguous.max(outcome.ambiguous);
1028 }
1029 let source_key = document_file_resolution_key(this.shared.project, source)?;
1030 this.insert_cause(&mut causes, &source_key)?;
1031 if count > 1 {
1032 result.ambiguous = result.ambiguous.max(std::num::NonZeroU32::new(count));
1033 result.target = None;
1034 }
1035 result.absent = count == 0;
1036 result.dependencies = causes.keys;
1037 let retained_bytes = result
1038 .retained_bytes()
1039 .saturating_add(STAGED_GRAPH_ROW_BYTES * 2)
1040 .saturating_add(key.0.capacity() as u64)
1041 .saturating_add(key.3.as_ref().map_or(0, String::capacity) as u64);
1042 Ok((result, retained_bytes))
1043 })?;
1044 let result = Arc::new(result);
1045 self.candidate_queries.insert(key, Arc::clone(&result));
1046 Ok(result)
1047 }
1048
1049 fn resolve(&mut self, request: Request) -> Result<Arc<CallBindingResolution>, CliError> {
1051 if let Some(result) = self.queries.get(&request) {
1052 return Ok(Arc::clone(result));
1053 }
1054 self.ambiguous = 0;
1055 let membership = if request.toolchain {
1056 None
1057 } else {
1058 Some(self.membership(&request.source)?)
1059 };
1060 let result = self.with_scratch(|this, scratch| {
1061 let mut causes = CauseWorkspace::new(scratch);
1062 let mut result = CallBindingResolution {
1063 followed_export: true,
1064 absent: !request.toolchain && request.namespace && !request.import,
1065 ..CallBindingResolution::default()
1066 };
1067 if request.toolchain {
1068 result.target = Some(this.external(&request.path.segments.join("::"))?);
1069 let key = document_file_resolution_key(this.shared.project, &request.source)?;
1070 this.insert_cause(&mut causes, &key)?;
1071 }
1072 if let Some(membership) = &membership {
1073 for key in &membership.causes {
1074 this.insert_cause(&mut causes, key)?;
1075 }
1076 }
1077 if let Some(owner) = membership
1078 .as_ref()
1079 .and_then(|membership| membership.owner.as_ref())
1080 {
1081 let mut logical = owner.logical.clone();
1082 logical.extend(request.module.iter().cloned());
1083 if let Some(module) = this.address(owner, &logical, &mut causes)? {
1084 let mut visited = BTreeSet::new();
1085 if request.import {
1086 let namespace = this.path(
1087 &module,
1088 &request.path,
1089 Namespace::Module,
1090 &logical,
1091 &mut causes,
1092 &mut visited,
1093 0,
1094 )?;
1095 let value = if request.namespace {
1096 None
1097 } else {
1098 this.path(
1099 &module,
1100 &request.path,
1101 Namespace::Value,
1102 &logical,
1103 &mut causes,
1104 &mut visited,
1105 0,
1106 )?
1107 };
1108 result.target = this.import_target(namespace, value)?;
1109 } else if !request.namespace {
1110 match this.path(
1111 &module,
1112 &request.path,
1113 Namespace::Value,
1114 &logical,
1115 &mut causes,
1116 &mut visited,
1117 0,
1118 )? {
1119 Some(Selection::Value(entity, _, _)) => result.target = Some(entity),
1120 Some(Selection::Absent) => result.absent = true,
1121 _ => {}
1122 }
1123 }
1124 }
1125 }
1126 if result.target.is_none() && this.ambiguous > 1 {
1127 result.ambiguous = std::num::NonZeroU32::new(this.ambiguous);
1128 }
1129 result.dependencies = causes.keys;
1130 let retained_bytes = result
1131 .retained_bytes()
1132 .saturating_add(STAGED_GRAPH_ROW_BYTES)
1133 .saturating_add(request.source.capacity() as u64)
1134 .saturating_add(
1135 (request.path.segments.capacity() * std::mem::size_of::<String>()) as u64,
1136 )
1137 .saturating_add(request.path.segments.iter().fold(0_u64, |bytes, part| {
1138 bytes.saturating_add(part.capacity() as u64)
1139 }));
1140 Ok((result, retained_bytes))
1141 })?;
1142 let result = Arc::new(result);
1143 self.queries.insert(request, Arc::clone(&result));
1144 Ok(result)
1145 }
1146
1147 fn import_target(
1149 &mut self,
1150 namespace: Option<Selection>,
1151 value: Option<Selection>,
1152 ) -> Result<Option<GraphEntity>, CliError> {
1153 let target = |selection: Selection| -> Result<Option<GraphEntity>, CliError> {
1154 let file = match selection {
1155 Selection::Absent => return Ok(None),
1156 Selection::Module(module) | Selection::Type { owner: module, .. } => module.file,
1157 Selection::Value(entity, _, _) => match entity.selector() {
1158 EntitySelector::External { .. } => return Ok(Some(entity)),
1159 EntitySelector::Symbol { symbol } => symbol.file.as_str().to_string(),
1160 _ => return Ok(None),
1161 },
1162 };
1163 Ok(Some(
1164 GraphEntity::new(
1165 self.shared.project,
1166 EntitySelector::File {
1167 path: RepositoryFilePath::new(std::path::Path::new(&file))
1168 .map_err(invalid_graph_contract)?,
1169 },
1170 self.shared.generation,
1171 )
1172 .map_err(invalid_graph_contract)?,
1173 ))
1174 };
1175 let namespace = namespace.map(target).transpose()?.flatten();
1176 let value = value.map(target).transpose()?.flatten();
1177 match (namespace, value) {
1178 (Some(left), Some(right)) if left.key() != right.key() => {
1179 self.ambiguous = self.ambiguous.max(2);
1180 Ok(None)
1181 }
1182 (Some(target), _) | (_, Some(target)) => Ok(Some(target)),
1183 (None, None) => Ok(None),
1184 }
1185 }
1186
1187 fn external(&self, identity: &str) -> Result<GraphEntity, CliError> {
1189 GraphEntity::new(
1190 self.shared.project,
1191 EntitySelector::External {
1192 external: ExternalSelector {
1193 system: super::GraphIdentityText::new(super::super::RUST_TOOLCHAIN_SYSTEM)
1194 .map_err(invalid_graph_contract)?,
1195 identity: super::GraphIdentityText::new(identity)
1196 .map_err(invalid_graph_contract)?,
1197 },
1198 },
1199 self.shared.generation,
1200 )
1201 .map_err(invalid_graph_contract)
1202 }
1203
1204 #[allow(clippy::too_many_arguments)]
1206 fn path(
1207 &mut self,
1208 owner: &Module,
1209 path: &SourceRustPath,
1210 namespace: Namespace,
1211 requester: &[String],
1212 causes: &mut CauseWorkspace<'_>,
1213 visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1214 depth: usize,
1215 ) -> Result<Option<Selection>, CliError> {
1216 self.shared.check_budget()?;
1217 if depth >= MAX_FORWARDING_DEPTH {
1218 return Ok(None);
1219 }
1220 let absolute_edition = if matches!(path.anchor, Anchor::Absolute | Anchor::Unqualified) {
1221 let manifest = self
1222 .shared
1223 .packages
1224 .package_owner(&owner.file)
1225 .map(|package| package.manifest.clone());
1226 if let Some(manifest) = manifest {
1227 self.cause(&manifest, causes)?;
1228 self.shared
1229 .source(&manifest)?
1230 .source
1231 .semantics()
1232 .and_then(SourceSemanticFacts::cargo)
1233 .and_then(|targets| targets.edition)
1234 } else {
1235 None
1236 }
1237 } else {
1238 None
1239 };
1240 if path.anchor == Anchor::Absolute
1241 && absolute_edition.is_some_and(|edition| edition >= 2018)
1242 {
1243 if path
1244 .segments
1245 .first()
1246 .is_some_and(|root| matches!(root.as_str(), "std" | "core" | "alloc"))
1247 {
1248 return Ok(Some(Selection::Value(
1249 self.external(&path.segments.join("::"))?,
1250 Visibility::Public,
1251 Vec::new(),
1252 )));
1253 }
1254 return Ok(None);
1255 }
1256 let logical = match path.anchor {
1257 Anchor::Crate => &[][..],
1258 Anchor::SelfModule => owner.logical.as_slice(),
1259 Anchor::Unqualified | Anchor::Absolute if absolute_edition == Some(2015) => &[][..],
1260 Anchor::Unqualified
1261 if absolute_edition.is_some_and(|edition| edition >= 2018)
1262 || owner.logical.is_empty() =>
1263 {
1264 owner.logical.as_slice()
1265 }
1266 Anchor::Super(parents) if parents <= owner.logical.len() => {
1267 &owner.logical[..owner.logical.len() - parents]
1268 }
1269 Anchor::Unqualified | Anchor::Super(_) | Anchor::Absolute => return Ok(None),
1270 };
1271 let Some(module) = self.address(owner, logical, causes)? else {
1272 return Ok(None);
1273 };
1274 self.select(
1275 &module,
1276 &path.segments,
1277 namespace,
1278 requester,
1279 causes,
1280 visited,
1281 depth,
1282 )
1283 }
1284
1285 #[allow(clippy::too_many_arguments)]
1287 fn select(
1288 &mut self,
1289 owner: &Module,
1290 parts: &[String],
1291 namespace: Namespace,
1292 requester: &[String],
1293 causes: &mut CauseWorkspace<'_>,
1294 visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1295 depth: usize,
1296 ) -> Result<Option<Selection>, CliError> {
1297 self.shared.check_budget()?;
1298 if depth >= MAX_FORWARDING_DEPTH {
1299 return Ok(None);
1300 }
1301 let frame_bytes = STAGED_GRAPH_ROW_BYTES.saturating_mul(2).saturating_add(
1303 module_bytes(owner)
1304 .saturating_mul(2)
1305 .saturating_add(std::mem::size_of_val(parts) as u64 * 2)
1306 .saturating_add(parts.iter().fold(0_u64, |bytes, part| {
1307 bytes.saturating_add(part.len() as u64 * 2)
1308 })),
1309 );
1310 self.shared.reserve_scratch(frame_bytes)?;
1311 let query = (owner.clone(), parts.to_vec(), namespace);
1312 if !visited.insert(query.clone()) {
1313 self.shared.release_scratch(frame_bytes)?;
1314 return Ok(None);
1315 }
1316 let result = self.select_inner(owner, parts, namespace, requester, causes, visited, depth);
1317 visited.remove(&query);
1318 self.shared.release_scratch(frame_bytes)?;
1319 result
1320 }
1321
1322 #[allow(clippy::too_many_arguments)]
1324 fn select_inner(
1325 &mut self,
1326 owner: &Module,
1327 parts: &[String],
1328 namespace: Namespace,
1329 requester: &[String],
1330 causes: &mut CauseWorkspace<'_>,
1331 visited: &mut BTreeSet<(Module, Vec<String>, Namespace)>,
1332 depth: usize,
1333 ) -> Result<Option<Selection>, CliError> {
1334 if !self.cause(&owner.file, causes)? {
1335 return Ok(None);
1336 }
1337 let Some(source) = self.source(&owner.file)? else {
1338 return Ok(None);
1339 };
1340 let Some((name, rest)) = parts.split_first() else {
1341 return Ok((namespace == Namespace::Module).then(|| Selection::Module(owner.clone())));
1342 };
1343 let wanted = if rest.is_empty() {
1344 namespace
1345 } else {
1346 Namespace::Module
1347 };
1348 let Some(facts) = source
1349 .source
1350 .semantics()
1351 .and_then(SourceSemanticFacts::rust)
1352 else {
1353 return Ok(None);
1354 };
1355 let slots = &self.slots[&owner.file];
1356 let values = slots
1357 .values
1358 .get(&owner.inline)
1359 .and_then(|slots| slots.get(name))
1360 .cloned()
1361 .unwrap_or_default();
1362 let modules = slots
1363 .modules
1364 .get(&owner.inline)
1365 .and_then(|slots| slots.get(name))
1366 .cloned()
1367 .unwrap_or_default();
1368 let mut count = 0_usize;
1369 let mut selected = None;
1370 let mut opposite = false;
1371 if wanted == Namespace::Module {
1372 for index in &modules {
1373 self.shared.control.check(IndexWorkStage::SymbolParsing)?;
1374 let module = &facts.modules[*index];
1375 count += 1;
1376 if modules.len() == 1 && visible(module.visibility, &owner.logical, requester) {
1377 selected = self
1378 .declared_module(owner, name, causes)?
1379 .map(Selection::Module);
1380 }
1381 }
1382 } else {
1383 opposite = !modules.is_empty();
1384 }
1385 for index in values {
1386 self.shared.control.check(IndexWorkStage::SymbolParsing)?;
1387 let declaration = &facts.declarations[index];
1388 let candidate = match (&declaration.binding, wanted) {
1389 (
1390 SourceRustBinding::TypeDeclaration(_) | SourceRustBinding::UnresolvedType,
1391 Namespace::Value,
1392 )
1393 | (
1394 SourceRustBinding::Declaration(_) | SourceRustBinding::Unresolved,
1395 Namespace::Module,
1396 ) => {
1397 opposite = true;
1398 continue;
1399 }
1400 (SourceRustBinding::Use(binding), Namespace::Value)
1401 if binding.role == UseRole::SelfNamespace =>
1402 {
1403 opposite = true;
1404 continue;
1405 }
1406 _ if !visible(declaration.visibility, &owner.logical, requester) => None,
1407 (SourceRustBinding::TypeDeclaration(_), Namespace::Module) => {
1408 Some(Selection::Type {
1409 owner: owner.clone(),
1410 name: name.clone(),
1411 visibility: declaration.visibility,
1412 })
1413 }
1414 (SourceRustBinding::Declaration(index), Namespace::Value) => {
1415 self.shared.target(&source, *index)?.map(|entity| {
1416 Selection::Value(entity, declaration.visibility, owner.logical.clone())
1417 })
1418 }
1419 (SourceRustBinding::Use(binding), _)
1420 if binding.supported && binding.role != UseRole::Wildcard =>
1421 {
1422 if let Some(identity) =
1423 self.slots[&owner.file].toolchain.get(&Arc::as_ptr(binding))
1424 {
1425 Some(Selection::Value(
1426 self.external(identity)?,
1427 Visibility::Public,
1428 Vec::new(),
1429 ))
1430 } else {
1431 self.path(
1432 owner,
1433 &binding.path,
1434 wanted,
1435 &owner.logical,
1436 causes,
1437 visited,
1438 depth + 1,
1439 )?
1440 .filter(|selection| selection_visible(selection, requester))
1441 }
1442 }
1443 _ => None,
1444 };
1445 if matches!(candidate, Some(Selection::Absent)) {
1446 opposite = true;
1447 continue;
1448 }
1449 count += 1;
1450 selected = candidate;
1451 }
1452 if count != 1 {
1453 self.ambiguous = self.ambiguous.max(u32::try_from(count).map_err(|error| {
1454 CliError::InvalidInput(format!("Rust slot count exceeds graph range: {error}"))
1455 })?);
1456 return Ok((count == 0 && opposite).then_some(Selection::Absent));
1457 }
1458 if rest.is_empty() {
1459 return Ok(selected);
1460 }
1461 if matches!(selected, Some(Selection::Absent)) {
1462 return Ok(selected);
1463 }
1464 if let Some(Selection::Value(entity, _, _)) = &selected
1465 && let EntitySelector::External { external } = entity.selector()
1466 {
1467 let identity = format!("{}::{}", external.identity.as_str(), rest.join("::"));
1468 return Ok(Some(Selection::Value(
1469 self.external(&identity)?,
1470 Visibility::Public,
1471 Vec::new(),
1472 )));
1473 }
1474 if let Some(Selection::Type { owner, name, .. }) = &selected {
1475 if rest.len() != 1 || namespace != Namespace::Value {
1476 return Ok(None);
1477 }
1478 let Some(indices) = self.slots.get(&owner.file).and_then(|slots| {
1479 slots
1480 .associated
1481 .get(&(Arc::clone(&owner.inline), name.clone(), rest[0].clone()))
1482 }) else {
1483 return Ok(None);
1484 };
1485 if indices.len() != 1 {
1486 self.ambiguous =
1487 self.ambiguous
1488 .max(u32::try_from(indices.len()).map_err(|error| {
1489 CliError::InvalidInput(format!(
1490 "Rust method count exceeds graph range: {error}"
1491 ))
1492 })?);
1493 return Ok(None);
1494 }
1495 let ordinal = indices[0];
1496 let Some(source) = self.source(&owner.file)? else {
1497 return Ok(None);
1498 };
1499 let Some(method) = source
1500 .source
1501 .semantics()
1502 .and_then(SourceSemanticFacts::rust)
1503 .and_then(|facts| facts.associated.get(ordinal))
1504 else {
1505 return Ok(None);
1506 };
1507 if !method.supported || !visible(method.visibility, &owner.logical, requester) {
1508 return Ok(None);
1509 }
1510 return method.declaration.map_or_else(
1511 || Ok(None),
1512 |ordinal| {
1513 self.shared.target(&source, ordinal).map(|entity| {
1514 entity.map(|entity| {
1515 Selection::Value(entity, method.visibility, owner.logical.clone())
1516 })
1517 })
1518 },
1519 );
1520 }
1521 let Some(Selection::Module(module)) = selected else {
1522 return Ok(None);
1523 };
1524 self.select(
1525 &module,
1526 rest,
1527 namespace,
1528 requester,
1529 causes,
1530 visited,
1531 depth + 1,
1532 )
1533 }
1534}
1535
1536fn selection_visible(selection: &Selection, requester: &[String]) -> bool {
1538 match selection {
1539 Selection::Absent => true,
1540 Selection::Value(_, visibility, owner) => visible(*visibility, owner, requester),
1541 Selection::Type {
1542 owner, visibility, ..
1543 } => visible(*visibility, &owner.logical, requester),
1544 Selection::Module(owner) => visible(
1545 owner.visibility,
1546 &owner.logical[..owner.logical.len().saturating_sub(1)],
1547 requester,
1548 ),
1549 }
1550}
1551
1552fn visible(visibility: Visibility, owner: &[String], requester: &[String]) -> bool {
1554 match visibility {
1555 Visibility::Public | Visibility::Crate => true,
1556 Visibility::Private => requester.starts_with(owner),
1557 Visibility::Unsupported => false,
1558 }
1559}
1560
1561fn parent(path: &str) -> &str {
1563 path.rsplit_once('/').map_or("", |(parent, _)| parent)
1564}
1565
1566fn join(directory: &str, path: &str) -> String {
1568 if directory.is_empty() {
1569 path.to_string()
1570 } else {
1571 format!("{directory}/{path}")
1572 }
1573}
1574
1575fn candidate_path(directory: &str, path: &str) -> Option<Vec<String>> {
1577 let relative = if directory.is_empty() {
1578 path
1579 } else {
1580 path.strip_prefix(directory)?.strip_prefix('/')?
1581 };
1582 let relative = relative.strip_suffix(".rs")?;
1583 let mut parts = relative
1584 .split('/')
1585 .map(ToString::to_string)
1586 .collect::<Vec<_>>();
1587 if parts.last().is_some_and(|part| part == "mod") {
1588 parts.pop();
1589 }
1590 (parts.len() <= MAX_FORWARDING_DEPTH).then_some(parts)
1591}
1592
1593#[cfg(test)]
1594mod tests {
1595 use super::super::super::{
1596 PackageIndex, ProjectResolutionRegistry, SourceBindingCache, build_entity_projection,
1597 };
1598 use super::super::BindingResolver;
1599 use super::{Module, Namespace, RustResolver, Visibility};
1600 use projectatlas_core::IndexGeneration;
1601 use projectatlas_core::{IndexCancellation, IndexWorkControl, IndexWorkFailure};
1602 use projectatlas_db::AtlasStore;
1603 use projectatlas_symbols::ConfiguredModuleResolution;
1604 use std::collections::{BTreeMap, BTreeSet};
1605 use std::error::Error;
1606 use std::io;
1607 use std::sync::Arc;
1608
1609 fn require(condition: bool, message: &str) -> Result<(), super::CliError> {
1611 if !condition {
1612 return Err(super::CliError::InvalidInput(message.to_string()));
1613 }
1614 Ok(())
1615 }
1616
1617 fn require_eq<T: std::fmt::Debug + PartialEq>(
1619 actual: &T,
1620 expected: &T,
1621 ) -> Result<(), super::CliError> {
1622 if actual != expected {
1623 return Err(super::CliError::InvalidInput(format!(
1624 "actual {actual:?}, expected {expected:?}"
1625 )));
1626 }
1627 Ok(())
1628 }
1629
1630 fn with_empty_resolver(
1632 check: impl FnOnce(&mut RustResolver<'_, '_, '_, '_>, &Module) -> Result<(), Box<dyn Error>>,
1633 ) -> Result<(), Box<dyn Error>> {
1634 with_source_resolver(&[], check)
1635 }
1636
1637 fn with_source_resolver(
1639 files: &[(&str, &str)],
1640 check: impl FnOnce(&mut RustResolver<'_, '_, '_, '_>, &Module) -> Result<(), Box<dyn Error>>,
1641 ) -> Result<(), Box<dyn Error>> {
1642 let temp = tempfile::tempdir()?;
1643 let root = temp.path();
1644 let mut nodes = Vec::new();
1645 for (path, source) in files {
1646 let native = root.join(path);
1647 if let Some(parent) = native.parent() {
1648 std::fs::create_dir_all(parent)?;
1649 }
1650 std::fs::write(native, source)?;
1651 nodes.push(projectatlas_core::Node {
1652 path: (*path).to_string(),
1653 kind: projectatlas_core::NodeKind::File,
1654 parent_path: path
1655 .rsplit_once('/')
1656 .map(|(directory, _)| directory.to_string()),
1657 extension: Some(".rs".to_string()),
1658 language: Some("rust".to_string()),
1659 size_bytes: Some(source.len() as u64),
1660 mtime_ns: None,
1661 content_hash: Some(blake3::hash(source.as_bytes()).to_hex().to_string()),
1662 });
1663 }
1664 let store = AtlasStore::open_for_project(&root.join("atlas.db"), root)?;
1665 let project = store
1666 .project_instance_id()?
1667 .ok_or_else(|| io::Error::other("test project identity missing"))?;
1668 let control = IndexWorkControl::new(IndexCancellation::new(), None);
1669 let generation = IndexGeneration::new(1);
1670 let graphs: &[&projectatlas_core::symbols::SymbolGraph] = &[];
1671 let packages = PackageIndex::from_graphs(graphs)?;
1672 let configured = ConfiguredModuleResolution::default();
1673 let entities =
1674 build_entity_projection(project, generation, &[], graphs, &packages, true, &control)?;
1675 let direct = BTreeSet::from(["src/bin/a.rs".to_string()]);
1676 let mut sources = SourceBindingCache::new(root, &nodes, Some(&direct));
1677 let mut candidates = ProjectResolutionRegistry::default();
1678 let replaced = BTreeSet::new();
1679 let mut shared = BindingResolver {
1680 store: &store,
1681 project,
1682 generation,
1683 base_generation: None,
1684 graphs: BTreeMap::new(),
1685 entities: &entities,
1686 packages: &packages,
1687 configured: &configured,
1688 sources: &mut sources,
1689 candidates: &mut candidates,
1690 replaced: &replaced,
1691 loaded: None,
1692 context: BTreeMap::new(),
1693 hydrated: BTreeMap::new(),
1694 modules: super::super::ModuleInterner::default(),
1695 queries: BTreeMap::new(),
1696 retained_bytes: 128 + std::mem::size_of::<super::super::ModuleInterner>() as u64,
1697 retained_limit: 64 * 1_024,
1698 control: &control,
1699 };
1700 let mut resolver = RustResolver {
1701 shared: &mut shared,
1702 memberships: BTreeMap::new(),
1703 roots: BTreeMap::new(),
1704 root_memberships: BTreeMap::new(),
1705 root_modules: BTreeMap::new(),
1706 slots: BTreeMap::new(),
1707 queries: BTreeMap::new(),
1708 ambiguous: 0,
1709 candidate_queries: BTreeMap::new(),
1710 };
1711 let module = Module {
1712 root: "src/lib.rs".to_string(),
1713 file: "missing.rs".to_string(),
1714 inline: Arc::from([]),
1715 logical: Vec::new(),
1716 directory: "src".to_string(),
1717 visibility: Visibility::Public,
1718 };
1719 check(&mut resolver, &module)
1720 }
1721
1722 #[test]
1723 fn root_directory_recovery_keeps_unrelated_target_sources_unopened()
1724 -> Result<(), Box<dyn Error>> {
1725 with_source_resolver(
1726 &[
1727 ("src/bin/a.rs", "mod needed; fn main(){}"),
1728 ("src/bin/b.rs", "fn main(){}"),
1729 ("examples/unrelated.rs", "mod unrelated; fn main(){}"),
1730 ],
1731 |resolver, _| {
1732 let roots: Arc<[super::RootCandidate]> =
1733 ["examples/unrelated.rs", "src/bin/a.rs", "src/bin/b.rs"]
1734 .into_iter()
1735 .map(|path| super::RootCandidate {
1736 path: path.to_string(),
1737 certain: true,
1738 cohort: None,
1739 })
1740 .collect::<Vec<_>>()
1741 .into();
1742 let index = resolver.root_membership_index("src/bin/a.rs", roots)?;
1743 require(
1744 resolver.shared.sources.captured.is_empty(),
1745 "path buckets opened source files",
1746 )?;
1747 let candidates = index
1748 .directories
1749 .get("src/bin")
1750 .ok_or_else(|| io::Error::other("root directory missing"))?;
1751 let modules = resolver.root_directory_modules(
1752 "src/bin/a.rs",
1753 &index,
1754 "src/bin",
1755 candidates,
1756 )?;
1757 require_eq(&modules.get("needed"), &Some(&vec![1]))?;
1758 require_eq(
1759 &resolver
1760 .shared
1761 .sources
1762 .captured
1763 .keys()
1764 .map(String::as_str)
1765 .collect::<Vec<_>>(),
1766 &vec!["src/bin/a.rs", "src/bin/b.rs"],
1767 )?;
1768 require_eq(&resolver.shared.sources.work.additional_candidates, &1)?;
1769 require(
1770 !resolver.slots.contains_key("examples/unrelated.rs"),
1771 "unrelated slots were retained",
1772 )?;
1773 let bytes = resolver.shared.retained_staging_bytes();
1774 let repeated = resolver.root_directory_modules(
1775 "src/bin/a.rs",
1776 &index,
1777 "src/bin",
1778 candidates,
1779 )?;
1780 require(
1781 Arc::ptr_eq(&modules, &repeated),
1782 "directory modules were rebuilt",
1783 )?;
1784 require_eq(&resolver.shared.retained_staging_bytes(), &bytes)?;
1785 Ok(())
1786 },
1787 )
1788 }
1789
1790 #[test]
1791 fn root_candidate_index_refuses_without_partial_publication_and_reuses_complete_state()
1792 -> Result<(), Box<dyn Error>> {
1793 with_empty_resolver(|resolver, _| {
1794 let cohort = Arc::new(super::CargoRootCohort {
1795 directory: Arc::from("src/bin"),
1796 key: super::document_file_resolution_key(resolver.shared.project, "Cargo.toml")?,
1797 });
1798 let roots: Arc<[super::RootCandidate]> = (0..256)
1799 .map(|ordinal| super::RootCandidate {
1800 path: format!("src/bin/t{ordinal:03}.rs"),
1801 certain: true,
1802 cohort: Some(Arc::clone(&cohort)),
1803 })
1804 .collect::<Vec<_>>()
1805 .into();
1806 let before = resolver.shared.retained_bytes;
1807 let limit = resolver.shared.retained_limit;
1808 resolver.shared.retained_limit = resolver.shared.retained_staging_bytes() + 1;
1809 let refused = resolver.root_membership_index("src/bin/t000.rs", Arc::clone(&roots));
1810 require(
1811 matches!(
1812 refused,
1813 Err(super::CliError::IndexWork(
1814 IndexWorkFailure::ResourceLimitExceeded { .. }
1815 ))
1816 ),
1817 "root index must refuse before publication",
1818 )?;
1819 require(
1820 resolver.root_memberships.is_empty(),
1821 "refused root index was published",
1822 )?;
1823 require_eq(&resolver.shared.retained_bytes, &before)?;
1824 resolver.shared.retained_limit = limit;
1825 let first = resolver.root_membership_index("src/bin/t000.rs", Arc::clone(&roots))?;
1826 let charged = resolver.shared.retained_bytes;
1827 let second = resolver.root_membership_index("src/bin/t255.rs", roots)?;
1828 require(
1829 Arc::ptr_eq(&first, &second),
1830 "completed root index was rebuilt",
1831 )?;
1832 require_eq(&resolver.shared.retained_bytes, &charged)?;
1833 let causes = first.directories.get("src/bin").map(|entry| &entry.causes);
1835 require_eq(&causes, &Some(&BTreeSet::from([cohort.key.clone()])))?;
1836 Ok(())
1837 })
1838 }
1839
1840 #[test]
1841 fn resolver_frames_refuse_before_erasing_retained_state() -> Result<(), Box<dyn Error>> {
1842 with_empty_resolver(|resolver, module| {
1843 let before = resolver.shared.retained_bytes;
1844 resolver.with_scratch(|this, scratch| {
1845 let mut causes = super::CauseWorkspace::new(scratch);
1846 let key = super::document_file_resolution_key(this.shared.project, &module.file)?;
1848 this.insert_cause(&mut causes, &key)?;
1849 this.shared.retained_limit = this.shared.retained_staging_bytes() + 1;
1850 this.shared.check_budget()?;
1851 let before_frame = this.shared.retained_bytes;
1852 let mut visited = BTreeSet::new();
1853 let result = this.select(
1854 module,
1855 &["absent".to_string()],
1856 Namespace::Value,
1857 &[],
1858 &mut causes,
1859 &mut visited,
1860 0,
1861 );
1862 require(
1863 matches!(
1864 result,
1865 Err(super::CliError::IndexWork(
1866 IndexWorkFailure::ResourceLimitExceeded { .. }
1867 ))
1868 ),
1869 "resolver boundary proof failed",
1870 )?;
1871 require_eq(&(this.shared.retained_bytes), &(before_frame))?;
1872 require(visited.is_empty(), "resolver boundary proof failed")?;
1873 require_eq(&(causes.keys), &(BTreeSet::from([key])))?;
1874 Ok(((), 0))
1875 })?;
1876 require_eq(&(resolver.shared.retained_bytes), &(before))?;
1877 Ok(())
1878 })
1879 }
1880
1881 #[test]
1882 fn resolver_frames_release_cycle_and_inner_error_ownership() -> Result<(), Box<dyn Error>> {
1883 with_empty_resolver(|resolver, module| {
1884 let before = resolver.shared.retained_bytes;
1885 resolver.with_scratch(|this, scratch| {
1886 let parts = vec!["absent".to_string()];
1887 let ancestor = (module.clone(), parts.clone(), Namespace::Value);
1888 let mut visited = BTreeSet::from([ancestor.clone()]);
1889 let mut causes = super::CauseWorkspace::new(scratch);
1890 let result = this.select(
1891 module,
1892 &parts,
1893 Namespace::Value,
1894 &[],
1895 &mut causes,
1896 &mut visited,
1897 0,
1898 )?;
1899 require(result.is_none(), "resolver boundary proof failed")?;
1900 require_eq(&(visited), &(BTreeSet::from([ancestor.clone()])))?;
1901 require_eq(&(this.shared.retained_bytes), &(before))?;
1902 let mut invalid = module.clone();
1903 invalid.file = "x".repeat(projectatlas_core::graph::MAX_GRAPH_IDENTITY_BYTES + 1);
1904 let result = this.select(
1905 &invalid,
1906 &parts,
1907 Namespace::Value,
1908 &[],
1909 &mut causes,
1910 &mut visited,
1911 0,
1912 );
1913 require(
1914 matches!(result, Err(super::CliError::InvalidInput(_))),
1915 "resolver boundary proof failed",
1916 )?;
1917 require_eq(&(visited), &(BTreeSet::from([ancestor])))?;
1918 require_eq(&(this.shared.retained_bytes), &(before))?;
1919 Ok(((), 0))
1920 })?;
1921 require_eq(&(resolver.shared.retained_bytes), &(before))?;
1922 Ok(())
1923 })
1924 }
1925
1926 #[test]
1927 fn cause_workspace_deduplicates_and_refuses_before_copying() -> Result<(), Box<dyn Error>> {
1928 with_empty_resolver(|resolver, _| {
1929 let before = resolver.shared.retained_bytes;
1930 resolver.with_scratch(|this, scratch| {
1931 let mut causes = super::CauseWorkspace::new(scratch);
1932 let first = super::document_file_resolution_key(this.shared.project, "first.rs")?;
1933 let next = super::document_file_resolution_key(this.shared.project, "next.rs")?;
1934 this.insert_cause(&mut causes, &first)?;
1935 let retained = this.shared.retained_bytes;
1936 this.insert_cause(&mut causes, &first)?;
1937 require_eq(&(this.shared.retained_bytes), &(retained))?;
1938 require_eq(&(causes.keys.len()), &(1))?;
1939 this.shared.retained_limit = this.shared.retained_staging_bytes();
1940 this.shared.check_budget()?;
1941 let result = this.insert_cause(&mut causes, &next);
1942 require(
1943 matches!(
1944 result,
1945 Err(super::CliError::IndexWork(
1946 IndexWorkFailure::ResourceLimitExceeded { .. }
1947 ))
1948 ),
1949 "resolver boundary proof failed",
1950 )?;
1951 require_eq(&(this.shared.retained_bytes), &(retained))?;
1952 require_eq(&(causes.keys), &(BTreeSet::from([first])))?;
1953 Ok(((), 0))
1954 })?;
1955 require_eq(&(resolver.shared.retained_bytes), &(before))?;
1956 Ok(())
1957 })
1958 }
1959
1960 #[test]
1961 fn cause_workspace_bounds_rows_and_releases_on_cancellation() -> Result<(), Box<dyn Error>> {
1962 with_empty_resolver(|resolver, _| {
1963 resolver.shared.retained_limit =
1964 super::super::super::super::MAX_PUBLICATION_STAGING_BYTES;
1965 let before = resolver.shared.retained_bytes;
1966 resolver.with_scratch(|this, scratch| {
1967 let mut causes = super::CauseWorkspace::new(scratch);
1968 for index in 0..super::super::GraphLimits::MAX_ROWS {
1969 let key = super::document_file_resolution_key(
1970 this.shared.project,
1971 &format!("file_{index}.rs"),
1972 )?;
1973 this.insert_cause(&mut causes, &key)?;
1974 }
1975 let retained = this.shared.retained_bytes;
1976 let excess = super::document_file_resolution_key(this.shared.project, "excess.rs")?;
1977 require(
1978 this.insert_cause(&mut causes, &excess).is_err(),
1979 "resolver boundary proof failed",
1980 )?;
1981 require_eq(
1982 &(causes.keys.len()),
1983 &(super::super::GraphLimits::MAX_ROWS as usize),
1984 )?;
1985 require_eq(&(this.shared.retained_bytes), &(retained))?;
1986 let duplicate =
1987 super::document_file_resolution_key(this.shared.project, "file_0.rs")?;
1988 this.insert_cause(&mut causes, &duplicate)?;
1989 require_eq(&(this.shared.retained_bytes), &(retained))?;
1990 Ok(((), 0))
1991 })?;
1992 require_eq(&(resolver.shared.retained_bytes), &(before))?;
1993 let cancelled = resolver.with_scratch(|this, scratch| {
1994 let mut causes = super::CauseWorkspace::new(scratch);
1995 let key = super::document_file_resolution_key(this.shared.project, "cancel.rs")?;
1996 this.insert_cause(&mut causes, &key)?;
1997 this.shared.control.cancel();
1998 this.insert_cause(&mut causes, &key)?;
1999 Ok(((), 0))
2000 });
2001 require(
2002 matches!(
2003 cancelled,
2004 Err(super::CliError::IndexWork(
2005 IndexWorkFailure::Cancelled { .. }
2006 ))
2007 ),
2008 "resolver boundary proof failed",
2009 )?;
2010 require_eq(&(resolver.shared.retained_bytes), &(before))?;
2011 Ok(())
2012 })
2013 }
2014
2015 #[test]
2016 fn root_workspace_refuses_growth_and_preserves_duplicate_certainty()
2017 -> Result<(), Box<dyn Error>> {
2018 with_empty_resolver(|resolver, _| {
2019 let before = resolver.shared.retained_bytes;
2020 resolver.with_scratch(|this, scratch| {
2021 let mut roots = BTreeMap::new();
2022 this.insert_root(scratch, &mut roots, "src/lib.rs".to_string(), true)?;
2023 let retained = this.shared.retained_bytes;
2024 this.shared.retained_limit = this.shared.retained_staging_bytes();
2025 this.shared.check_budget()?;
2026 this.insert_root(scratch, &mut roots, "src/lib.rs".to_string(), false)?;
2027 require_eq(&this.shared.retained_bytes, &retained)?;
2028 require_eq(&roots, &BTreeMap::from([("src/lib.rs".to_string(), false)]))?;
2029 let refusal =
2030 this.insert_root(scratch, &mut roots, "src/main.rs".to_string(), true);
2031 require(
2032 matches!(
2033 refusal,
2034 Err(super::CliError::IndexWork(
2035 IndexWorkFailure::ResourceLimitExceeded { .. }
2036 ))
2037 ),
2038 "root growth was not refused",
2039 )?;
2040 require_eq(&this.shared.retained_bytes, &retained)?;
2041 require_eq(&roots.len(), &1)?;
2042 Ok(((), 0))
2043 })?;
2044 require_eq(&resolver.shared.retained_bytes, &before)?;
2045 Ok(())
2046 })
2047 }
2048
2049 #[test]
2050 fn alias_workspace_refusal_cleans_frames_and_preserves_source_cache()
2051 -> Result<(), Box<dyn Error>> {
2052 with_empty_resolver(|resolver, _| {
2053 let shared = &mut *resolver.shared;
2054 let references =
2055 projectatlas_symbols::parse_import_references("import {run} from './worker.js';");
2056 let reference = references
2057 .first()
2058 .ok_or_else(|| io::Error::other("native import reference missing"))?;
2059 let query = shared.query("main.js", reference, None)?;
2060 let graph = projectatlas_symbols::extract_symbol_graph(
2061 "main.js",
2062 Some("javascript"),
2063 "import {run} from './worker.js'; run();",
2064 );
2065 let keys = ["first.js", "second.js"]
2066 .into_iter()
2067 .map(|path| super::document_file_resolution_key(shared.project, path))
2068 .collect::<Result<Vec<_>, _>>()?;
2069 let source = Arc::new(super::BindingSource {
2070 source: super::SourceGraph::Persisted(graph),
2071 slots: BTreeMap::new(),
2072 modules: BTreeMap::from([(query.module.to_string(), keys.clone())]),
2073 parents: Vec::new(),
2074 });
2075 shared.reserve_scratch(super::STAGED_GRAPH_ROW_BYTES * 8)?;
2076 shared
2077 .context
2078 .insert(query.source.clone(), Arc::clone(&source));
2079 let before = shared.retained_bytes;
2080 shared.retained_limit = shared.retained_staging_bytes()
2081 + query.retained_bytes().saturating_mul(2)
2082 + super::STAGED_GRAPH_ROW_BYTES
2083 + keys[0].canonical_identity().len() as u64;
2084 shared.check_budget()?;
2085 let mut visited = BTreeMap::new();
2086 let result = shared.resolve(query.clone(), &mut visited, 0);
2087 require(
2088 matches!(
2089 result,
2090 Err(super::CliError::IndexWork(
2091 IndexWorkFailure::ResourceLimitExceeded { .. }
2092 ))
2093 ),
2094 "alias workspace growth was not refused",
2095 )?;
2096 require_eq(&shared.retained_bytes, &before)?;
2097 require(
2098 visited.is_empty() && shared.queries.is_empty(),
2099 "alias refusal retained a frame or failed cache entry",
2100 )?;
2101 require(
2102 Arc::ptr_eq(
2103 shared
2104 .context
2105 .get(&query.source)
2106 .ok_or_else(|| io::Error::other("source cache missing"))?,
2107 &source,
2108 ),
2109 "alias refusal replaced captured context",
2110 )?;
2111 shared.retained_limit = 64 * 1_024;
2112 let outcome = shared.resolve(query.clone(), &mut visited, 0)?;
2113 let mut expected = keys.into_iter().collect::<BTreeSet<_>>();
2114 expected.insert(super::document_file_resolution_key(
2115 shared.project,
2116 &query.source,
2117 )?);
2118 require_eq(&outcome.dependencies, &expected)?;
2119 require(visited.is_empty(), "alias success retained a frame")?;
2120 let retained = shared.retained_bytes;
2121 let cached = shared.resolve(query, &mut visited, 0)?;
2122 require(
2123 Arc::ptr_eq(&outcome, &cached),
2124 "alias query did not reuse its completed outcome",
2125 )?;
2126 require_eq(&shared.retained_bytes, &retained)?;
2127 Ok(())
2128 })
2129 }
2130
2131 #[test]
2132 fn alias_workspace_cycles_retain_only_completed_owners_and_errors_release_frames()
2133 -> Result<(), Box<dyn Error>> {
2134 with_empty_resolver(|resolver, _| {
2135 let shared = &mut *resolver.shared;
2136 let (_, graph, facts) =
2137 projectatlas_symbols::extract_symbol_graph_with_semantics_controlled(
2138 "self.js",
2139 Some("javascript"),
2140 "export {run} from './self.js';",
2141 shared.control,
2142 )?
2143 .into_parts();
2144 let slot = facts
2145 .exports()
2146 .iter()
2147 .position(|export| export.name == "run")
2148 .ok_or_else(|| io::Error::other("native reexport slot missing"))?;
2149 let projectatlas_symbols::SourceExportBinding::ReExport(reference) =
2150 &facts.exports()[slot].binding
2151 else {
2152 return Err(io::Error::other("native reexport reference missing").into());
2153 };
2154 let query = shared.query("self.js", reference, None)?;
2155 let key = super::document_file_resolution_key(shared.project, "self.js")?;
2156 let entity = super::GraphEntity::new(
2157 shared.project,
2158 super::EntitySelector::File {
2159 path: projectatlas_core::graph::RepositoryFilePath::new(std::path::Path::new(
2160 "self.js",
2161 ))?,
2162 },
2163 shared.generation,
2164 )?;
2165 shared.candidates.insert_candidate(&key, &entity)?;
2166 let source = Arc::new(super::BindingSource {
2167 source: super::SourceGraph::Captured(Arc::new(
2168 super::super::super::CapturedSourceGraph {
2169 graph,
2170 semantics: facts,
2171 admission: super::super::super::GraphIdentityAdmission::default(),
2172 },
2173 )),
2174 slots: BTreeMap::from([("run".to_string(), vec![slot])]),
2175 modules: BTreeMap::from([(query.module.to_string(), vec![key.clone()])]),
2176 parents: Vec::new(),
2177 });
2178 shared.reserve_scratch(super::STAGED_GRAPH_ROW_BYTES * 8)?;
2179 shared.context.insert(query.source.clone(), source);
2180 let before = shared.retained_bytes;
2181 let frame_query = super::super::BindingQuery {
2182 source: "unindexed.js".to_string(),
2183 ..query.clone()
2184 };
2185 shared.retained_limit = shared.retained_staging_bytes() + frame_query.retained_bytes();
2186 shared.check_budget()?;
2187 let mut visited = BTreeMap::new();
2188 let refusal = shared.resolve(frame_query, &mut visited, 0);
2189 require(
2190 matches!(
2191 refusal,
2192 Err(super::CliError::IndexWork(
2193 IndexWorkFailure::ResourceLimitExceeded { .. }
2194 ))
2195 ),
2196 "second query copy was not admitted before cloning",
2197 )?;
2198 require_eq(&shared.retained_bytes, &before)?;
2199 require(
2200 visited.is_empty() && shared.queries.is_empty(),
2201 "frame refusal retained owned state",
2202 )?;
2203 shared.retained_limit = 64 * 1_024;
2204 let outcome = shared.resolve(query.clone(), &mut visited, 0)?;
2205 require(
2206 outcome.followed_export && outcome.target.is_none(),
2207 "self reexport did not abstain",
2208 )?;
2209 require_eq(&outcome.dependencies, &BTreeSet::from([key]))?;
2210 require_eq(
2211 &shared.retained_bytes,
2212 &(before + outcome.retained_bytes() + query.retained_bytes()),
2213 )?;
2214 require(visited.is_empty(), "cycle retained an ancestor frame")?;
2215 let retained = shared.retained_bytes;
2216 let cached = shared.resolve(query, &mut visited, 0)?;
2217 require(
2218 Arc::ptr_eq(&outcome, &cached),
2219 "cycle did not reuse completed root outcome",
2220 )?;
2221 require_eq(&shared.retained_bytes, &retained)?;
2222 let references =
2223 projectatlas_symbols::parse_import_references("import {run} from './absent.js';");
2224 let reference = references
2225 .first()
2226 .ok_or_else(|| io::Error::other("native missing-source import absent"))?;
2227 let missing = shared.query("missing.js", reference, None)?;
2228 let retained = shared.retained_bytes;
2230 require(
2231 shared.resolve(missing, &mut visited, 0).is_err(),
2232 "missing source unexpectedly resolved",
2233 )?;
2234 require(visited.is_empty(), "alias error retained a frame")?;
2235 require_eq(&shared.retained_bytes, &retained)?;
2236 Ok(())
2237 })
2238 }
2239
2240 #[test]
2241 fn loaded_lookup_keys_reserve_before_copy_and_reuse_completed_admission()
2242 -> Result<(), Box<dyn Error>> {
2243 with_empty_resolver(|resolver, _| {
2244 let shared = &mut *resolver.shared;
2245 let key = super::document_file_resolution_key(shared.project, "worker.js")?;
2246 shared.loaded = Some(BTreeSet::new());
2247 let before = shared.retained_bytes;
2248 shared.retained_limit = shared.retained_staging_bytes() + 1;
2249 shared.check_budget()?;
2250 let refusal = shared.module_file(std::slice::from_ref(&key));
2251 require(
2252 matches!(
2253 refusal,
2254 Err(super::CliError::IndexWork(
2255 IndexWorkFailure::ResourceLimitExceeded { .. }
2256 ))
2257 ),
2258 "lookup copy was not refused before read",
2259 )?;
2260 require_eq(&shared.retained_bytes, &before)?;
2261 require(
2262 shared.loaded.as_ref().is_some_and(BTreeSet::is_empty),
2263 "refused lookup was marked loaded",
2264 )?;
2265 shared.retained_limit = 64 * 1_024;
2266 require_eq(&shared.module_file(std::slice::from_ref(&key))?, &None)?;
2267 require(
2268 shared
2269 .loaded
2270 .as_ref()
2271 .is_some_and(|loaded| loaded.contains(&key)),
2272 "completed lookup was not retained",
2273 )?;
2274 let retained = shared.retained_bytes;
2275 shared.module_file(std::slice::from_ref(&key))?;
2276 require_eq(&shared.retained_bytes, &retained)?;
2277 Ok(())
2278 })
2279 }
2280
2281 fn cached_candidates(
2283 resolver: &mut RustResolver<'_, '_, '_, '_>,
2284 ) -> Result<
2285 (
2286 super::SourceSemanticFacts,
2287 projectatlas_symbols::SourceRustCallImports,
2288 ),
2289 Box<dyn Error>,
2290 > {
2291 const SOURCE: &str = "src/main.rs";
2292 let extracted = projectatlas_symbols::extract_symbol_graph_with_semantics_controlled(
2293 SOURCE,
2294 Some("rust"),
2295 "use crate::one::run; use crate::two::run; fn caller(){run();}",
2296 resolver.shared.control,
2297 )?;
2298 let (_, _, facts) = extracted.into_parts();
2299 require_eq(&(facts.imports().len()), &(2))?;
2300 for (index, import) in facts.imports().iter().enumerate() {
2301 let binding = import
2302 .rust
2303 .as_ref()
2304 .ok_or_else(|| io::Error::other("native import binding missing"))?;
2305 let request = super::Request {
2306 source: SOURCE.to_string(),
2307 module: Arc::clone(&binding.module),
2308 path: binding.path.clone(),
2309 namespace: false,
2310 import: false,
2311 toolchain: false,
2312 };
2313 let key = super::document_file_resolution_key(
2314 resolver.shared.project,
2315 &format!("dependency_{index}.rs"),
2316 )?;
2317 let outcome = Arc::new(super::CallBindingResolution {
2318 absent: true,
2319 dependencies: BTreeSet::from([key]),
2320 ..super::CallBindingResolution::default()
2321 });
2322 resolver.charge(
2323 outcome
2324 .retained_bytes()
2325 .saturating_add(super::STAGED_GRAPH_ROW_BYTES * 4),
2326 )?;
2327 resolver.queries.insert(request, outcome);
2328 }
2329 let call = projectatlas_symbols::SourceRustCallImports {
2330 relation: 0,
2331 bindings: Arc::from([(0, 0), (1, 0)]),
2332 declarations: Arc::from([]),
2333 member: None,
2334 };
2335 Ok((facts, call))
2336 }
2337
2338 #[test]
2339 fn candidate_union_refusal_preserves_cached_children() -> Result<(), Box<dyn Error>> {
2340 with_empty_resolver(|resolver, _| {
2341 let (facts, call) = cached_candidates(resolver)?;
2342 let before = resolver.shared.retained_bytes;
2343 resolver.shared.retained_limit =
2344 resolver.shared.retained_staging_bytes() + super::STAGED_GRAPH_ROW_BYTES;
2345 resolver.shared.check_budget()?;
2346 let result = resolver.resolve_candidates("src/main.rs", &facts, &call);
2347 require(
2348 matches!(
2349 result,
2350 Err(super::CliError::IndexWork(
2351 IndexWorkFailure::ResourceLimitExceeded { .. }
2352 ))
2353 ),
2354 "resolver boundary proof failed",
2355 )?;
2356 require_eq(&(resolver.shared.retained_bytes), &(before))?;
2357 require_eq(&(resolver.queries.len()), &(call.bindings.len()))?;
2358 require(
2359 resolver.candidate_queries.is_empty(),
2360 "resolver boundary proof failed",
2361 )?;
2362 Ok(())
2363 })
2364 }
2365
2366 #[test]
2367 fn candidate_union_transfers_unique_causes_once_and_reuses_cache() -> Result<(), Box<dyn Error>>
2368 {
2369 with_empty_resolver(|resolver, _| {
2370 let (facts, call) = cached_candidates(resolver)?;
2371 let result = resolver.resolve_candidates("src/main.rs", &facts, &call)?;
2372 require(result.absent, "resolver boundary proof failed")?;
2373 require(result.target.is_none(), "resolver boundary proof failed")?;
2374 let expected = ["src/main.rs", "dependency_0.rs", "dependency_1.rs"]
2375 .into_iter()
2376 .map(|path| super::document_file_resolution_key(resolver.shared.project, path))
2377 .collect::<Result<BTreeSet<_>, _>>()?;
2378 require_eq(&(result.dependencies), &(expected))?;
2379 let retained = resolver.shared.retained_bytes;
2380 let repeated = resolver.resolve_candidates("src/main.rs", &facts, &call)?;
2381 require(
2382 Arc::ptr_eq(&result, &repeated),
2383 "resolver boundary proof failed",
2384 )?;
2385 require_eq(&(resolver.shared.retained_bytes), &(retained))?;
2386 Ok(())
2387 })
2388 }
2389}