Skip to main content

shape_runtime/
stdlib_metadata.rs

1//! Stdlib metadata extractor for LSP introspection
2//!
3//! This module parses Shape stdlib files to extract function and pattern
4//! metadata for use in LSP completions and hover information.
5
6use crate::metadata::{FunctionCategory, FunctionInfo, ParameterInfo, TypeInfo};
7use shape_ast::ast::{
8    BuiltinFunctionDecl, BuiltinTypeDecl, DocComment, FunctionDef, Item, Program, TypeAnnotation,
9};
10use shape_ast::error::Result;
11#[cfg(test)]
12use shape_ast::parser::parse_program;
13use std::path::{Path, PathBuf};
14
15/// Metadata extracted from Shape stdlib
16#[derive(Debug, Default)]
17pub struct StdlibMetadata {
18    /// Exported functions from stdlib
19    pub functions: Vec<FunctionInfo>,
20    /// Exported patterns from stdlib
21    pub patterns: Vec<PatternInfo>,
22    /// Declaration-only intrinsic functions from std/core
23    pub intrinsic_functions: Vec<FunctionInfo>,
24    /// Declaration-only intrinsic types from std/core
25    pub intrinsic_types: Vec<TypeInfo>,
26}
27
28/// Pattern metadata for LSP
29#[derive(Debug, Clone)]
30pub struct PatternInfo {
31    /// Pattern name
32    pub name: String,
33    /// Pattern signature
34    pub signature: String,
35    /// Description (if available from comments)
36    pub description: String,
37    /// Parameters (pattern variables)
38    pub parameters: Vec<ParameterInfo>,
39}
40
41impl StdlibMetadata {
42    /// Create empty stdlib metadata
43    pub fn empty() -> Self {
44        Self::default()
45    }
46
47    /// Load and parse all stdlib modules from the given path
48    pub fn load(stdlib_path: &Path) -> Result<Self> {
49        let mut functions = Vec::new();
50        let mut patterns = Vec::new();
51        let mut intrinsic_functions = Vec::new();
52        let mut intrinsic_types = Vec::new();
53
54        if !stdlib_path.exists() {
55            return Ok(Self::empty());
56        }
57
58        // Use the unified module loader for stdlib discovery + parsing.
59        let mut loader = crate::module_loader::ModuleLoader::new();
60        loader.set_stdlib_path(stdlib_path.to_path_buf());
61
62        for import_path in loader.list_stdlib_module_imports()? {
63            let module_path = import_path
64                .strip_prefix("std::")
65                .unwrap_or(&import_path)
66                .replace("::", "/");
67            match loader.load_module(&import_path) {
68                Ok(module) => {
69                    Self::extract_from_program(
70                        &module.ast,
71                        &module_path,
72                        &mut functions,
73                        &mut patterns,
74                        &mut intrinsic_functions,
75                        &mut intrinsic_types,
76                    );
77                }
78                Err(_) => {
79                    // Skip modules that fail to parse/compile metadata.
80                }
81            }
82        }
83
84        Ok(Self {
85            functions,
86            patterns,
87            intrinsic_functions,
88            intrinsic_types,
89        })
90    }
91
92    fn extract_from_program(
93        program: &Program,
94        module_path: &str,
95        functions: &mut Vec<FunctionInfo>,
96        _patterns: &mut Vec<PatternInfo>,
97        intrinsic_functions: &mut Vec<FunctionInfo>,
98        intrinsic_types: &mut Vec<TypeInfo>,
99    ) {
100        for item in &program.items {
101            match item {
102                Item::Function(func, span) => {
103                    // All top-level functions are considered exports
104                    functions.push(Self::function_to_info(
105                        func,
106                        module_path,
107                        program.docs.comment_for_span(*span),
108                    ));
109                }
110                Item::Export(export, span) => {
111                    // Handle explicit exports
112                    match &export.item {
113                        shape_ast::ast::ExportItem::Function(func) => {
114                            functions.push(Self::function_to_info(
115                                func,
116                                module_path,
117                                program.docs.comment_for_span(*span),
118                            ));
119                        }
120                        shape_ast::ast::ExportItem::TypeAlias(_) => {}
121                        shape_ast::ast::ExportItem::Named(_) => {}
122                        shape_ast::ast::ExportItem::Enum(_) => {}
123                        shape_ast::ast::ExportItem::Struct(_) => {}
124                        shape_ast::ast::ExportItem::Interface(_) => {}
125                        shape_ast::ast::ExportItem::Trait(_) => {}
126                        shape_ast::ast::ExportItem::ForeignFunction(_) => {
127                            // Foreign functions are not stdlib intrinsics
128                        }
129                    }
130                }
131                Item::BuiltinTypeDecl(type_decl, span) => {
132                    intrinsic_types
133                        .push(Self::builtin_type_to_info(type_decl, program.docs.comment_for_span(*span)));
134                }
135                Item::BuiltinFunctionDecl(func_decl, span) => {
136                    intrinsic_functions.push(Self::builtin_function_to_info(
137                        func_decl,
138                        module_path,
139                        program.docs.comment_for_span(*span),
140                    ));
141                }
142                _ => {}
143            }
144        }
145    }
146
147    /// Infer function category from module path (domain-agnostic)
148    ///
149    /// Uses directory structure to determine category:
150    /// - core/math, core/statistics → Math
151    /// - */indicators/*, */backtesting/*, */simulation/* → Simulation
152    /// - */patterns/* → Utility
153    /// - Default → Utility
154    fn infer_category_from_path(module_path: &str) -> FunctionCategory {
155        let path_lower = module_path.to_lowercase().replace("::", "/");
156
157        // Check path components for categorization
158        if path_lower.contains("/math") || path_lower.contains("/statistics") {
159            FunctionCategory::Math
160        } else if path_lower.contains("/indicators")
161            || path_lower.contains("/backtesting")
162            || path_lower.contains("/simulation")
163        {
164            FunctionCategory::Simulation
165        } else if path_lower.contains("/patterns") {
166            FunctionCategory::Utility
167        } else {
168            FunctionCategory::Utility
169        }
170    }
171
172    fn function_to_info(
173        func: &FunctionDef,
174        module_path: &str,
175        doc: Option<&DocComment>,
176    ) -> FunctionInfo {
177        let params: Vec<ParameterInfo> = func
178            .params
179            .iter()
180            .map(|p| ParameterInfo {
181                name: p.simple_name().unwrap_or("_").to_string(),
182                param_type: p
183                    .type_annotation
184                    .as_ref()
185                    .map(Self::format_type_annotation)
186                    .unwrap_or_else(|| "any".to_string()),
187                optional: p.default_value.is_some(),
188                description: doc
189                    .and_then(|comment| comment.param_doc(p.simple_name().unwrap_or("_")))
190                    .unwrap_or_default()
191                    .to_string(),
192                constraints: None,
193            })
194            .collect();
195
196        let return_type = func
197            .return_type
198            .as_ref()
199            .map(Self::format_type_annotation)
200            .unwrap_or_else(|| "any".to_string());
201
202        let param_strs: Vec<String> = params
203            .iter()
204            .map(|p| {
205                if p.optional {
206                    format!("{}?: {}", p.name, p.param_type)
207                } else {
208                    format!("{}: {}", p.name, p.param_type)
209                }
210            })
211            .collect();
212
213        let signature = format!(
214            "{}({}) -> {}",
215            func.name,
216            param_strs.join(", "),
217            return_type
218        );
219
220        // Determine category based on path structure (domain-agnostic)
221        let category = Self::infer_category_from_path(module_path);
222
223        FunctionInfo {
224            name: func.name.clone(),
225            signature,
226            description: doc.map(Self::doc_text).unwrap_or_default(),
227            category,
228            parameters: params,
229            return_type,
230            example: doc.and_then(|comment| comment.example_doc()).map(str::to_string),
231            implemented: true,
232            comptime_only: false,
233        }
234    }
235
236    fn builtin_type_to_info(type_decl: &BuiltinTypeDecl, doc: Option<&DocComment>) -> TypeInfo {
237        TypeInfo {
238            name: type_decl.name.clone(),
239            description: doc.map(Self::doc_text).unwrap_or_default(),
240        }
241    }
242
243    fn builtin_function_to_info(
244        func: &BuiltinFunctionDecl,
245        module_path: &str,
246        doc: Option<&DocComment>,
247    ) -> FunctionInfo {
248        let params: Vec<ParameterInfo> = func
249            .params
250            .iter()
251            .map(|p| ParameterInfo {
252                name: p.simple_name().unwrap_or("_").to_string(),
253                param_type: p
254                    .type_annotation
255                    .as_ref()
256                    .map(Self::format_type_annotation)
257                    .unwrap_or_else(|| "any".to_string()),
258                optional: p.default_value.is_some(),
259                description: doc
260                    .and_then(|comment| comment.param_doc(p.simple_name().unwrap_or("_")))
261                    .unwrap_or_default()
262                    .to_string(),
263                constraints: None,
264            })
265            .collect();
266        let return_type = Self::format_type_annotation(&func.return_type);
267        let type_params_str = func
268            .type_params
269            .as_ref()
270            .filter(|params| !params.is_empty())
271            .map(|params| {
272                format!(
273                    "<{}>",
274                    params
275                        .iter()
276                        .map(|p| p.name.as_str())
277                        .collect::<Vec<_>>()
278                        .join(", ")
279                )
280            })
281            .unwrap_or_default();
282        let signature = format!(
283            "{}{}({}) -> {}",
284            func.name,
285            type_params_str,
286            params
287                .iter()
288                .map(|p| format!("{}: {}", p.name, p.param_type))
289                .collect::<Vec<_>>()
290                .join(", "),
291            return_type
292        );
293        FunctionInfo {
294            name: func.name.clone(),
295            signature,
296            description: doc.map(Self::doc_text).unwrap_or_default(),
297            category: Self::infer_category_from_path(module_path),
298            parameters: params,
299            return_type,
300            example: doc.and_then(|comment| comment.example_doc()).map(str::to_string),
301            implemented: true,
302            comptime_only: crate::builtin_metadata::is_comptime_builtin_function(&func.name),
303        }
304    }
305
306    fn doc_text(comment: &DocComment) -> String {
307        if !comment.body.is_empty() {
308            comment.body.clone()
309        } else {
310            comment.summary.clone()
311        }
312    }
313
314    fn format_type_annotation(ty: &TypeAnnotation) -> String {
315        match ty {
316            TypeAnnotation::Basic(name) | TypeAnnotation::Reference(name) => name.clone(),
317            TypeAnnotation::Array(inner) => format!("{}[]", Self::format_type_annotation(inner)),
318            TypeAnnotation::Tuple(items) => format!(
319                "[{}]",
320                items
321                    .iter()
322                    .map(Self::format_type_annotation)
323                    .collect::<Vec<_>>()
324                    .join(", ")
325            ),
326            TypeAnnotation::Object(fields) => {
327                let inner = fields
328                    .iter()
329                    .map(|f| {
330                        if f.optional {
331                            format!(
332                                "{}?: {}",
333                                f.name,
334                                Self::format_type_annotation(&f.type_annotation)
335                            )
336                        } else {
337                            format!(
338                                "{}: {}",
339                                f.name,
340                                Self::format_type_annotation(&f.type_annotation)
341                            )
342                        }
343                    })
344                    .collect::<Vec<_>>()
345                    .join(", ");
346                format!("{{ {} }}", inner)
347            }
348            TypeAnnotation::Function { params, returns } => {
349                let param_list = params
350                    .iter()
351                    .map(|p| {
352                        let ty = Self::format_type_annotation(&p.type_annotation);
353                        if let Some(name) = &p.name {
354                            if p.optional {
355                                format!("{}?: {}", name, ty)
356                            } else {
357                                format!("{}: {}", name, ty)
358                            }
359                        } else {
360                            ty
361                        }
362                    })
363                    .collect::<Vec<_>>()
364                    .join(", ");
365                format!(
366                    "({}) -> {}",
367                    param_list,
368                    Self::format_type_annotation(returns)
369                )
370            }
371            TypeAnnotation::Union(types) => types
372                .iter()
373                .map(Self::format_type_annotation)
374                .collect::<Vec<_>>()
375                .join(" | "),
376            TypeAnnotation::Intersection(types) => types
377                .iter()
378                .map(Self::format_type_annotation)
379                .collect::<Vec<_>>()
380                .join(" + "),
381            TypeAnnotation::Generic { name, args } => format!(
382                "{}<{}>",
383                name,
384                args.iter()
385                    .map(Self::format_type_annotation)
386                    .collect::<Vec<_>>()
387                    .join(", ")
388            ),
389            TypeAnnotation::Void => "void".to_string(),
390            TypeAnnotation::Never => "never".to_string(),
391            TypeAnnotation::Null => "null".to_string(),
392            TypeAnnotation::Undefined => "undefined".to_string(),
393            TypeAnnotation::Dyn(bounds) => format!("dyn {}", bounds.join(" + ")),
394        }
395    }
396}
397
398/// Get the default stdlib path
399pub fn default_stdlib_path() -> PathBuf {
400    // Explicit override for non-workspace environments (packaged installs, custom dev layouts).
401    if let Ok(path) = std::env::var("SHAPE_STDLIB_PATH") {
402        return PathBuf::from(path);
403    }
404
405    // Workspace builds use the canonical stdlib source of truth.
406    let workspace_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../shape-core/stdlib");
407    if workspace_path.is_dir() {
408        return workspace_path;
409    }
410
411    // Published crates carry a vendored stdlib copy inside shape-runtime itself.
412    let packaged_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("stdlib-src");
413    if packaged_path.is_dir() {
414        return packaged_path;
415    }
416
417    packaged_path
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423    use std::collections::BTreeMap;
424    use std::path::{Path, PathBuf};
425
426    fn collect_shape_files(root: &Path) -> BTreeMap<PathBuf, String> {
427        let mut files = BTreeMap::new();
428        for entry in walkdir::WalkDir::new(root)
429            .into_iter()
430            .filter_map(|entry| entry.ok())
431            .filter(|entry| entry.file_type().is_file())
432        {
433            let path = entry.path();
434            if path.extension().and_then(|ext| ext.to_str()) != Some("shape") {
435                continue;
436            }
437            let rel = path
438                .strip_prefix(root)
439                .expect("vendored stdlib file should be under root")
440                .to_path_buf();
441            let content = std::fs::read_to_string(path)
442                .unwrap_or_else(|err| panic!("failed to read {}: {}", path.display(), err));
443            files.insert(rel, content);
444        }
445        files
446    }
447
448    #[test]
449    fn test_load_stdlib() {
450        let stdlib_path = default_stdlib_path();
451        if stdlib_path.exists() {
452            let metadata = StdlibMetadata::load(&stdlib_path).unwrap();
453            // Stdlib may or may not have functions depending on development state
454            println!("Stdlib path: {:?}", stdlib_path);
455            println!("Found {} stdlib functions", metadata.functions.len());
456            for func in &metadata.functions {
457                println!("  - {}: {}", func.name, func.signature);
458            }
459            println!("Found {} stdlib patterns", metadata.patterns.len());
460            // Note: stdlib functions (like sma, ema) will be added as stdlib is developed
461        } else {
462            println!("Stdlib path does not exist: {:?}", stdlib_path);
463        }
464    }
465
466    #[test]
467    fn test_empty_stdlib() {
468        let metadata = StdlibMetadata::empty();
469        assert!(metadata.functions.is_empty());
470        assert!(metadata.patterns.is_empty());
471    }
472
473    #[test]
474    fn test_parse_all_stdlib_files() {
475        let stdlib_path = default_stdlib_path();
476        println!("Stdlib path: {:?}", stdlib_path);
477
478        // Test each file individually
479        let files = [
480            "core/snapshot.shape",
481            "core/math.shape",
482            "finance/indicators/moving_averages.shape",
483        ];
484
485        for file in &files {
486            let path = stdlib_path.join(file);
487            if path.exists() {
488                let content = std::fs::read_to_string(&path).unwrap();
489                match parse_program(&content) {
490                    Ok(program) => {
491                        let func_count = program
492                            .items
493                            .iter()
494                            .filter(|i| {
495                                matches!(
496                                    i,
497                                    shape_ast::ast::Item::Function(_, _)
498                                        | shape_ast::ast::Item::Export(_, _)
499                                )
500                            })
501                            .count();
502                        println!("✓ {} parsed: {} items", file, func_count);
503                    }
504                    Err(e) => {
505                        panic!("✗ {} FAILED to parse: {:?}", file, e);
506                    }
507                }
508            } else {
509                println!("⚠ {} not found", file);
510            }
511        }
512    }
513
514    #[test]
515    fn test_intrinsic_declarations_loaded_from_std_core() {
516        let stdlib_path = default_stdlib_path();
517        if !stdlib_path.exists() {
518            return;
519        }
520
521        let metadata = StdlibMetadata::load(&stdlib_path).unwrap();
522        assert!(
523            metadata
524                .intrinsic_types
525                .iter()
526                .any(|t| t.name == "AnyError"),
527            "expected AnyError intrinsic type from std::core declarations"
528        );
529        let abs = metadata
530            .intrinsic_functions
531            .iter()
532            .find(|f| f.name == "abs")
533            .expect("abs intrinsic declaration should exist");
534        assert_eq!(abs.signature, "abs(value: number) -> number");
535        assert!(
536            abs.description.contains("absolute value"),
537            "abs description should come from doc comments"
538        );
539    }
540
541    #[test]
542    fn test_vendored_stdlib_matches_workspace_copy() {
543        let workspace_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../shape-core/stdlib");
544        let packaged_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("stdlib-src");
545
546        if !workspace_path.is_dir() || !packaged_path.is_dir() {
547            return;
548        }
549
550        let workspace_files = collect_shape_files(&workspace_path);
551        let packaged_files = collect_shape_files(&packaged_path);
552        assert_eq!(
553            packaged_files, workspace_files,
554            "shape-runtime/stdlib-src is out of sync with crates/shape-core/stdlib"
555        );
556    }
557}