Repository: sharkdp/numbat (rust)

Issue:
Unknown module prelude when installing via cargo-install
Hey!

When installing numbat with `cargo install` we then can't run anything because it’s missing its prelude:
```
% numbat 1
error: while resolving imports in
  ┌─ <input>:1:5
  │
1 │ use prelude
  │     ^^^^^^^ Unknown module

Make sure that you have properly set up Numbat's module path.
Interpreter error in Prelude code
```

Do you think it would be doable to shove all the prelude directly in the binary? 🤔 

Pull request description:
Builtin modules
This changeset adds a fixed version of the Numbat standard library (`prelude`) to the binary itself. This way, the installation procedure for the typical user is greatly simplified. They just need one executable.

Customization is still possible, as described here: https://numbat.dev/doc/cli-customization.html

closes #165

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index cbc40b2..06a231f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -120,6 +120,15 @@ version = "2.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
 
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bstr"
 version = "1.6.2"
@@ -237,19 +246,68 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
 [[package]]
 name = "difflib"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
 
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys 0.3.7",
+]
+
 [[package]]
 name = "dirs"
 version = "5.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
 dependencies = [
- "dirs-sys",
+ "dirs-sys 0.4.1",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
 ]
 
 [[package]]
@@ -348,6 +406,16 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
 [[package]]
 name = "getrandom"
 version = "0.2.10"
