Skip to main content

lisette_semantics/cache/
go_stdlib.rs

1use rustc_hash::FxHashMap as HashMap;
2use std::fs;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6use stdlib::{Target, get_go_stdlib_typedef};
7
8use super::types::CachedDefinition;
9use super::{COMPILER_VERSION_HASH, GO_STDLIB_HASH};
10use crate::checker::registration::extract_package_directive;
11use crate::store::Store;
12
13#[derive(Serialize, Deserialize)]
14pub struct GoStdlibCache {
15    pub content_hash: u64,
16    pub compiler_version: u64,
17    pub modules: HashMap<String, GoModuleCache>,
18}
19
20#[derive(Serialize, Deserialize)]
21pub struct GoModuleCache {
22    pub definitions: HashMap<String, CachedDefinition>,
23    /// Go module imports (e.g., `["go:io", "go:sync"]`).
24    pub go_imports: Vec<String>,
25}
26
27fn cache_file_name(target: Target) -> String {
28    format!("stdlib_defs_{}.bin", target.cache_segment())
29}
30
31fn cache_path(target: Target) -> Option<PathBuf> {
32    let home = std::env::var("HOME").ok()?;
33    Some(
34        PathBuf::from(home)
35            .join(".lisette")
36            .join("cache")
37            .join(cache_file_name(target)),
38    )
39}
40
41pub fn try_load_go_stdlib_cache(target: Target) -> Option<GoStdlibCache> {
42    let path = cache_path(target)?;
43    let bytes = fs::read(&path).ok()?;
44    let cache: GoStdlibCache = match bincode::deserialize(&bytes) {
45        Ok(cache) => cache,
46        Err(_) => {
47            let _ = fs::remove_file(&path);
48            return None;
49        }
50    };
51
52    if cache.content_hash != GO_STDLIB_HASH || cache.compiler_version != COMPILER_VERSION_HASH {
53        let _ = fs::remove_file(&path);
54        return None;
55    }
56
57    Some(cache)
58}
59
60pub fn save_go_stdlib_cache(store: &Store, go_module_ids: &[String], target: Target) {
61    let Some(path) = cache_path(target) else {
62        return;
63    };
64
65    let mut modules = HashMap::default();
66    // Go definitions don't reference files, so file_id_to_index is always empty.
67    let empty_file_map = HashMap::default();
68    for module_id in go_module_ids {
69        let Some(module) = store.get_module(module_id) else {
70            continue;
71        };
72        let definitions: HashMap<String, CachedDefinition> = module
73            .definitions
74            .iter()
75            .map(|(name, definition)| {
76                (
77                    name.to_string(),
78                    CachedDefinition::from_definition(definition, &empty_file_map),
79                )
80            })
81            .collect();
82
83        let go_imports = get_go_imports_from_source(module_id, target);
84
85        modules.insert(
86            module_id.clone(),
87            GoModuleCache {
88                definitions,
89                go_imports,
90            },
91        );
92    }
93
94    let cache = GoStdlibCache {
95        content_hash: GO_STDLIB_HASH,
96        compiler_version: COMPILER_VERSION_HASH,
97        modules,
98    };
99
100    let Ok(bytes) = bincode::serialize(&cache) else {
101        return;
102    };
103
104    let Some(parent) = path.parent() else {
105        return;
106    };
107    let _ = fs::create_dir_all(parent);
108
109    let temp_path = super::global_cache_temp_path(&path);
110    if fs::write(&temp_path, &bytes).is_err() {
111        return;
112    }
113    if fs::rename(&temp_path, &path).is_err() {
114        let _ = fs::remove_file(&temp_path);
115        return;
116    }
117    super::prune_legacy_global_caches(parent, "stdlib_defs");
118}
119
120/// Load a Go module and its transitive deps from cache, recursively.
121pub fn load_cached_go_module(
122    store: &mut Store,
123    module_id: &str,
124    cache: &GoStdlibCache,
125    target: Target,
126) {
127    if store.is_visited(module_id) {
128        return;
129    }
130
131    let Some(cached) = cache.modules.get(module_id) else {
132        return;
133    };
134
135    // Load transitive deps first
136    let imports = cached.go_imports.clone();
137    for dep in &imports {
138        load_cached_go_module(store, dep, cache, target);
139    }
140
141    if store.is_visited(module_id) {
142        return; // May have been loaded as a transitive dep of a sibling
143    }
144
145    register_cached_go_module(store, module_id, cached, target);
146}
147
148fn register_cached_go_module(
149    store: &mut Store,
150    module_id: &str,
151    cached: &GoModuleCache,
152    target: Target,
153) {
154    store.add_module(module_id);
155    store.mark_visited(module_id);
156
157    if let Some(go_pkg) = module_id.strip_prefix("go:")
158        && let Some(source) = get_go_stdlib_typedef(go_pkg, target)
159        && let Some(pkg_name) = extract_package_directive(source)
160        && module_id.rsplit('/').next() != Some(pkg_name.as_str())
161    {
162        store
163            .go_package_names
164            .insert(module_id.to_string(), pkg_name);
165    }
166
167    // Go modules don't need files registered — they're internal and filtered out
168    // of diagnostic rendering. We use an empty file_ids slice for span restoration
169    // (all spans will get file_id 0, which is fine for Go stdlib).
170    let file_ids: &[u32] = &[];
171
172    let module = store.get_module_mut(module_id).unwrap();
173    for (qualified_name, cached_definition) in &cached.definitions {
174        let definition = cached_definition.to_definition(file_ids);
175        module
176            .definitions
177            .insert(qualified_name.clone().into(), definition);
178    }
179}
180
181/// Extract Go imports from a module's `.d.lis` source without parsing.
182fn get_go_imports_from_source(module_id: &str, target: Target) -> Vec<String> {
183    let Some(go_pkg) = module_id.strip_prefix("go:") else {
184        return vec![];
185    };
186    let Some(source) = get_go_stdlib_typedef(go_pkg, target) else {
187        return vec![];
188    };
189    source
190        .lines()
191        .filter_map(|line| {
192            let line = line.trim();
193            let rest = line.strip_prefix("import \"go:")?;
194            let pkg = rest.strip_suffix('"')?;
195            Some(format!("go:{pkg}"))
196        })
197        .collect()
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn cache_file_name_includes_target_only() {
206        let target = Target::new("darwin", "arm64");
207        assert_eq!(cache_file_name(target), "stdlib_defs_darwin_arm64.bin");
208    }
209}