1use crate::configuration::BytecodeExecutor;
7
8use shape_ast::Program;
9use shape_ast::ast::{ExportItem, Item};
10use shape_ast::parser::parse_program;
11use shape_runtime::module_loader::ModuleCode;
12
13pub fn build_graph_and_stdlib_names(
22 program: &Program,
23 loader: &mut shape_runtime::module_loader::ModuleLoader,
24 extensions: &[shape_runtime::module_exports::ModuleExports],
25) -> std::result::Result<
26 (
27 std::sync::Arc<crate::module_graph::ModuleGraph>,
28 std::collections::HashSet<String>,
29 Vec<String>,
30 ),
31 shape_ast::error::ShapeError,
32> {
33 let prelude_imports = crate::module_graph::collect_prelude_import_paths(loader);
34 let graph =
35 crate::module_graph::build_module_graph(program, loader, extensions, &prelude_imports)
36 .map_err(|e| shape_ast::error::ShapeError::ModuleError {
37 message: e.to_string(),
38 module_path: None,
39 })?;
40 let graph = std::sync::Arc::new(graph);
41
42 let mut stdlib_names = std::collections::HashSet::new();
43 for prelude_path in &prelude_imports {
44 if let Some(dep_id) = graph.id_for_path(prelude_path) {
45 let dep_node = graph.node(dep_id);
46 for export_name in dep_node.interface.exports.keys() {
47 stdlib_names.insert(export_name.clone());
48 stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
49 }
50 }
51 }
52
53 Ok((graph, stdlib_names, prelude_imports))
54}
55
56pub(crate) fn annotate_program_native_abi_package_key(
58 program: &mut Program,
59 package_key: Option<&str>,
60) {
61 let Some(package_key) = package_key else {
62 return;
63 };
64 for item in &mut program.items {
65 annotate_item_native_abi_package_key(item, package_key);
66 }
67}
68
69fn annotate_item_native_abi_package_key(item: &mut Item, package_key: &str) {
70 match item {
71 Item::ForeignFunction(def, _) => {
72 if let Some(native) = def.native_abi.as_mut()
73 && native.package_key.is_none()
74 {
75 native.package_key = Some(package_key.to_string());
76 }
77 }
78 Item::Export(export, _) => {
79 if let ExportItem::ForeignFunction(def) = &mut export.item
80 && let Some(native) = def.native_abi.as_mut()
81 && native.package_key.is_none()
82 {
83 native.package_key = Some(package_key.to_string());
84 }
85 }
86 Item::Module(module, _) => {
87 for nested in &mut module.items {
88 annotate_item_native_abi_package_key(nested, package_key);
89 }
90 }
91 _ => {}
92 }
93}
94
95
96impl BytecodeExecutor {
97 pub fn set_module_loader(&mut self, mut loader: shape_runtime::module_loader::ModuleLoader) {
102 if !self.dependency_paths.is_empty() {
103 loader.set_dependency_paths(self.dependency_paths.clone());
104 }
105 self.register_extension_artifacts_in_loader(&mut loader);
106 self.module_loader = Some(loader);
107 }
108
109 pub(crate) fn register_extension_artifacts_in_loader(
110 &self,
111 loader: &mut shape_runtime::module_loader::ModuleLoader,
112 ) {
113 for module in &self.extensions {
114 for artifact in &module.module_artifacts {
115 let code = match (&artifact.source, &artifact.compiled) {
116 (Some(source), Some(compiled)) => ModuleCode::Both {
117 source: std::sync::Arc::from(source.as_str()),
118 compiled: std::sync::Arc::from(compiled.clone()),
119 },
120 (Some(source), None) => {
121 ModuleCode::Source(std::sync::Arc::from(source.as_str()))
122 }
123 (None, Some(compiled)) => {
124 ModuleCode::Compiled(std::sync::Arc::from(compiled.clone()))
125 }
126 (None, None) => continue,
127 };
128 loader.register_extension_module(artifact.module_path.clone(), code);
129 }
130
131 for (_filename, source) in &module.shape_sources {
133 if !loader.has_extension_module(&module.name) {
134 loader.register_extension_module(
135 module.name.clone(),
136 ModuleCode::Source(std::sync::Arc::from(source.as_str())),
137 );
138 }
139 }
140 }
141 }
142
143 pub fn module_loader_mut(&mut self) -> Option<&mut shape_runtime::module_loader::ModuleLoader> {
145 self.module_loader.as_mut()
146 }
147
148 pub fn resolve_file_imports_with_context(
156 &mut self,
157 program: &Program,
158 context_dir: Option<&std::path::Path>,
159 ) {
160 use shape_ast::ast::Item;
161
162 let loader = match self.module_loader.as_mut() {
163 Some(l) => l,
164 None => return,
165 };
166 let context_dir = context_dir.map(std::path::Path::to_path_buf);
167
168 let import_paths: Vec<String> = program
170 .items
171 .iter()
172 .filter_map(|item| {
173 if let Item::Import(import_stmt, _) = item {
174 Some(import_stmt.from.clone())
175 } else {
176 None
177 }
178 })
179 .filter(|path| !path.is_empty())
180 .collect();
181
182 for module_path in &import_paths {
183 let _ = loader.load_module_with_context(module_path, context_dir.as_ref());
187 }
188
189 let mut loaded_module_paths: Vec<String> = loader
192 .loaded_modules()
193 .into_iter()
194 .map(str::to_string)
195 .collect();
196 loaded_module_paths.sort();
197
198 for module_path in loaded_module_paths {
199 self.compiled_module_paths.insert(module_path);
200 }
201 }
202
203 pub fn resolve_file_imports(&mut self, program: &Program) {
205 self.resolve_file_imports_with_context(program, None);
206 }
207
208 pub fn resolve_file_imports_from_source(
210 &mut self,
211 source: &str,
212 context_dir: Option<&std::path::Path>,
213 ) {
214 match parse_program(source) {
215 Ok(program) => self.resolve_file_imports_with_context(&program, context_dir),
216 Err(e) => eprintln!(
217 "Warning: failed to parse source for import pre-resolution: {}",
218 e
219 ),
220 }
221 }
222
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::VMConfig;
229 use crate::compiler::BytecodeCompiler;
230 use crate::executor::VirtualMachine;
231 use crate::module_graph;
232 use shape_value::{KindedSlot, NativeKind};
233
234 fn slot_as_number(slot: &KindedSlot) -> Option<f64> {
240 match slot.kind() {
241 NativeKind::Int64 => slot.as_i64().map(|n| n as f64),
242 NativeKind::Float64 => slot.as_f64(),
243 _ => None,
244 }
245 }
246
247 fn compile_program_with_graph(
249 source: &str,
250 extra_paths: &[std::path::PathBuf],
251 ) -> shape_ast::error::Result<crate::bytecode::BytecodeProgram> {
252 let program = shape_ast::parser::parse_program(source)?;
253 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
254 for p in extra_paths {
255 loader.add_module_path(p.clone());
256 }
257 let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
258 let graph = module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
259 .map_err(|e| shape_ast::error::ShapeError::ModuleError {
260 message: e.to_string(),
261 module_path: None,
262 })?;
263 let graph = std::sync::Arc::new(graph);
264
265 let mut stdlib_names = std::collections::HashSet::new();
266 for prelude_path in &prelude_imports {
267 if let Some(dep_id) = graph.id_for_path(prelude_path) {
268 let dep_node = graph.node(dep_id);
269 for export_name in dep_node.interface.exports.keys() {
270 stdlib_names.insert(export_name.clone());
271 stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
272 }
273 }
274 }
275
276 let mut compiler = BytecodeCompiler::new();
277 compiler.stdlib_function_names = stdlib_names;
278 compiler.compile_with_graph_and_prelude(&program, graph, &prelude_imports)
279 }
280
281 #[test]
282 fn test_graph_prelude_provides_stdlib_definitions() {
283 let bytecode = compile_program_with_graph("let x = 42\nx", &[])
285 .expect("compile with graph prelude should succeed");
286 assert!(
287 !bytecode.functions.is_empty(),
288 "bytecode should contain prelude-compiled functions"
289 );
290 }
291
292 #[test]
293 fn test_graph_prelude_includes_math_functions() {
294 let program = shape_ast::parser::parse_program("let x = 1\nx").expect("parse");
296 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
297 let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
298 let graph =
299 module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
300 .expect("graph build");
301
302 let math_id = graph.id_for_path("std::core::math");
304 assert!(math_id.is_some(), "graph should contain std::core::math");
305
306 let math_node = graph.node(math_id.unwrap());
307 assert!(
308 math_node.interface.exports.contains_key("sum"),
309 "std::core::math should export 'sum'"
310 );
311 }
312
313 #[test]
314 fn test_graph_compiles_with_engine() {
315 let mut executor = crate::configuration::BytecodeExecutor::new();
317 let mut engine =
318 shape_runtime::engine::ShapeEngine::new().expect("engine creation failed");
319 engine.load_stdlib().expect("load stdlib");
320
321 let program = shape_ast::parser::parse_program("let x = 42\nx").expect("parse");
322 let bytecode = executor
323 .compile_program_for_inspection(&mut engine, &program)
324 .expect("compile with graph pipeline should succeed");
325
326 assert!(
327 !bytecode.functions.is_empty(),
328 "bytecode should contain prelude-compiled functions"
329 );
330 }
331
332 #[test]
333 fn test_graph_file_dependency_named_import() {
334 let tmp = tempfile::tempdir().expect("temp dir");
336 let mod_dir = tmp.path().join("mymod");
337 std::fs::create_dir_all(&mod_dir).expect("create mymod dir");
338 std::fs::write(
339 mod_dir.join("index.shape"),
340 r#"
341pub fn alpha() -> int { 1 }
342pub fn beta() -> int { 2 }
343pub fn gamma() -> int { 3 }
344"#,
345 )
346 .expect("write index.shape");
347
348 let source = r#"
349from mymod use { alpha, beta, gamma }
350alpha() + beta() + gamma()
351"#;
352 let bytecode = compile_program_with_graph(source, &[tmp.path().to_path_buf()])
353 .expect("named import from file dependency should compile");
354
355 let mut vm = VirtualMachine::new(VMConfig::default());
356 vm.load_program(bytecode);
357 let result = vm.execute(None).expect("execute");
358 assert_eq!(slot_as_number(&result).unwrap(), 6.0);
359 }
360
361 #[test]
362 fn test_graph_namespace_import_enables_qualified_calls() {
363 let tmp = tempfile::tempdir().expect("temp dir");
364 let mod_dir = tmp.path().join("mymod");
365 std::fs::create_dir_all(&mod_dir).expect("create module dir");
366 std::fs::write(
367 mod_dir.join("index.shape"),
368 r#"
369pub fn alpha() -> int { 1 }
370pub fn beta() -> int { alpha() + 1 }
371"#,
372 )
373 .expect("write index.shape");
374
375 let bytecode = compile_program_with_graph(
376 r#"
377use mymod
378mymod::beta()
379"#,
380 &[tmp.path().to_path_buf()],
381 )
382 .expect("namespace call should compile");
383
384 let mut vm = VirtualMachine::new(VMConfig::default());
385 vm.load_program(bytecode);
386 let result = vm.execute(None).expect("execute");
387 assert_eq!(slot_as_number(&result).unwrap(), 2.0);
388 }
389
390 #[test]
391 fn test_graph_cycle_detection() {
392 let tmp = tempfile::tempdir().expect("temp dir");
394 std::fs::write(
395 tmp.path().join("a.shape"),
396 "use b\npub fn fa() -> int { 1 }\n",
397 )
398 .expect("write a.shape");
399 std::fs::write(
400 tmp.path().join("b.shape"),
401 "use a\npub fn fb() -> int { 2 }\n",
402 )
403 .expect("write b.shape");
404
405 let source = "use a\na::fa()\n";
406 let result = compile_program_with_graph(source, &[tmp.path().to_path_buf()]);
407 assert!(
408 result.is_err(),
409 "circular import should produce an error"
410 );
411 let err_msg = format!("{}", result.unwrap_err());
412 assert!(
413 err_msg.to_lowercase().contains("circular")
414 || err_msg.to_lowercase().contains("cyclic"),
415 "error should mention circularity, got: {}",
416 err_msg
417 );
418 }
419
420 #[test]
421 fn test_graph_stdlib_names_include_qualified() {
422 let program = shape_ast::parser::parse_program("1").expect("parse");
424 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
425 let prelude_imports = module_graph::collect_prelude_import_paths(&mut loader);
426 let graph =
427 module_graph::build_module_graph(&program, &mut loader, &[], &prelude_imports)
428 .expect("graph build");
429
430 let mut stdlib_names = std::collections::HashSet::new();
431 for prelude_path in &prelude_imports {
432 if let Some(dep_id) = graph.id_for_path(prelude_path) {
433 let dep_node = graph.node(dep_id);
434 for export_name in dep_node.interface.exports.keys() {
435 stdlib_names.insert(export_name.clone());
436 stdlib_names.insert(format!("{}::{}", prelude_path, export_name));
437 }
438 }
439 }
440
441 assert!(
442 stdlib_names.contains("sum"),
443 "stdlib_names should contain bare name 'sum'"
444 );
445 assert!(
446 stdlib_names.contains("std::core::math::sum"),
447 "stdlib_names should contain qualified name 'std::core::math::sum'"
448 );
449 }
450
451 #[test]
457 fn test_type_alias_forward_reference_under_graph_compilation() {
458 let bytecode = compile_program_with_graph(
461 r#"
462 fn make_val() -> MyInt { 42 }
463 type MyInt = int
464 make_val()
465 "#,
466 &[],
467 )
468 .expect("compile with forward type alias should succeed");
469 let mut vm = VirtualMachine::new(VMConfig::default());
470 vm.load_program(bytecode);
471 let result = vm.execute(None).expect("execute failed");
472 assert_eq!(result.as_i64(), Some(42));
473 }
474}