1mod rust;
4
5use super::{
6 CliError, EntityProjection, MAX_INCREMENTAL_RESOLUTION_ITEMS, PackageIndex,
7 ProjectResolutionRegistry, ResolutionCandidateKind, STAGED_GRAPH_ROW_BYTES, SourceBindingCache,
8 SourceGraph, append_persisted_resolution_candidates, dependency_closure_limit,
9 document_file_resolution_key, enforce_resolution_registry_budget_with_limit,
10 entity_retained_bytes, invalid_graph_contract, registry_resolution_matches, relation_reference,
11 resolution_key_limit_failure, resolution_retained_bytes, source_symbol_entity,
12};
13use projectatlas_core::graph::{
14 CanonicalResolutionKey, EntitySelector, GraphEntity, GraphIdentityText, GraphLimits,
15 ProjectInstanceId, RelationResolution,
16};
17use projectatlas_core::language::{SemanticProviderOwner, language_capability};
18use projectatlas_core::symbols::{SymbolGraph, SymbolKind, SymbolRelation};
19use projectatlas_core::{IndexGeneration, IndexWorkControl, IndexWorkStage};
20use projectatlas_db::AtlasStore;
21use projectatlas_symbols::{
22 ConfiguredModuleResolution, ResolutionKeyProjection, ResolutionProjectionError,
23 SourceCallBinding, SourceSemanticFacts, derive_resolution_keys_with_context,
24};
25use projectatlas_symbols::{ImportReference, SourceExportBinding, SourceExportNamespace};
26use std::cmp::Ordering;
27use std::collections::{BTreeMap, BTreeSet, HashMap};
28use std::sync::Arc;
29
30const MAX_FORWARDING_DEPTH: usize = 64;
32
33#[derive(Default)]
35pub(super) struct CallBindingResolution {
36 target: Option<GraphEntity>,
38 ambiguous: Option<std::num::NonZeroU32>,
40 absent: bool,
42 followed_export: bool,
44 dependencies: BTreeSet<CanonicalResolutionKey>,
46}
47
48impl CallBindingResolution {
49 pub(super) fn resolution(
51 &self,
52 relation: &SymbolRelation,
53 ) -> Result<RelationResolution, CliError> {
54 if let Some(candidates) = self.ambiguous {
55 return Ok(RelationResolution::Ambiguous {
56 reference: GraphIdentityText::new(relation_reference(relation))
57 .map_err(invalid_graph_contract)?,
58 candidates,
59 });
60 }
61 self.target.as_ref().map_or_else(
62 || {
63 Ok(RelationResolution::Unresolved {
64 reference: GraphIdentityText::new(relation_reference(relation))
65 .map_err(invalid_graph_contract)?,
66 })
67 },
68 |target| {
69 if matches!(target.selector(), EntitySelector::External { .. }) {
70 RelationResolution::external(target).map_err(invalid_graph_contract)
71 } else {
72 RelationResolution::resolved(target).map_err(invalid_graph_contract)
73 }
74 },
75 )
76 }
77
78 pub(super) fn external_target(&self) -> Option<&GraphEntity> {
80 self.target
81 .as_ref()
82 .filter(|target| matches!(target.selector(), EntitySelector::External { .. }))
83 }
84
85 pub(super) fn dependencies(&self) -> &BTreeSet<CanonicalResolutionKey> {
87 &self.dependencies
88 }
89
90 fn retained_bytes(&self) -> u64 {
92 self.dependencies
93 .iter()
94 .fold(STAGED_GRAPH_ROW_BYTES, |bytes, key| {
95 bytes
96 .saturating_add(STAGED_GRAPH_ROW_BYTES)
97 .saturating_add(key.canonical_identity().len() as u64)
98 })
99 .saturating_add(self.target.as_ref().map_or(0, entity_retained_bytes))
100 }
101}
102
103#[derive(Default)]
105struct ResolverScratch {
106 retained_bytes: u64,
108}
109
110impl ResolverScratch {
111 fn reserve(
113 &mut self,
114 shared: &mut BindingResolver<'_, '_, '_>,
115 bytes: u64,
116 ) -> Result<(), CliError> {
117 shared.reserve_scratch(bytes)?;
118 self.retained_bytes = self.retained_bytes.saturating_add(bytes);
119 Ok(())
120 }
121}
122
123struct CauseWorkspace<'a> {
125 keys: BTreeSet<CanonicalResolutionKey>,
127 scratch: &'a mut ResolverScratch,
129}
130
131impl<'a> CauseWorkspace<'a> {
132 fn new(scratch: &'a mut ResolverScratch) -> Self {
134 Self {
135 keys: BTreeSet::new(),
136 scratch,
137 }
138 }
139}
140
141pub(super) type CallBindings = BTreeMap<String, BTreeMap<usize, Arc<CallBindingResolution>>>;
143
144#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
146struct ModuleId(usize);
147
148#[derive(Default)]
150struct ModuleInterner {
151 allocations: BTreeMap<(usize, usize), (Arc<str>, ModuleId)>,
153 values: HashMap<Arc<str>, ModuleId>,
155}
156
157impl ModuleInterner {
158 fn intern<E>(
160 &mut self,
161 module: Arc<str>,
162 admit: impl FnOnce(u64) -> Result<(), E>,
163 ) -> Result<ModuleId, E> {
164 let allocation = (module.as_ptr() as usize, module.len());
165 if let Some((_, id)) = self.allocations.get(&allocation) {
166 return Ok(*id);
167 }
168 let existing = self.values.get(module.as_ref()).copied();
169 let entries = if existing.is_some() { 1 } else { 2 };
170 admit(STAGED_GRAPH_ROW_BYTES.saturating_mul(entries))?;
171 let id = if let Some(id) = existing {
172 id
173 } else {
174 let id = ModuleId(self.values.len());
175 self.values.insert(Arc::clone(&module), id);
176 id
177 };
178 self.allocations.insert(allocation, (module, id));
179 Ok(id)
180 }
181}
182
183#[derive(Clone)]
185struct BindingQuery {
186 source: String,
188 module: Arc<str>,
190 module_id: ModuleId,
192 imported: Option<String>,
194 member: Option<String>,
196}
197
198impl BindingQuery {
199 fn new<E>(
201 source: &str,
202 reference: &ImportReference,
203 member: Option<&str>,
204 modules: &mut ModuleInterner,
205 admit: impl FnOnce(u64) -> Result<(), E>,
206 ) -> Result<Self, E> {
207 let module = reference.shared_module();
208 let module_id = modules.intern(Arc::clone(&module), admit)?;
209 Ok(Self {
210 source: source.to_string(),
211 module,
212 module_id,
213 imported: reference.imported().map(ToString::to_string),
214 member: member.map(ToString::to_string),
215 })
216 }
217
218 fn identity(&self) -> (&str, Option<&str>, Option<&str>, ModuleId) {
220 (
221 &self.source,
222 self.imported.as_deref(),
223 self.member.as_deref(),
224 self.module_id,
225 )
226 }
227
228 fn retained_bytes(&self) -> u64 {
230 STAGED_GRAPH_ROW_BYTES
231 .saturating_add(self.source.capacity() as u64)
232 .saturating_add(self.imported.as_ref().map_or(0, String::capacity) as u64)
233 .saturating_add(self.member.as_ref().map_or(0, String::capacity) as u64)
234 }
235}
236
237impl PartialEq for BindingQuery {
238 fn eq(&self, other: &Self) -> bool {
239 self.identity() == other.identity()
240 }
241}
242
243impl Eq for BindingQuery {}
244
245impl PartialOrd for BindingQuery {
246 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
247 Some(self.cmp(other))
248 }
249}
250
251impl Ord for BindingQuery {
252 fn cmp(&self, other: &Self) -> Ordering {
253 self.identity().cmp(&other.identity())
254 }
255}
256
257enum SourceProjection<'g> {
259 Staged(&'g ResolutionKeyProjection),
261 Captured(ResolutionKeyProjection),
263}
264
265impl SourceProjection<'_> {
266 fn get(&self) -> &ResolutionKeyProjection {
268 match self {
269 Self::Staged(keys) => keys,
270 Self::Captured(keys) => keys,
271 }
272 }
273}
274
275struct BindingSource<'g> {
277 source: SourceGraph<'g>,
279 slots: BTreeMap<String, Vec<usize>>,
281 modules: BTreeMap<String, Vec<CanonicalResolutionKey>>,
283 parents: Vec<Option<GraphIdentityText>>,
285}
286
287fn relation_keys(
289 projection: &ResolutionKeyProjection,
290 relation: usize,
291) -> &[CanonicalResolutionKey] {
292 projection
293 .relation_keys()
294 .binary_search_by_key(
295 &relation,
296 projectatlas_symbols::RelationResolutionKeys::relation_index,
297 )
298 .map_or(&[], |index| projection.relation_keys()[index].keys())
299}
300
301fn needs_forwarding(
303 keys: &[CanonicalResolutionKey],
304 candidates: &ProjectResolutionRegistry,
305) -> bool {
306 keys.iter().any(|key| {
307 candidates
308 .forwarding_key_digests
309 .contains(&key.digest_bytes())
310 }) || !keys
311 .iter()
312 .any(|key| candidates.candidate_digests_by_key.contains_key(key))
313}
314
315struct BindingResolver<'a, 'g, 'n> {
317 store: &'a AtlasStore,
319 project: ProjectInstanceId,
321 generation: IndexGeneration,
323 base_generation: Option<IndexGeneration>,
325 graphs: BTreeMap<&'g str, (&'g SymbolGraph, Option<&'g SourceSemanticFacts>)>,
327 entities: &'g EntityProjection,
329 packages: &'a PackageIndex,
331 configured: &'a ConfiguredModuleResolution,
333 sources: &'a mut SourceBindingCache<'n>,
335 candidates: &'a mut ProjectResolutionRegistry,
337 replaced: &'a BTreeSet<String>,
339 loaded: Option<BTreeSet<CanonicalResolutionKey>>,
341 context: BTreeMap<String, Arc<BindingSource<'g>>>,
343 hydrated: BTreeMap<String, Option<GraphEntity>>,
345 modules: ModuleInterner,
347 queries: BTreeMap<BindingQuery, BTreeMap<usize, Arc<CallBindingResolution>>>,
349 retained_bytes: u64,
351 retained_limit: u64,
353 control: &'a IndexWorkControl,
355}
356
357#[allow(clippy::too_many_arguments)]
359pub(super) fn resolve_source_bindings<'g>(
360 store: &AtlasStore,
361 project: ProjectInstanceId,
362 generation: IndexGeneration,
363 graphs: &[&'g SourceGraph<'g>],
364 packages: &PackageIndex,
365 configured: &ConfiguredModuleResolution,
366 sources: &mut SourceBindingCache<'_>,
367 entities: &'g EntityProjection,
368 candidates: &mut ProjectResolutionRegistry,
369 loaded: Option<BTreeSet<CanonicalResolutionKey>>,
370 replaced: &BTreeSet<String>,
371 control: &IndexWorkControl,
372 staging_limit: u64,
373) -> Result<(CallBindings, u64), CliError> {
374 control.check(IndexWorkStage::SymbolParsing)?;
375 let rust_needed = graphs.iter().any(|graph| {
376 graph
377 .semantics()
378 .is_some_and(|facts| facts.rust().is_some())
379 && packages.package_owner(&graph.graph().path).is_some()
380 });
381 let mut needed = rust_needed;
382 'sources: for graph in graphs {
383 control.check(IndexWorkStage::SymbolParsing)?;
384 if graph
385 .graph()
386 .language
387 .as_deref()
388 .and_then(language_capability)
389 .and_then(|capability| capability.effective_semantic_provider())
390 != Some(SemanticProviderOwner::EcmaScript)
391 {
392 continue;
393 }
394 let Some(facts) = graph.semantics() else {
395 continue;
396 };
397 let Some(projection) = entities.keys_by_graph.get(&graph.graph().path) else {
398 continue;
399 };
400 for call in facts.calls() {
401 control.check(IndexWorkStage::SymbolParsing)?;
402 if matches!(call.binding, SourceCallBinding::Imported { .. })
403 && needs_forwarding(relation_keys(projection, call.relation), candidates)
404 {
405 needed = true;
406 break 'sources;
407 }
408 }
409 }
410 if !needed {
411 return Ok((CallBindings::new(), 0));
412 }
413 let loaded_bytes = loaded.as_ref().map_or(0, |keys| {
414 keys.iter()
415 .map(super::resolution_workspace_key_bytes)
416 .fold(0_u64, u64::saturating_add)
417 });
418 let mut resolver = BindingResolver {
419 store,
420 project,
421 generation,
422 base_generation: store.repository_graph_generation()?,
423 graphs: graphs
424 .iter()
425 .map(|graph| {
426 (
427 graph.graph().path.as_str(),
428 (graph.graph(), graph.semantics()),
429 )
430 })
431 .collect(),
432 entities,
433 packages,
434 configured,
435 sources,
436 candidates,
437 replaced,
438 loaded,
439 context: BTreeMap::new(),
440 modules: ModuleInterner::default(),
441 queries: BTreeMap::new(),
442 hydrated: BTreeMap::new(),
443 retained_bytes: (graphs.len() as u64)
444 .saturating_mul(STAGED_GRAPH_ROW_BYTES)
445 .saturating_add(std::mem::size_of::<ModuleInterner>() as u64)
446 .saturating_add(loaded_bytes),
447 control,
448 retained_limit: staging_limit,
449 };
450 resolver.check_budget()?;
451 let mut bindings = CallBindings::new();
452 for graph in graphs {
453 control.check(IndexWorkStage::SymbolParsing)?;
454 let raw = graph.graph();
455 if raw
456 .language
457 .as_deref()
458 .and_then(language_capability)
459 .and_then(|capability| capability.effective_semantic_provider())
460 != Some(SemanticProviderOwner::EcmaScript)
461 {
462 continue;
463 }
464 let Some(facts) = graph.semantics() else {
465 continue;
466 };
467 let Some(projection) = entities.keys_by_graph.get(&raw.path) else {
468 continue;
469 };
470 for call in facts.calls() {
471 control.check(IndexWorkStage::SymbolParsing)?;
472 let SourceCallBinding::Imported {
473 import,
474 reference,
475 member,
476 ..
477 } = &call.binding
478 else {
479 continue;
480 };
481 let reference = &facts.imports()[*import].references[*reference];
482 if reference.imported().is_none() && member.is_none() {
483 continue;
484 }
485 let ordinary = relation_keys(projection, call.relation);
486 if !needs_forwarding(ordinary, resolver.candidates) {
487 continue;
488 }
489 let query = resolver.query(&raw.path, reference, member.as_deref())?;
490 let outcome = resolver.resolve(query, &mut BTreeMap::new(), 0)?;
491 if !outcome.followed_export {
492 continue;
493 }
494 bindings
495 .entry(raw.path.clone())
496 .or_default()
497 .insert(call.relation, outcome);
498 resolver.retained_bytes = resolver
499 .retained_bytes
500 .saturating_add(STAGED_GRAPH_ROW_BYTES)
501 .saturating_add(raw.path.len() as u64);
502 resolver.check_budget()?;
503 }
504 }
505 rust::resolve_rust_bindings(&mut resolver, graphs, &mut bindings)?;
506 let mut shared = BTreeSet::new();
508 let retained = bindings.iter().fold(0_u64, |bytes, (path, calls)| {
509 calls.values().fold(
510 bytes
511 .saturating_add(path.len() as u64)
512 .saturating_add((calls.len() as u64).saturating_mul(STAGED_GRAPH_ROW_BYTES)),
513 |bytes, outcome| {
514 if shared.insert(Arc::as_ptr(outcome)) {
515 bytes.saturating_add(outcome.retained_bytes())
516 } else {
517 bytes
518 }
519 },
520 )
521 });
522 Ok((bindings, retained))
523}
524
525impl<'g> BindingResolver<'_, 'g, '_> {
526 fn query(
528 &mut self,
529 source: &str,
530 reference: &ImportReference,
531 member: Option<&str>,
532 ) -> Result<BindingQuery, CliError> {
533 self.check_budget()?;
534 let before = self.retained_staging_bytes();
535 let limit = self.retained_limit;
536 let control = self.control;
537 let retained = &mut self.retained_bytes;
538 BindingQuery::new(source, reference, member, &mut self.modules, |bytes| {
539 control.check(IndexWorkStage::SymbolParsing)?;
540 enforce_resolution_registry_budget_with_limit(before.saturating_add(bytes), limit)?;
541 *retained = retained.saturating_add(bytes);
542 Ok(())
543 })
544 }
545
546 fn check_budget(&self) -> Result<(), CliError> {
548 self.control.check(IndexWorkStage::SymbolParsing)?;
549 enforce_resolution_registry_budget_with_limit(
550 self.retained_staging_bytes(),
551 self.retained_limit,
552 )
553 }
554
555 fn retained_staging_bytes(&self) -> u64 {
557 self.entities
558 .retained_bytes
559 .saturating_add(self.candidates.retained_bytes)
560 .saturating_add(self.sources.retained_bytes)
561 .saturating_add(self.retained_bytes)
562 }
563
564 fn reserve_scratch(&mut self, bytes: u64) -> Result<(), CliError> {
566 let before = self.retained_bytes;
567 self.retained_bytes = before.saturating_add(bytes);
568 if let Err(error) = self.check_budget() {
569 self.retained_bytes = before;
570 return Err(error);
571 }
572 Ok(())
573 }
574
575 fn release_scratch(&mut self, bytes: u64) -> Result<(), CliError> {
577 self.retained_bytes = self.retained_bytes.checked_sub(bytes).ok_or_else(|| {
578 CliError::InvalidInput("resolution workspace accounting is unbalanced".to_string())
579 })?;
580 Ok(())
581 }
582
583 fn with_scratch<T>(
585 &mut self,
586 operation: impl FnOnce(&mut Self, &mut ResolverScratch) -> Result<(T, u64), CliError>,
587 ) -> Result<T, CliError> {
588 let mut scratch = ResolverScratch::default();
589 let result = operation(self, &mut scratch);
590 self.release_scratch(scratch.retained_bytes)?;
591 let (value, retained_bytes) = result?;
592 self.reserve_scratch(retained_bytes)?;
593 Ok(value)
594 }
595
596 fn insert_cause(
598 &mut self,
599 causes: &mut CauseWorkspace<'_>,
600 key: &CanonicalResolutionKey,
601 ) -> Result<(), CliError> {
602 self.check_budget()?;
603 if causes.keys.contains(key) {
604 return Ok(());
605 }
606 if causes.keys.len() >= GraphLimits::MAX_ROWS as usize {
607 return Err(resolution_key_limit_failure(
608 causes.keys.len().saturating_add(1),
609 ));
610 }
611 causes.scratch.reserve(
612 self,
613 STAGED_GRAPH_ROW_BYTES.saturating_add(key.canonical_identity().len() as u64),
614 )?;
615 causes.keys.insert(key.clone());
616 Ok(())
617 }
618
619 fn source(&mut self, path: &str) -> Result<Arc<BindingSource<'g>>, CliError> {
621 if let Some(source) = self.context.get(path) {
622 return Ok(Arc::clone(source));
623 }
624 self.check_budget()?;
625 let mut projection_bytes = 0;
626 let (source, projection) = if let Some((graph, Some(semantics))) =
627 self.graphs.get(path).copied()
628 {
629 let keys = self.entities.keys_by_graph.get(path).ok_or_else(|| {
630 CliError::InvalidInput("binding source has no admitted key projection".to_string())
631 })?;
632 (
633 SourceGraph::Staged {
634 graph,
635 semantics: Some(semantics),
636 },
637 SourceProjection::Staged(keys),
638 )
639 } else {
640 let source_limit = self
641 .retained_limit
642 .saturating_sub(self.entities.retained_bytes)
643 .saturating_sub(self.candidates.retained_bytes)
644 .saturating_sub(self.retained_bytes);
645 self.sources.set_retained_limit(source_limit)?;
646 let captured = self.sources.capture(path, self.control)?;
647 self.check_budget()?;
648 let keys = match derive_resolution_keys_with_context(
649 self.project,
650 self.packages.package_name(path),
651 &captured.graph,
652 super::resolution_projection_context(path, self.packages, self.configured)
653 .with_source_semantics(&captured.semantics)
654 .with_retained_bytes_limit(
655 self.retained_limit
656 .saturating_sub(self.retained_staging_bytes()),
657 ),
658 ) {
659 Ok(keys) => keys,
660 Err(ResolutionProjectionError::Contract(failure)) => failure.into_parts().2,
661 Err(ResolutionProjectionError::KeyLimit { requested, .. }) => {
662 return Err(resolution_key_limit_failure(requested));
663 }
664 Err(ResolutionProjectionError::MemoryLimit { limit, requested }) => {
665 return Err(super::resolution_projection_memory_failure(
666 limit, requested,
667 ));
668 }
669 Err(ResolutionProjectionError::SourceEvidenceMismatch) => {
670 return Err(CliError::InvalidInput(
671 "captured binding source changed its ordinal proof".to_string(),
672 ));
673 }
674 };
675 projection_bytes = resolution_retained_bytes(&keys);
676 self.retained_bytes = self.retained_bytes.saturating_add(projection_bytes);
677 (
678 SourceGraph::Captured(captured),
679 SourceProjection::Captured(keys),
680 )
681 };
682 let mut slots = BTreeMap::<String, Vec<usize>>::new();
683 let mut modules = BTreeMap::<String, BTreeSet<CanonicalResolutionKey>>::new();
684 if let Some(facts) = source.semantics() {
685 if facts.exports_complete() {
686 for (index, export) in facts.exports().iter().enumerate() {
687 self.control.check(IndexWorkStage::SymbolParsing)?;
688 if export.namespace == SourceExportNamespace::Value {
689 let bytes = (4 * std::mem::size_of::<usize>()) as u64
690 + if slots.contains_key(&export.name) {
691 0
692 } else {
693 STAGED_GRAPH_ROW_BYTES.saturating_add(export.name.len() as u64)
694 };
695 self.reserve_scratch(bytes)?;
696 slots.entry(export.name.clone()).or_default().push(index);
697 }
698 }
699 }
700 for import in facts.imports() {
701 self.control.check(IndexWorkStage::SymbolParsing)?;
702 if let Some(module) = &import.module {
703 if !modules.contains_key(module) {
704 self.reserve_scratch(
705 STAGED_GRAPH_ROW_BYTES.saturating_add(module.len() as u64),
706 )?;
707 modules.insert(module.clone(), BTreeSet::new());
708 }
709 for key in relation_keys(projection.get(), import.relation) {
710 if !modules.get(module).is_some_and(|keys| keys.contains(key)) {
711 self.reserve_scratch(
712 STAGED_GRAPH_ROW_BYTES
713 .saturating_add(key.canonical_identity().len() as u64),
714 )?;
715 if let Some(keys) = modules.get_mut(module) {
716 keys.insert(key.clone());
717 }
718 }
719 }
720 }
721 }
722 }
723 let vector_rows = modules.values().map(BTreeSet::len).sum::<usize>();
724 self.reserve_scratch(
725 (vector_rows as u64)
726 .saturating_mul(std::mem::size_of::<CanonicalResolutionKey>() as u64),
727 )?;
728 let modules = modules
729 .into_iter()
730 .map(|(name, keys)| (name, keys.into_iter().collect::<Vec<_>>()))
731 .collect::<BTreeMap<_, _>>();
732 let parents = projectatlas_symbols::declaration_identity::qualified_symbol_parents_checked(
733 source.graph(),
734 &mut |bytes| self.reserve_scratch(bytes),
735 )?;
736 self.reserve_scratch(STAGED_GRAPH_ROW_BYTES.saturating_add(path.len() as u64))?;
737 drop(projection);
738 self.retained_bytes = self.retained_bytes.saturating_sub(projection_bytes);
739 let source = Arc::new(BindingSource {
740 source,
741 slots,
742 modules,
743 parents,
744 });
745 self.context.insert(path.to_string(), Arc::clone(&source));
746 self.check_budget()?;
747 Ok(source)
748 }
749
750 fn module_file(&mut self, keys: &[CanonicalResolutionKey]) -> Result<Option<String>, CliError> {
752 if self.loaded.is_some() {
753 let needed = self.with_scratch(|this, scratch| {
754 let mut needed = CauseWorkspace::new(scratch);
755 for key in keys {
756 if !this
757 .loaded
758 .as_ref()
759 .is_some_and(|loaded| loaded.contains(key))
760 {
761 this.insert_cause(&mut needed, key)?;
762 }
763 }
764 let needed = needed.keys.into_iter().collect::<Vec<_>>();
765 if !needed.is_empty() {
766 this.control.check(IndexWorkStage::SymbolParsing)?;
767 let candidates = this
768 .store
769 .repository_resolution_candidates_for_keys_with_byte_limit(
770 this.project,
771 &needed,
772 MAX_INCREMENTAL_RESOLUTION_ITEMS,
773 this.retained_limit
774 .saturating_sub(this.retained_staging_bytes()),
775 )
776 .map_err(super::resolution_read_error)?;
777 if candidates.page.truncated {
778 return Err(dependency_closure_limit(
779 this.sources.root,
780 std::iter::empty(),
781 GraphLimits::MAX_ROWS as usize + 1,
782 ));
783 }
784 this.candidates.retained_limit = this
785 .retained_limit
786 .saturating_sub(this.entities.retained_bytes)
787 .saturating_sub(this.sources.retained_bytes)
788 .saturating_sub(this.retained_bytes);
789 append_persisted_resolution_candidates(
790 this.candidates,
791 this.project,
792 this.generation,
793 candidates,
794 this.replaced,
795 this.control,
796 )?;
797 this.check_budget()?;
798 }
799 let bytes = scratch.retained_bytes;
800 Ok((needed, bytes))
801 })?;
802 if let Some(loaded) = &mut self.loaded {
803 loaded.extend(needed);
804 }
805 }
806 let matches = registry_resolution_matches(
807 keys,
808 ResolutionCandidateKind::Any,
809 self.candidates,
810 &self.entities.entity_by_digest,
811 self.control,
812 )?;
813 let path = if matches.count == 1 {
814 matches.first.and_then(|entity| match entity.selector() {
815 EntitySelector::File { path } => Some(path.as_str().to_string()),
816 _ => None,
817 })
818 } else {
819 None
820 };
821 self.check_budget()?;
822 Ok(path)
823 }
824
825 fn resolve(
827 &mut self,
828 query: BindingQuery,
829 visited: &mut BTreeMap<BindingQuery, usize>,
830 depth: usize,
831 ) -> Result<Arc<CallBindingResolution>, CliError> {
832 self.check_budget()?;
833 if let Some(outcome) = self
834 .queries
835 .get(&query)
836 .and_then(|depths| depths.get(&depth))
837 {
838 return Ok(Arc::clone(outcome));
839 }
840 if depth >= MAX_FORWARDING_DEPTH {
841 return Ok(Arc::new(CallBindingResolution::default()));
842 }
843 let outcome = self.with_scratch(|this, scratch| {
844 let mut outcome = CallBindingResolution::default();
845 let mut causes = CauseWorkspace::new(scratch);
846 causes
847 .scratch
848 .reserve(this, query.retained_bytes().saturating_mul(2))?;
849 visited.insert(query.clone(), depth);
850 let result: Result<(), CliError> = (|| {
851 let source = this.source(&query.source)?;
852 let keys = source
853 .modules
854 .get(query.module.as_ref())
855 .map(Vec::as_slice)
856 .unwrap_or_default();
857 for key in keys {
858 this.insert_cause(&mut causes, key)?;
859 }
860 let key = document_file_resolution_key(this.project, &query.source)?;
861 this.insert_cause(&mut causes, &key)?;
862 if let Some(path) = this.module_file(keys)? {
863 let path = path.as_str();
864 let key = document_file_resolution_key(this.project, path)?;
865 this.insert_cause(&mut causes, &key)?;
866 let target = this.source(path)?;
867 let selected = if let Some(name) = &query.imported {
868 Some((name.as_str(), query.member.as_deref()))
869 } else {
870 query.member.as_deref().map(|member| {
871 member
872 .split_once('.')
873 .map_or((member, None), |(name, member)| (name, Some(member)))
874 })
875 };
876 if let Some((name, member)) = selected
877 && let Some(indices) = target.slots.get(name)
878 && indices.len() == 1
879 && let Some(fact) = target
880 .source
881 .semantics()
882 .and_then(|facts| facts.exports().get(indices[0]))
883 {
884 outcome.followed_export = true;
885 match &fact.binding {
886 SourceExportBinding::LocalDeclaration(index) if member.is_none() => {
887 outcome.target = this.target(&target, *index)?;
888 }
889 SourceExportBinding::ReExport(reference) => {
890 let forwarded_query = this.query(path, reference, member)?;
891 if let Some(first) = visited.get(&forwarded_query).copied() {
892 for (cycle, _) in
895 visited.iter().filter(|(_, position)| **position >= first)
896 {
897 let key = document_file_resolution_key(
898 this.project,
899 &cycle.source,
900 )?;
901 this.insert_cause(&mut causes, &key)?;
902 }
903 } else {
904 let forwarded =
905 this.resolve(forwarded_query, visited, depth + 1)?;
906 outcome.target.clone_from(&forwarded.target);
907 for key in &forwarded.dependencies {
908 this.insert_cause(&mut causes, key)?;
909 }
910 }
911 }
912 SourceExportBinding::LocalDeclaration(_)
913 | SourceExportBinding::RustReExport(_)
914 | SourceExportBinding::AmbiguousDeclarations(_)
915 | SourceExportBinding::UnresolvedDeclarations(_)
916 | SourceExportBinding::Unresolved => {}
917 }
918 }
919 }
920 Ok(())
921 })();
922 visited.remove(&query);
923 result?;
924 outcome.dependencies = causes.keys;
925 let retained_bytes = outcome
926 .retained_bytes()
927 .saturating_add(query.retained_bytes());
928 Ok((outcome, retained_bytes))
929 })?;
930 let outcome = Arc::new(outcome);
931 self.queries
932 .entry(query)
933 .or_default()
934 .insert(depth, Arc::clone(&outcome));
935 Ok(outcome)
936 }
937
938 fn target(
940 &mut self,
941 source: &BindingSource<'_>,
942 index: usize,
943 ) -> Result<Option<GraphEntity>, CliError> {
944 let graph = source.source.graph();
945 let Some(symbol) = graph.symbols.get(index) else {
946 return Ok(None);
947 };
948 if matches!(
949 symbol.kind,
950 SymbolKind::Type
951 | SymbolKind::Interface
952 | SymbolKind::Trait
953 | SymbolKind::Import
954 | SymbolKind::Module
955 ) {
956 return Ok(None);
957 }
958 let parent = source.parents.get(index).cloned().flatten();
959 let expected = source_symbol_entity(self.project, self.generation, graph, symbol, parent)?;
960 if let Some(entity) = self.entities.entity_by_digest.get(expected.key().digest()) {
961 return Ok(Some(entity.clone()));
962 }
963 if let Some(entity) = self
964 .candidates
965 .supplemental_entities_by_digest
966 .get(expected.key().digest())
967 {
968 return Ok(Some(entity.clone()));
969 }
970 if let Some(entity) = self.hydrated.get(expected.key().digest()) {
971 return Ok(entity.clone());
972 }
973 let Some(base_generation) = self.base_generation else {
974 return Ok(None);
975 };
976 self.control.check(IndexWorkStage::SymbolParsing)?;
977 let budget = projectatlas_db::RepositoryGraphReadBudget::new(
978 1,
979 1,
980 projectatlas_db::RepositoryGraphReadBudget::MAX_DECODED_BYTES,
981 1,
982 1,
983 )
984 .map_err(invalid_graph_contract)?;
985 let digest = expected.key().digest().to_string();
986 let stored = self.store.repository_graph_entity_bounded(
987 expected.key(),
988 base_generation,
989 budget,
990 Some(self.control),
991 )?;
992 self.control.check(IndexWorkStage::SymbolParsing)?;
993 let entity = stored
994 .rows
995 .into_iter()
996 .next()
997 .filter(|entity| entity.selector() == expected.selector())
998 .map(|_stored| expected);
999 self.retained_bytes = self
1000 .retained_bytes
1001 .saturating_add(STAGED_GRAPH_ROW_BYTES)
1002 .saturating_add(digest.len() as u64)
1003 .saturating_add(entity.as_ref().map_or(0, entity_retained_bytes));
1004 self.hydrated.insert(digest, entity.clone());
1005 self.check_budget()?;
1006 Ok(entity)
1007 }
1008}
1009
1010#[cfg(test)]
1011mod tests {
1012 use super::{BindingQuery, ModuleInterner, STAGED_GRAPH_ROW_BYTES};
1013 use projectatlas_core::{IndexCancellation, IndexWorkControl};
1014 use projectatlas_symbols::SourceExportBinding;
1015 use std::error::Error;
1016
1017 #[test]
1018 fn forwarding_query_module_identity_uses_values_across_allocations()
1019 -> Result<(), Box<dyn Error>> {
1020 let mut modules = ModuleInterner::default();
1021 let first =
1022 projectatlas_symbols::parse_import_references("import {run} from './target.js';");
1023 let second =
1024 projectatlas_symbols::parse_import_references("import {run} from './target.js';");
1025 let first_reference = first.first().ok_or("first native import missing")?;
1026 let second_reference = second.first().ok_or("second native import missing")?;
1027 if std::sync::Arc::ptr_eq(
1028 &first_reference.shared_module(),
1029 &second_reference.shared_module(),
1030 ) {
1031 return Err("independent imports unexpectedly share one allocation".into());
1032 }
1033 let first = BindingQuery::new("barrel.js", first_reference, None, &mut modules, |_| {
1034 Ok::<(), std::convert::Infallible>(())
1035 })?;
1036 let second = BindingQuery::new("barrel.js", second_reference, None, &mut modules, |_| {
1037 Ok::<(), std::convert::Infallible>(())
1038 })?;
1039 if first != second || first.cmp(&second) != std::cmp::Ordering::Equal {
1040 return Err("equal module values did not retain exact query identity".into());
1041 }
1042 let mut cache = std::collections::BTreeMap::new();
1043 cache.insert(first.clone(), 7);
1044 if cache.get(&second) != Some(&7) {
1045 return Err("equal allocation missed query cache".into());
1046 }
1047 let mut visited = std::collections::BTreeMap::new();
1048 visited.insert(first, 3);
1049 if visited.get(&second) != Some(&3)
1050 || modules.values.len() != 1
1051 || modules.allocations.len() != 2
1052 {
1053 return Err("visited identity or retained module witnesses changed".into());
1054 }
1055 Ok(())
1056 }
1057
1058 #[test]
1059 fn module_identity_admission_preserves_exact_values_and_owned_allocations()
1060 -> Result<(), Box<dyn Error>> {
1061 let mut modules = ModuleInterner::default();
1062 let first: std::sync::Arc<str> = format!("./{}a", "x".repeat(262_144)).into();
1063 let rejected = modules.intern(std::sync::Arc::clone(&first), |bytes| {
1064 super::enforce_resolution_registry_budget_with_limit(bytes, STAGED_GRAPH_ROW_BYTES)
1065 });
1066 if rejected.is_ok() || !modules.values.is_empty() || !modules.allocations.is_empty() {
1067 return Err("refused module admission retained metadata".into());
1068 }
1069 let mut admitted = Vec::new();
1070 let mut admit = |bytes| {
1071 admitted.push(bytes);
1072 Ok::<(), std::convert::Infallible>(())
1073 };
1074 let id = modules.intern(std::sync::Arc::clone(&first), &mut admit)?;
1075 if modules.intern(std::sync::Arc::clone(&first), &mut admit)? != id {
1076 return Err("repeated allocation changed module identity".into());
1077 }
1078 let equal: std::sync::Arc<str> = std::sync::Arc::from(first.as_ref());
1079 let retained = std::sync::Arc::downgrade(&equal);
1080 if modules.intern(equal, &mut admit)? != id || retained.upgrade().is_none() {
1081 return Err("equal value or retained allocation witness changed".into());
1082 }
1083 let different: std::sync::Arc<str> = format!("./{}b", "x".repeat(262_144)).into();
1084 if modules.intern(different, &mut admit)? == id {
1085 return Err("different module values acquired one identity".into());
1086 }
1087 if admitted
1088 != [
1089 STAGED_GRAPH_ROW_BYTES * 2,
1090 STAGED_GRAPH_ROW_BYTES,
1091 STAGED_GRAPH_ROW_BYTES * 2,
1092 ]
1093 || modules.values.len() != 2
1094 || modules.allocations.len() != 3
1095 {
1096 return Err("module metadata admission changed".into());
1097 }
1098 let cancellation = IndexCancellation::new();
1099 cancellation.cancel();
1100 let control = IndexWorkControl::new(cancellation, None);
1101 let result = modules.intern(std::sync::Arc::from("./canceled.js"), |_| {
1102 control.check(projectatlas_core::IndexWorkStage::SymbolParsing)
1103 });
1104 if result.is_ok() || modules.values.len() != 2 || modules.allocations.len() != 3 {
1105 return Err("canceled module admission retained metadata".into());
1106 }
1107 Ok(())
1108 }
1109
1110 #[test]
1111 fn forwarding_queries_share_source_modules_before_cache_lookup() -> Result<(), Box<dyn Error>> {
1112 const SOURCE: &str = "barrel.js";
1113 let module = format!("./{}", "x".repeat(262_144));
1114 let slots = (0..32)
1115 .map(|index| format!("run as e{index}"))
1116 .collect::<Vec<_>>()
1117 .join(",");
1118 let content = format!("export {{{slots}}} from \"{module}\";");
1119 let control = IndexWorkControl::new(IndexCancellation::new(), None);
1120 let (_, _, facts) = projectatlas_symbols::extract_symbol_graph_with_semantics_controlled(
1121 SOURCE,
1122 Some("javascript"),
1123 &content,
1124 &control,
1125 )?
1126 .into_parts();
1127 if facts.exports().len() != 32 {
1128 return Err("native reexport slots missing".into());
1129 }
1130 let mut modules = ModuleInterner::default();
1131 for export in facts.exports() {
1132 let SourceExportBinding::ReExport(reference) = &export.binding else {
1133 return Err("native reexport evidence missing".into());
1134 };
1135 let query = BindingQuery::new(SOURCE, reference, None, &mut modules, |_| {
1136 Ok::<(), std::convert::Infallible>(())
1137 })?;
1138 if query.module.as_ptr() != reference.module().as_ptr() {
1139 return Err("query copied source-owned module before cache lookup".into());
1140 }
1141 let copied = query.clone();
1142 if copied.module.as_ptr() != query.module.as_ptr() {
1143 return Err("resolver frame copied shared query module".into());
1144 }
1145 }
1146 Ok(())
1147 }
1148}