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::Interface(i) => {
570 names.push(i.name.clone());
571 }
572 shape_ast::ast::ExportItem::Trait(t) => {
573 names.push(t.name.clone());
574 }
575 shape_ast::ast::ExportItem::Annotation(annotation) => {
576 names.push(annotation.name.clone());
577 }
578 shape_ast::ast::ExportItem::ForeignFunction(f) => {
579 names.push(f.name.clone());
580 }
581 },
582 _ => {}
583 }
584 }
585
586 names.sort();
587 names.dedup();
588 names
589}
590
591#[cfg(test)]
592mod tests {
593 use super::*;
594
595 fn discover_system_library_alias() -> Option<String> {
596 let candidates = [
597 "libm.so.6",
598 "libc.so.6",
599 "libSystem.B.dylib",
600 "kernel32.dll",
601 "ucrtbase.dll",
602 ];
603 for candidate in candidates {
604 if unsafe { libloading::Library::new(candidate) }.is_ok() {
605 return Some(candidate.to_string());
606 }
607 }
608 None
609 }
610
611 #[test]
612 fn test_path_to_module_path_basic() {
613 let base = Path::new("/project");
614 assert_eq!(
615 path_to_module_path(Path::new("/project/main.shape"), base),
616 "main"
617 );
618 assert_eq!(
619 path_to_module_path(Path::new("/project/utils/helpers.shape"), base),
620 "utils::helpers"
621 );
622 }
623
624 #[test]
625 fn test_path_to_module_path_index() {
626 let base = Path::new("/project");
627 assert_eq!(
628 path_to_module_path(Path::new("/project/utils/index.shape"), base),
629 "utils"
630 );
631 assert_eq!(
632 path_to_module_path(Path::new("/project/index.shape"), base),
633 ""
634 );
635 }
636
637 #[test]
638 fn test_compile_temp_project() {
639 let tmp = tempfile::tempdir().expect("temp dir");
640 let root = tmp.path();
641
642 std::fs::write(
644 root.join("shape.toml"),
645 r#"
646[project]
647name = "test-bundle"
648version = "0.1.0"
649"#,
650 )
651 .expect("write shape.toml");
652
653 std::fs::write(root.join("main.shape"), "pub fn run() { 42 }").expect("write main");
655 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
656 std::fs::write(root.join("utils/helpers.shape"), "pub fn helper() { 1 }")
657 .expect("write helpers");
658
659 let project =
660 shape_runtime::project::find_project_root(root).expect("should find project root");
661
662 let bundle = BundleCompiler::compile(&project).expect("compilation should succeed");
663
664 assert_eq!(bundle.metadata.name, "test-bundle");
665 assert_eq!(bundle.metadata.version, "0.1.0");
666 assert!(
667 bundle.modules.len() >= 2,
668 "should have at least 2 modules, got {}",
669 bundle.modules.len()
670 );
671
672 let main_mod = bundle.modules.iter().find(|m| m.module_path == "main");
673 assert!(main_mod.is_some(), "should have main module");
674
675 let helpers_mod = bundle
676 .modules
677 .iter()
678 .find(|m| m.module_path == "utils::helpers");
679 assert!(helpers_mod.is_some(), "should have utils::helpers module");
680 }
681
682 #[test]
683 fn test_compile_with_stdlib_imports() {
684 let tmp = tempfile::tempdir().expect("temp dir");
685 let root = tmp.path();
686
687 std::fs::write(
688 root.join("shape.toml"),
689 r#"
690[project]
691name = "test-stdlib-imports"
692version = "0.1.0"
693"#,
694 )
695 .expect("write shape.toml");
696
697 std::fs::write(
700 root.join("main.shape"),
701 r#"
702from std::core::native use { ptr_new_cell }
703
704pub fn make_cell() {
705 let cell = ptr_new_cell()
706 cell
707}
708"#,
709 )
710 .expect("write main.shape");
711
712 let project =
713 shape_runtime::project::find_project_root(root).expect("should find project root");
714
715 let bundle = BundleCompiler::compile(&project)
716 .expect("compilation with stdlib imports should succeed");
717
718 assert_eq!(bundle.metadata.name, "test-stdlib-imports");
719 let main_mod = bundle.modules.iter().find(|m| m.module_path == "main");
720 assert!(main_mod.is_some(), "should have main module");
721 }
722
723 #[test]
724 fn test_compile_embeds_transitive_native_scopes_from_shapec_dependencies() {
725 let Some(alias) = discover_system_library_alias() else {
726 return;
728 };
729
730 let tmp = tempfile::tempdir().expect("temp dir");
731 let leaf_dir = tmp.path().join("leaf");
732 let mid_dir = tmp.path().join("mid");
733 std::fs::create_dir_all(&leaf_dir).expect("create leaf dir");
734 std::fs::create_dir_all(&mid_dir).expect("create mid dir");
735
736 std::fs::write(
737 leaf_dir.join("shape.toml"),
738 format!(
739 r#"
740[project]
741name = "leaf"
742version = "1.2.3"
743
744[native-dependencies]
745duckdb = {{ provider = "system", version = "1.0.0", linux = "{alias}", macos = "{alias}", windows = "{alias}" }}
746"#
747 ),
748 )
749 .expect("write leaf shape.toml");
750 std::fs::write(leaf_dir.join("main.shape"), "pub fn leaf_marker() { 1 }")
751 .expect("write leaf source");
752
753 let leaf_project = shape_runtime::project::find_project_root(&leaf_dir)
754 .expect("leaf project root should resolve");
755 let leaf_bundle = BundleCompiler::compile(&leaf_project).expect("compile leaf bundle");
756 let leaf_bundle_path = tmp.path().join("leaf.shapec");
757 leaf_bundle
758 .write_to_file(&leaf_bundle_path)
759 .expect("write leaf bundle");
760 assert!(
761 leaf_bundle
762 .native_dependency_scopes
763 .iter()
764 .any(|scope| scope.package_key == "leaf@1.2.3"
765 && scope.dependencies.contains_key("duckdb")),
766 "leaf bundle should embed its native dependency scope"
767 );
768
769 std::fs::write(
770 mid_dir.join("shape.toml"),
771 r#"
772[project]
773name = "mid"
774version = "0.4.0"
775
776[dependencies]
777leaf = { path = "../leaf.shapec" }
778"#,
779 )
780 .expect("write mid shape.toml");
781 std::fs::write(mid_dir.join("main.shape"), "pub fn mid_marker() { 2 }")
782 .expect("write mid source");
783
784 let mid_project =
785 shape_runtime::project::find_project_root(&mid_dir).expect("mid project root");
786 let mid_bundle = BundleCompiler::compile(&mid_project).expect("compile mid bundle");
787
788 assert!(
789 mid_bundle
790 .native_dependency_scopes
791 .iter()
792 .any(|scope| scope.package_key == "leaf@1.2.3"
793 && scope.dependencies.contains_key("duckdb")),
794 "mid bundle should preserve transitive native scopes from leaf.shapec"
795 );
796 }
797
798 #[test]
799 fn test_bundle_submodule_imports() {
800 let tmp = tempfile::tempdir().expect("temp dir");
802 let root = tmp.path();
803
804 std::fs::write(
805 root.join("shape.toml"),
806 r#"
807[project]
808name = "test-submod-imports"
809version = "0.1.0"
810"#,
811 )
812 .expect("write shape.toml");
813
814 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
815 std::fs::write(
816 root.join("utils/helpers.shape"),
817 "pub fn helper_val() -> int { 42 }",
818 )
819 .expect("write helpers");
820
821 std::fs::write(
822 root.join("main.shape"),
823 r#"
824from utils::helpers use { helper_val }
825
826pub fn run() -> int {
827 helper_val()
828}
829"#,
830 )
831 .expect("write main");
832
833 let project =
834 shape_runtime::project::find_project_root(root).expect("should find project root");
835 let bundle = BundleCompiler::compile(&project)
836 .expect("bundle with submodule imports should compile");
837 assert!(
838 bundle.modules.iter().any(|m| m.module_path == "main"),
839 "should have main module"
840 );
841 }
842
843 #[test]
844 fn test_bundle_chained_submodule_imports() {
845 let tmp = tempfile::tempdir().expect("temp dir");
847 let root = tmp.path();
848
849 std::fs::write(
850 root.join("shape.toml"),
851 r#"
852[project]
853name = "test-chained-imports"
854version = "0.1.0"
855"#,
856 )
857 .expect("write shape.toml");
858
859 std::fs::create_dir_all(root.join("utils")).expect("create utils dir");
860 std::fs::write(
861 root.join("utils/constants.shape"),
862 "pub fn pi() -> number { 3.14159 }",
863 )
864 .expect("write constants");
865
866 std::fs::write(
867 root.join("utils/math.shape"),
868 r#"
869from utils::constants use { pi }
870
871pub fn circle_area(r: number) -> number {
872 pi() * r * r
873}
874"#,
875 )
876 .expect("write math");
877
878 std::fs::write(
879 root.join("main.shape"),
880 r#"
881from utils::math use { circle_area }
882
883pub fn run() -> number {
884 circle_area(2.0)
885}
886"#,
887 )
888 .expect("write main");
889
890 let project =
891 shape_runtime::project::find_project_root(root).expect("should find project root");
892 let bundle =
893 BundleCompiler::compile(&project).expect("bundle with chained imports should compile");
894 assert!(
895 bundle.modules.iter().any(|m| m.module_path == "main"),
896 "should have main module"
897 );
898 }
899
900 #[test]
901 fn test_bundle_submodule_imports_with_shared_dependency() {
902 let tmp = tempfile::tempdir().expect("temp dir");
906 let root = tmp.path();
907
908 std::fs::write(
909 root.join("shape.toml"),
910 r#"
911[project]
912name = "test-shared-dep"
913version = "0.1.0"
914"#,
915 )
916 .expect("write shape.toml");
917
918 std::fs::create_dir_all(root.join("lib")).expect("create lib dir");
919 std::fs::write(
920 root.join("lib/constants.shape"),
921 r#"
922pub fn pi() -> number { 3.14159 }
923pub fn e() -> number { 2.71828 }
924"#,
925 )
926 .expect("write constants");
927
928 std::fs::write(
929 root.join("lib/math.shape"),
930 r#"
931from lib::constants use { pi }
932
933pub fn circle_area(r: number) -> number {
934 pi() * r * r
935}
936"#,
937 )
938 .expect("write math");
939
940 std::fs::write(
941 root.join("lib/format.shape"),
942 r#"
943from lib::constants use { e }
944
945pub fn euler() -> number {
946 e()
947}
948"#,
949 )
950 .expect("write format");
951
952 std::fs::write(
953 root.join("main.shape"),
954 r#"
955from lib::math use { circle_area }
956from lib::format use { euler }
957
958pub fn run() -> number {
959 circle_area(1.0) + euler()
960}
961"#,
962 )
963 .expect("write main");
964
965 let project =
966 shape_runtime::project::find_project_root(root).expect("should find project root");
967 let bundle = BundleCompiler::compile(&project)
968 .expect("bundle with shared dependency should compile");
969 assert!(
970 bundle.modules.iter().any(|m| m.module_path == "main"),
971 "should have main module"
972 );
973 }
974}