1use crate::error::{Error, ResolveError};
4
5use itertools::Itertools;
6use wesl_core::StaticPackage;
7use wgsl_parse::syntax::{ModulePath, PathOrigin, TranslationUnit};
8use wgsl_types::inst::LiteralInstance;
9
10use std::{
11 borrow::Cow,
12 collections::HashMap,
13 fs,
14 path::{self, Path, PathBuf},
15};
16
17type E = ResolveError;
18
19pub trait Resolver {
27 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError>;
29 fn display_name(&self, _path: &ModulePath) -> Option<String> {
31 None
32 }
33 fn fs_path(&self, _module_path: &ModulePath) -> Result<PathBuf, ResolveError> {
35 Err(ResolveError::FilesystemNotSupported)
36 }
37 fn module_path(&self, _fs_path: &Path) -> Result<ModulePath, ResolveError> {
39 Err(ResolveError::FilesystemNotSupported)
40 }
41 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
43 path.clone()
44 }
45}
46
47pub trait AsyncResolver: Resolver {
48 fn resolve_source_async<'a>(
50 &'a self,
51 path: &ModulePath,
52 ) -> impl Future<Output = Result<Cow<'a, str>, ResolveError>> {
53 async { self.resolve_source(path) }
54 }
55}
56
57impl<T: Resolver + ?Sized> Resolver for Box<T> {
58 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
59 (**self).resolve_source(path)
60 }
61 fn display_name(&self, path: &ModulePath) -> Option<String> {
62 (**self).display_name(path)
63 }
64 fn fs_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
65 (**self).fs_path(path)
66 }
67 fn module_path(&self, path: &Path) -> Result<ModulePath, ResolveError> {
68 (**self).module_path(path)
69 }
70 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
71 (**self).canonical_path(path)
72 }
73}
74
75impl<T: Resolver + ?Sized> Resolver for &T {
76 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
77 (**self).resolve_source(path)
78 }
79 fn display_name(&self, path: &ModulePath) -> Option<String> {
80 (**self).display_name(path)
81 }
82 fn fs_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
83 (**self).fs_path(path)
84 }
85 fn module_path(&self, path: &Path) -> Result<ModulePath, ResolveError> {
86 (**self).module_path(path)
87 }
88 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
89 (**self).canonical_path(path)
90 }
91}
92
93#[derive(Default, Clone, Debug)]
97pub struct NoResolver;
98
99impl Resolver for NoResolver {
100 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
101 Err(E::ModuleNotFound(
102 path.clone(),
103 "no module resolver, imports are effectively disabled here".to_string(),
104 ))
105 }
106}
107
108pub struct FileResolver {
112 pkg_root_dir: PathBuf,
113 extension: &'static str,
114}
115
116impl Default for FileResolver {
117 fn default() -> Self {
118 Self {
119 pkg_root_dir: "./shaders".into(),
120 extension: "wesl",
121 }
122 }
123}
124
125impl FileResolver {
126 pub fn new(pkg_root_dir: impl AsRef<Path>) -> Self {
130 Self {
131 pkg_root_dir: pkg_root_dir.as_ref().to_path_buf(),
132 extension: "wesl",
133 }
134 }
135
136 pub fn set_root(&mut self, pkg_root_dir: impl AsRef<Path>) {
138 self.pkg_root_dir = pkg_root_dir.as_ref().to_path_buf();
139 }
140
141 pub fn set_extension(&mut self, extension: &'static str) {
143 self.extension = extension;
144 }
145
146 fn file_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
147 if path.origin.is_package() {
148 return Err(E::ModuleNotFound(
149 path.clone(),
150 "this is an external package import, not a file import. Use `package::` or `super::` for file imports."
151 .to_string(),
152 ));
153 }
154 let mut fs_path = self.pkg_root_dir.to_path_buf();
155 fs_path.extend(&path.components);
156 if path.is_root() {
157 fs_path.push("package");
159 }
160 fs_path.set_extension(self.extension);
161 if fs_path.exists() {
162 Ok(fs_path)
163 } else {
164 let wgsl_fs_path = fs_path.with_extension("wgsl");
165 if wgsl_fs_path.exists() {
166 Ok(wgsl_fs_path)
167 } else {
168 Err(E::FileNotFound(fs_path, "physical file".to_string()))
169 }
170 }
171 }
172}
173
174impl Resolver for FileResolver {
175 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
176 let fs_path = self.file_path(path)?;
177 let source = fs::read_to_string(&fs_path)
178 .map_err(|_| E::FileNotFound(fs_path, "physical file".to_string()))?;
179
180 Ok(source.into())
181 }
182 fn display_name(&self, path: &ModulePath) -> Option<String> {
183 self.file_path(path)
184 .ok()
185 .map(|fs_path| fs_path.display().to_string())
186 }
187 fn fs_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
188 self.file_path(path)
189 }
190 fn module_path(&self, fs_path: &Path) -> Result<ModulePath, ResolveError> {
191 let abs_root = path::absolute(&self.pkg_root_dir).map_err(ResolveError::Io)?;
192 let abs_path = path::absolute(fs_path).map_err(ResolveError::Io)?;
193 let rel_path = abs_path
194 .strip_prefix(&abs_root)
195 .map_err(|_| ResolveError::FileEscapesRoot(abs_path.clone(), abs_root))?;
196
197 Ok(ModulePath::from_path(rel_path))
199 }
200}
201
202#[derive(Default)]
207pub struct VirtualResolver<'a> {
208 files: HashMap<ModulePath, Cow<'a, str>>,
209}
210
211impl<'a> VirtualResolver<'a> {
212 pub fn new() -> Self {
214 Self {
215 files: HashMap::new(),
216 }
217 }
218
219 pub fn add_module(&mut self, path: ModulePath, file: Cow<'a, str>) {
223 self.files.insert(path, file);
224 }
225
226 pub fn add_translation_unit(&mut self, path: ModulePath, translation_unit: TranslationUnit) {
230 self.files
231 .insert(path, Cow::Owned(translation_unit.to_string()));
232 }
233
234 pub fn get_module(&self, path: &ModulePath) -> Result<&str, ResolveError> {
236 let source = self
237 .files
238 .get(path)
239 .ok_or_else(|| E::ModuleNotFound(path.clone(), "virtual module".to_string()))?;
240 Ok(source)
241 }
242
243 pub fn modules(&self) -> impl Iterator<Item = (&ModulePath, &str)> {
245 self.files.iter().map(|(res, file)| (res, &**file))
246 }
247}
248
249impl Resolver for VirtualResolver<'_> {
250 fn resolve_source<'b>(&'b self, path: &ModulePath) -> Result<Cow<'b, str>, ResolveError> {
251 let source = self.get_module(path)?;
252 Ok(source.into())
253 }
254}
255
256pub trait ResolveFn: Fn(&mut TranslationUnit) -> Result<(), Error> {}
258impl<T: Fn(&mut TranslationUnit) -> Result<(), Error>> ResolveFn for T {}
259
260pub struct Router {
267 mount_points: Vec<(ModulePath, Box<dyn Resolver>)>,
268 fallback: Option<Box<dyn Resolver>>,
269}
270
271impl Router {
275 pub fn new() -> Self {
277 Self {
278 mount_points: Vec::new(),
279 fallback: None,
280 }
281 }
282
283 pub fn mount_resolver(&mut self, prefix: ModulePath, resolver: impl Resolver + 'static) {
291 self.mount_points.push((prefix, Box::new(resolver)));
292 }
293
294 pub fn mount_fallback_resolver(&mut self, resolver: impl Resolver + 'static) {
296 self.fallback = Some(Box::new(resolver));
297 }
298
299 fn route(&self, path: &ModulePath) -> Result<(&dyn Resolver, ModulePath), ResolveError> {
300 if let Some((mount_path, resolver)) = self
301 .mount_points
302 .iter()
303 .filter(|(prefix, _)| path.starts_with(prefix))
304 .max_by_key(|(prefix, _)| prefix.components.len())
305 {
306 let components = path
307 .components
308 .iter()
309 .skip(mount_path.components.len())
310 .cloned()
311 .collect_vec();
312
313 let suffix = ModulePath::new(PathOrigin::Absolute, components);
314 Ok((resolver, suffix))
315 } else if let Some(resolver) = &self.fallback {
316 Ok((resolver, path.clone()))
317 } else {
318 Err(E::ModuleNotFound(
319 path.clone(),
320 "no mount point".to_string(),
321 ))
322 }
323 }
324}
325
326impl Default for Router {
327 fn default() -> Self {
328 Self::new()
329 }
330}
331
332impl Resolver for Router {
333 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
334 let (resolver, path) = self.route(path)?;
335 resolver.resolve_source(&path)
336 }
337 fn display_name(&self, path: &ModulePath) -> Option<String> {
338 let (resolver, path) = self.route(path).ok()?;
339 resolver.display_name(&path)
340 }
341 fn fs_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
342 let (resolver, path) = self.route(path)?;
343 resolver.fs_path(&path)
344 }
345 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
346 match self.route(path) {
348 Ok((resolver, routed)) if routed == *path => resolver.canonical_path(path),
349 _ => path.clone(),
350 }
351 }
352}
353
354#[derive(Default)]
358pub struct PackageResolver {
359 packages: Vec<&'static StaticPackage>,
360}
361
362impl PackageResolver {
363 pub fn new() -> Self {
365 Self {
366 packages: Vec::new(),
367 }
368 }
369
370 pub fn add_package(&mut self, pkg: &'static StaticPackage) {
372 self.packages.push(pkg);
373 }
374}
375
376impl Resolver for PackageResolver {
377 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<std::borrow::Cow<'a, str>, E> {
378 let pkg_path = match &path.origin {
379 PathOrigin::Package(pkg) => pkg,
380 _ => {
381 return Err(E::ModuleNotFound(
382 path.clone(),
383 "resolver can only resolve package imports".to_string(),
384 ));
385 }
386 };
387
388 let pkg_parts = pkg_path.split('/').collect_vec();
392
393 let root_pkg = pkg_parts
394 .first()
395 .and_then(|name| self.packages.iter().find(|p| p.root.name == *name))
396 .ok_or_else(|| {
397 E::ModuleNotFound(
398 path.clone(),
399 format!("dependency `{}` not found", pkg_parts.iter().format("/"),),
400 )
401 })?;
402
403 let pkg = pkg_parts.iter().skip(1).try_fold(root_pkg, |dep, name| {
404 dep.dependencies
405 .iter()
406 .find(|p| p.root.name == *name)
407 .ok_or_else(|| {
408 E::ModuleNotFound(
409 path.clone(),
410 format!(
411 "dependency `{}` not found in package path `{}`",
412 name,
413 pkg_parts.iter().format("/"),
414 ),
415 )
416 })
417 })?;
418
419 let mut cur_mod = pkg.root;
422 for comp in &path.components {
423 if let Some(submod) = cur_mod.submodules.iter().find(|m| m.name == comp) {
424 cur_mod = submod;
425 } else {
426 return Err(E::ModuleNotFound(
427 path.clone(),
428 format!("in module `{}`, no submodule named `{comp}`", cur_mod.name),
429 ));
430 }
431 }
432 Ok(cur_mod.source.into())
433 }
434
435 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
436 let PathOrigin::Package(pkg_path) = &path.origin else {
437 return path.clone();
438 };
439
440 let pkg_parts = pkg_path.split('/').collect_vec();
442 let Some(root_pkg) = pkg_parts
443 .first()
444 .and_then(|name| self.packages.iter().find(|p| p.root.name == *name))
445 else {
446 return path.clone();
447 };
448 let Ok(pkg) = pkg_parts.iter().skip(1).try_fold(root_pkg, |dep, name| {
449 dep.dependencies
450 .iter()
451 .find(|p| p.root.name == *name)
452 .ok_or(())
453 }) else {
454 return path.clone();
455 };
456
457 let mut queue = std::collections::VecDeque::new();
459 queue.extend(self.packages.iter().map(|p| (p.root.name.to_string(), *p)));
460 let mut visited = Vec::new();
461 while let Some((name, candidate)) = queue.pop_front() {
462 if std::ptr::eq(candidate, *pkg) {
463 return ModulePath::new(PathOrigin::Package(name), path.components.clone());
464 }
465 if visited.contains(&std::ptr::from_ref(candidate)) {
466 continue;
467 }
468 visited.push(std::ptr::from_ref(candidate));
469 for dep in candidate.dependencies {
470 queue.push_back((format!("{name}/{}", dep.root.name), *dep));
471 }
472 }
473 path.clone()
474 }
475}
476
477#[derive(Clone, Debug, Default, PartialEq)]
486pub struct Constants {
487 constants: HashMap<String, LiteralInstance>,
488}
489
490impl Constants {
491 pub fn new() -> Self {
492 Default::default()
493 }
494
495 pub fn set(&mut self, name: impl ToString, value: impl Into<LiteralInstance>) {
496 self.constants.insert(name.to_string(), value.into());
497 }
498
499 pub fn iter(&self) -> impl Iterator<Item = (&String, &LiteralInstance)> {
500 self.constants.iter()
501 }
502}
503
504impl FromIterator<(String, LiteralInstance)> for Constants {
505 fn from_iter<T: IntoIterator<Item = (String, LiteralInstance)>>(iter: T) -> Self {
506 Self {
507 constants: HashMap::from_iter(iter),
508 }
509 }
510}
511
512#[derive(Default)]
517pub struct StandardResolver {
518 pkg: PackageResolver,
519 files: FileResolver,
520 constants: Constants,
521}
522
523impl StandardResolver {
524 pub fn new(pkg_root_dir: impl AsRef<Path>) -> Self {
528 Self {
529 pkg: PackageResolver::new(),
530 files: FileResolver::new(pkg_root_dir),
531 constants: Constants::new(),
532 }
533 }
534
535 pub fn add_package(&mut self, pkg: &'static StaticPackage) {
537 self.pkg.add_package(pkg)
538 }
539
540 pub fn add_constant(&mut self, name: impl ToString, value: impl Into<LiteralInstance>) {
553 self.constants.set(name, value);
554 }
555
556 fn generate_constant_module(&self) -> String {
558 self.constants
559 .iter()
560 .map(|(name, value)| format!("public const {name} = {value};"))
561 .join("\n")
562 }
563}
564
565impl Resolver for StandardResolver {
566 fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError> {
567 if let PathOrigin::Package(pkg_name) = &path.origin
571 && (pkg_name == "constants" || pkg_name.ends_with("/constants"))
572 {
573 return Ok(self.generate_constant_module().into());
574 }
575
576 if path.origin.is_package() {
577 self.pkg.resolve_source(path)
578 } else {
579 self.files.resolve_source(path)
580 }
581 }
582 fn display_name(&self, path: &ModulePath) -> Option<String> {
583 if path.origin.is_package() {
584 self.pkg.display_name(path)
585 } else {
586 self.files.display_name(path)
587 }
588 }
589 fn fs_path(&self, path: &ModulePath) -> Result<PathBuf, ResolveError> {
590 if path.origin.is_package() {
591 self.pkg.fs_path(path)
592 } else {
593 self.files.fs_path(path)
594 }
595 }
596 fn module_path(&self, path: &Path) -> Result<ModulePath, ResolveError> {
597 self.files.module_path(path)
598 }
599 fn canonical_path(&self, path: &ModulePath) -> ModulePath {
600 if let PathOrigin::Package(pkg_name) = &path.origin
602 && (pkg_name == "constants" || pkg_name.ends_with("/constants"))
603 {
604 return ModulePath::new(
605 PathOrigin::Package("constants".to_string()),
606 path.components.clone(),
607 );
608 }
609
610 if path.origin.is_package() {
611 self.pkg.canonical_path(path)
612 } else {
613 path.clone()
614 }
615 }
616}
617
618#[cfg(test)]
619mod test {
620 use wesl_core::StaticPackageModule;
621
622 use crate::{CompileOptions, Compiler};
623
624 use super::*;
625
626 #[test]
627 fn router_resolver() {
628 let mut r = Router::new();
629
630 let mut v1 = VirtualResolver::new();
631 v1.add_module("package".parse().unwrap(), "m1".into());
632 v1.add_module("package::foo".parse().unwrap(), "m2".into());
633 v1.add_module("package::bar".parse().unwrap(), "m3".into());
634 r.mount_resolver("package".parse().unwrap(), v1);
635
636 let mut v2 = VirtualResolver::new();
637 v2.add_module("package".parse().unwrap(), "m4".into());
638 v2.add_module("package::baz".parse().unwrap(), "m5".into());
639 r.mount_resolver("package::bar".parse().unwrap(), v2);
640
641 let mut v3 = VirtualResolver::new();
642 v3.add_module("foo::bar".parse().unwrap(), "m6".into());
643 r.mount_fallback_resolver(v3);
644
645 assert_eq!(r.resolve_source(&"package".parse().unwrap()).unwrap(), "m1");
646 assert_eq!(
647 r.resolve_source(&"package::foo".parse().unwrap()).unwrap(),
648 "m2"
649 );
650 assert_eq!(
651 r.resolve_source(&"package::bar".parse().unwrap()).unwrap(),
652 "m4"
653 );
654 assert_eq!(
655 r.resolve_source(&"package::bar::baz".parse().unwrap())
656 .unwrap(),
657 "m5"
658 );
659 assert_eq!(
660 r.resolve_source(&"foo::bar".parse().unwrap()).unwrap(),
661 "m6"
662 );
663 }
664
665 #[test]
666 fn canonical_paths() {
667 static C_ROOT: StaticPackageModule = StaticPackageModule {
668 name: "c",
669 source: "",
670 submodules: &[],
671 };
672 static C: StaticPackage = StaticPackage {
673 crate_name: "c",
674 root: &C_ROOT,
675 dependencies: &[],
676 };
677 static D1_ROOT: StaticPackageModule = StaticPackageModule {
678 name: "d",
679 source: "",
680 submodules: &[],
681 };
682 static D1: StaticPackage = StaticPackage {
683 crate_name: "d",
684 root: &D1_ROOT,
685 dependencies: &[],
686 };
687 static D2_ROOT: StaticPackageModule = StaticPackageModule {
688 name: "d",
689 source: "",
690 submodules: &[],
691 };
692 static D2: StaticPackage = StaticPackage {
693 crate_name: "d",
694 root: &D2_ROOT,
695 dependencies: &[],
696 };
697 static A_ROOT: StaticPackageModule = StaticPackageModule {
698 name: "a",
699 source: "",
700 submodules: &[],
701 };
702 static A: StaticPackage = StaticPackage {
703 crate_name: "a",
704 root: &A_ROOT,
705 dependencies: &[&C, &D1],
706 };
707 static B_ROOT: StaticPackageModule = StaticPackageModule {
708 name: "b",
709 source: "",
710 submodules: &[],
711 };
712 static B: StaticPackage = StaticPackage {
713 crate_name: "b",
714 root: &B_ROOT,
715 dependencies: &[&C, &D2],
716 };
717 static E_ROOT: StaticPackageModule = StaticPackageModule {
718 name: "e",
719 source: "",
720 submodules: &[],
721 };
722 static E: StaticPackage = StaticPackage {
723 crate_name: "e",
724 root: &E_ROOT,
725 dependencies: &[&C],
726 };
727
728 let mut r = PackageResolver::new();
729 r.add_package(&A);
730 r.add_package(&B);
731 r.add_package(&E);
732
733 let canon = |s: &str| r.canonical_path(&s.parse().unwrap());
734
735 assert_eq!(canon("a/c::foo"), canon("b/c::foo"));
737 assert_eq!(canon("a/c::foo"), "a/c::foo".parse().unwrap());
738
739 assert_eq!(canon("e/c::foo"), "a/c::foo".parse().unwrap());
741
742 assert_eq!(canon("a/d::foo"), "a/d::foo".parse().unwrap());
744 assert_eq!(canon("b/d::foo"), "b/d::foo".parse().unwrap());
745
746 assert_eq!(canon("a::foo"), "a::foo".parse().unwrap());
747
748 assert_eq!(canon("z/c::foo"), "z/c::foo".parse().unwrap());
749 }
750
751 #[test]
752 fn type_virtual_constants() {
754 let mut std = StandardResolver::new(".");
756 std.add_constant("TAU", std::f64::consts::TAU);
758 std.add_constant("LIGHTING_ANGLE", 10.0f32);
760 std.add_constant("Z_ROTATION", -10i32);
762 std.add_constant("H", 12u32);
764 std.add_constant("BRIGHTEN", false);
766
767 let mut v = VirtualResolver::new();
769 v.add_module(
770 "package::color_math".parse().unwrap(),
771 r#"
774 import constants::{TAU, H, BRIGHTEN};
775
776 fn color_sweep(h: u32) -> f32 {
777 let color = cos(h + vec3(0.0, 1.0, 2.0) * TAU / 3.0);
778 if (BRIGHTEN) {
779 color += 0.1;
780 }
781
782 return color;
783 }
784
785 @fragment
786 fn fragment() -> @location(0) vec4<f32> {
787 return vec4(color_sweep(H), color_sweep(H + 0.1), color_sweep(H + 0.2), 1.0);
788 }
789 "#
790 .into(),
791 );
792
793 let mut r = Router::new();
796 r.mount_resolver(ModulePath::new_root(), v);
797 r.mount_fallback_resolver(std);
798
799 Compiler::new_with_resolver(CompileOptions::default(), r)
801 .compile_module(&"package::color_math".parse().unwrap())
802 .unwrap();
803 }
804
805 #[test]
806 fn resolve_virtual_constants() {
808 let mut sr = StandardResolver::new(".");
810
811 sr.add_constant("PI", std::f64::consts::PI);
813 sr.add_constant("E", std::f64::consts::E);
814 sr.add_constant("NEG_2", -2i32);
816 sr.add_constant("ONE", 1u32);
817 sr.add_constant("F32_MAX", f32::MAX);
818 sr.add_constant("IS_HEAVY", false);
819 sr.add_constant("NUM_CONSTS", sr.constants.iter().count() as i64);
820
821 let generated = sr.generate_constant_module();
823 assert!(generated.contains(&format!("const PI = {:?};", std::f64::consts::PI)));
825 assert!(generated.contains(&format!("const E = {:?};", std::f64::consts::E)));
826 assert!(generated.contains("const NEG_2 = -2i;"));
827 assert!(generated.contains("const ONE = 1u;"));
828 assert!(generated.contains(&format!("const F32_MAX = {}f;", f32::MAX)));
829 assert!(generated.contains("const IS_HEAVY = false;"));
830 assert!(generated.contains(&format!(
831 "const NUM_CONSTS = {};",
832 (sr.constants.iter().count() as i64) - 1
833 )));
834
835 let constants_src = sr.resolve_source(&"constants".parse().unwrap()).unwrap();
838 assert_eq!(constants_src, generated);
839
840 let src_comp = sr
842 .resolve_source(&"constants::PI".parse().unwrap())
843 .unwrap();
844 assert_eq!(src_comp, generated);
845 }
846}