Skip to main content

code_moniker_workspace/snapshot/inventory/
mod.rs

1mod catalog;
2mod facets;
3mod set;
4
5use std::collections::BTreeSet;
6use std::hash::{Hash, Hasher};
7use std::sync::Arc;
8
9use code_moniker_core::core::shape::Shape;
10use code_moniker_core::core::uri::{UriConfig, from_uri};
11use rustc_hash::FxHashMap;
12
13use super::{RecordTable, ResourceGeneration, SourceFileRecord, SourceId, SymbolId, SymbolRecord};
14
15pub use catalog::SymbolOrdinalCatalog;
16pub use facets::SymbolInventoryFacets;
17pub use set::SymbolSet;
18
19#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
20pub struct SymbolOrdinal(u32);
21
22impl SymbolOrdinal {
23	pub fn from_index(value: usize) -> Self {
24		assert!(
25			u32::try_from(value).is_ok(),
26			"symbol index exceeds u32 range"
27		);
28		Self(value as u32)
29	}
30
31	pub fn as_usize(self) -> usize {
32		self.0 as usize
33	}
34
35	pub fn raw(self) -> u32 {
36		self.0
37	}
38}
39
40#[derive(Clone, Debug, Eq, Hash, PartialEq)]
41pub struct InventorySegment {
42	pub kind: Arc<str>,
43	pub name: Arc<str>,
44}
45
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct InventorySymbol {
48	pub id: SymbolId,
49	pub source: SourceId,
50	pub identity: Arc<str>,
51	pub name: Arc<str>,
52	pub kind: Arc<str>,
53	pub shape: Arc<str>,
54	pub visibility: Arc<str>,
55	pub language: Arc<str>,
56	pub source_path: Arc<str>,
57	pub source_root: usize,
58	pub srcset: Arc<str>,
59	pub line_range: Option<(u32, u32)>,
60	pub parent: Option<SymbolId>,
61	pub segments: Arc<[InventorySegment]>,
62}
63
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct SymbolInventoryIndex {
66	generation: ResourceGeneration,
67	catalog: Arc<SymbolOrdinalCatalog>,
68	records: FxHashMap<SymbolOrdinal, InventorySymbol>,
69	all_symbols: SymbolSet,
70	facets: SymbolInventoryFacets,
71	compact_identities: FxHashMap<u64, SymbolOrdinal>,
72	compact_identity_collisions: FxHashMap<u64, Vec<SymbolOrdinal>>,
73}
74
75impl Default for SymbolInventoryIndex {
76	fn default() -> Self {
77		Self::empty(ResourceGeneration::new(0))
78	}
79}
80
81impl SymbolInventoryIndex {
82	pub(crate) fn estimated_heap_bytes(&self) -> usize {
83		let mut strings = std::collections::HashSet::<(usize, usize)>::new();
84		let mut string_bytes = 0usize;
85		let mut segment_bytes = 0usize;
86		for record in self.records.values() {
87			for value in [
88				&record.identity,
89				&record.name,
90				&record.kind,
91				&record.shape,
92				&record.visibility,
93				&record.language,
94				&record.source_path,
95				&record.srcset,
96			] {
97				if strings.insert((value.as_ptr() as usize, value.len())) {
98					string_bytes += value.len();
99				}
100			}
101			segment_bytes += record.segments.len() * std::mem::size_of::<InventorySegment>();
102			for segment in record.segments.iter() {
103				for value in [&segment.kind, &segment.name] {
104					if strings.insert((value.as_ptr() as usize, value.len())) {
105						string_bytes += value.len();
106					}
107				}
108			}
109		}
110		self.records.capacity()
111			* (std::mem::size_of::<SymbolOrdinal>() + std::mem::size_of::<InventorySymbol>())
112			+ self.catalog.estimated_heap_bytes()
113			+ self.all_symbols.estimated_heap_bytes()
114			+ self.facets.estimated_heap_bytes()
115			+ self.compact_identities.capacity()
116				* (std::mem::size_of::<u64>() + std::mem::size_of::<SymbolOrdinal>())
117			+ self
118				.compact_identity_collisions
119				.values()
120				.map(|ordinals| ordinals.capacity() * std::mem::size_of::<SymbolOrdinal>())
121				.sum::<usize>()
122			+ self.compact_identity_collisions.capacity()
123				* (std::mem::size_of::<u64>() + std::mem::size_of::<Vec<SymbolOrdinal>>())
124			+ segment_bytes
125			+ string_bytes
126	}
127
128	pub fn empty(generation: ResourceGeneration) -> Self {
129		Self {
130			generation,
131			catalog: Arc::new(SymbolOrdinalCatalog::default()),
132			records: FxHashMap::default(),
133			all_symbols: SymbolSet::new(),
134			facets: SymbolInventoryFacets::default(),
135			compact_identities: FxHashMap::default(),
136			compact_identity_collisions: FxHashMap::default(),
137		}
138	}
139
140	pub fn build(
141		generation: ResourceGeneration,
142		sources: &[SourceFileRecord],
143		symbols: &RecordTable<SymbolRecord>,
144	) -> Self {
145		let mut inventory = Self::empty(generation);
146		for symbol in symbols.iter() {
147			let fallback = missing_source(symbol.source);
148			let source = sources.get(symbol.source.file()).unwrap_or(&fallback);
149			index_record(&mut inventory, symbol, source, None);
150		}
151		inventory
152	}
153
154	pub fn refresh(
155		&self,
156		generation: ResourceGeneration,
157		sources: &[SourceFileRecord],
158		symbols: &RecordTable<SymbolRecord>,
159		changed_files: &BTreeSet<usize>,
160	) -> Self {
161		refresh_inventory(self, generation, sources, symbols, changed_files)
162	}
163
164	pub fn generation(&self) -> ResourceGeneration {
165		self.generation
166	}
167
168	pub fn catalog(&self) -> &Arc<SymbolOrdinalCatalog> {
169		&self.catalog
170	}
171
172	pub fn all_symbols(&self) -> &SymbolSet {
173		&self.all_symbols
174	}
175
176	pub fn record(&self, ordinal: SymbolOrdinal) -> Option<&InventorySymbol> {
177		self.records.get(&ordinal)
178	}
179
180	pub fn record_by_id(&self, id: &SymbolId) -> Option<&InventorySymbol> {
181		self.catalog
182			.ordinal(id)
183			.and_then(|ordinal| self.record(ordinal))
184	}
185
186	pub fn symbol_ids_by_identity(&self, identity: &str) -> Vec<SymbolId> {
187		self.facets
188			.symbols_by_identity(identity)
189			.into_iter()
190			.flat_map(SymbolSet::iter)
191			.filter_map(|ordinal| self.catalog.id(ordinal).copied())
192			.collect()
193	}
194
195	pub fn symbol_ids_by_compact_identity(&self, compact: &str) -> Vec<SymbolId> {
196		let hash = compact_identity_hash(compact);
197		let candidates = self
198			.compact_identity_collisions
199			.get(&hash)
200			.map(Vec::as_slice)
201			.or_else(|| self.compact_identities.get(&hash).map(std::slice::from_ref))
202			.unwrap_or_default();
203		candidates
204			.iter()
205			.copied()
206			.filter_map(|ordinal| self.record(ordinal))
207			.filter(|record| {
208				compact_record_identity(record.identity.as_ref()).as_deref() == Some(compact)
209			})
210			.map(|record| record.id)
211			.collect()
212	}
213
214	pub fn facets(&self) -> &SymbolInventoryFacets {
215		&self.facets
216	}
217}
218
219fn index_record(
220	inventory: &mut SymbolInventoryIndex,
221	symbol: &SymbolRecord,
222	source: &SourceFileRecord,
223	preferred_ordinal: Option<SymbolOrdinal>,
224) {
225	let catalog = Arc::make_mut(&mut inventory.catalog);
226	let ordinal = match preferred_ordinal {
227		Some(ordinal) => {
228			catalog.bind_id(ordinal, symbol.id);
229			ordinal
230		}
231		None => catalog.push(symbol.id),
232	};
233	if inventory.records.contains_key(&ordinal) {
234		unindex_record(inventory, ordinal);
235	}
236	let record = inventory_record(symbol, source);
237	inventory.all_symbols.insert(ordinal);
238	facets::insert_facets(&mut inventory.facets, &record, ordinal);
239	if let Some(compact) = compact_record_identity(record.identity.as_ref()) {
240		index_compact_identity(inventory, compact_identity_hash(&compact), ordinal);
241	}
242	inventory.records.insert(ordinal, record);
243}
244
245fn inventory_record(symbol: &SymbolRecord, source: &SourceFileRecord) -> InventorySymbol {
246	let segments = parse_segments(&symbol.identity);
247	let srcset = segments
248		.iter()
249		.filter(|segment| segment.kind.as_ref() == "srcset")
250		.map(|segment| segment.name.as_ref())
251		.collect::<Vec<_>>()
252		.join(".");
253	InventorySymbol {
254		id: symbol.id,
255		source: symbol.source,
256		identity: Arc::clone(&symbol.identity),
257		name: Arc::from(symbol.name.as_str()),
258		kind: Arc::from(symbol.kind.as_str()),
259		shape: Arc::from(Shape::for_kind(symbol.kind.as_bytes()).as_str()),
260		visibility: Arc::from(symbol.visibility.as_str()),
261		language: Arc::from(source.language.as_str()),
262		source_path: Arc::from(source.path.as_str()),
263		source_root: source.source_root,
264		srcset: Arc::from(srcset),
265		line_range: symbol.line_range,
266		parent: symbol.parent,
267		segments: Arc::from(segments),
268	}
269}
270
271fn parse_segments(identity: &str) -> Vec<InventorySegment> {
272	let marker = "+moniker://";
273	let scheme_end = identity.find(marker).map(|index| index + marker.len());
274	let Some(scheme) = scheme_end.and_then(|end| identity.get(..end)) else {
275		return Vec::new();
276	};
277	let Ok(moniker) = from_uri(identity, &UriConfig { scheme }) else {
278		return Vec::new();
279	};
280	moniker
281		.as_view()
282		.segments()
283		.filter_map(|segment| {
284			Some(InventorySegment {
285				kind: Arc::from(std::str::from_utf8(segment.kind).ok()?),
286				name: Arc::from(std::str::from_utf8(segment.name).ok()?),
287			})
288		})
289		.collect()
290}
291
292fn unindex_record(inventory: &mut SymbolInventoryIndex, ordinal: SymbolOrdinal) {
293	let Some(record) = inventory.records.remove(&ordinal) else {
294		return;
295	};
296	inventory.all_symbols.remove(ordinal);
297	facets::remove_facets(&mut inventory.facets, &record, ordinal);
298	if let Some(compact) = compact_record_identity(record.identity.as_ref()) {
299		let hash = compact_identity_hash(&compact);
300		unindex_compact_identity(inventory, hash, ordinal);
301	}
302}
303
304fn index_compact_identity(inventory: &mut SymbolInventoryIndex, hash: u64, ordinal: SymbolOrdinal) {
305	let Some(existing) = inventory.compact_identities.get(&hash).copied() else {
306		inventory.compact_identities.insert(hash, ordinal);
307		return;
308	};
309	let collisions = inventory
310		.compact_identity_collisions
311		.entry(hash)
312		.or_insert_with(|| vec![existing]);
313	if !collisions.contains(&ordinal) {
314		collisions.push(ordinal);
315	}
316}
317
318fn unindex_compact_identity(
319	inventory: &mut SymbolInventoryIndex,
320	hash: u64,
321	ordinal: SymbolOrdinal,
322) {
323	if let Some(collisions) = inventory.compact_identity_collisions.get_mut(&hash) {
324		collisions.retain(|candidate| *candidate != ordinal);
325		match collisions.as_slice() {
326			[] => {
327				inventory.compact_identities.remove(&hash);
328				inventory.compact_identity_collisions.remove(&hash);
329			}
330			[remaining] => {
331				inventory.compact_identities.insert(hash, *remaining);
332				inventory.compact_identity_collisions.remove(&hash);
333			}
334			_ => {}
335		}
336	} else if inventory.compact_identities.get(&hash) == Some(&ordinal) {
337		inventory.compact_identities.remove(&hash);
338	}
339}
340
341fn compact_identity_hash(compact: &str) -> u64 {
342	let mut hasher = rustc_hash::FxHasher::default();
343	compact.hash(&mut hasher);
344	hasher.finish()
345}
346
347fn compact_record_identity(identity: &str) -> Option<String> {
348	let scheme_end = identity.find("://")? + 3;
349	let scheme = &identity[..scheme_end];
350	crate::code::compact_identity(identity, scheme)
351}
352
353fn refresh_inventory(
354	previous: &SymbolInventoryIndex,
355	generation: ResourceGeneration,
356	sources: &[SourceFileRecord],
357	symbols: &RecordTable<SymbolRecord>,
358	changed_files: &BTreeSet<usize>,
359) -> SymbolInventoryIndex {
360	let mut inventory = previous.clone();
361	inventory.generation = generation;
362	let pending_retire = changed_ordinals(&inventory, changed_files);
363	let mut reusable_ordinals = FxHashMap::<(SourceId, Arc<str>), Vec<SymbolOrdinal>>::default();
364	for ordinal in &pending_retire {
365		if let Some(record) = inventory.record(*ordinal) {
366			reusable_ordinals
367				.entry((record.source, Arc::clone(&record.identity)))
368				.or_default()
369				.push(*ordinal);
370		}
371	}
372	for ordinal in &pending_retire {
373		unindex_record(&mut inventory, *ordinal);
374		Arc::make_mut(&mut inventory.catalog).unbind_id(*ordinal);
375	}
376	for file in changed_files {
377		for symbol in symbols.file_records(*file) {
378			let fallback = missing_source(symbol.source);
379			let source = sources.get(*file).unwrap_or(&fallback);
380			let key = (symbol.source, Arc::clone(&symbol.identity));
381			let preferred = reusable_ordinals.get_mut(&key).and_then(Vec::pop);
382			index_record(&mut inventory, symbol, source, preferred);
383		}
384	}
385	for ordinal in pending_retire {
386		if inventory.catalog.id(ordinal).is_none() {
387			Arc::make_mut(&mut inventory.catalog).retire(ordinal);
388		}
389	}
390	inventory
391}
392
393fn missing_source(source: SourceId) -> SourceFileRecord {
394	SourceFileRecord {
395		id: source,
396		uri: String::new(),
397		source_root: 0,
398		path: String::new(),
399		rel_path: String::new(),
400		anchor: String::new(),
401		language: String::new(),
402		text: String::new(),
403	}
404}
405
406fn changed_ordinals(
407	inventory: &SymbolInventoryIndex,
408	changed_files: &BTreeSet<usize>,
409) -> Vec<SymbolOrdinal> {
410	let mut changed = SymbolSet::new();
411	for file in changed_files {
412		if let Some(symbols) = inventory.facets.symbols_by_source(SourceId::at(*file)) {
413			changed.union_with(symbols);
414		}
415	}
416	changed.iter().collect()
417}
418
419#[cfg(test)]
420mod tests;