Skip to main content

code_moniker_workspace/snapshot/
model.rs

1use std::mem::size_of;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::time::Duration;
5
6use super::records::RecordTable;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
9pub struct ResourceGeneration(u64);
10
11impl ResourceGeneration {
12	pub fn new(value: u64) -> Self {
13		Self(value)
14	}
15
16	pub fn value(self) -> u64 {
17		self.0
18	}
19}
20
21#[derive(Clone, Debug)]
22pub struct WorkspaceRequest {
23	pub label: String,
24	pub catalog: CatalogRequest,
25	cancellation: WorkspaceCancellation,
26}
27
28impl PartialEq for WorkspaceRequest {
29	fn eq(&self, other: &Self) -> bool {
30		self.label == other.label && self.catalog == other.catalog
31	}
32}
33
34impl Eq for WorkspaceRequest {}
35
36#[derive(Clone, Debug, Default)]
37pub struct WorkspaceCancellation(Arc<AtomicBool>);
38
39impl WorkspaceCancellation {
40	pub fn cancel(&self) {
41		self.0.store(true, Ordering::Release);
42	}
43
44	pub fn is_cancelled(&self) -> bool {
45		self.0.load(Ordering::Acquire)
46	}
47
48	pub fn check(&self, resource: WorkspaceResource) -> WorkspaceResult<()> {
49		if self.is_cancelled() {
50			return Err(WorkspaceFailure::new(resource, "workspace build cancelled"));
51		}
52		Ok(())
53	}
54}
55
56#[derive(Clone, Copy, Debug, Eq, PartialEq)]
57pub enum CatalogRequest {
58	Refresh,
59	ReuseCurrent,
60}
61
62impl WorkspaceRequest {
63	pub fn new(label: impl Into<String>) -> Self {
64		Self {
65			label: label.into(),
66			catalog: CatalogRequest::Refresh,
67			cancellation: WorkspaceCancellation::default(),
68		}
69	}
70
71	pub fn with_cancellation(mut self, cancellation: WorkspaceCancellation) -> Self {
72		self.cancellation = cancellation;
73		self
74	}
75
76	pub fn cancellation(&self) -> &WorkspaceCancellation {
77		&self.cancellation
78	}
79
80	pub fn reuse_current_catalog(mut self) -> Self {
81		self.catalog = CatalogRequest::ReuseCurrent;
82		self
83	}
84
85	pub fn should_reuse_current_catalog(&self) -> bool {
86		self.catalog == CatalogRequest::ReuseCurrent
87	}
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
91pub struct SourceId {
92	file: u32,
93}
94
95impl SourceId {
96	pub fn at(file: usize) -> Self {
97		Self { file: file as u32 }
98	}
99
100	pub fn parse(value: &str) -> Option<Self> {
101		let rest = value.strip_prefix("source:")?;
102		let file = rest.split(':').next()?;
103		Some(Self {
104			file: file.parse().ok()?,
105		})
106	}
107
108	pub fn file(self) -> usize {
109		self.file as usize
110	}
111}
112
113impl std::fmt::Display for SourceId {
114	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115		write!(formatter, "source:{}", self.file)
116	}
117}
118
119#[derive(Clone, Debug, Eq, PartialEq)]
120pub struct SourceUnit {
121	pub id: SourceId,
122	pub display_name: String,
123	pub language: Option<String>,
124}
125
126impl SourceUnit {
127	pub fn new(id: SourceId, display_name: impl Into<String>) -> Self {
128		Self {
129			id,
130			display_name: display_name.into(),
131			language: None,
132		}
133	}
134
135	pub fn with_language(
136		id: SourceId,
137		display_name: impl Into<String>,
138		language: impl Into<String>,
139	) -> Self {
140		Self {
141			id,
142			display_name: display_name.into(),
143			language: Some(language.into()),
144		}
145	}
146}
147
148#[derive(Clone, Debug, Eq, PartialEq)]
149pub struct SourceCatalog {
150	pub generation: ResourceGeneration,
151	pub sources: Vec<SourceUnit>,
152}
153
154impl SourceCatalog {
155	pub fn new(generation: ResourceGeneration, mut sources: Vec<SourceUnit>) -> Self {
156		sources.shrink_to_fit();
157		Self {
158			generation,
159			sources,
160		}
161	}
162}
163
164#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
165pub struct SymbolId {
166	file: u32,
167	def: u32,
168}
169
170impl SymbolId {
171	pub fn at(file: usize, def: usize) -> Self {
172		Self {
173			file: file as u32,
174			def: def as u32,
175		}
176	}
177
178	pub fn parse(value: &str) -> Option<Self> {
179		let rest = value.strip_prefix("symbol:")?;
180		let (file, def) = rest.split_once(':')?;
181		Some(Self {
182			file: file.parse().ok()?,
183			def: def.parse().ok()?,
184		})
185	}
186
187	pub fn file(self) -> usize {
188		self.file as usize
189	}
190
191	pub fn def(self) -> usize {
192		self.def as usize
193	}
194}
195
196impl std::fmt::Display for SymbolId {
197	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198		write!(formatter, "symbol:{}:{}", self.file, self.def)
199	}
200}
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
203pub struct SymbolLocation {
204	pub file: usize,
205	pub symbol: usize,
206}
207
208#[derive(Clone, Debug, Eq, PartialEq)]
209pub struct SymbolRecord {
210	pub id: SymbolId,
211	pub source: SourceId,
212	pub identity: Arc<str>,
213	pub name: String,
214	pub kind: String,
215	pub visibility: String,
216	pub signature: String,
217	pub call_name: Option<String>,
218	pub call_arity: Option<usize>,
219	pub navigable: bool,
220	pub line_range: Option<(u32, u32)>,
221	pub parent: Option<SymbolId>,
222}
223
224impl SymbolRecord {
225	pub fn new(
226		id: SymbolId,
227		source: SourceId,
228		name: impl Into<String>,
229		kind: impl Into<String>,
230	) -> Self {
231		Self {
232			identity: Arc::from(id.to_string()),
233			id,
234			source,
235			name: name.into(),
236			kind: kind.into(),
237			visibility: String::new(),
238			signature: String::new(),
239			call_name: None,
240			call_arity: None,
241			navigable: true,
242			line_range: None,
243			parent: None,
244		}
245	}
246}
247
248#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
249pub struct ReferenceId {
250	file: u32,
251	reference: u32,
252}
253
254impl ReferenceId {
255	pub fn at(file: usize, reference: usize) -> Self {
256		Self {
257			file: file as u32,
258			reference: reference as u32,
259		}
260	}
261
262	pub fn parse(value: &str) -> Option<Self> {
263		let rest = value.strip_prefix("reference:")?;
264		let (file, reference) = rest.split_once(':')?;
265		Some(Self {
266			file: file.parse().ok()?,
267			reference: reference.parse().ok()?,
268		})
269	}
270
271	pub fn file(self) -> usize {
272		self.file as usize
273	}
274
275	pub fn reference(self) -> usize {
276		self.reference as usize
277	}
278}
279
280impl std::fmt::Display for ReferenceId {
281	fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
282		write!(formatter, "reference:{}:{}", self.file, self.reference)
283	}
284}
285
286#[derive(Clone, Debug, Eq, PartialEq)]
287pub struct ReferenceRecord {
288	pub id: ReferenceId,
289	pub source: SourceId,
290	pub source_symbol: SymbolId,
291	pub target_identity: Arc<str>,
292	pub kind: String,
293	pub call_name: Option<String>,
294	pub call_arity: Option<usize>,
295	pub confidence: Option<String>,
296	pub receiver: Option<String>,
297	pub alias: Option<String>,
298	pub line_range: Option<(u32, u32)>,
299}
300
301impl ReferenceRecord {
302	pub fn new(
303		id: ReferenceId,
304		source: SourceId,
305		source_symbol: SymbolId,
306		target_identity: impl Into<Arc<str>>,
307		kind: impl Into<String>,
308		line_range: Option<(u32, u32)>,
309	) -> Self {
310		Self {
311			id,
312			source,
313			source_symbol,
314			target_identity: target_identity.into(),
315			kind: kind.into(),
316			call_name: None,
317			call_arity: None,
318			confidence: None,
319			receiver: None,
320			alias: None,
321			line_range,
322		}
323	}
324
325	pub fn with_call_metadata(
326		mut self,
327		call_name: Option<String>,
328		call_arity: Option<usize>,
329	) -> Self {
330		self.call_name = call_name;
331		self.call_arity = call_arity;
332		self
333	}
334
335	pub fn with_metadata(
336		mut self,
337		confidence: Option<String>,
338		receiver: Option<String>,
339		alias: Option<String>,
340	) -> Self {
341		self.confidence = confidence;
342		self.receiver = receiver;
343		self.alias = alias;
344		self
345	}
346}
347
348#[derive(Clone, Debug, Eq, PartialEq)]
349pub struct SourceFileRecord {
350	pub id: SourceId,
351	pub uri: String,
352	pub source_root: usize,
353	pub path: String,
354	pub rel_path: String,
355	pub anchor: String,
356	pub language: String,
357	pub text: String,
358}
359
360#[derive(Clone, Debug, Eq, PartialEq)]
361pub struct CodeIndex {
362	pub generation: ResourceGeneration,
363	pub catalog_generation: ResourceGeneration,
364	pub identity_scheme: String,
365	pub sources: Vec<SourceFileRecord>,
366	pub symbols: RecordTable<SymbolRecord>,
367	pub references: RecordTable<ReferenceRecord>,
368	pub inventory: Arc<super::SymbolInventoryIndex>,
369	pub timings: CodeIndexTimings,
370}
371
372#[derive(Clone, Debug, Default, Eq, PartialEq)]
373pub struct CodeIndexTimings {
374	pub extract_sources: Duration,
375	pub semantic_index: Duration,
376	pub total: Duration,
377	pub extraction: Vec<ExtractionMeasurement>,
378}
379
380#[derive(Clone, Debug, Default, Eq, PartialEq)]
381pub struct ExtractionMeasurement {
382	pub language: &'static str,
383	pub cache: &'static str,
384	pub files: usize,
385	pub source_bytes: usize,
386	pub duration: Duration,
387}
388
389impl CodeIndex {
390	pub fn new(
391		generation: ResourceGeneration,
392		catalog_generation: ResourceGeneration,
393		symbols: Vec<SymbolRecord>,
394	) -> Self {
395		Self::with_references(generation, catalog_generation, symbols, Vec::new())
396	}
397
398	pub fn with_references(
399		generation: ResourceGeneration,
400		catalog_generation: ResourceGeneration,
401		symbols: Vec<SymbolRecord>,
402		references: Vec<ReferenceRecord>,
403	) -> Self {
404		let sources = Vec::new();
405		let symbols = RecordTable::from_records(symbols);
406		let inventory = Arc::new(super::SymbolInventoryIndex::build(
407			generation, &sources, &symbols,
408		));
409		Self {
410			generation,
411			catalog_generation,
412			identity_scheme: crate::DEFAULT_IDENTITY_SCHEME.to_string(),
413			sources,
414			symbols,
415			references: RecordTable::from_records(references),
416			inventory,
417			timings: CodeIndexTimings::default(),
418		}
419	}
420}
421
422#[derive(Clone, Debug, Eq, PartialEq)]
423pub struct LinkageEdge {
424	pub reference: ReferenceId,
425	pub target: SymbolId,
426	pub evidence: ResolutionEvidence,
427}
428
429impl LinkageEdge {
430	pub fn new(reference: ReferenceId, target: SymbolId) -> Self {
431		Self::with_evidence(reference, target, ResolutionEvidence::ExactBinding)
432	}
433
434	pub fn with_evidence(
435		reference: ReferenceId,
436		target: SymbolId,
437		evidence: ResolutionEvidence,
438	) -> Self {
439		Self {
440			reference,
441			target,
442			evidence,
443		}
444	}
445}
446
447#[derive(Clone, Copy, Debug, Eq, PartialEq)]
448pub enum ResolutionEvidence {
449	ExactBinding,
450	LocalBinding,
451	GlobalBinding,
452	TypeConstraint,
453	Mro,
454	Injected,
455	NameMatch,
456}
457
458impl ResolutionEvidence {
459	pub fn as_str(self) -> &'static str {
460		match self {
461			Self::ExactBinding => "exact_binding",
462			Self::LocalBinding => "local_binding",
463			Self::GlobalBinding => "global_binding",
464			Self::TypeConstraint => "type_constraint",
465			Self::Mro => "mro",
466			Self::Injected => "injected",
467			Self::NameMatch => "name_match",
468		}
469	}
470
471	pub fn rank(self) -> u8 {
472		match self {
473			Self::ExactBinding => 100,
474			Self::LocalBinding | Self::TypeConstraint => 90,
475			Self::Mro => 85,
476			Self::GlobalBinding | Self::Injected => 80,
477			Self::NameMatch => 10,
478		}
479	}
480}
481
482#[derive(Clone, Copy, Debug, Eq, PartialEq)]
483pub enum CandidateReason {
484	WeakNameMatch,
485	MultipleTargets,
486	AmbiguousLookup,
487}
488
489impl CandidateReason {
490	pub fn as_str(self) -> &'static str {
491		match self {
492			Self::WeakNameMatch => "weak_name_match",
493			Self::MultipleTargets => "multiple_targets",
494			Self::AmbiguousLookup => "ambiguous_lookup",
495		}
496	}
497}
498
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum CandidateScope {
501	Local,
502	Global,
503	Builtin,
504	Injected,
505	Unknown,
506}
507
508impl CandidateScope {
509	pub fn as_str(self) -> &'static str {
510		match self {
511			Self::Local => "local",
512			Self::Global => "global",
513			Self::Builtin => "builtin",
514			Self::Injected => "injected",
515			Self::Unknown => "unknown",
516		}
517	}
518}
519
520#[derive(Clone, Debug, Eq, PartialEq)]
521pub struct CandidateReference {
522	pub reference: ReferenceId,
523	pub targets: Vec<SymbolId>,
524	pub reason: CandidateReason,
525	pub scope: CandidateScope,
526	pub evidence: ResolutionEvidence,
527}
528
529impl CandidateReference {
530	pub fn new(
531		reference: ReferenceId,
532		targets: Vec<SymbolId>,
533		reason: CandidateReason,
534		scope: CandidateScope,
535		evidence: ResolutionEvidence,
536	) -> Self {
537		Self {
538			reference,
539			targets,
540			reason,
541			scope,
542			evidence,
543		}
544	}
545}
546
547#[derive(Clone, Copy, Debug, Eq, PartialEq)]
548pub enum DynamicReason {
549	ConditionalCompilation,
550	DynamicAttribute,
551	DescriptorOrFrameworkInjected,
552	DuckTypedCandidateSet,
553	MixinContract,
554	ExternalDependencyUnindexed,
555	RuntimeImport,
556	RuntimeMutation,
557	PreprocessorExpansion,
558	InsufficientLocalFacts,
559}
560
561impl DynamicReason {
562	pub fn as_str(self) -> &'static str {
563		match self {
564			Self::ConditionalCompilation => "conditional_compilation",
565			Self::DynamicAttribute => "dynamic_attribute",
566			Self::DescriptorOrFrameworkInjected => "descriptor_or_framework_injected",
567			Self::DuckTypedCandidateSet => "duck_typed_candidate_set",
568			Self::MixinContract => "mixin_contract",
569			Self::ExternalDependencyUnindexed => "external_dependency_unindexed",
570			Self::RuntimeImport => "runtime_import",
571			Self::RuntimeMutation => "runtime_mutation",
572			Self::PreprocessorExpansion => "preprocessor_expansion",
573			Self::InsufficientLocalFacts => "insufficient_local_facts",
574		}
575	}
576}
577
578#[derive(Clone, Debug, Eq, PartialEq)]
579pub struct DynamicReference {
580	pub reference: ReferenceId,
581	pub target_identity: Arc<str>,
582	pub reason: DynamicReason,
583	pub candidates: Vec<SymbolId>,
584}
585
586impl DynamicReference {
587	pub fn new(
588		reference: ReferenceId,
589		target_identity: impl Into<Arc<str>>,
590		reason: DynamicReason,
591		candidates: Vec<SymbolId>,
592	) -> Self {
593		Self {
594			reference,
595			target_identity: target_identity.into(),
596			reason,
597			candidates,
598		}
599	}
600}
601
602#[derive(Clone, Copy, Debug, Eq, PartialEq)]
603pub enum ExternalReferenceOrigin {
604	Sdk,
605	Dependency,
606	Injected,
607	UnknownExternal,
608}
609
610impl ExternalReferenceOrigin {
611	pub fn label(self) -> &'static str {
612		match self {
613			Self::Sdk => "sdk",
614			Self::Dependency => "dependency",
615			Self::Injected => "injected",
616			Self::UnknownExternal => "unknown_external",
617		}
618	}
619}
620
621#[derive(Clone, Debug, Eq, PartialEq)]
622pub struct ExternalReference {
623	pub reference: ReferenceId,
624	pub target_identity: Arc<str>,
625	pub origin: ExternalReferenceOrigin,
626}
627
628impl ExternalReference {
629	pub fn new(
630		reference: ReferenceId,
631		target_identity: impl Into<Arc<str>>,
632		origin: ExternalReferenceOrigin,
633	) -> Self {
634		Self {
635			reference,
636			target_identity: target_identity.into(),
637			origin,
638		}
639	}
640}
641
642/// Why linkage could not bind a reference. The taxonomy is part of the
643/// resolution contract: consumers can split external-by-design from real
644/// resolution gaps instead of reading one opaque count.
645#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
646pub enum UnresolvedReason {
647	ManifestBlocked,
648	Visibility,
649	LanguageBoundary,
650	MissingQuery,
651	NoCandidate,
652	Ambiguous,
653	UnsupportedLanguageRule,
654	IncompleteExtractorMetadata,
655}
656
657impl UnresolvedReason {
658	pub fn as_str(&self) -> &'static str {
659		match self {
660			Self::ManifestBlocked => "manifest_blocked",
661			Self::Visibility => "visibility",
662			Self::LanguageBoundary => "language_boundary",
663			Self::MissingQuery => "missing_query",
664			Self::NoCandidate => "no_candidate",
665			Self::Ambiguous => "ambiguous",
666			Self::UnsupportedLanguageRule => "unsupported_language_rule",
667			Self::IncompleteExtractorMetadata => "incomplete_extractor_metadata",
668		}
669	}
670}
671
672#[derive(Clone, Debug, Eq, PartialEq)]
673pub struct UnresolvedReference {
674	pub reference: ReferenceId,
675	pub target_identity: Arc<str>,
676	pub reason: UnresolvedReason,
677}
678
679impl UnresolvedReference {
680	pub fn new(
681		reference: ReferenceId,
682		target_identity: impl Into<Arc<str>>,
683		reason: UnresolvedReason,
684	) -> Self {
685		Self {
686			reference,
687			target_identity: target_identity.into(),
688			reason,
689		}
690	}
691}
692
693#[derive(Clone, Debug, Eq, PartialEq)]
694pub struct LinkageSnapshot {
695	pub generation: ResourceGeneration,
696	pub index_generation: ResourceGeneration,
697	pub resolved_refs: usize,
698	pub candidate_refs: usize,
699	pub external_refs: usize,
700	pub dynamic_refs: usize,
701	pub blocked_refs: usize,
702	/// Compatibility counter for manifest-policy blocks.
703	pub manifest_blocked_refs: usize,
704	pub unresolved_refs: usize,
705	/// Compatibility counter for existing consumers. Candidate references are
706	/// now stored separately and never projected as graph edges.
707	pub ambiguous_refs: usize,
708	pub resolved: Vec<LinkageEdge>,
709	pub candidates: Vec<CandidateReference>,
710	pub external: Vec<ExternalReference>,
711	pub dynamic: Vec<DynamicReference>,
712	pub blocked: Vec<UnresolvedReference>,
713	/// Compatibility view containing only `ManifestBlocked` entries.
714	pub manifest_blocked: Vec<UnresolvedReference>,
715	pub unresolved: Vec<UnresolvedReference>,
716	pub read_index: LinkageReadIndexHandle,
717}
718
719#[derive(Debug)]
720pub struct LinkageReadIndex {
721	pub(crate) incoming: rustc_hash::FxHashMap<SymbolId, Vec<ReferenceId>>,
722	pub(crate) targets: rustc_hash::FxHashMap<ReferenceId, SymbolId>,
723	pub(crate) ordinals: rustc_hash::FxHashMap<SymbolId, u32>,
724	pub(crate) symbols: rustc_hash::FxHashMap<u32, SymbolId>,
725	pub(crate) outgoing: OutgoingPathIndex,
726}
727
728type OutgoingPathIndex =
729	rustc_hash::FxHashMap<u32, rustc_hash::FxHashMap<Arc<str>, Vec<ReferenceId>>>;
730
731impl LinkageReadIndex {
732	pub(crate) fn estimated_heap_bytes(&self) -> usize {
733		let incoming = self.incoming.capacity()
734			* (size_of::<SymbolId>() + size_of::<Vec<ReferenceId>>())
735			+ self
736				.incoming
737				.values()
738				.map(|references| references.capacity() * size_of::<ReferenceId>())
739				.sum::<usize>();
740		let targets = self.targets.capacity() * (size_of::<ReferenceId>() + size_of::<SymbolId>());
741		let ordinals = self.ordinals.capacity() * (size_of::<SymbolId>() + size_of::<u32>());
742		let symbols = self.symbols.capacity() * (size_of::<u32>() + size_of::<SymbolId>());
743		let mut relation_strings = std::collections::HashSet::<(usize, usize)>::new();
744		let outgoing = self.outgoing.capacity()
745			* (size_of::<u32>() + size_of::<rustc_hash::FxHashMap<Arc<str>, Vec<ReferenceId>>>())
746			+ self
747				.outgoing
748				.values()
749				.map(|relations| {
750					relations.capacity() * (size_of::<Arc<str>>() + size_of::<Vec<ReferenceId>>())
751						+ relations
752							.iter()
753							.map(|(relation, references)| {
754								let string_bytes = if relation_strings
755									.insert((relation.as_ptr() as usize, relation.len()))
756								{
757									relation.len()
758								} else {
759									0
760								};
761								string_bytes + references.capacity() * size_of::<ReferenceId>()
762							})
763							.sum::<usize>()
764				})
765				.sum::<usize>();
766		incoming + targets + ordinals + symbols + outgoing
767	}
768
769	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
770		let mut incoming = rustc_hash::FxHashMap::<SymbolId, Vec<ReferenceId>>::default();
771		let mut targets = rustc_hash::FxHashMap::<ReferenceId, SymbolId>::default();
772		for edge in edges {
773			let LinkageEdge {
774				reference, target, ..
775			} = edge.clone();
776			targets.entry(reference).or_insert(target);
777			incoming.entry(target).or_default().push(reference);
778		}
779		Self {
780			incoming,
781			targets,
782			ordinals: rustc_hash::FxHashMap::default(),
783			symbols: rustc_hash::FxHashMap::default(),
784			outgoing: rustc_hash::FxHashMap::default(),
785		}
786	}
787
788	pub(crate) fn from_edges_with_ordinals(
789		edges: &[LinkageEdge],
790		references: &RecordTable<ReferenceRecord>,
791		ordinals: impl IntoIterator<Item = (u32, SymbolId)>,
792	) -> Self {
793		let mut index = Self::from_edges(edges);
794		for (ordinal, symbol) in ordinals {
795			index.symbols.insert(ordinal, symbol);
796			index.ordinals.insert(symbol, ordinal);
797		}
798		index.outgoing = outgoing_path_index(references, &index.ordinals);
799		index
800	}
801
802	pub fn incoming(&self, symbol: &SymbolId) -> &[ReferenceId] {
803		self.incoming.get(symbol).map(Vec::as_slice).unwrap_or(&[])
804	}
805
806	pub fn resolved_target(&self, reference: &ReferenceId) -> Option<&SymbolId> {
807		self.targets.get(reference)
808	}
809
810	pub(crate) fn ordinal(&self, symbol: &SymbolId) -> Option<u32> {
811		self.ordinals.get(symbol).copied()
812	}
813
814	pub(crate) fn symbol(&self, ordinal: u32) -> Option<SymbolId> {
815		self.symbols.get(&ordinal).copied()
816	}
817
818	pub(crate) fn outgoing(&self, ordinal: u32, relation: &str) -> &[ReferenceId] {
819		self.outgoing
820			.get(&ordinal)
821			.and_then(|by_relation| by_relation.get(relation))
822			.map(Vec::as_slice)
823			.unwrap_or(&[])
824	}
825
826	pub(crate) fn outgoing_relations(&self, ordinal: u32) -> impl Iterator<Item = &str> {
827		self.outgoing
828			.get(&ordinal)
829			.into_iter()
830			.flat_map(|by_relation| by_relation.keys().map(AsRef::as_ref))
831	}
832
833	#[cfg(test)]
834	pub(crate) fn active_symbol_slots(&self) -> usize {
835		self.symbols.len()
836	}
837}
838
839fn outgoing_path_index(
840	references: &RecordTable<ReferenceRecord>,
841	ordinals: &rustc_hash::FxHashMap<SymbolId, u32>,
842) -> OutgoingPathIndex {
843	let mut index = OutgoingPathIndex::default();
844	for reference in references.iter() {
845		let Some(source_ordinal) = ordinals.get(&reference.source_symbol).copied() else {
846			continue;
847		};
848		let by_relation = index.entry(source_ordinal).or_default();
849		if let Some(outgoing) = by_relation.get_mut(reference.kind.as_str()) {
850			outgoing.push(reference.id);
851		} else {
852			by_relation.insert(Arc::from(reference.kind.as_str()), vec![reference.id]);
853		}
854	}
855	index
856}
857
858#[derive(Clone, Debug, Default)]
859pub struct LinkageReadIndexHandle(Option<Arc<LinkageReadIndex>>);
860
861impl LinkageReadIndexHandle {
862	pub(crate) fn estimated_heap_bytes(&self) -> usize {
863		self.0.as_deref().map_or(0, |index| {
864			size_of::<LinkageReadIndex>() + index.estimated_heap_bytes()
865		})
866	}
867
868	pub fn from_edges(edges: &[LinkageEdge]) -> Self {
869		Self(Some(Arc::new(LinkageReadIndex::from_edges(edges))))
870	}
871
872	pub(crate) fn from_edges_with_ordinals(
873		edges: &[LinkageEdge],
874		references: &RecordTable<ReferenceRecord>,
875		ordinals: impl IntoIterator<Item = (u32, SymbolId)>,
876	) -> Self {
877		Self(Some(Arc::new(LinkageReadIndex::from_edges_with_ordinals(
878			edges, references, ordinals,
879		))))
880	}
881
882	pub fn get(&self) -> Option<&LinkageReadIndex> {
883		self.0.as_deref()
884	}
885}
886
887impl PartialEq for LinkageReadIndexHandle {
888	fn eq(&self, _other: &Self) -> bool {
889		true
890	}
891}
892
893impl Eq for LinkageReadIndexHandle {}
894
895impl LinkageSnapshot {
896	pub fn new(
897		generation: ResourceGeneration,
898		index_generation: ResourceGeneration,
899		resolved_refs: usize,
900		unresolved_refs: usize,
901	) -> Self {
902		Self {
903			generation,
904			index_generation,
905			resolved_refs,
906			candidate_refs: 0,
907			external_refs: 0,
908			dynamic_refs: 0,
909			blocked_refs: 0,
910			manifest_blocked_refs: 0,
911			unresolved_refs,
912			ambiguous_refs: 0,
913			resolved: Vec::new(),
914			candidates: Vec::new(),
915			external: Vec::new(),
916			dynamic: Vec::new(),
917			blocked: Vec::new(),
918			manifest_blocked: Vec::new(),
919			unresolved: Vec::new(),
920			read_index: LinkageReadIndexHandle::default(),
921		}
922	}
923
924	pub fn with_refs(
925		generation: ResourceGeneration,
926		index_generation: ResourceGeneration,
927		mut resolved: Vec<LinkageEdge>,
928		mut unresolved: Vec<UnresolvedReference>,
929	) -> Self {
930		resolved.shrink_to_fit();
931		unresolved.shrink_to_fit();
932		let read_index = LinkageReadIndexHandle::from_edges(&resolved);
933		Self {
934			generation,
935			index_generation,
936			resolved_refs: resolved.len(),
937			candidate_refs: 0,
938			external_refs: 0,
939			dynamic_refs: 0,
940			blocked_refs: 0,
941			manifest_blocked_refs: 0,
942			unresolved_refs: unresolved.len(),
943			ambiguous_refs: 0,
944			resolved,
945			candidates: Vec::new(),
946			external: Vec::new(),
947			dynamic: Vec::new(),
948			blocked: Vec::new(),
949			manifest_blocked: Vec::new(),
950			unresolved,
951			read_index,
952		}
953	}
954}
955
956#[derive(Clone, Debug, Eq, PartialEq)]
957pub struct ChangeOverlay {
958	pub generation: ResourceGeneration,
959	pub catalog_generation: ResourceGeneration,
960	pub index_generation: ResourceGeneration,
961	pub scope: String,
962	pub resources: Vec<ChangeResource>,
963	pub diagnostics: Vec<String>,
964	pub changed_symbols: Vec<SymbolId>,
965	pub changes: Vec<ChangeRecord>,
966}
967
968pub struct ChangeOverlayReport {
969	pub generation: ResourceGeneration,
970	pub catalog_generation: ResourceGeneration,
971	pub index_generation: ResourceGeneration,
972	pub scope: String,
973	pub resources: Vec<ChangeResource>,
974	pub diagnostics: Vec<String>,
975	pub changes: Vec<ChangeRecord>,
976}
977
978impl ChangeOverlay {
979	pub fn new(
980		generation: ResourceGeneration,
981		catalog_generation: ResourceGeneration,
982		index_generation: ResourceGeneration,
983		mut changed_symbols: Vec<SymbolId>,
984	) -> Self {
985		changed_symbols.shrink_to_fit();
986		Self {
987			generation,
988			catalog_generation,
989			index_generation,
990			scope: "HEAD..worktree".to_string(),
991			resources: Vec::new(),
992			diagnostics: Vec::new(),
993			changed_symbols,
994			changes: Vec::new(),
995		}
996	}
997
998	pub fn with_records(
999		generation: ResourceGeneration,
1000		catalog_generation: ResourceGeneration,
1001		index_generation: ResourceGeneration,
1002		mut changes: Vec<ChangeRecord>,
1003	) -> Self {
1004		changes.shrink_to_fit();
1005		let changed_symbols = changes.iter().filter_map(|change| change.symbol).fold(
1006			Vec::new(),
1007			|mut out, symbol| {
1008				if !out.contains(&symbol) {
1009					out.push(symbol);
1010				}
1011				out
1012			},
1013		);
1014		let mut changed_symbols = changed_symbols;
1015		changed_symbols.shrink_to_fit();
1016		Self {
1017			generation,
1018			catalog_generation,
1019			index_generation,
1020			scope: "HEAD..worktree".to_string(),
1021			resources: Vec::new(),
1022			diagnostics: Vec::new(),
1023			changed_symbols,
1024			changes,
1025		}
1026	}
1027
1028	pub fn from_report(report: ChangeOverlayReport) -> Self {
1029		let mut resources = report.resources;
1030		let mut diagnostics = report.diagnostics;
1031		resources.shrink_to_fit();
1032		diagnostics.shrink_to_fit();
1033		let mut overlay = Self::with_records(
1034			report.generation,
1035			report.catalog_generation,
1036			report.index_generation,
1037			report.changes,
1038		);
1039		overlay.scope = report.scope;
1040		overlay.resources = resources;
1041		overlay.diagnostics = diagnostics;
1042		overlay
1043	}
1044}
1045
1046#[derive(Clone, Debug, Eq, PartialEq)]
1047pub struct ChangeResource {
1048	pub available: bool,
1049	pub label: String,
1050	pub message: String,
1051}
1052
1053#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1054pub enum ChangeStatus {
1055	Added,
1056	Modified,
1057	Removed,
1058}
1059
1060impl ChangeStatus {
1061	pub fn label(self) -> &'static str {
1062		match self {
1063			Self::Added => "added",
1064			Self::Modified => "modified",
1065			Self::Removed => "removed",
1066		}
1067	}
1068}
1069
1070#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
1071pub struct ChangeId(String);
1072
1073impl ChangeId {
1074	pub fn new(value: impl Into<String>) -> Self {
1075		Self(value.into())
1076	}
1077
1078	pub fn as_str(&self) -> &str {
1079		&self.0
1080	}
1081}
1082
1083#[derive(Clone, Debug, Eq, PartialEq)]
1084pub struct ChangeRecord {
1085	pub id: ChangeId,
1086	pub status: ChangeStatus,
1087	pub source: Option<SourceId>,
1088	pub source_uri: Option<String>,
1089	pub symbol: Option<SymbolId>,
1090	pub identity: String,
1091	pub language: String,
1092	pub file_path: String,
1093	pub name: String,
1094	pub kind: String,
1095	pub line_range: Option<(u32, u32)>,
1096	pub hunk_count: usize,
1097}
1098
1099#[derive(Clone, Debug, Eq, PartialEq)]
1100pub struct ChangeRecordCoreFields {
1101	pub id: ChangeId,
1102	pub status: ChangeStatus,
1103	pub identity: String,
1104	pub language: String,
1105	pub file_path: String,
1106	pub name: String,
1107	pub kind: String,
1108	pub line_range: Option<(u32, u32)>,
1109	pub hunk_count: usize,
1110}
1111
1112impl ChangeRecord {
1113	pub fn new(fields: ChangeRecordCoreFields) -> Self {
1114		Self {
1115			id: fields.id,
1116			status: fields.status,
1117			source: None,
1118			source_uri: None,
1119			symbol: None,
1120			identity: fields.identity,
1121			language: fields.language,
1122			file_path: fields.file_path,
1123			name: fields.name,
1124			kind: fields.kind,
1125			line_range: fields.line_range,
1126			hunk_count: fields.hunk_count,
1127		}
1128	}
1129
1130	pub fn with_source(mut self, source: SourceId, source_uri: impl Into<String>) -> Self {
1131		self.source = Some(source);
1132		self.source_uri = Some(source_uri.into());
1133		self
1134	}
1135
1136	pub fn with_symbol(mut self, symbol: SymbolId) -> Self {
1137		self.symbol = Some(symbol);
1138		self
1139	}
1140}
1141
1142#[derive(Clone, Debug, Eq, PartialEq)]
1143pub struct WorkspaceSnapshot {
1144	pub generation: ResourceGeneration,
1145	pub catalog: SourceCatalog,
1146	pub index: CodeIndex,
1147	pub linkage: LinkageSnapshot,
1148	pub changes: ChangeOverlay,
1149	pub timings: WorkspaceTimings,
1150}
1151
1152#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
1153pub struct WorkspaceTimings {
1154	pub source_catalog: Duration,
1155	pub extract_sources: Duration,
1156	pub semantic_index: Duration,
1157	pub code_index: Duration,
1158	pub linkage: Duration,
1159	pub change_overlay: Duration,
1160	pub total: Duration,
1161}
1162
1163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1164pub enum WorkspaceResource {
1165	SourceCatalog,
1166	CodeIndex,
1167	LinkageSnapshot,
1168	ChangeOverlay,
1169}
1170
1171#[derive(Clone, Debug, Eq, PartialEq)]
1172pub struct WorkspaceFailure {
1173	pub resource: WorkspaceResource,
1174	pub message: String,
1175}
1176
1177impl WorkspaceFailure {
1178	pub fn new(resource: WorkspaceResource, message: impl Into<String>) -> Self {
1179		Self {
1180			resource,
1181			message: message.into(),
1182		}
1183	}
1184}
1185
1186pub type WorkspaceResult<T> = Result<T, WorkspaceFailure>;
1187
1188#[derive(Clone, Debug, Eq, PartialEq)]
1189pub enum WorkspaceTransition {
1190	Ready {
1191		generation: ResourceGeneration,
1192	},
1193	Failed {
1194		failure: WorkspaceFailure,
1195		preserved_generation: Option<ResourceGeneration>,
1196	},
1197}