1use std::collections::HashMap;
7use std::time::Instant;
8
9use ryo_analysis::SymbolId;
10
11use crate::id::SuggestId;
12use crate::suggest::{compute_priority, OpportunityId, SafetyLevel, SuggestOpportunity};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub enum PrecheckStatus {
17 #[default]
19 NotChecked,
20 Passed,
22 Failed,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub struct SuggestIndex(pub(crate) usize);
29
30impl SuggestIndex {
31 pub fn as_usize(self) -> usize {
33 self.0
34 }
35}
36
37#[derive(Debug)]
39pub struct StoredSuggestion {
40 pub opportunity: SuggestOpportunity,
42
43 pub suggest_idx: SuggestIndex,
45
46 pub safety: SafetyLevel,
48
49 pub priority: u8,
52
53 pub precheck_status: PrecheckStatus,
55
56 pub generation: u32,
58
59 pub closed: bool,
61
62 pub close_reason: Option<String>,
64
65 pub created_at: Instant,
67
68 pub closed_at: Option<Instant>,
70}
71
72impl StoredSuggestion {
73 pub fn new(
81 opportunity: SuggestOpportunity,
82 suggest_idx: SuggestIndex,
83 safety: SafetyLevel,
84 pattern_weight: f32,
85 ) -> Self {
86 let priority = compute_priority(opportunity.confidence, safety, pattern_weight);
87 Self {
88 opportunity,
89 suggest_idx,
90 safety,
91 priority,
92 precheck_status: PrecheckStatus::NotChecked,
93 generation: 0,
94 closed: false,
95 close_reason: None,
96 created_at: Instant::now(),
97 closed_at: None,
98 }
99 }
100
101 pub fn new_with_priority(
106 opportunity: SuggestOpportunity,
107 suggest_idx: SuggestIndex,
108 safety: SafetyLevel,
109 priority: u8,
110 ) -> Self {
111 Self {
112 opportunity,
113 suggest_idx,
114 safety,
115 priority,
116 precheck_status: PrecheckStatus::NotChecked,
117 generation: 0,
118 closed: false,
119 close_reason: None,
120 created_at: Instant::now(),
121 closed_at: None,
122 }
123 }
124
125 pub fn close(&mut self, reason: impl Into<String>) {
127 self.closed = true;
128 self.close_reason = Some(reason.into());
129 self.closed_at = Some(Instant::now());
130 }
131
132 pub fn bump_generation(&mut self) {
134 self.generation += 1;
135 }
136}
137
138pub struct SuggestStore {
140 suggestions: HashMap<u32, StoredSuggestion>,
142
143 symbol_to_suggests: HashMap<SymbolId, Vec<u32>>,
145
146 dedup_index: HashMap<(SuggestIndex, OpportunityId), u32>,
150
151 next_index: u32,
153}
154
155impl Default for SuggestStore {
156 fn default() -> Self {
157 Self::new()
158 }
159}
160
161impl SuggestStore {
162 pub fn new() -> Self {
164 Self {
165 suggestions: HashMap::new(),
166 symbol_to_suggests: HashMap::new(),
167 dedup_index: HashMap::new(),
168 next_index: 1, }
170 }
171
172 pub fn insert(&mut self, suggestion: StoredSuggestion) -> Option<SuggestId> {
177 let dedup_key = (suggestion.suggest_idx, suggestion.opportunity.id);
178
179 if let Some(&existing_idx) = self.dedup_index.get(&dedup_key) {
181 if self
182 .suggestions
183 .get(&existing_idx)
184 .is_some_and(|s| !s.closed)
185 {
186 return None; }
188 }
189
190 let index = self.next_index;
191 self.next_index += 1;
192
193 let generation = suggestion.generation;
194 let targets = suggestion.opportunity.targets.clone();
195
196 for target in targets {
198 self.symbol_to_suggests
199 .entry(target)
200 .or_default()
201 .push(index);
202 }
203
204 self.dedup_index.insert(dedup_key, index);
205 self.suggestions.insert(index, suggestion);
206
207 Some(SuggestId::new(index, generation))
208 }
209
210 pub fn get(&self, id: SuggestId) -> Option<&StoredSuggestion> {
212 self.suggestions
213 .get(&id.index())
214 .filter(|sug| sug.generation == id.generation() && !sug.closed)
215 }
216
217 pub fn get_mut(&mut self, id: SuggestId) -> Option<&mut StoredSuggestion> {
219 self.suggestions
220 .get_mut(&id.index())
221 .filter(|sug| sug.generation == id.generation() && !sug.closed)
222 }
223
224 pub fn remove_for_symbol(&mut self, symbol: &SymbolId) {
226 if let Some(indices) = self.symbol_to_suggests.remove(symbol) {
227 for index in indices {
228 if let Some(removed) = self.suggestions.remove(&index) {
229 let dedup_key = (removed.suggest_idx, removed.opportunity.id);
230 self.dedup_index.remove(&dedup_key);
231 }
232 }
233 }
234 }
235
236 pub fn invalidate_for_symbol(&mut self, symbol: &SymbolId) {
238 if let Some(indices) = self.symbol_to_suggests.get(symbol) {
239 for &index in indices {
240 if let Some(sug) = self.suggestions.get_mut(&index) {
241 sug.bump_generation();
242 }
243 }
244 }
245 }
246
247 pub fn is_valid(&self, id: SuggestId) -> bool {
249 self.get(id).is_some()
250 }
251
252 pub fn current_generation(&self, id: SuggestId) -> Option<u32> {
254 self.suggestions.get(&id.index()).map(|s| s.generation)
255 }
256
257 pub fn close(&mut self, id: SuggestId, reason: impl Into<String>) -> bool {
259 if let Some(sug) = self.get_mut(id) {
260 sug.close(reason);
261 true
262 } else {
263 false
264 }
265 }
266
267 pub fn iter(&self) -> impl Iterator<Item = (SuggestId, &StoredSuggestion)> {
269 self.suggestions
270 .iter()
271 .filter(|(_, s)| !s.closed)
272 .map(|(&index, sug)| (SuggestId::new(index, sug.generation), sug))
273 }
274
275 pub fn len(&self) -> usize {
277 self.suggestions.iter().filter(|(_, s)| !s.closed).count()
278 }
279
280 pub fn is_empty(&self) -> bool {
282 self.len() == 0
283 }
284
285 pub fn total_count(&self) -> usize {
287 self.suggestions.len()
288 }
289
290 pub fn clear(&mut self) {
292 self.suggestions.clear();
293 self.symbol_to_suggests.clear();
294 self.dedup_index.clear();
295 self.next_index = 1;
296 }
297}
298
299#[derive(Debug, Clone)]
301pub struct GcConfig {
302 pub max_closed_age: std::time::Duration,
304
305 pub max_suggestions: usize,
307
308 pub gc_interval: std::time::Duration,
310}
311
312impl Default for GcConfig {
313 fn default() -> Self {
314 Self {
315 max_closed_age: std::time::Duration::from_secs(300), max_suggestions: 1000,
317 gc_interval: std::time::Duration::from_secs(60), }
319 }
320}
321
322impl SuggestStore {
323 pub fn gc(&mut self, config: &GcConfig, valid_symbols: &impl Fn(&SymbolId) -> bool) {
329 let now = Instant::now();
330 let mut to_remove = Vec::new();
331
332 for (&index, sug) in self.suggestions.iter() {
333 if sug.closed {
335 if let Some(closed_at) = sug.closed_at {
336 if now.duration_since(closed_at) > config.max_closed_age {
337 to_remove.push(index);
338 continue;
339 }
340 }
341 }
342
343 let any_valid = sug.opportunity.targets.iter().any(valid_symbols);
345 if !any_valid {
346 to_remove.push(index);
347 }
348 }
349
350 for index in to_remove {
352 if let Some(sug) = self.suggestions.remove(&index) {
353 let dedup_key = (sug.suggest_idx, sug.opportunity.id);
354 self.dedup_index.remove(&dedup_key);
355 for target in &sug.opportunity.targets {
356 if let Some(indices) = self.symbol_to_suggests.get_mut(target) {
357 indices.retain(|&i| i != index);
358 }
359 }
360 }
361 }
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::suggest::{OpportunityContext, OpportunityId, SuggestLocation};
369
370 fn make_opportunity(id: u32, targets: Vec<SymbolId>) -> SuggestOpportunity {
371 SuggestOpportunity::new(
372 OpportunityId::new(id),
373 targets,
374 SuggestLocation::for_test("test.rs", "Test"),
375 "Test suggestion",
376 0.9,
377 OpportunityContext::Derive {
378 derive_name: "Default".into(),
379 missing_impls: vec![],
380 },
381 )
382 }
383
384 #[test]
385 fn test_suggest_id_format() {
386 let id = SuggestId::new(1, 0);
387 assert_eq!(id.to_string(), "S001g0");
388
389 let id2 = SuggestId::new(42, 3);
390 assert_eq!(id2.to_string(), "S042g3");
391 }
392
393 #[test]
394 fn test_suggest_id_parse() {
395 let id: SuggestId = "S001g0".parse().unwrap();
396 assert_eq!(id.index(), 1);
397 assert_eq!(id.generation(), 0);
398
399 let id2: SuggestId = "S042g3".parse().unwrap();
400 assert_eq!(id2.index(), 42);
401 assert_eq!(id2.generation(), 3);
402 }
403
404 #[test]
405 fn test_store_insert_and_get() {
406 let mut store = SuggestStore::new();
407 let sym = SymbolId::parse("100v1").unwrap();
408 let opp = make_opportunity(1, vec![sym]);
409 let sug = StoredSuggestion::new(opp, SuggestIndex(0), SafetyLevel::Auto, 1.0);
410
411 let id = store.insert(sug).expect("first insert should succeed");
412 assert_eq!(id.index(), 1);
413 assert_eq!(id.generation(), 0);
414
415 let retrieved = store.get(id).unwrap();
416 assert_eq!(retrieved.safety, SafetyLevel::Auto);
417 }
418
419 #[test]
420 fn test_store_invalidation() {
421 let mut store = SuggestStore::new();
422 let sym = SymbolId::parse("100v1").unwrap();
423 let opp = make_opportunity(1, vec![sym]);
424 let sug = StoredSuggestion::new(opp, SuggestIndex(0), SafetyLevel::Auto, 1.0);
425
426 let id = store.insert(sug).expect("insert should succeed");
427 assert!(store.is_valid(id));
428
429 store.invalidate_for_symbol(&sym);
431
432 assert!(!store.is_valid(id));
434
435 let new_gen = store.current_generation(id).unwrap();
437 assert_eq!(new_gen, 1);
438 }
439
440 #[test]
441 fn test_store_close() {
442 let mut store = SuggestStore::new();
443 let sym = SymbolId::parse("100v1").unwrap();
444 let opp = make_opportunity(1, vec![sym]);
445 let sug = StoredSuggestion::new(opp, SuggestIndex(0), SafetyLevel::Auto, 1.0);
446
447 let id = store.insert(sug).expect("insert should succeed");
448 assert!(store.is_valid(id));
449 assert_eq!(store.len(), 1);
450
451 store.close(id, "Applied");
452 assert!(!store.is_valid(id));
453 assert_eq!(store.len(), 0);
454 assert_eq!(store.total_count(), 1); }
456
457 #[test]
458 fn test_store_remove_for_symbol() {
459 let mut store = SuggestStore::new();
460 let sym1 = SymbolId::parse("100v1").unwrap();
461 let sym2 = SymbolId::parse("200v1").unwrap();
462
463 let opp1 = make_opportunity(1, vec![sym1]);
464 let opp2 = make_opportunity(2, vec![sym2]);
465
466 let sug1 = StoredSuggestion::new(opp1, SuggestIndex(0), SafetyLevel::Auto, 1.0);
467 let sug2 = StoredSuggestion::new(opp2, SuggestIndex(0), SafetyLevel::Auto, 1.0);
468
469 let id1 = store.insert(sug1).expect("insert sug1");
470 let id2 = store.insert(sug2).expect("insert sug2");
471
472 assert_eq!(store.len(), 2);
473
474 store.remove_for_symbol(&sym1);
475
476 assert!(!store.is_valid(id1));
477 assert!(store.is_valid(id2));
478 assert_eq!(store.len(), 1);
479 }
480
481 #[test]
482 fn test_store_iter() {
483 let mut store = SuggestStore::new();
484 let sym = SymbolId::parse("100v1").unwrap();
485
486 for i in 0..5 {
487 let opp = make_opportunity(i, vec![sym]);
488 let sug = StoredSuggestion::new(opp, SuggestIndex(0), SafetyLevel::Auto, 1.0);
489 store.insert(sug);
490 }
491
492 let ids: Vec<_> = store.iter().map(|(id, _)| id).collect();
493 assert_eq!(ids.len(), 5);
494 }
495}