1use crate::bytecode;
6use crate::compiler::BytecodeCompiler;
7use crate::module_resolution::annotate_program_native_abi_package_key;
8use sha2::{Digest, Sha256};
9use shape_ast::parser::parse_program;
10use shape_runtime::module_manifest::ModuleManifest;
11use shape_runtime::package_bundle::{
12 BundleMetadata, BundledModule, BundledNativeDependencyScope, PackageBundle,
13};
14use shape_runtime::project::ProjectRoot;
15use std::collections::{HashMap, HashSet, VecDeque};
16use std::path::{Path, PathBuf};
17use std::time::SystemTime;
18
19pub struct BundleCompiler;
21
22impl BundleCompiler {
23 pub fn compile(project: &ProjectRoot) -> Result<PackageBundle, String> {
25 let root = &project.root_path;
26
27 let shape_files = discover_shape_files(root, project)?;
29
30 if shape_files.is_empty() {
31 return Err("No .shape files found in project".to_string());
32 }
33
34 let mut modules = Vec::new();
36 let mut all_sources = String::new();
37 let mut docs: HashMap<String, Vec<shape_runtime::doc_extract::DocItem>> = HashMap::new();
38 let mut compiled_programs: Vec<(String, Vec<String>, Option<bytecode::Program>)> =
40 Vec::new();
41
42 let mut loader = shape_runtime::module_loader::ModuleLoader::new();
43 loader.set_project_root(root, &project.resolved_module_paths());
44 let dependency_paths: HashMap<String, PathBuf> = project
45 .config
46 .dependencies
47 .iter()
48 .filter_map(|(name, spec)| match spec {
49 shape_runtime::project::DependencySpec::Detailed(detail) => {
50 detail.path.as_ref().map(|path| {
51 let dep_path = root.join(path);
52 let canonical = dep_path.canonicalize().unwrap_or(dep_path);
53 (name.clone(), canonical)
54 })
55 }
56 _ => None,
57 })
58 .collect();
59 if !dependency_paths.is_empty() {
60 loader.set_dependency_paths(dependency_paths);
61 }
62 let known_bindings = Vec::new();
63 let native_resolution_context =
64 shape_runtime::native_resolution::resolve_native_dependencies_for_project(
65 project,
66 &root.join("shape.lock"),
67 project.config.build.external.mode,
68 )
69 .map_err(|e| format!("Failed to resolve native dependencies for bundle: {}", e))?;
70 let root_package_key =
71 shape_runtime::project::normalize_package_identity(root, &project.config).2;
72
73 for (file_path, module_path) in &shape_files {
74 let source = std::fs::read_to_string(file_path)
75 .map_err(|e| format!("Failed to read '{}': {}", file_path.display(), e))?;
76
77 let mut hasher = Sha256::new();
79 hasher.update(source.as_bytes());
80 let source_hash = format!("{:x}", hasher.finalize());
81
82 all_sources.push_str(&source);
84
85 let mut ast = parse_program(&source)
87 .map_err(|e| format!("Failed to parse '{}': {}", file_path.display(), e))?;
88 annotate_program_native_abi_package_key(&mut ast, Some(root_package_key.as_str()));
89
90 let module_docs = shape_runtime::doc_extract::extract_docs_from_ast(&source, &ast);
92 if !module_docs.is_empty() {
93 docs.insert(module_path.clone(), module_docs);
94 }
95
96 let export_names = collect_export_names(&ast);
98
99 let (graph, stdlib_names, prelude_imports) =
101 crate::module_resolution::build_graph_and_stdlib_names(&ast, &mut loader, &[])
102 .map_err(|e| {
103 format!(
104 "Failed to build module graph for '{}': {}",
105 file_path.display(),
106 e
107 )
108 })?;
109
110 let mut compiler = BytecodeCompiler::new();
111 compiler.stdlib_function_names = stdlib_names;
112 compiler.register_known_bindings(&known_bindings);
113 compiler.native_resolution_context = Some(native_resolution_context.clone());
114 compiler.set_source_dir(root.clone());
115 let bytecode = compiler
116 .compile_with_graph_and_prelude(&ast, graph, &prelude_imports)
117 .map_err(|e| format!("Failed to compile '{}': {}", file_path.display(), e))?;
118
119 let content_addressed = bytecode.content_addressed.clone();
121
122 let bytecode_bytes = rmp_serde::to_vec(&bytecode).map_err(|e| {
124 format!(
125 "Failed to serialize bytecode for '{}': {}",
126 file_path.display(),
127 e
128 )
129 })?;
130
131 compiled_programs.push((module_path.clone(), export_names.clone(), content_addressed));
132
133 modules.push(BundledModule {
134 module_path: module_path.clone(),
135 bytecode_bytes,
136 export_names,
137 source_hash,
138 });
139 }
140
141 let mut hasher = Sha256::new();
143 hasher.update(all_sources.as_bytes());
144 let source_hash = format!("{:x}", hasher.finalize());
145
146 let mut dependencies = HashMap::new();
148 for (name, spec) in &project.config.dependencies {
149 let version = match spec {
150 shape_runtime::project::DependencySpec::Version(v) => v.clone(),
151 shape_runtime::project::DependencySpec::Detailed(d) => {
152 d.version.clone().unwrap_or_else(|| "local".to_string())
153 }
154 };
155 dependencies.insert(name.clone(), version);
156 }
157
158 let native_dependency_scopes = collect_native_dependency_scopes(root, &project.config)
159 .map_err(|e| {
160 format!(
161 "Failed to collect transitive native dependency scopes for bundle: {}",
162 e
163 )
164 })?;
165 let native_portable = native_dependency_scopes
166 .iter()
167 .all(native_dependency_scope_is_portable);
168
169 let readme = ["README.md", "readme.md", "Readme.md"]
171 .iter()
172 .map(|name| root.join(name))
173 .find(|p| p.is_file())
174 .and_then(|p| std::fs::read_to_string(p).ok());
175
176 let built_at = SystemTime::now()
178 .duration_since(SystemTime::UNIX_EPOCH)
179 .map(|d| d.as_secs())
180 .unwrap_or(0);
181
182 let metadata = BundleMetadata {
183 name: project.config.project.name.clone(),
184 version: project.config.project.version.clone(),
185 compiler_version: env!("CARGO_PKG_VERSION").to_string(),
186 source_hash,
187 bundle_kind: "portable-bytecode".to_string(),
188 build_host: format!("{}-{}", std::env::consts::ARCH, std::env::consts::OS),
189 native_portable,
190 entry_module: project
191 .config
192 .project
193 .entry
194 .as_ref()
195 .map(|e| path_to_module_path(Path::new(e), root)),
196 built_at,
197 readme,
198 };
199
200 let mut blob_store: HashMap<[u8; 32], Vec<u8>> = HashMap::new();
202 let mut manifests: Vec<ModuleManifest> = Vec::new();
203
204 for (module_path, export_names, content_addressed) in &compiled_programs {
205 if let Some(ca) = content_addressed {
206 for (hash, blob) in &ca.function_store {
208 if let Ok(blob_bytes) = rmp_serde::to_vec(blob) {
209 blob_store.insert(hash.0, blob_bytes);
210 }
211 }
212
213 let mut manifest =
215 ModuleManifest::new(module_path.clone(), metadata.version.clone());
216
217 for export_name in export_names {
219 for (hash, blob) in &ca.function_store {
220 if blob.name == *export_name {
221 manifest.add_export(export_name.clone(), hash.0);
222 break;
223 }
224 }
225 }
226
227 let mut seen_schemas = std::collections::HashSet::new();
229 for (_hash, blob) in &ca.function_store {
230 for schema_name in &blob.type_schemas {
231 if seen_schemas.insert(schema_name.clone()) {
232 let schema_hash = Sha256::digest(schema_name.as_bytes());
233 let mut hash_bytes = [0u8; 32];
234 hash_bytes.copy_from_slice(&schema_hash);
235 manifest.add_type_schema(schema_name.clone(), hash_bytes);
236 }
237 }
238 }
239
240 for (_export_name, export_hash) in &manifest.exports {
242 let mut closure = Vec::new();
243 let mut visited = std::collections::HashSet::new();
244 let mut queue = vec![*export_hash];
245 while let Some(h) = queue.pop() {
246 if !visited.insert(h) {
247 continue;
248 }
249 if let Some(blob) = ca.function_store.get(&crate::bytecode::FunctionHash(h))
250 {
251 for dep in &blob.dependencies {
252 closure.push(dep.0);
253 queue.push(dep.0);
254 }
255 }
256 }
257 closure.sort();
258 closure.dedup();
259 manifest.dependency_closure.insert(*export_hash, closure);
260 }
261
262 manifest.finalize();
263 manifests.push(manifest);
264 }
265 }
266
267 Ok(PackageBundle {
268 metadata,
269 modules,
270 dependencies,
271 blob_store,
272 manifests,
273 native_dependency_scopes,
274 docs,
275 })
276 }
277}
278
279fn merge_native_scope(
280 scopes: &mut HashMap<String, BundledNativeDependencyScope>,
281 scope: BundledNativeDependencyScope,
282) {
283 if let Some(existing) = scopes.get_mut(&scope.package_key) {
284 existing.dependencies.extend(scope.dependencies);
285 return;
286 }
287 scopes.insert(scope.package_key.clone(), scope);
288}
289
290fn collect_native_dependency_scopes(
291 root_path: &Path,
292 project: &shape_runtime::project::ShapeProject,
293) -> Result<Vec<BundledNativeDependencyScope>, String> {
294 let (root_name, root_version, root_key) =
295 shape_runtime::project::normalize_package_identity(root_path, project);
296
297 let mut queue: VecDeque<(
298 PathBuf,
299 shape_runtime::project::ShapeProject,
300 String,
301 String,
302 String,
303 )> = VecDeque::new();
304 queue.push_back((
305 root_path.to_path_buf(),
306 project.clone(),
307 root_name,
308 root_version,
309 root_key,
310 ));
311
312 let mut scopes_by_key: HashMap<String, BundledNativeDependencyScope> = HashMap::new();
313 let mut visited_roots: HashSet<PathBuf> = HashSet::new();
314
315 while let Some((package_root, package, package_name, package_version, package_key)) =
316 queue.pop_front()
317 {
318 let canonical_root = package_root
319 .canonicalize()
320 .unwrap_or_else(|_| package_root.clone());
321 if !visited_roots.insert(canonical_root.clone()) {
322 continue;
323 }
324
325 let native_deps = package.native_dependencies().map_err(|e| {
326 format!(
327 "invalid [native-dependencies] in package '{}': {}",
328 package_name, e
329 )
330 })?;
331 if !native_deps.is_empty() {
332 merge_native_scope(
333 &mut scopes_by_key,
334 BundledNativeDependencyScope {
335 package_name: package_name.clone(),
336 package_version: package_version.clone(),
337 package_key: package_key.clone(),
338 dependencies: native_deps,
339 },
340 );
341 }
342
343 if package.dependencies.is_empty() {
344 continue;
345 }
346
347 let Some(resolver) =
348 shape_runtime::dependency_resolver::DependencyResolver::new(canonical_root.clone())
349 else {
350 continue;
351 };
352 let resolved = resolver.resolve(&package.dependencies).map_err(|e| {
353 format!(
354 "failed to resolve dependencies for package '{}': {}",
355 package_name, e
356 )
357 })?;
358
359 for resolved_dep in resolved {
360 if resolved_dep
361 .path
362 .extension()
363 .is_some_and(|ext| ext == "shapec")
364 {
365 let bundle = shape_runtime::package_bundle::PackageBundle::read_from_file(
366 &resolved_dep.path,
367 )
368 .map_err(|e| {
369 format!(
370 "failed to read dependency bundle '{}': {}",
371 resolved_dep.path.display(),
372 e
373 )
374 })?;
375 for scope in bundle.native_dependency_scopes {
376 merge_native_scope(&mut scopes_by_key, scope);
377 }
378 continue;
379 }
380
381 let dep_root = resolved_dep.path;
382 let dep_toml = dep_root.join("shape.toml");
383 let dep_source = match std::fs::read_to_string(&dep_toml) {
384 Ok(content) => content,
385 Err(_) => continue,
386 };
387 let dep_project = shape_runtime::project::parse_shape_project_toml(&dep_source)
388 .map_err(|err| {
389 format!(
390 "failed to parse dependency project '{}': {}",
391 dep_toml.display(),
392 err
393 )
394 })?;
395 let (dep_name, dep_version, dep_key) =
396 shape_runtime::project::normalize_package_identity_with_fallback(
397 &dep_root,
398 &dep_project,
399 &resolved_dep.name,
400 &resolved_dep.version,
401 );
402 queue.push_back((dep_root, dep_project, dep_name, dep_version, dep_key));
403 }
404 }
405
406 let mut scopes: Vec<_> = scopes_by_key.into_values().collect();
407 scopes.sort_by(|a, b| a.package_key.cmp(&b.package_key));
408 Ok(scopes)
409}
410
411fn native_spec_is_portable(spec: &shape_runtime::project::NativeDependencySpec) -> bool {
412 use shape_runtime::project::{NativeDependencyProvider, NativeDependencySpec};
413
414 match spec {
415 NativeDependencySpec::Simple(value) => !is_path_like_native_spec(value),
416 NativeDependencySpec::Detailed(detail) => {
417 if matches!(
418 spec.provider_for_host(),
419 NativeDependencyProvider::Path | NativeDependencyProvider::Vendored
420 ) {
421 return false;
422 }
423 for target in detail.targets.values() {
424 if target
425 .resolve()
426 .as_deref()
427 .is_some_and(is_path_like_native_spec)
428 {
429 return false;
430 }
431 }
432 for value in [&detail.path, &detail.linux, &detail.macos, &detail.windows] {
433 if value.as_deref().is_some_and(is_path_like_native_spec) {
434 return false;
435 }
436 }
437 true
438 }
439 }
440}
441
442fn native_dependency_scope_is_portable(scope: &BundledNativeDependencyScope) -> bool {
443 scope.dependencies.values().all(native_spec_is_portable)
444}
445
446fn is_path_like_native_spec(spec: &str) -> bool {
447 let path = Path::new(spec);
448 path.is_absolute()
449 || spec.starts_with("./")
450 || spec.starts_with("../")
451 || spec.contains('/')
452 || spec.contains('\\')
453 || (spec.len() >= 2 && spec.as_bytes()[1] == b':')
454}
455
456fn discover_shape_files(
458 root: &Path,
459 project: &ProjectRoot,
460) -> Result<Vec<(PathBuf, String)>, String> {
461 let mut files = Vec::new();
462
463 collect_shape_files(root, root, &mut files)?;
465
466 for module_path in project.resolved_module_paths() {
468 if module_path.exists() && module_path.is_dir() {
469 collect_shape_files(&module_path, &module_path, &mut files)?;
470 }
471 }
472
473 files.sort_by(|a, b| a.0.cmp(&b.0));
475 files.dedup_by(|a, b| a.0 == b.0);
476
477 Ok(files)
478}
479
480fn collect_shape_files(
482 dir: &Path,
483 base: &Path,
484 files: &mut Vec<(PathBuf, String)>,
485) -> Result<(), String> {
486 let entries = std::fs::read_dir(dir)
487 .map_err(|e| format!("Failed to read directory '{}': {}", dir.display(), e))?;
488
489 for entry in entries {
490 let entry = entry.map_err(|e| format!("Failed to read dir entry: {}", e))?;
491 let path = entry.path();
492 let file_name = entry.file_name().to_string_lossy().to_string();
493
494 if file_name.starts_with('.') || file_name == "target" || file_name == "node_modules" {
496 continue;
497 }
498
499 if path.is_dir() {
500 collect_shape_files(&path, base, files)?;
501 } else if path.extension().and_then(|e| e.to_str()) == Some("shape") {
502 let module_path = path_to_module_path(&path, base);
503 files.push((path, module_path));
504 }
505 }
506
507 Ok(())
508}
509
510fn path_to_module_path(path: &Path, base: &Path) -> String {
517 let relative = path.strip_prefix(base).unwrap_or(path);
518
519 let without_ext = relative.with_extension("");
520 let parts: Vec<&str> = without_ext
521 .components()
522 .filter_map(|c| match c {
523 std::path::Component::Normal(s) => s.to_str(),
524 _ => None,
525 })
526 .collect();
527
528 if parts.last() == Some(&"index") && parts.len() > 1 {
530 parts[..parts.len() - 1].join("::")
531 } else if parts.last() == Some(&"index") {
532 String::new()
534 } else {
535 parts.join("::")
536 }
537}
538
539fn collect_export_names(program: &shape_ast::ast::Program) -> Vec<String> {
541 let mut names = Vec::new();
542
543 for item in &program.items {
544 match item {
545 shape_ast::ast::Item::Export(export, _) => match &export.item {
546 shape_ast::ast::ExportItem::Function(func) => {
547 names.push(func.name.clone());
548 }
549 shape_ast::ast::ExportItem::BuiltinFunction(func) => {
550 names.push(func.name.clone());
551 }
552 shape_ast::ast::ExportItem::BuiltinType(ty) => {
553 names.push(ty.name.clone());
554 }
555 shape_ast::ast::ExportItem::Named(specs) => {
556 for spec in specs {
557 names.push(spec.alias.clone().unwrap_or_else(|| spec.name.clone()));
558 }
559 }
560 shape_ast::ast::ExportItem::TypeAlias(alias) => {
561 names.push(alias.name.clone());
562 }
563 shape_ast::ast::ExportItem::Enum(e) => {
564 names.push(e.name.clone());
565 }
566 shape_ast::ast::ExportItem::Struct(s) => {
567 names.push(s.name.clone());
568 }
569 shape_ast::ast::ExportItem::Trait(t) => {
570 names.push(t.name.clone());
571 }
572 shape_ast::ast::ExportItem::Annotation(annotation) => {
573 names.push(annotation.name.clone());
574 }
575 shape_ast::ast::ExportItem::ForeignFunction(f) => {
576 names.push(f.name.clone());
577 }
578 },
579 _ => {}
580 }
581 }
582
583 names.sort();
584 names.dedup();
585 names
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn discover_system_library_alias() -> Option<String> {
593 let candidates = [
594 "libm.so.6",
595 "libc.so.6",
596 "libSystem.B.dylib",
597 "kernel32.dll",
598 "ucrtbase.dll",
599 ];
600 for candidate in candidates {
601 if unsafe { libloading::Library::new(candidate) }.is_ok() {
602 return Some(candidate.to_string());
603 }
604 }
605 None
606 }
607
608 #[test]
609 fn test_path_to_module_path_basic() {
610 let base = Path::new("/project");
611 assert_eq!(
612 path_to_module_path(Path::new("/project/main.shape"), base),
613 "main"
614 );
615 assert_eq!(
616 path_to_module_path(Path::new("/project/utils/helpers.shape"), base),
617 "utils::helpers"
618 );
619 }
620
621 #[test]
622 fn test_path_to_module_path_index() {
623 let base = Path::new("/project");
624 assert_eq!(
625 path_to_module_path(Path::new("/project/utils/index.shape"), base),
626 "utils"
627 );
628 assert_eq!(
629 path_to_module_path(Path::new("/project/index.shape"), base),
630 ""
631 );
632 }
633
634 #[test]
635 fn test_compile_temp_project() {
636 let tmp = tempfile::tempdir().expect("temp dir");
637 let root = tmp.path();
638
639 std::fs::write(
641 root.join("shape.toml"),
642 r#"
643[project]
644name = "test-bundle"
645version = "0.1.0"
646"#,
647 )
648 .expect("write shape.toml");
649
650 std::fs::write(root.join("main.shape"), "pub fn run() { 42 }").expect("write main");
652 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
653 std::fs::write(root.join("utils/helpers.shape"), "pub fn helper() { 1 }")
654 .expect("write helpers");
655
656 let project =
657 shape_runtime::project::find_project_root(root).expect("should find project root");
658
659 let bundle = BundleCompiler::compile(&project).expect("compilation should succeed");
660
661 assert_eq!(bundle.metadata.name, "test-bundle");
662 assert_eq!(bundle.metadata.version, "0.1.0");
663 assert!(
664 bundle.modules.len() >= 2,
665 "should have at least 2 modules, got {}",
666 bundle.modules.len()
667 );
668
669 let main_mod = bundle.modules.iter().find(|m| m.module_path == "main");
670 assert!(main_mod.is_some(), "should have main module");
671
672 let helpers_mod = bundle
673 .modules
674 .iter()
675 .find(|m| m.module_path == "utils::helpers");
676 assert!(helpers_mod.is_some(), "should have utils::helpers module");
677 }
678
679 #[test]
680 fn test_compile_with_stdlib_imports() {
681 let tmp = tempfile::tempdir().expect("temp dir");
682 let root = tmp.path();
683
684 std::fs::write(
685 root.join("shape.toml"),
686 r#"
687[project]
688name = "test-stdlib-imports"
689version = "0.1.0"
690"#,
691 )
692 .expect("write shape.toml");
693
694 std::fs::write(
697 root.join("main.shape"),
698 r#"
699from std::core::native use { ptr_new_cell }
700
701pub fn make_cell() {
702 let cell = ptr_new_cell()
703 cell
704}
705"#,
706 )
707 .expect("write main.shape");
708
709 let project =
710 shape_runtime::project::find_project_root(root).expect("should find project root");
711
712 let bundle = BundleCompiler::compile(&project)
713 .expect("compilation with stdlib imports should succeed");
714
715 assert_eq!(bundle.metadata.name, "test-stdlib-imports");
716 let main_mod = bundle.modules.iter().find(|m| m.module_path == "main");
717 assert!(main_mod.is_some(), "should have main module");
718 }
719
720 #[test]
721 fn test_compile_embeds_transitive_native_scopes_from_shapec_dependencies() {
722 let Some(alias) = discover_system_library_alias() else {
723 return;
725 };
726
727 let tmp = tempfile::tempdir().expect("temp dir");
728 let leaf_dir = tmp.path().join("leaf");
729 let mid_dir = tmp.path().join("mid");
730 std::fs::create_dir_all(&leaf_dir).expect("create leaf dir");
731 std::fs::create_dir_all(&mid_dir).expect("create mid dir");
732
733 std::fs::write(
734 leaf_dir.join("shape.toml"),
735 format!(
736 r#"
737[project]
738name = "leaf"
739version = "1.2.3"
740
741[native-dependencies]
742duckdb = {{ provider = "system", version = "1.0.0", linux = "{alias}", macos = "{alias}", windows = "{alias}" }}
743"#
744 ),
745 )
746 .expect("write leaf shape.toml");
747 std::fs::write(leaf_dir.join("main.shape"), "pub fn leaf_marker() { 1 }")
748 .expect("write leaf source");
749
750 let leaf_project = shape_runtime::project::find_project_root(&leaf_dir)
751 .expect("leaf project root should resolve");
752 let leaf_bundle = BundleCompiler::compile(&leaf_project).expect("compile leaf bundle");
753 let leaf_bundle_path = tmp.path().join("leaf.shapec");
754 leaf_bundle
755 .write_to_file(&leaf_bundle_path)
756 .expect("write leaf bundle");
757 assert!(
758 leaf_bundle
759 .native_dependency_scopes
760 .iter()
761 .any(|scope| scope.package_key == "leaf@1.2.3"
762 && scope.dependencies.contains_key("duckdb")),
763 "leaf bundle should embed its native dependency scope"
764 );
765
766 std::fs::write(
767 mid_dir.join("shape.toml"),
768 r#"
769[project]
770name = "mid"
771version = "0.4.0"
772
773[dependencies]
774leaf = { path = "../leaf.shapec" }
775"#,
776 )
777 .expect("write mid shape.toml");
778 std::fs::write(mid_dir.join("main.shape"), "pub fn mid_marker() { 2 }")
779 .expect("write mid source");
780
781 let mid_project =
782 shape_runtime::project::find_project_root(&mid_dir).expect("mid project root");
783 let mid_bundle = BundleCompiler::compile(&mid_project).expect("compile mid bundle");
784
785 assert!(
786 mid_bundle
787 .native_dependency_scopes
788 .iter()
789 .any(|scope| scope.package_key == "leaf@1.2.3"
790 && scope.dependencies.contains_key("duckdb")),
791 "mid bundle should preserve transitive native scopes from leaf.shapec"
792 );
793 }
794
795 #[test]
796 fn test_bundle_submodule_imports() {
797 let tmp = tempfile::tempdir().expect("temp dir");
799 let root = tmp.path();
800
801 std::fs::write(
802 root.join("shape.toml"),
803 r#"
804[project]
805name = "test-submod-imports"
806version = "0.1.0"
807"#,
808 )
809 .expect("write shape.toml");
810
811 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
812 std::fs::write(
813 root.join("utils/helpers.shape"),
814 "pub fn helper_val() -> int { 42 }",
815 )
816 .expect("write helpers");
817
818 std::fs::write(
819 root.join("main.shape"),
820 r#"
821from utils::helpers use { helper_val }
822
823pub fn run() -> int {
824 helper_val()
825}
826"#,
827 )
828 .expect("write main");
829
830 let project =
831 shape_runtime::project::find_project_root(root).expect("should find project root");
832 let bundle = BundleCompiler::compile(&project)
833 .expect("bundle with submodule imports should compile");
834 assert!(
835 bundle.modules.iter().any(|m| m.module_path == "main"),
836 "should have main module"
837 );
838 }
839
840 #[test]
841 fn test_bundle_chained_submodule_imports() {
842 let tmp = tempfile::tempdir().expect("temp dir");
844 let root = tmp.path();
845
846 std::fs::write(
847 root.join("shape.toml"),
848 r#"
849[project]
850name = "test-chained-imports"
851version = "0.1.0"
852"#,
853 )
854 .expect("write shape.toml");
855
856 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
857 std::fs::write(
858 root.join("utils/constants.shape"),
859 "pub fn pi() -> number { 3.14159 }",
860 )
861 .expect("write constants");
862
863 std::fs::write(
864 root.join("utils/math.shape"),
865 r#"
866from utils::constants use { pi }
867
868pub fn circle_area(r: number) -> number {
869 pi() * r * r
870}
871"#,
872 )
873 .expect("write math");
874
875 std::fs::write(
876 root.join("main.shape"),
877 r#"
878from utils::math use { circle_area }
879
880pub fn run() -> number {
881 circle_area(2.0)
882}
883"#,
884 )
885 .expect("write main");
886
887 let project =
888 shape_runtime::project::find_project_root(root).expect("should find project root");
889 let bundle =
890 BundleCompiler::compile(&project).expect("bundle with chained imports should compile");
891 assert!(
892 bundle.modules.iter().any(|m| m.module_path == "main"),
893 "should have main module"
894 );
895 }
896
897 #[test]
898 fn test_bundle_submodule_imports_with_shared_dependency() {
899 let tmp = tempfile::tempdir().expect("temp dir");
903 let root = tmp.path();
904
905 std::fs::write(
906 root.join("shape.toml"),
907 r#"
908[project]
909name = "test-shared-dep"
910version = "0.1.0"
911"#,
912 )
913 .expect("write shape.toml");
914
915 std::fs::create_dir_all(root.join("lib")).expect("create lib dir");
916 std::fs::write(
917 root.join("lib/constants.shape"),
918 r#"
919pub fn pi() -> number { 3.14159 }
920pub fn e() -> number { 2.71828 }
921"#,
922 )
923 .expect("write constants");
924
925 std::fs::write(
926 root.join("lib/math.shape"),
927 r#"
928from lib::constants use { pi }
929
930pub fn circle_area(r: number) -> number {
931 pi() * r * r
932}
933"#,
934 )
935 .expect("write math");
936
937 std::fs::write(
938 root.join("lib/format.shape"),
939 r#"
940from lib::constants use { e }
941
942pub fn euler() -> number {
943 e()
944}
945"#,
946 )
947 .expect("write format");
948
949 std::fs::write(
950 root.join("main.shape"),
951 r#"
952from lib::math use { circle_area }
953from lib::format use { euler }
954
955pub fn run() -> number {
956 circle_area(1.0) + euler()
957}
958"#,
959 )
960 .expect("write main");
961
962 let project =
963 shape_runtime::project::find_project_root(root).expect("should find project root");
964 let bundle = BundleCompiler::compile(&project)
965 .expect("bundle with shared dependency should compile");
966 assert!(
967 bundle.modules.iter().any(|m| m.module_path == "main"),
968 "should have main module"
969 );
970 }
971}