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