Skip to main content

shape_lsp/completion/
stdlib_methods.rs

1//! Stdlib type-method cache for completions.
2//!
3//! At LSP completion time, `let xs = [1, 2, 3]; xs.` should surface
4//! `Array.map` / `Array.filter` / etc. — but those methods live in pure
5//! Shape source in `stdlib-src/core/vec.shape` (`extend Vec<T>`), not in
6//! the runtime `MethodTable` (which only carries universal methods like
7//! `toString`/`type`).
8//!
9//! This module loads stdlib `.shape` modules once via `ModuleLoader`,
10//! extracts `extend`/`impl` methods via the same `extract_type_methods`
11//! used for user code, and exposes them as a process-wide cache keyed by
12//! the receiver type name (e.g. `"Vec"`, `"String"`, `"HashMap"`,
13//! `"Option"`, `"Result"`). The completion dispatcher merges this cache
14//! into the per-request `impl_methods` map, parallel to the existing
15//! `imports::extension_type_methods()` plumbing for extension `.shape`
16//! sources.
17//!
18//! Lazy + cached via `OnceLock`: paid once per process, no recompute on
19//! every keystroke.
20
21use crate::type_inference::{MethodCompletionInfo, extract_type_methods};
22use shape_ast::parser::parse_program;
23use shape_runtime::module_loader::ModuleLoader;
24use shape_runtime::stdlib_metadata::default_stdlib_path;
25use std::collections::HashMap;
26use std::sync::OnceLock;
27
28type MethodMap = HashMap<String, Vec<MethodCompletionInfo>>;
29
30static STDLIB_TYPE_METHODS: OnceLock<MethodMap> = OnceLock::new();
31
32/// Get stdlib type-methods, keyed by receiver type name.
33///
34/// Returns an empty map if the stdlib path is not discoverable (which can
35/// happen in non-workspace packaged environments without `SHAPE_STDLIB_PATH`).
36pub fn stdlib_type_methods() -> &'static MethodMap {
37    STDLIB_TYPE_METHODS.get_or_init(load_stdlib_type_methods)
38}
39
40fn load_stdlib_type_methods() -> MethodMap {
41    let mut out: MethodMap = HashMap::new();
42
43    let stdlib_path = default_stdlib_path();
44    if !stdlib_path.is_dir() {
45        return out;
46    }
47
48    let mut loader = ModuleLoader::new();
49    loader.set_stdlib_path(stdlib_path);
50
51    let imports = match loader.list_stdlib_module_imports() {
52        Ok(imports) => imports,
53        Err(_) => return out,
54    };
55
56    for import_path in imports {
57        let Ok(module) = loader.load_module(&import_path) else {
58            continue;
59        };
60        merge_methods(&mut out, extract_type_methods(&module.ast));
61    }
62
63    out
64}
65
66fn merge_methods(dst: &mut MethodMap, src: MethodMap) {
67    for (type_name, methods) in src {
68        let entry = dst.entry(type_name).or_default();
69        for m in methods {
70            if !entry.iter().any(|existing| existing.name == m.name) {
71                entry.push(m);
72            }
73        }
74    }
75}
76
77/// Test-helper that bypasses the `OnceLock` cache for repeatable per-test
78/// loads. Production callers use `stdlib_type_methods()`.
79#[cfg(test)]
80fn load_stdlib_type_methods_uncached() -> MethodMap {
81    load_stdlib_type_methods()
82}
83
84/// Fallback path for sources that don't parse via the module loader (e.g.
85/// when running outside the workspace and `SHAPE_STDLIB_PATH` points at a
86/// directory the loader can't introspect). Walks the directory tree and
87/// parses each `.shape` file directly. Returns empty on any failure.
88#[allow(dead_code)]
89fn load_stdlib_type_methods_via_walk() -> MethodMap {
90    let mut out: MethodMap = HashMap::new();
91    let stdlib_path = default_stdlib_path();
92    if !stdlib_path.is_dir() {
93        return out;
94    }
95    let mut stack = vec![stdlib_path];
96    while let Some(dir) = stack.pop() {
97        let Ok(entries) = std::fs::read_dir(&dir) else {
98            continue;
99        };
100        for entry in entries.flatten() {
101            let path = entry.path();
102            if path.is_dir() {
103                stack.push(path);
104            } else if path.extension().is_some_and(|e| e == "shape") {
105                if let Ok(src) = std::fs::read_to_string(&path) {
106                    if let Ok(program) = parse_program(&src) {
107                        merge_methods(&mut out, extract_type_methods(&program));
108                    }
109                }
110            }
111        }
112    }
113    out
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn stdlib_vec_methods_include_map_and_filter() {
122        // `extend Vec<T> { method map<U>(...) ... }` lives in
123        // `stdlib-src/core/vec.shape`. The completion path keys these
124        // under "Vec" because `normalize_type_for_methods` strips
125        // generics and maps `Array` → `Vec`.
126        let methods = load_stdlib_type_methods_uncached();
127        let vec_methods = methods.get("Vec").cloned().unwrap_or_default();
128        let names: Vec<&str> = vec_methods.iter().map(|m| m.name.as_str()).collect();
129        assert!(
130            names.contains(&"map"),
131            "expected Vec.map from stdlib extend block, got {:?}",
132            names
133        );
134        assert!(
135            names.contains(&"filter"),
136            "expected Vec.filter from stdlib extend block, got {:?}",
137            names
138        );
139    }
140
141    #[test]
142    fn stdlib_type_methods_cached_call_returns_same_handle() {
143        // Two calls to the public API yield the same `&'static` reference.
144        let a = stdlib_type_methods();
145        let b = stdlib_type_methods();
146        assert!(std::ptr::eq(a, b));
147    }
148}