1use 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#[derive(Debug, Default)]
17pub struct StdlibMetadata {
18 pub functions: Vec<FunctionInfo>,
20 pub patterns: Vec<PatternInfo>,
22 pub intrinsic_functions: Vec<FunctionInfo>,
24 pub intrinsic_types: Vec<TypeInfo>,
26}
27
28#[derive(Debug, Clone)]
30pub struct PatternInfo {
31 pub name: String,
33 pub signature: String,
35 pub description: String,
37 pub parameters: Vec<ParameterInfo>,
39}
40
41impl StdlibMetadata {
42 pub fn empty() -> Self {
44 Self::default()
45 }
46
47 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 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 }
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 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 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 }
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 fn infer_category_from_path(module_path: &str) -> FunctionCategory {
155 let path_lower = module_path.to_lowercase().replace("::", "/");
156
157 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 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
398pub fn default_stdlib_path() -> PathBuf {
400 if let Ok(path) = std::env::var("SHAPE_STDLIB_PATH") {
402 return PathBuf::from(path);
403 }
404
405 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 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 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 } 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 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}