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, FunctionDef, Item, Program, Span, 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            let source = loader
68                .resolve_module_path(&import_path)
69                .ok()
70                .and_then(|path| std::fs::read_to_string(path).ok())
71                .unwrap_or_default();
72            match loader.load_module(&import_path) {
73                Ok(module) => {
74                    Self::extract_from_program(
75                        &module.ast,
76                        &module_path,
77                        &source,
78                        &mut functions,
79                        &mut patterns,
80                        &mut intrinsic_functions,
81                        &mut intrinsic_types,
82                    );
83                }
84                Err(_) => {
85                    // Skip modules that fail to parse/compile metadata.
86                }
87            }
88        }
89
90        Ok(Self {
91            functions,
92            patterns,
93            intrinsic_functions,
94            intrinsic_types,
95        })
96    }
97
98    fn extract_from_program(
99        program: &Program,
100        module_path: &str,
101        source: &str,
102        functions: &mut Vec<FunctionInfo>,
103        _patterns: &mut Vec<PatternInfo>,
104        intrinsic_functions: &mut Vec<FunctionInfo>,
105        intrinsic_types: &mut Vec<TypeInfo>,
106    ) {
107        for item in &program.items {
108            match item {
109                Item::Function(func, _) => {
110                    // All top-level functions are considered exports
111                    functions.push(Self::function_to_info(func, module_path));
112                }
113                Item::Export(export, _) => {
114                    // Handle explicit exports
115                    match &export.item {
116                        shape_ast::ast::ExportItem::Function(func) => {
117                            functions.push(Self::function_to_info(func, module_path));
118                        }
119                        shape_ast::ast::ExportItem::TypeAlias(_) => {}
120                        shape_ast::ast::ExportItem::Named(_) => {}
121                        shape_ast::ast::ExportItem::Enum(_) => {}
122                        shape_ast::ast::ExportItem::Struct(_) => {}
123                        shape_ast::ast::ExportItem::Interface(_) => {}
124                        shape_ast::ast::ExportItem::Trait(_) => {}
125                        shape_ast::ast::ExportItem::ForeignFunction(_) => {
126                            // Foreign functions are not stdlib intrinsics
127                        }
128                    }
129                }
130                Item::BuiltinTypeDecl(type_decl, span) => {
131                    intrinsic_types.push(Self::builtin_type_to_info(type_decl, source, *span));
132                }
133                Item::BuiltinFunctionDecl(func_decl, span) => {
134                    intrinsic_functions.push(Self::builtin_function_to_info(
135                        func_decl,
136                        module_path,
137                        source,
138                        *span,
139                    ));
140                }
141                _ => {}
142            }
143        }
144    }
145
146    /// Infer function category from module path (domain-agnostic)
147    ///
148    /// Uses directory structure to determine category:
149    /// - core/math, core/statistics → Math
150    /// - */indicators/*, */backtesting/*, */simulation/* → Simulation
151    /// - */patterns/* → Utility
152    /// - Default → Utility
153    fn infer_category_from_path(module_path: &str) -> FunctionCategory {
154        let path_lower = module_path.to_lowercase().replace("::", "/");
155
156        // Check path components for categorization
157        if path_lower.contains("/math") || path_lower.contains("/statistics") {
158            FunctionCategory::Math
159        } else if path_lower.contains("/indicators")
160            || path_lower.contains("/backtesting")
161            || path_lower.contains("/simulation")
162        {
163            FunctionCategory::Simulation
164        } else if path_lower.contains("/patterns") {
165            FunctionCategory::Utility
166        } else {
167            FunctionCategory::Utility
168        }
169    }
170
171    fn function_to_info(func: &FunctionDef, module_path: &str) -> FunctionInfo {
172        let params: Vec<ParameterInfo> = func
173            .params
174            .iter()
175            .map(|p| ParameterInfo {
176                name: p.simple_name().unwrap_or("_").to_string(),
177                param_type: p
178                    .type_annotation
179                    .as_ref()
180                    .map(Self::format_type_annotation)
181                    .unwrap_or_else(|| "any".to_string()),
182                optional: p.default_value.is_some(),
183                description: String::new(),
184                constraints: None,
185            })
186            .collect();
187
188        let return_type = func
189            .return_type
190            .as_ref()
191            .map(Self::format_type_annotation)
192            .unwrap_or_else(|| "any".to_string());
193
194        let param_strs: Vec<String> = params
195            .iter()
196            .map(|p| {
197                if p.optional {
198                    format!("{}?: {}", p.name, p.param_type)
199                } else {
200                    format!("{}: {}", p.name, p.param_type)
201                }
202            })
203            .collect();
204
205        let signature = format!(
206            "{}({}) -> {}",
207            func.name,
208            param_strs.join(", "),
209            return_type
210        );
211
212        // Determine category based on path structure (domain-agnostic)
213        let category = Self::infer_category_from_path(module_path);
214
215        FunctionInfo {
216            name: func.name.clone(),
217            signature,
218            description: format!("Function from stdlib.{}", module_path),
219            category,
220            parameters: params,
221            return_type,
222            example: None,
223            implemented: true,
224            comptime_only: false,
225        }
226    }
227
228    fn builtin_type_to_info(type_decl: &BuiltinTypeDecl, source: &str, span: Span) -> TypeInfo {
229        let description = extract_doc_comment(source, span)
230            .filter(|s| !s.is_empty())
231            .unwrap_or_else(|| format!("Builtin type `{}`", type_decl.name));
232        TypeInfo {
233            name: type_decl.name.clone(),
234            description,
235        }
236    }
237
238    fn builtin_function_to_info(
239        func: &BuiltinFunctionDecl,
240        module_path: &str,
241        source: &str,
242        span: Span,
243    ) -> FunctionInfo {
244        let params: Vec<ParameterInfo> = func
245            .params
246            .iter()
247            .map(|p| ParameterInfo {
248                name: p.simple_name().unwrap_or("_").to_string(),
249                param_type: p
250                    .type_annotation
251                    .as_ref()
252                    .map(Self::format_type_annotation)
253                    .unwrap_or_else(|| "any".to_string()),
254                optional: p.default_value.is_some(),
255                description: String::new(),
256                constraints: None,
257            })
258            .collect();
259        let return_type = Self::format_type_annotation(&func.return_type);
260        let type_params_str = func
261            .type_params
262            .as_ref()
263            .filter(|params| !params.is_empty())
264            .map(|params| {
265                format!(
266                    "<{}>",
267                    params
268                        .iter()
269                        .map(|p| p.name.as_str())
270                        .collect::<Vec<_>>()
271                        .join(", ")
272                )
273            })
274            .unwrap_or_default();
275        let signature = format!(
276            "{}{}({}) -> {}",
277            func.name,
278            type_params_str,
279            params
280                .iter()
281                .map(|p| format!("{}: {}", p.name, p.param_type))
282                .collect::<Vec<_>>()
283                .join(", "),
284            return_type
285        );
286        let description = extract_doc_comment(source, span)
287            .filter(|s| !s.is_empty())
288            .unwrap_or_else(|| format!("Builtin function `{}`", func.name));
289
290        FunctionInfo {
291            name: func.name.clone(),
292            signature,
293            description,
294            category: Self::infer_category_from_path(module_path),
295            parameters: params,
296            return_type,
297            example: None,
298            implemented: true,
299            comptime_only: crate::builtin_metadata::is_comptime_builtin_function(&func.name),
300        }
301    }
302
303    fn format_type_annotation(ty: &TypeAnnotation) -> String {
304        match ty {
305            TypeAnnotation::Basic(name) | TypeAnnotation::Reference(name) => name.clone(),
306            TypeAnnotation::Array(inner) => format!("{}[]", Self::format_type_annotation(inner)),
307            TypeAnnotation::Tuple(items) => format!(
308                "[{}]",
309                items
310                    .iter()
311                    .map(Self::format_type_annotation)
312                    .collect::<Vec<_>>()
313                    .join(", ")
314            ),
315            TypeAnnotation::Object(fields) => {
316                let inner = fields
317                    .iter()
318                    .map(|f| {
319                        if f.optional {
320                            format!(
321                                "{}?: {}",
322                                f.name,
323                                Self::format_type_annotation(&f.type_annotation)
324                            )
325                        } else {
326                            format!(
327                                "{}: {}",
328                                f.name,
329                                Self::format_type_annotation(&f.type_annotation)
330                            )
331                        }
332                    })
333                    .collect::<Vec<_>>()
334                    .join(", ");
335                format!("{{ {} }}", inner)
336            }
337            TypeAnnotation::Function { params, returns } => {
338                let param_list = params
339                    .iter()
340                    .map(|p| {
341                        let ty = Self::format_type_annotation(&p.type_annotation);
342                        if let Some(name) = &p.name {
343                            if p.optional {
344                                format!("{}?: {}", name, ty)
345                            } else {
346                                format!("{}: {}", name, ty)
347                            }
348                        } else {
349                            ty
350                        }
351                    })
352                    .collect::<Vec<_>>()
353                    .join(", ");
354                format!(
355                    "({}) -> {}",
356                    param_list,
357                    Self::format_type_annotation(returns)
358                )
359            }
360            TypeAnnotation::Union(types) => types
361                .iter()
362                .map(Self::format_type_annotation)
363                .collect::<Vec<_>>()
364                .join(" | "),
365            TypeAnnotation::Intersection(types) => types
366                .iter()
367                .map(Self::format_type_annotation)
368                .collect::<Vec<_>>()
369                .join(" + "),
370            TypeAnnotation::Optional(inner) => format!("{}?", Self::format_type_annotation(inner)),
371            TypeAnnotation::Generic { name, args } => format!(
372                "{}<{}>",
373                name,
374                args.iter()
375                    .map(Self::format_type_annotation)
376                    .collect::<Vec<_>>()
377                    .join(", ")
378            ),
379            TypeAnnotation::Void => "void".to_string(),
380            TypeAnnotation::Any => "any".to_string(),
381            TypeAnnotation::Never => "never".to_string(),
382            TypeAnnotation::Null => "null".to_string(),
383            TypeAnnotation::Undefined => "undefined".to_string(),
384            TypeAnnotation::Dyn(bounds) => format!("dyn {}", bounds.join(" + ")),
385        }
386    }
387}
388
389fn extract_doc_comment(source: &str, span: Span) -> Option<String> {
390    if source.is_empty() {
391        return None;
392    }
393
394    let clamped_start = span.start.min(source.len());
395    let line_index = source[..clamped_start]
396        .bytes()
397        .filter(|b| *b == b'\n')
398        .count();
399    let lines: Vec<&str> = source.lines().collect();
400    if lines.is_empty() || line_index == 0 {
401        return None;
402    }
403
404    let mut docs = Vec::new();
405    let mut idx = line_index.saturating_sub(1) as isize;
406
407    while idx >= 0 {
408        let line = lines[idx as usize].trim_start();
409        if let Some(rest) = line.strip_prefix("///") {
410            docs.push(rest.trim_start().to_string());
411            idx -= 1;
412            continue;
413        }
414        if line.is_empty() && !docs.is_empty() {
415            idx -= 1;
416            continue;
417        }
418        break;
419    }
420
421    if docs.is_empty() {
422        None
423    } else {
424        docs.reverse();
425        Some(docs.join("\n"))
426    }
427}
428
429/// Get the default stdlib path
430pub fn default_stdlib_path() -> PathBuf {
431    // Explicit override for non-workspace environments (packaged installs, custom dev layouts).
432    if let Ok(path) = std::env::var("SHAPE_STDLIB_PATH") {
433        return PathBuf::from(path);
434    }
435
436    // Canonical workspace location.
437    PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../shape-core/stdlib")
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_load_stdlib() {
446        let stdlib_path = default_stdlib_path();
447        if stdlib_path.exists() {
448            let metadata = StdlibMetadata::load(&stdlib_path).unwrap();
449            // Stdlib may or may not have functions depending on development state
450            println!("Stdlib path: {:?}", stdlib_path);
451            println!("Found {} stdlib functions", metadata.functions.len());
452            for func in &metadata.functions {
453                println!("  - {}: {}", func.name, func.signature);
454            }
455            println!("Found {} stdlib patterns", metadata.patterns.len());
456            // Note: stdlib functions (like sma, ema) will be added as stdlib is developed
457        } else {
458            println!("Stdlib path does not exist: {:?}", stdlib_path);
459        }
460    }
461
462    #[test]
463    fn test_empty_stdlib() {
464        let metadata = StdlibMetadata::empty();
465        assert!(metadata.functions.is_empty());
466        assert!(metadata.patterns.is_empty());
467    }
468
469    #[test]
470    fn test_parse_all_stdlib_files() {
471        let stdlib_path = default_stdlib_path();
472        println!("Stdlib path: {:?}", stdlib_path);
473
474        // Test each file individually
475        let files = [
476            "core/snapshot.shape",
477            "core/math.shape",
478            "finance/indicators/moving_averages.shape",
479        ];
480
481        for file in &files {
482            let path = stdlib_path.join(file);
483            if path.exists() {
484                let content = std::fs::read_to_string(&path).unwrap();
485                match parse_program(&content) {
486                    Ok(program) => {
487                        let func_count = program
488                            .items
489                            .iter()
490                            .filter(|i| {
491                                matches!(
492                                    i,
493                                    shape_ast::ast::Item::Function(_, _)
494                                        | shape_ast::ast::Item::Export(_, _)
495                                )
496                            })
497                            .count();
498                        println!("✓ {} parsed: {} items", file, func_count);
499                    }
500                    Err(e) => {
501                        panic!("✗ {} FAILED to parse: {:?}", file, e);
502                    }
503                }
504            } else {
505                println!("⚠ {} not found", file);
506            }
507        }
508    }
509
510    #[test]
511    fn test_intrinsic_declarations_loaded_from_std_core() {
512        let stdlib_path = default_stdlib_path();
513        if !stdlib_path.exists() {
514            return;
515        }
516
517        let metadata = StdlibMetadata::load(&stdlib_path).unwrap();
518        assert!(
519            metadata
520                .intrinsic_types
521                .iter()
522                .any(|t| t.name == "AnyError"),
523            "expected AnyError intrinsic type from std::core declarations"
524        );
525        let abs = metadata
526            .intrinsic_functions
527            .iter()
528            .find(|f| f.name == "abs")
529            .expect("abs intrinsic declaration should exist");
530        assert_eq!(abs.signature, "abs(value: number) -> number");
531        assert!(
532            abs.description.contains("absolute value"),
533            "abs description should come from doc comments"
534        );
535    }
536}