@@ -558,6 +626,7 @@ dependencies = [
  "num-traits",
  "numbat-exchange-rates",
  "pretty_dtoa",
+ "rust-embed",
  "strsim",
  "thiserror",
  "unicode-ident",
@@ -572,7 +641,7 @@ dependencies = [
  "assert_cmd",
  "clap",
  "colored",
- "dirs",
+ "dirs 5.0.1",
  "itertools",
  "numbat",
  "predicates",
@@ -746,6 +815,41 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rust-embed"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "shellexpand",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
 [[package]]
 name = "rustix"
 version = "0.38.14"
@@ -822,6 +926,15 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "scopeguard"
 version = "1.2.0"
@@ -858,6 +971,26 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shellexpand"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
+dependencies = [
+ "dirs 4.0.0",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.11.1"
@@ -953,6 +1086,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.13"
@@ -1009,6 +1148,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
 [[package]]
 name = "wait-timeout"
 version = "0.2.0"
@@ -1018,6 +1163,16 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.11.0+wasi-snapshot-preview1"
diff --git a/README.md b/README.md
index eae5fa8..1060c8a 100644
--- a/README.md
+++ b/README.md
@@ -141,10 +141,6 @@ please consider contributing. Or try [GNU units](https://www.gnu.org/software/un
 
 ## Development
 
-Clone the repository and make sure to properly set up the Numbat modules path. This can
-either be done by copying the `modules/` folder to the [appropriate location](https://numbat.dev/doc/cli-customization.html#module-paths)
-on your operating system, or by pointing the `NUMBAT_MODULES_PATH` environment variable to the `modules/` folder.
-
 Run Numbat CLI
 ```
 cargo run -- <numbat args>
@@ -159,3 +155,13 @@ Run all tests
 ```
 cargo test
 ```
+
+### Working on the `prelude`
+
+If you are working on [Numbats standard library](modules/), it is convenient to point
+the `NUMBAT_MODULES_PATH` environment variable to the `modules/` folder. This way,
+you don't have to recompile Numbat to see your changes.
+
+Alternatively, you can create a symlink from `~/.config/numbat/modules` to the `modules/`
+folder in the repository (see [this page](https://numbat.dev/doc/cli-customization.html#module-paths)
+for the standard paths on other operating systems.
diff --git a/book/src/cli-customization.md b/book/src/cli-customization.md
index 63d641a..fb4cf58 100644
--- a/book/src/cli-customization.md
+++ b/book/src/cli-customization.md
@@ -2,9 +2,9 @@
 
 ## Startup
 
-By default, Numbat will load the following files during startup:
+By default, Numbat will load the following modules/files during startup, in order:
 
-- Numbat Prelude (a module called `prelude`, from `<module-path>/prelude.nbt`)
+- Numbat Prelude (a module called `prelude`, either from `<module-path>/prelude.nbt` if available, or the builtin version)
 - The user initialization file, if available (a file called `init.nbt` from `<config-path>/init.nbt`)
 
 ### Config path
@@ -19,12 +19,14 @@ Numbat's configuration folder (`<config-path>` above) can be found under:
 
 ## Module paths
 
-Numbat will load modules from the following directories (`<module-path>` above).
+Numbat will load modules from the following sources.
 Entries higher up in the list take precedence.
 
+* `$NUMBAT_MODULES_PATH` — If present, this environment variable can point to a single directory or contain a `:`-separated list of paths
 * `<config-path>/modules` — User-customized module folder
-* `/usr/share/numbat/modules` — on Linux and macOS
-* `C:\Program Files\numbat\modules` — on Windows
+* `/usr/share/numbat/modules` — System-wide module folder (Linux and macOS)
+* `C:\Program Files\numbat\modules` — System-wide module folder (Windows)
+* Builtin modules inside the `numbat` binary
 
 ## Customization
 
@@ -36,17 +38,23 @@ create a `init.nbt` file in your config folder (e.g. `~/.config/numbat/init.nbt`
 ### Custom modules
 
 You can also create your own modules that can be loaded on demand. To this end,
-create a new file, say `<module-path>/finance.nbt` in one of the module folders (e.g. `~/.config/numbat/modules/finance.nbt` on Linux). This module can then be loaded using
+create a new file, say `<module-path>/user/finance.nbt` in one of the module folders
+(e.g. `~/.config/numbat/modules/custom/finance.nbt` on Linux). This module can then be
+loaded using
 
 ``` numbat
-use finance
+use custom::finance
 ```
 
 in your Numbat scripts or in the REPL. You can also load custom modules from `init.nbt`
 if you want to have them available all the time.
 
-You can also organize modules into subfolders (e.g. `<module-path>/finance/functions.nbt`). In that case, you can load them using
+You can also organize modules into subfolders (e.g. `<module-path>/custom/finance/functions.nbt`).
+In that case, you can load them using
 
 ``` numbat
-use finance::functions
+use custom::finance::functions
 ```
+
+In fact, the `custom` folder is just a convention to avoid name clashes with the
+[standard library](https://github.com/sharkdp/numbat/tree/master/modules).
diff --git a/book/src/cli-installation.md b/book/src/cli-installation.md
index 0089614..8bf4335 100644
--- a/book/src/cli-installation.md
+++ b/book/src/cli-installation.md
@@ -17,14 +17,6 @@ to try it immediately (unpacks to `/tmp` and executes from there).
 For a proper installation, download the latest release
 [here](https://github.com/sharkdp/numbat/releases).
 
-Unpack the archive and move the `modules/` folder to `~/.config/numbat`,
-or one of the [other supported directories](./cli-customization.md#module-paths).
-
-``` bash
-mkdir -p ~/.config/numbat
-cp -r numbat/modules ~/.config/numbat/
-```
-
 ## From source
 
 Clone the Git repository, and build Numbat with `cargo`:
@@ -35,10 +27,8 @@ cd numbat/
 cargo install -f --path numbat-cli
 ```
 
-And copy the `modules/` folder to `~/.config/numbat/`, or the
-[respective folder on your operating system](./cli-customization.md#module-paths).
+Or install the latest release using
 
 ``` bash
-mkdir -p ~/.config/numbat
-cp -r modules/ ~/.config/numbat/
+cargo install numbat-cli
 ```
diff --git a/numbat-cli/src/main.rs b/numbat-cli/src/main.rs
index 4adad7d..10f5905 100644
--- a/numbat-cli/src/main.rs
+++ b/numbat-cli/src/main.rs
@@ -8,8 +8,9 @@ use highlighter::NumbatHighlighter;
 
 use numbat::diagnostic::ErrorDiagnostic;
 use numbat::markup as m;
+use numbat::module_importer::{BuiltinModuleImporter, ChainedImporter, FileSystemImporter};
 use numbat::pretty_print::PrettyPrint;
-use numbat::resolver::{CodeSource, FileSystemImporter, ResolverError};
+use numbat::resolver::CodeSource;
 use numbat::{Context, ExitStatus, InterpreterResult, NumbatError};
 use numbat::{InterpreterSettings, NameResolutionError, RuntimeError, Type};
 
@@ -100,11 +101,16 @@ impl Cli {
     fn new() -> Self {
         let args = Args::parse();
 
-        let mut importer = FileSystemImporter::default();
+        let mut fs_importer = FileSystemImporter::default();
         for path in Self::get_modules_paths() {
-            importer.add_path(path);
+            fs_importer.add_path(path);
         }
 
+        let importer = ChainedImporter::new(
+            Box::new(fs_importer),
+            Box::new(BuiltinModuleImporter::default()),
+        );
+
         let mut context = Context::new(importer);
         context.set_debug(args.debug);
 
@@ -414,12 +420,6 @@ impl Cli {
             }
             Err(NumbatError::ResolverError(e)) => {
                 self.print_diagnostic(e.clone());
-                if matches!(&e, ResolverError::UnknownModule(_, module_path) if module_path.0 == &["prelude"])
-                {
-                    eprintln!("Make sure that you have installed Numbat's standard library");
-                    eprintln!("in one of the standard locations, or set NUMBAT_MODULES_PATH.");
-                    eprintln!("For details, see https://numbat.dev/doc/cli-installation.html");
-                }
                 execution_mode.exit_status_in_case_of_error()
             }
             Err(NumbatError::NameResolutionError(
@@ -465,6 +465,7 @@ impl Cli {
         }
 
         paths.push(Self::get_config_path().join("modules"));
+
         if cfg!(unix) {
             paths.push("/usr/share/numbat/modules".into());
         } else {
diff --git a/numbat-wasm/Cargo.lock b/numbat-wasm/Cargo.lock
index fc7adb7..8dd6955 100644
--- a/numbat-wasm/Cargo.lock
+++ b/numbat-wasm/Cargo.lock
@@ -21,6 +21,21 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
 [[package]]
 name = "bumpalo"
 version = "3.13.0"
@@ -74,6 +89,55 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "cpufeatures"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "crypto-common",
+]
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
 [[package]]
 name = "either"
 version = "1.8.1"
@@ -95,6 +159,27 @@ dependencies = [
  "percent-encoding",
 ]
 
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "wasi",
+]
+
 [[package]]
 name = "heck"
 version = "0.4.1"
@@ -136,9 +221,9 @@ dependencies = [
 
 [[package]]
 name = "itertools"
-version = "0.10.5"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
 dependencies = [
  "either",
 ]
@@ -226,7 +311,7 @@ dependencies = [
 
 [[package]]
 name = "numbat"
-version = "1.5.0"
+version = "1.6.0"
 dependencies = [
  "codespan-reporting",
  "heck",
@@ -237,6 +322,7 @@ dependencies = [
  "num-traits",
  "numbat-exchange-rates",
  "pretty_dtoa",
+ "rust-embed",
  "strsim",
  "thiserror",
  "unicode-ident",
@@ -245,7 +331,7 @@ dependencies = [
 
 [[package]]
 name = "numbat-exchange-rates"
-version = "0.1.0"
+version = "0.2.0"
 dependencies = [
  "attohttpc",
  "quick-xml",
@@ -297,9 +383,9 @@ dependencies = [
 
 [[package]]
 name = "quick-xml"
-version = "0.29.0"
+version = "0.30.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51"
+checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
 dependencies = [
  "memchr",
 ]
@@ -313,6 +399,26 @@ dependencies = [
  "proc-macro2",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
 [[package]]
 name = "ring"
 version = "0.16.20"
@@ -328,6 +434,41 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rust-embed"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40"
+dependencies = [
+ "rust-embed-impl",
+ "rust-embed-utils",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-impl"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "rust-embed-utils",
+ "shellexpand",
+ "syn",
+ "walkdir",
+]
+
+[[package]]
+name = "rust-embed-utils"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada"
+dependencies = [
+ "sha2",
+ "walkdir",
+]
+
 [[package]]
 name = "rustls"
 version = "0.21.7"
@@ -356,6 +497,15 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "scoped-tls"
 version = "1.0.1"
@@ -372,6 +522,26 @@ dependencies = [
  "untrusted",
 ]
 
+[[package]]
+name = "sha2"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shellexpand"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4"
+dependencies = [
+ "dirs",
+]
+
 [[package]]
 name = "spin"
 version = "0.5.2"
@@ -439,6 +609,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
 [[package]]
 name = "unicode-bidi"
 version = "0.3.13"
@@ -495,6 +671,28 @@ version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
 
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
 [[package]]
 name = "wasm-bindgen"
 version = "0.2.87"
diff --git a/numbat-wasm/src/lib.rs b/numbat-wasm/src/lib.rs
index ceda8ae..e6fd13c 100644
--- a/numbat-wasm/src/lib.rs
+++ b/numbat-wasm/src/lib.rs
@@ -1,13 +1,12 @@
 mod jquery_terminal_formatter;
 mod utils;
-mod wasm_importer;
 
 use numbat::diagnostic::ErrorDiagnostic;
+use numbat::module_importer::BuiltinModuleImporter;
 use std::sync::{Arc, Mutex};
 use wasm_bindgen::prelude::*;
 
 use jquery_terminal_formatter::{jt_format, JqueryTerminalFormatter};
-use wasm_importer::WasmImporter;
 
 use numbat::markup::Formatter;
 use numbat::pretty_print::PrettyPrint;
@@ -36,7 +35,7 @@ pub struct Numbat {
 #[wasm_bindgen]
 impl Numbat {
     pub fn new() -> Self {
-        let mut ctx = Context::new(WasmImporter {});
+        let mut ctx = Context::new(BuiltinModuleImporter::default());
         let _ = ctx.interpret("use prelude", CodeSource::Internal).unwrap();
         Numbat { ctx }
     }
diff --git a/numbat-wasm/src/wasm_importer.rs b/numbat-wasm/src/wasm_importer.rs
deleted file mode 100644
index 20793cb..0000000
--- a/numbat-wasm/src/wasm_importer.rs
+++ /dev/null
@@ -1,48 +0,0 @@
-use std::path::PathBuf;
-
-use numbat::resolver::{ModuleImporter, ModulePath};
-
-#[derive(Debug, Clone, Default)]
-pub struct WasmImporter {}
-
-impl ModuleImporter for WasmImporter {
-    fn import(&self, path: &ModulePath) -> Option<(String, Option<PathBuf>)> {
-        // TODO: yeah, this is not great.
-
-        let path = path.to_string();
-        let code = match path.as_str() {
-            "core::dimensions" => include_str!("../../modules/core/dimensions.nbt"),
-            "core::quantity" => include_str!("../../modules/core/quantity.nbt"),
-            "core::scalar" => include_str!("../../modules/core/scalar.nbt"),
-            "math::constants" => include_str!("../../modules/math/constants.nbt"),
-            "math::functions" => include_str!("../../modules/math/functions.nbt"),
-            "math::trigonometry_extra" => include_str!("../../modules/math/trigonometry_extra.nbt"),
-            "physics::constants" => include_str!("../../modules/physics/constants.nbt"),
-            "physics::temperature_conversion" => {
-                include_str!("../../modules/physics/temperature_conversion.nbt")
-            }
-            "prelude" => include_str!("../../modules/prelude.nbt"),
-            "units::astronomical" => include_str!("../../modules/units/astronomical.nbt"),
-            "units::bit" => include_str!("../../modules/units/bit.nbt"),
-            "units::cgs" => include_str!("../../modules/units/cgs.nbt"),
-            "units::currencies" => include_str!("../../modules/units/currencies.nbt"),
-            "units::currency" => include_str!("../../modules/units/currency.nbt"),
-            "units::fff" => include_str!("../../modules/units/fff.nbt"),
-            "units::hartree" => include_str!("../../modules/units/hartree.nbt"),
-            "units::imperial" => include_str!("../../modules/units/imperial.nbt"),
-            "units::misc" => include_str!("../../modules/units/misc.nbt"),
-            "units::nautical" => include_str!("../../modules/units/nautical.nbt"),
-            "units::partsperx" => include_str!("../../modules/units/partsperx.nbt"),
-            "units::placeholder" => include_str!("../../modules/units/placeholder.nbt"),
-            "units::planck" => include_str!("../../modules/units/planck.nbt"),
-            "units::si" => include_str!("../../modules/units/si.nbt"),
-            "units::stoney" => include_str!("../../modules/units/stoney.nbt"),
-            "units::time" => include_str!("../../modules/units/time.nbt"),
-            "units::us_customary" => include_str!("../../modules/units/us_customary.nbt"),
-
-            _ => return None,
-        };
-
-        Some((code.to_string(), None))
-    }
-}
diff --git a/numbat/Cargo.toml b/numbat/Cargo.toml
index 45d16e5..0b04123 100644
--- a/numbat/Cargo.toml
+++ b/numbat/Cargo.toml
@@ -26,6 +26,7 @@ heck = { version = "0.4.1", features = ["unicode"] }
 unicode-ident = "1.0.11"
 unicode-width = "0.1.10"
 libc = "0.2.147"
+rust-embed = { version = "8.0.0", features = ["interpolate-folder-path"] }
 
 [dev-dependencies]
 approx = "0.5"
diff --git a/numbat/examples/unit_graph.rs b/numbat/examples/unit_graph.rs
index 02441e5..ea8458d 100644
--- a/numbat/examples/unit_graph.rs
+++ b/numbat/examples/unit_graph.rs
@@ -5,10 +5,7 @@
 //
 
 use itertools::Itertools;
-use numbat::{
-    resolver::{CodeSource, FileSystemImporter},
-    Context,
-};
+use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context};
 
 fn main() {
     let mut importer = FileSystemImporter::default();
diff --git a/numbat/src/lib.rs b/numbat/src/lib.rs
index ca3804e..c950618 100644
--- a/numbat/src/lib.rs
+++ b/numbat/src/lib.rs
@@ -11,6 +11,7 @@ mod interpreter;
 pub mod keywords;
 pub mod markup;
 mod math;
+pub mod module_importer;
 mod name_resolution;
 mod number;
 mod parser;
@@ -38,11 +39,10 @@ use diagnostic::ErrorDiagnostic;
 use dimension::DimensionRegistry;
 use interpreter::Interpreter;
 use keywords::KEYWORDS;
+use module_importer::{ModuleImporter, NullImporter};
 use prefix_transformer::Transformer;
 use registry::BaseRepresentationFactor;
 use resolver::CodeSource;
-use resolver::ModuleImporter;
-use resolver::NullImporter;
 use resolver::Resolver;
 use resolver::ResolverError;
 use thiserror::Error;
diff --git a/numbat/src/module_importer.rs b/numbat/src/module_importer.rs
new file mode 100644
index 0000000..7a7e084
--- /dev/null
+++ b/numbat/src/module_importer.rs
@@ -0,0 +1,101 @@
+use std::{
+    fs,
+    path::{Path, PathBuf},
+};
+
+use rust_embed::RustEmbed;
+
+use crate::resolver::ModulePath;
+
+pub trait ModuleImporter {
+    fn import(&self, path: &ModulePath) -> Option<(String, Option<PathBuf>)>;
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct NullImporter {}
+
+impl ModuleImporter for NullImporter {
+    fn import(&self, _: &ModulePath) -> Option<(String, Option<PathBuf>)> {
+        None
+    }
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct FileSystemImporter {
+    root_paths: Vec<PathBuf>,
+}
+
+impl FileSystemImporter {
+    pub fn add_path<P: AsRef<Path>>(&mut self, path: P) {
+        self.root_paths.push(path.as_ref().to_owned());
+    }
+}
+
+impl ModuleImporter for FileSystemImporter {
+    fn import(&self, module_path: &ModulePath) -> Option<(String, Option<PathBuf>)> {
+        for path in &self.root_paths {
+            let mut path = path.clone();
+            for part in &module_path.0 {
+                path = path.join(part);
+            }
+
+            path.set_extension("nbt");
+
+            if let Ok(code) = fs::read_to_string(&path) {
+                return Some((code, Some(path.to_owned())));
+            }
+        }
+
+        None
+    }
+}
+
+#[derive(RustEmbed)]
+#[folder = "$CARGO_MANIFEST_DIR/../modules/"]
+struct BuiltinAssets;
+
+#[derive(Debug, Clone, Default)]
+
+pub struct BuiltinModuleImporter {}
+
+impl ModuleImporter for BuiltinModuleImporter {
+    fn import(&self, module_path: &ModulePath) -> Option<(String, Option<PathBuf>)> {
+        let mut path = PathBuf::new();
+        for part in &module_path.0 {
+            path = path.join(part);
+        }
+
+        path.set_extension("nbt");
+
+        BuiltinAssets::get(&path.to_string_lossy())
+            .map(|embedded_file| {
+                let content = embedded_file.data.into_owned();
+                String::from_utf8(content).expect("Numbat modules are properly UTF-8 encoded")
+            })
+            .map(|content| (content, None))
+    }
+}
+
+pub struct ChainedImporter {
+    main: Box<dyn ModuleImporter + Send>,
+    fallback: Box<dyn ModuleImporter + Send>,
+}
+
+impl ChainedImporter {
+    pub fn new(
+        main: Box<dyn ModuleImporter + Send>,
+        fallback: Box<dyn ModuleImporter + Send>,
+    ) -> Self {
+        Self { main, fallback }
+    }
+}
+
+impl ModuleImporter for ChainedImporter {
+    fn import(&self, path: &ModulePath) -> Option<(String, Option<PathBuf>)> {
+        if let result @ Some(_) = self.main.import(path) {
+            result
+        } else {
+            self.fallback.import(path)
+        }
+    }
+}
diff --git a/numbat/src/resolver.rs b/numbat/src/resolver.rs
index 24e92c8..c9a4b72 100644
--- a/numbat/src/resolver.rs
+++ b/numbat/src/resolver.rs
@@ -1,9 +1,8 @@
-use std::{
-    fs,
-    path::{Path, PathBuf},
-};
+use std::path::PathBuf;
 
-use crate::{ast::Statement, parser::parse, span::Span, ParseError};
+use crate::{
+    ast::Statement, module_importer::ModuleImporter, parser::parse, span::Span, ParseError,
+};
 
 use codespan_reporting::files::SimpleFiles;
 use thiserror::Error;
@@ -130,49 +129,6 @@ impl Resolver {
     }
 }
 
-pub trait ModuleImporter {
-    fn import(&self, path: &ModulePath) -> Option<(String, Option<PathBuf>)>;
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct NullImporter {}
-
-impl ModuleImporter for NullImporter {
-    fn import(&self, _: &ModulePath) -> Option<(String, Option<PathBuf>)> {
-        None
-    }
-}
-
-#[derive(Debug, Clone, Default)]
-pub struct FileSystemImporter {
-    root_paths: Vec<PathBuf>,
-}
-
-impl FileSystemImporter {
-    pub fn add_path<P: AsRef<Path>>(&mut self, path: P) {
-        self.root_paths.push(path.as_ref().to_owned());
-    }
-}
-
-impl ModuleImporter for FileSystemImporter {
-    fn import(&self, module_path: &ModulePath) -> Option<(String, Option<PathBuf>)> {
-        for path in &self.root_paths {
-            let mut path = path.clone();
-            for part in &module_path.0 {
-                path = path.join(part);
-            }
-
-            path.set_extension("nbt");
-
-            if let Ok(code) = fs::read_to_string(&path) {
-                return Some((code, Some(path.to_owned())));
-            }
-        }
-
-        None
-    }
-}
-
 #[cfg(test)]
 mod tests {
     use crate::{


Test patch:
diff --git a/numbat/tests/common.rs b/numbat/tests/common.rs
index 7a967cc..e0e36c2 100644
--- a/numbat/tests/common.rs
+++ b/numbat/tests/common.rs
@@ -1,14 +1,14 @@
 use std::path::Path;
 
-use numbat::{
-    resolver::{CodeSource, FileSystemImporter},
-    Context,
-};
+use numbat::{module_importer::FileSystemImporter, resolver::CodeSource, Context};
 
 pub fn get_test_context_without_prelude() -> Context {
-    let module_path = Path::new(&std::env::var_os("CARGO_MANIFEST_DIR").unwrap())
-        .join("..")
-        .join("modules");
+    let module_path = Path::new(
+        &std::env::var_os("CARGO_MANIFEST_DIR")
+            .expect("CARGO_MANIFEST_DIR variable should be set when calling 'cargo test'"),
+    )
+    .join("..")
+    .join("modules");
 
     let mut importer = FileSystemImporter::default();
     importer.add_path(module_path);