Skip to main content

wesl/
resolver.rs

1//! [`Resolver`] trait and implementations.
2
3use 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
19/// A Resolver implements the module resolution algorithm: it returns a module contents
20/// associated with a module path.
21///
22/// Typically implementations of [`Resolver`] only implement [`Resolver::resolve_source`].
23///
24/// Calls to `Resolver` functions must respect these preconditions:
25/// * the import path must not be relative.
26pub trait Resolver {
27    /// Try to resolve a source file identified by a module path.
28    fn resolve_source<'a>(&'a self, path: &ModulePath) -> Result<Cow<'a, str>, ResolveError>;
29    /// Get the display name of the module path. Implementing this is optional.
30    fn display_name(&self, _path: &ModulePath) -> Option<String> {
31        None
32    }
33    /// Get the filesystem path of the module path. Implementing this is optional.
34    fn fs_path(&self, _module_path: &ModulePath) -> Result<PathBuf, ResolveError> {
35        Err(ResolveError::FilesystemNotSupported)
36    }
37    /// Get the module path of the filesystem path. Implementing this is optional.
38    fn module_path(&self, _fs_path: &Path) -> Result<ModulePath, ResolveError> {
39        Err(ResolveError::FilesystemNotSupported)
40    }
41    /// Get the canonical form of a module path.
42    fn canonical_path(&self, path: &ModulePath) -> ModulePath {
43        path.clone()
44    }
45}
46
47pub trait AsyncResolver: Resolver {
48    /// Async version of [`Resolver::resolve_source`].
49    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/// A resolver that never resolves anything.
94///
95/// Returns [`ResolveError::ModuleNotFound`] when calling [`Resolver::resolve_source`].
96#[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
108/// A resolver that looks for files in the filesystem.
109///
110/// It simply translates module paths to file paths. This is the intended behavior.
111pub 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    /// Create a new resolver.
127    ///
128    /// `pkg_root_dir` is the package root directory which `package::` paths refer to.
129    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    /// Set the the package root directory which `package::` paths refer to.
137    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    /// Look for files that ends with a different extension. Default: "wesl".
142    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            // the root module path (package) corresponds to the special file `package.wesl` in the root directory.
158            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        // TODO: from_path can panic, we don't want that here.
198        Ok(ModulePath::from_path(rel_path))
199    }
200}
201
202/// A resolver that resolves in-memory modules added with [`Self::add_module`].
203///
204/// Use-cases are platforms that lack a filesystem (e.g. WASM), tests or
205/// runtime-generated files.
206#[derive(Default)]
207pub struct VirtualResolver<'a> {
208    files: HashMap<ModulePath, Cow<'a, str>>,
209}
210
211impl<'a> VirtualResolver<'a> {
212    /// Create a new resolver.
213    pub fn new() -> Self {
214        Self {
215            files: HashMap::new(),
216        }
217    }
218
219    /// Resolve imports of `path` with the given WESL string.
220    ///
221    /// The path must not be relative.
222    pub fn add_module(&mut self, path: ModulePath, file: Cow<'a, str>) {
223        self.files.insert(path, file);
224    }
225
226    /// Resolve imports of `path` with the given [`TranslationUnit`].
227    ///
228    /// The path must not be relative.
229    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    /// Get a module registered with [`Self::add_module`].
235    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    /// Iterate over all registered modules.
244    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
256// trait alias
257pub trait ResolveFn: Fn(&mut TranslationUnit) -> Result<(), Error> {}
258impl<T: Fn(&mut TranslationUnit) -> Result<(), Error>> ResolveFn for T {}
259
260/// A resolver that can dispatch imports to several sub-resolvers based on the import
261/// path prefix.
262///
263/// Add sub-resolvers with [`Self::mount_resolver`].
264///
265/// This resolver is not thread-safe (not [`Send`] or [`Sync`]).
266pub struct Router {
267    mount_points: Vec<(ModulePath, Box<dyn Resolver>)>,
268    fallback: Option<Box<dyn Resolver>>,
269}
270
271/// Dispatches resolution of a module path to sub-resolvers.
272///
273/// See documentation in [`Self::mount_resolver`]
274impl Router {
275    /// Create a new resolver.
276    pub fn new() -> Self {
277        Self {
278            mount_points: Vec::new(),
279            fallback: None,
280        }
281    }
282
283    /// Mount a resolver at a given path prefix.
284    ///
285    /// All import paths starting with `prefix` will be dispatched to the resolver with
286    /// the suffix of the path. The prefix path must have an `Absolute` or `Package`
287    /// origin and the suffix path will be given an `Absolute` origin.
288    ///
289    /// If none of the `prefix`es match, the fallback resolver will be used.
290    pub fn mount_resolver(&mut self, prefix: ModulePath, resolver: impl Resolver + 'static) {
291        self.mount_points.push((prefix, Box::new(resolver)));
292    }
293
294    /// Mount a fallback resolver that is used when no other prefix match.
295    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        // a rewritten path is in the sub-resolver's namespace
347        match self.route(path) {
348            Ok((resolver, routed)) if routed == *path => resolver.canonical_path(path),
349            _ => path.clone(),
350        }
351    }
352}
353
354/// A resolver that only resolves module paths that refer to modules in external packages.
355///
356/// Register external packages with [`Self::add_package`].
357#[derive(Default)]
358pub struct PackageResolver {
359    packages: Vec<&'static StaticPackage>,
360}
361
362impl PackageResolver {
363    /// Create a new resolver.
364    pub fn new() -> Self {
365        Self {
366            packages: Vec::new(),
367        }
368    }
369
370    /// Add a package to the resolver.
371    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        // This is a hack: when the package name contains `/`, it corresponds to a sub-dependency
389        // of a package dependency. The name is created by the import resolution algorithm.
390        // (see ModulePath::join_path)
391        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        // TODO: the resolution algorithm is currently not spec-compliant.
420        // https://github.com/webgpu-tools/wesl-spec/blob/imports-update/Imports.md
421        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        // same package lookup as resolve_source, but failures return the path unchanged
441        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        // pick the first name found in BFS order over registered packages
458        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/// Numeric constants (WESL feature).
478///
479/// Numeric constants live in a special package named `constants`. This package is
480/// *virtual*, meaning it doesn't exist on the filesystem. Constants can be accessed
481/// by importing them: `import constants::MY_CONSTANT;`.
482///
483/// The type is specified by the variant of [`LiteralInstance`].
484/// The most flexible instance type is `AbstractFloat`, since it can be implicitly converted to all scalar types.
485#[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/// The resolver that implements the WESL standard.
513///
514/// It resolves modules in external packages registered with [`Self::add_package`] and
515/// modules in the local package with the filesystem.
516#[derive(Default)]
517pub struct StandardResolver {
518    pkg: PackageResolver,
519    files: FileResolver,
520    constants: Constants,
521}
522
523impl StandardResolver {
524    /// Create a new resolver.
525    ///
526    /// `pkg_root_dir` is the package root directory which `package::` paths refer to.
527    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    /// Add an external package.
536    pub fn add_package(&mut self, pkg: &'static StaticPackage) {
537        self.pkg.add_package(pkg)
538    }
539
540    /// Add a numeric constant.
541    ///
542    /// Numeric constants live WESL's special package named `constants`. This package is
543    /// *virtual*, meaning it doesn't exist on the filesystem. Constants can be accessed
544    /// by importing them: `import constants::MY_CONSTANT;`.
545    ///
546    /// The type is specified by the variant of [`LiteralInstance`].
547    /// If specifying a constant that is used with multiple different types or
548    /// a constant that benefits from precision, like π, use AbstractFloat,
549    /// which can be implicitly converted to all scalar types.
550    ///
551    /// Note: [`LiteralInstance`] implements [`From`] for all standard numeric types
552    pub fn add_constant(&mut self, name: impl ToString, value: impl Into<LiteralInstance>) {
553        self.constants.set(name, value);
554    }
555
556    /// Generate a module with all declared virtual constants in the resolver
557    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        // a special case to handle the constants virtual module. For now, this module
568        // is shared for all sub-dependencies.
569        // TODO: in the future we'll change that.
570        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        // the constants module is shared for all sub-dependencies
601        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        // a and b depend on the same instance of c
736        assert_eq!(canon("a/c::foo"), canon("b/c::foo"));
737        assert_eq!(canon("a/c::foo"), "a/c::foo".parse().unwrap());
738
739        // e reaches the same static through its own dependency
740        assert_eq!(canon("e/c::foo"), "a/c::foo".parse().unwrap());
741
742        // two versions of d
743        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    /// Test WGSL type casting of virtual constants
753    fn type_virtual_constants() {
754        // standard resolver to register some constants
755        let mut std = StandardResolver::new(".");
756        // AbstractFloat
757        std.add_constant("TAU", std::f64::consts::TAU);
758        // f32
759        std.add_constant("LIGHTING_ANGLE", 10.0f32);
760        // i32
761        std.add_constant("Z_ROTATION", -10i32);
762        // u32
763        std.add_constant("H", 12u32);
764        // bool
765        std.add_constant("BRIGHTEN", false);
766
767        // use virtual resolver for the main module
768        let mut v = VirtualResolver::new();
769        v.add_module(
770            "package::color_math".parse().unwrap(),
771            // the main module imports constants::TAU and uses it in a context that requires f32,
772            // therfor it will be cast from AbstractFloat
773            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        // route package imports whose prefix is "constants" to the StandardResolver
794        // and absolute module paths to the VirtualResolver.
795        let mut r = Router::new();
796        r.mount_resolver(ModulePath::new_root(), v);
797        r.mount_fallback_resolver(std);
798
799        // compile to test imports and casting
800        Compiler::new_with_resolver(CompileOptions::default(), r)
801            .compile_module(&"package::color_math".parse().unwrap())
802            .unwrap();
803    }
804
805    #[test]
806    /// Test resolving virtual constants from `add_constant`
807    fn resolve_virtual_constants() {
808        // todo impl `add_constant` for VirtualResolver then use that
809        let mut sr = StandardResolver::new(".");
810
811        // add math constants
812        sr.add_constant("PI", std::f64::consts::PI);
813        sr.add_constant("E", std::f64::consts::E);
814        // add misc constants
815        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        // generate the virtual module
822        let generated = sr.generate_constant_module();
823        // test that it contains the consts with correct values
824        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        // resolve the package path with the origin `constants`,
836        // the source of which should be the same as the generated module
837        let constants_src = sr.resolve_source(&"constants".parse().unwrap()).unwrap();
838        assert_eq!(constants_src, generated);
839
840        // resolving a path with components should return the same
841        let src_comp = sr
842            .resolve_source(&"constants::PI".parse().unwrap())
843            .unwrap();
844        assert_eq!(src_comp, generated);
845    }
846}