Repository: mitsuhiko/minijinja (language: rust)

Issue:
Remove 'source lifetime
Today the engine holds `&'source str` all over the place in the instructions and by extension in the templates. This is a design pillar that I originally inherited from the initial API that was inspired by tinytemplate. It's a pretty clever setup but it has the disadvantage that the API cannot deal with dynamic templates well.

The solution to this problem so far has been the `source` feature which introduces a `Source` type which lets the engine lie about lifetimes through the use of `self-cell` and `memo-map`.

There is a alternative that could be evaluated where these borrows to the source string are not taking place through the references but string handles. That would likely require significant changes in the lexer and potentially the parser as well, but the changes to the instructions are probably quite limited.

The way this could work is that the instructions that currently hold an `&'source str` would instead hold something like a `StrHandle` which looks something like this:

```rust
struct StrHandle {
    offset: u32,
    len: u32,
    #[cfg(debug_assertions)]
    instructions_id: u64,
}

impl StrHandle {
    pub fn as_str(&self, instr: &Instructions) -> &str {
        debug_assert_eq!(instr.id, self.instructions_id);
        &instr.source[self.offset as usize..self.offset as usize + self.len as usize]
    }
}
```

Obviously one complexity with this approach is that at no point the handles must be separated from the instructions they are contained in.  It also comes at least through the safe interface with additional cost as Rust will require a lot more bounds checks.  `unsafe` would work here, but actually making a safe interface where someone does not change the source string underneath the instructions accidentally could be quite tricky.

The benefit of doing this would be that the engine ends up with less dependencies, overall less unsafe code and a nicer API.


Discussion / hints:
One other thing is that the engine has a few places where that basic model does not work well:

* `Token::Str` holds a `Cow` so that it can store unescaped strings
* for internal loop usage `Instruction::Lookup` and `Instruction::GetAttr` also get static strings at times
* Internal maps often hold `&'source str` and with that change they would need to hold a `String` instead since we could not adjacently borrow

I think a model that could work is to have a `StrCache<'source>` which is used everywhere where currently `&'source str` is used to hold the source and then a few `StrHandle` given out by the `StrCache<'source>`. For as long as also for release builds an internal ID is given out that could be made safe without incurring bounds checks. For the few places where a `&'static str` is placed there, I think the handle could be allowed to store references to static strings or specific instructions are used instead. For the the `Cow` case that only exists in the `Token` but never in `Instruction` so that problem is much smaller in comparison. In the instructions it's already converted into a `Value` which clones.
I made some small changes internally that address some of these issues. In d53160d3ff7f54e11696d804708a7e3a7e4b5c2b I removed static strings added to instructions for loop internals and in 6e60023395ad3b6fe20aec9b843a65dda8978c54 I removed the `Cow` from the string token.
I had back out one of these due to performance regressions. 
I have a new way now in which I think I want to go about this. The `'source` lifetime is quite useful for multiple scenarios so it does not need outright removing. What actually however is annoying is that you don't have a convenient way to add owned strings into the environment. This runs against a bunch of lifetime issues, but that is already true for the `Source` feature too.

I think what I might be doing for MiniJinja 1.0 and to close off this issue is the following:

* When the `source` feature is enabled, the Engine *always* holds a `Source` object internally to replace the template map
* `Environment::add_template` stays and borrows
* `Environment::add_owned_template` (name TDB) is does what today is `env.source_mut().add_template(...)`
* Hide the `Source` object internally.

Since after you can add owned templates there is not a lot of reason to actually expose the `Source` object. The main of the `Source` object which at that point is internal is to set a loader:

Before:

```rust
let mut env = Environment::new();
env.set_source(Source::with_loader(|name| {
    if name == "layout.html" {
        Ok(Some("...".into()))
    } else {
        Ok(None)
    }
}));
```

After:

```rust
let mut env = Environment::new();
env.set_loader(|name| {
    if name == "layout.html" {
        Ok(Some("...".into()))
    } else {
        Ok(None)
    }
});
```

And for owned templates:

Before:

```rust
let mut env = Environment::new();
let mut source = Source::new();
source.add_template("index.html", "...").unwrap();
env.set_source(source);
```

After:

```rust
let mut env = Environment::new();
env.add_owned_template("index.html", "...").unwrap();
```

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b559978d..a2c12ed1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ All notable changes to MiniJinja are documented here.
 
 ## 1.0.0
 
+- Removed `Source` and the `source` feature.  The replacement is the new `loader`
+  feature and the functionality of the source is moved directly into the
+  `Environment`.  This also adds `Environment::clear_templates` to unload
+  all already loaded templates.  (#275)
+
 - Added `Environment::template_from_str` and `Environment::template_from_named_str`
   to compile templates for temporary use.  (#274)
 
@@ -74,6 +79,28 @@ All notable changes to MiniJinja are documented here.
   was unreliably maintained in the engine and caused issues with recursive
   calls.
 
+- `Environment::source`, `Environment::set_source` and the `Source` type
+  together with the `source` feature were removed.  The replacement is the
+  new `loader` feature which adds the `add_template_owned` and `set_loader`
+  APIs.
+
+    Before:
+
+    ```rust
+    let mut source = Source::with_loader(|name| ...);
+    source.add_template("foo", "...").unwrap();
+    let mut env = Environment::new();
+    env.set_source(source);
+    ```
+
+    After:
+
+    ```rust
+    let mut env = Environment::new();
+    env.set_loader(|name| ...);
+    env.add_template_owned("foo", "...").unwrap();
+    ```
+
 ## 0.34.0
 
 - Updated `self_cell` and `percent-encoding` dependencies.  (#264)
diff --git a/Makefile b/Makefile
index 6640e1db..5244f635 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
-DOC_FEATURES=source,json,urlencode,testutils,custom_syntax
-TEST_FEATURES=unstable_machinery,builtins,source,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax
+DOC_FEATURES=loader,json,urlencode,testutils,custom_syntax
+TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax
 
 .PHONY: all
 all: test
@@ -52,10 +52,10 @@ check:
 	@cd minijinja; cargo check --all-features
 	@echo "check custom-delimiters:"
 	@cd minijinja; cargo check --features=custom_syntax
-	@echo "check custom-delimiters+source:"
-	@cd minijinja; cargo check --features=custom_syntax,source
-	@echo "check source:"
-	@cd minijinja; cargo check --features=source
+	@echo "check custom-delimiters+loader:"
+	@cd minijinja; cargo check --features=custom_syntax,loader
+	@echo "check loader:"
+	@cd minijinja; cargo check --features=loader
 	@echo "check macro only:"
 	@cd minijinja; cargo check --no-default-features --features macros
 	@echo "check multi_template only:"
diff --git a/examples/README.md b/examples/README.md
index b0a25c70..ef702573 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -8,6 +8,7 @@ the `cargo run` command.  Alternatively you can do `cargo run -p example-name`.
 
 * [autoreload](autoreload): shows how to use auto reloading.
 * [build-script](build-script): Demonstrates how to generate Rust code with MiniJinja in build scripts.
+* [custom-loader](custom-loader): shows how to load templates dynamically at runtime with a custom loader.
 * [debug](debug): contains an example showing the built-in `debug()` function.
 * [dsl](dsl): shows how to use MiniJinja has a DSL.
 * [dynamic-context](dynamic-context): demonstrates how to use dynamic objects as template context.
@@ -21,14 +22,13 @@ the `cargo run` command.  Alternatively you can do `cargo run -p example-name`.
 * [invalid-value](invalid-value): demonstrates how the engine deals with invalid values.
 * [load-lazy](load-lazy): Demonstrates how to load data lazy on demand.
 * [load-resource](load-resource): Demonstrates how to load files dynamically from disk within templates.
-* [loader](loader): shows how to load templates dynamically at runtime.
 * [macros](macros): Demonstrates how to use macros and imports.
 * [minimal](minimal): a Hello World example without default features.
+* [path-loader](path-loader): Demonstrates how to load templates from disk with the `loader` feature.
 * [recursive-for](recursive-for): demonstrates the recursive for loop.
 * [render-macro](render-macro): minimal Hello World example using the `render!` macro.
 * [render-template](render-template): CLI app that renders templates from string.
 * [render-value](render-value): Demonstrates how `Value` can be passed as `Serialize` as context.
-* [source](source): Demonstrates how to load templates from disk with the `source` feature.
 * [stack-ref](stack-ref): Shows how the `minijinja-stack-ref` crate can be used to reference stack values.
 * [template-module](template-module): Demonstrates what can be done with template module objects.
 * [wasm-yew](wasm-yew): Shows how to use MiniJinja with WASM in the browser.
diff --git a/examples/autoreload/Cargo.toml b/examples/autoreload/Cargo.toml
index e82d6769..7c2d4d18 100644
--- a/examples/autoreload/Cargo.toml
+++ b/examples/autoreload/Cargo.toml
@@ -6,5 +6,5 @@ edition = "2021"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-minijinja = { path = "../../minijinja", features = ["source"] }
+minijinja = { path = "../../minijinja", features = ["loader"] }
 minijinja-autoreload = { path = "../../minijinja-autoreload" }
diff --git a/examples/autoreload/src/main.rs b/examples/autoreload/src/main.rs
index e1004174..54a3c44d 100644
--- a/examples/autoreload/src/main.rs
+++ b/examples/autoreload/src/main.rs
@@ -4,7 +4,7 @@ use std::thread;
 use std::time::Duration;
 
 use minijinja::context;
-use minijinja::{Environment, Source};
+use minijinja::{path_loader, Environment};
 use minijinja_autoreload::AutoReloader;
 
 fn main() {
@@ -14,15 +14,14 @@ fn main() {
     // The closure is invoked every time the environment is outdated to
     // recreate it.
     let reloader = AutoReloader::new(move |notifier| {
-        let mut env = Environment::new();
         let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
+        let mut env = Environment::new();
+        env.set_loader(path_loader(&template_path));
 
         // if watch_path is never called, no fs watcher is created
         if !disable_autoreload {
             notifier.watch_path(&template_path, true);
         }
-
-        env.set_source(Source::from_path(&template_path));
         Ok(env)
     });
 
diff --git a/examples/loader/Cargo.toml b/examples/custom-loader/Cargo.toml
similarity index 64%
rename from examples/loader/Cargo.toml
rename to examples/custom-loader/Cargo.toml
index 865d5696..02a77e6b 100644
--- a/examples/loader/Cargo.toml
+++ b/examples/custom-loader/Cargo.toml
@@ -1,9 +1,9 @@
 [package]
-name = "loader"
+name = "custom-loader"
 version = "0.1.0"
 edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-minijinja = { path = "../../minijinja", features = ["source"] }
+minijinja = { path = "../../minijinja", features = ["loader"] }
diff --git a/examples/loader/README.md b/examples/custom-loader/README.md
similarity index 53%
rename from examples/loader/README.md
rename to examples/custom-loader/README.md
index 2c573943..d4789154 100644
--- a/examples/loader/README.md
+++ b/examples/custom-loader/README.md
@@ -1,7 +1,8 @@
-# loader
+# custom-loader
 
 This example demonstrates how templates can be loaded at runtime from
-the file system (in this case from the `templates` folder).
+the file system (in this case from the `templates` folder) with a manual
+loader implementation.
 
 ```console
 $ cargo run
diff --git a/examples/loader/src/main.rs b/examples/custom-loader/src/main.rs
similarity index 89%
rename from examples/loader/src/main.rs
rename to examples/custom-loader/src/main.rs
index 991d7ba0..09be9672 100644
--- a/examples/loader/src/main.rs
+++ b/examples/custom-loader/src/main.rs
@@ -1,12 +1,12 @@
 use std::fs;
 
-use minijinja::{context, Environment, Error, ErrorKind, Source};
+use minijinja::{context, Environment, Error, ErrorKind};
 
 fn main() {
     let mut env = Environment::new();
     let template_path = std::env::current_dir().unwrap().join("templates");
 
-    env.set_source(Source::with_loader(move |name| {
+    env.set_loader(move |name| {
         let pieces = name.split('/');
         let mut path = template_path.clone();
         for piece in pieces {
@@ -30,7 +30,7 @@ fn main() {
                 }
             }
         }
-    }));
+    });
 
     let tmpl = env.get_template("hello.txt").unwrap();
     println!(
diff --git a/examples/loader/templates/hello.txt b/examples/custom-loader/templates/hello.txt
similarity index 100%
rename from examples/loader/templates/hello.txt
rename to examples/custom-loader/templates/hello.txt
diff --git a/examples/loader/templates/layout.txt b/examples/custom-loader/templates/layout.txt
similarity index 100%
rename from examples/loader/templates/layout.txt
rename to examples/custom-loader/templates/layout.txt
diff --git a/examples/source/Cargo.toml b/examples/path-loader/Cargo.toml
similarity index 67%
rename from examples/source/Cargo.toml
rename to examples/path-loader/Cargo.toml
index 1583bc1a..02aa0046 100644
--- a/examples/source/Cargo.toml
+++ b/examples/path-loader/Cargo.toml
@@ -1,10 +1,10 @@
 [package]
-name = "source"
+name = "path-loader"
 version = "0.1.0"
 edition = "2018"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-minijinja = { path = "../../minijinja", features = ["source"] }
+minijinja = { path = "../../minijinja", features = ["loader"] }
 once_cell = "1.13.1"
diff --git a/examples/path-loader/README.md b/examples/path-loader/README.md
new file mode 100644
index 00000000..faf7eac1
--- /dev/null
+++ b/examples/path-loader/README.md
@@ -0,0 +1,8 @@
+# path-loader
+
+Demonstrates the `loader` feature for loading templates from disk with
+the `path_loader` function.
+
+```console
+$ cargo run
+```
diff --git a/examples/source/src/main.rs b/examples/path-loader/src/main.rs
similarity index 74%
rename from examples/source/src/main.rs
rename to examples/path-loader/src/main.rs
index eef03049..eb2465c8 100644
--- a/examples/source/src/main.rs
+++ b/examples/path-loader/src/main.rs
@@ -1,9 +1,9 @@
-use minijinja::{context, Environment, Source};
+use minijinja::{context, path_loader, Environment};
 use once_cell::sync::Lazy;
 
 static ENV: Lazy<Environment<'static>> = Lazy::new(|| {
     let mut env = Environment::new();
-    env.set_source(Source::from_path("templates"));
+    env.set_loader(path_loader("templates"));
     env
 });
 
diff --git a/examples/source/templates/hello.txt b/examples/path-loader/templates/hello.txt
similarity index 100%
rename from examples/source/templates/hello.txt
rename to examples/path-loader/templates/hello.txt
diff --git a/examples/source/templates/layout.txt b/examples/path-loader/templates/layout.txt
similarity index 100%
rename from examples/source/templates/layout.txt
rename to examples/path-loader/templates/layout.txt
diff --git a/examples/source/README.md b/examples/source/README.md
deleted file mode 100644
index 35d9fdc7..00000000
--- a/examples/source/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-# source
-
-Demonstrates the `source` feature for loading templates from disk.
-
-```console
-$ cargo run
-```
diff --git a/minijinja-autoreload/Cargo.toml b/minijinja-autoreload/Cargo.toml
index f8dddd68..0fd22d17 100644
--- a/minijinja-autoreload/Cargo.toml
+++ b/minijinja-autoreload/Cargo.toml
@@ -23,4 +23,4 @@ minijinja = { version = "0.34.0", path = "../minijinja", default-features = fals
 notify = { version = "5.0.0", optional = true, default-features = false, features = ["macos_fsevent"] }
 
 [dev-dependencies]
-minijinja = { version = "0.34.0", path = "../minijinja", features = ["source"] }
+minijinja = { version = "0.34.0", path = "../minijinja", features = ["loader"] }
diff --git a/minijinja-autoreload/src/lib.rs b/minijinja-autoreload/src/lib.rs
index b4a5bd79..7394ae71 100644
--- a/minijinja-autoreload/src/lib.rs
+++ b/minijinja-autoreload/src/lib.rs
@@ -20,13 +20,13 @@
 //! ```
 //! # fn test() -> Result<(), minijinja::Error> {
 //! use minijinja_autoreload::AutoReloader;
-//! use minijinja::{Source, Environment};
+//! use minijinja::{Environment, path_loader};
 //!
 //! let reloader = AutoReloader::new(|notifier| {
-//!     let mut env = Environment::new();
 //!     let template_path = "path/to/templates";
+//!     let mut env = Environment::new();
+//!     env.set_loader(path_loader(template_path));
 //!     notifier.watch_path(template_path, true);
-//!     env.set_source(Source::from_path(template_path));
 //!     Ok(env)
 //! });
 //!
diff --git a/minijinja-contrib/Cargo.toml b/minijinja-contrib/Cargo.toml
index 9cc07a96..975299fe 100644
--- a/minijinja-contrib/Cargo.toml
+++ b/minijinja-contrib/Cargo.toml
@@ -18,5 +18,5 @@ rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "doc-header.html"]
 minijinja = { version = "0.34.0", path = "../minijinja", default-features = false }
 
 [dev-dependencies]
-minijinja = { version = "0.34.0", path = "../minijinja", features = ["source"] }
+minijinja = { version = "0.34.0", path = "../minijinja", features = ["loader"] }
 similar-asserts = "1.4.2"
\ No newline at end of file
diff --git a/minijinja-py/Cargo.toml b/minijinja-py/Cargo.toml
index b48dbc6b..93ffff52 100644
--- a/minijinja-py/Cargo.toml
+++ b/minijinja-py/Cargo.toml
@@ -9,6 +9,6 @@ name = "minijinja_py"
 crate-type = ["cdylib"]
 
 [dependencies]
-minijinja = { version = "0.34.0", path = "../minijinja", features = ["source", "json", "urlencode", "fuel", "preserve_order", "speedups", "custom_syntax"] }
+minijinja = { version = "0.34.0", path = "../minijinja", features = ["loader", "json", "urlencode", "fuel", "preserve_order", "speedups", "custom_syntax"] }
 once_cell = "1.17.0"
 pyo3 = { version = "0.18.0", features = ["extension-module", "serde", "abi3-py38"] }
diff --git a/minijinja-py/README.md b/minijinja-py/README.md
index d874e8b8..8cfe7919 100644
--- a/minijinja-py/README.md
+++ b/minijinja-py/README.md
@@ -71,9 +71,8 @@ loss of information.
 
 MiniJinja's Python bindings inherit the underlying behavior of how MiniJinja loads
 templates.  Templates are loaded on first use and then cached.  The templates are
-loaded via a "source" (called `loader` in MiniJinja's Python bindings).  To trigger
-a reload you can call `env.reload()` or alternatively set `env.reload_before_render`
-to `True`.
+loaded via a loader.  To trigger a reload you can call `env.reload()` or
+alternatively set `env.reload_before_render` to `True`.
 
 ```python
 def my_loader(name):
diff --git a/minijinja-py/src/environment.rs b/minijinja-py/src/environment.rs
index eee41490..dcda70f4 100644
--- a/minijinja-py/src/environment.rs
+++ b/minijinja-py/src/environment.rs
@@ -3,9 +3,7 @@ use std::sync::atomic::{AtomicBool, AtomicPtr, Ordering};
 use std::sync::Mutex;
 
 use minijinja::value::{Rest, Value};
-use minijinja::{
-    context, escape_formatter, AutoEscape, Error, Source, State, Syntax, UndefinedBehavior,
-};
+use minijinja::{context, escape_formatter, AutoEscape, Error, State, Syntax, UndefinedBehavior};
 use pyo3::conversion::AsPyPointer;
 use pyo3::exceptions::PyRuntimeError;
 use pyo3::prelude::*;
@@ -366,8 +364,8 @@ impl Environment {
         let mut inner = self.inner.lock().unwrap();
         inner.loader = callback.clone();
 
-        let mut source = if let Some(callback) = callback {
-            Source::with_loader(move |name| {
+        if let Some(callback) = callback {
+            inner.env.set_loader(move |name| {
                 Python::with_gil(|py| {
                     let callback = callback.as_ref(py);
                     let rv = callback
@@ -380,11 +378,7 @@ impl Environment {
                     }
                 })
             })
-        } else {
-            Source::new()
-        };
-        source.set_syntax(Syntax::default()).map_err(to_py_error)?;
-        inner.env.set_source(source);
+        }
 
         Ok(())
     }
@@ -397,12 +391,12 @@ impl Environment {
 
     /// Triggers a reload of the templates.
     pub fn reload(&self) -> PyResult<()> {
-        let loader = self.inner.lock().unwrap().loader.as_ref().cloned();
-        if let Some(loader) = loader {
-            Python::with_gil(|py| self.set_loader(Some(loader.as_ref(py))))
-        } else {
-            Ok(())
+        let mut inner = self.inner.lock().unwrap();
+        let loader = inner.loader.as_ref().cloned();
+        if loader.is_some() {
+            inner.env.clear_templates();
         }
+        Ok(())
     }
 
     /// Can be used to instruct the environment to automatically reload templates
@@ -480,22 +474,20 @@ impl Environment {
     /// Manually adds a template to the environment.
     pub fn add_template(&self, name: &str, source: &str) -> PyResult<()> {
         let mut inner = self.inner.lock().unwrap();
-        if inner.env.source().is_none() {
-            inner.env.set_source(Source::new());
-        }
         inner
             .env
-            .source_mut()
-            .unwrap()
-            .add_template(name, source)
+            .add_template_owned(name, source)
             .map_err(to_py_error)
     }
 
     /// Removes a loaded template.
     pub fn remove_template(&self, name: &str) {
-        if let Some(source) = self.inner.lock().unwrap().env.source_mut() {
-            source.remove_template(name);
-        }
+        self.inner.lock().unwrap().env.remove_template(name);
+    }
+
+    /// Clears all loaded templates.
+    pub fn clear_templates(&self) {
+        self.inner.lock().unwrap().env.clear_templates();
     }
 
     /// Renders a template looked up from the loader.
diff --git a/minijinja/Cargo.toml b/minijinja/Cargo.toml
index 3ec1b14a..0be54b6c 100644
--- a/minijinja/Cargo.toml
+++ b/minijinja/Cargo.toml
@@ -13,7 +13,7 @@ rust-version = "1.61"
 exclude = ["tests"]
 
 [package.metadata.docs.rs]
-features = ["source", "json", "urlencode", "testutils", "custom_syntax"]
+features = ["loader", "json", "urlencode", "testutils", "custom_syntax"]
 rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "doc-header.html"]
 
 [features]
@@ -23,7 +23,7 @@ default = ["builtins", "debug", "deserialization", "macros", "multi_template", "
 preserve_order = ["indexmap"]
 deserialization = []
 debug = []
-source = ["self_cell", "memo-map"]
+loader = ["self_cell", "memo-map"]
 unicode = ["unicode-ident"]
 testutils = []
 custom_syntax = ["dep:aho-corasick"]
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index bd1f17eb..48c38b14 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -1,6 +1,7 @@
 use std::borrow::Cow;
 use std::collections::BTreeMap;
 use std::fmt;
+use std::marker::PhantomData;
 use std::sync::Arc;
 
 use serde::Serialize;
@@ -19,19 +20,28 @@ use crate::{defaults, filters, functions, tests};
 
 type TemplateMap<'source> = BTreeMap<&'source str, Arc<CompiledTemplate<'source>>>;
 
-#[derive(Clone)]
-enum Source<'source> {
-    Borrowed(TemplateMap<'source>, SyntaxConfig),
-    #[cfg(feature = "source")]
-    Owned(crate::source::Source),
+/// Internal container that holds either a source or the map of
+/// templates directly if there is no source.
+#[derive(Clone, Default)]
+struct TemplateStore<'source> {
+    #[cfg(feature = "loader")]
+    source: crate::source::Source,
+    #[cfg(not(feature = "loader"))]
+    map: TemplateMap<'source>,
+    #[cfg(not(feature = "loader"))]
+    syntax_config: SyntaxConfig,
+    _marker: PhantomData<TemplateMap<'source>>,
 }
 
-impl<'source> fmt::Debug for Source<'source> {
+impl<'source> fmt::Debug for TemplateStore<'source> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        match self {
-            Self::Borrowed(tmpls, _) => fmt::Debug::fmt(&BTreeMapKeysDebug(tmpls), f),
-            #[cfg(feature = "source")]
-            Self::Owned(arg0) => fmt::Debug::fmt(arg0, f),
+        #[cfg(feature = "loader")]
+        {
+            self.source.fmt(f)
+        }
+        #[cfg(not(feature = "loader"))]
+        {
+            BTreeMapKeysDebug(&self.map).fmt(f)
         }
     }
 }
@@ -47,13 +57,6 @@ type FormatterFunc = dyn Fn(&mut Output, &State, &Value) -> Result<(), Error> +
 /// The environment holds references to the source the templates were created from.
 /// This makes it very inconvenient to pass around unless the templates are static
 /// strings.
-#[cfg_attr(
-    feature = "source",
-    doc = "
-For situations where you want to load dynamic templates and share the
-environment it's recommended to turn on the `source` feature and to use the
-[`Source`](crate::source::Source) type with the environment."
-)]
 ///
 /// There are generally two ways to construct an environment:
 ///
@@ -63,7 +66,7 @@ environment it's recommended to turn on the `source` feature and to use the
 /// * [`Environment::empty`] creates a completely blank environment.
 #[derive(Clone)]
 pub struct Environment<'source> {
-    templates: Source<'source>,
+    templates: TemplateStore<'source>,
     filters: BTreeMap<Cow<'source, str>, filters::BoxedFilter>,
     tests: BTreeMap<Cow<'source, str>, tests::BoxedTest>,
     pub(crate) globals: BTreeMap<Cow<'source, str>, Value>,
@@ -102,7 +105,7 @@ impl<'source> Environment<'source> {
     /// [`empty`](Environment::empty) method.
     pub fn new() -> Environment<'source> {
         Environment {
-            templates: Source::Borrowed(Default::default(), Default::default()),
+            templates: TemplateStore::default(),
             filters: defaults::get_builtin_filters(),
             tests: defaults::get_builtin_tests(),
             globals: defaults::get_globals(),
@@ -122,7 +125,7 @@ impl<'source> Environment<'source> {
     /// logic for auto escaping configured.
     pub fn empty() -> Environment<'source> {
         Environment {
-            templates: Source::Borrowed(Default::default(), Default::default()),
+            templates: TemplateStore::default(),
             filters: Default::default(),
             tests: Default::default(),
             globals: Default::default(),
@@ -142,39 +145,114 @@ impl<'source> Environment<'source> {
     /// it.  To look up a loaded template use the [`get_template`](Self::get_template)
     /// method.
     ///
+    /// ```
+    /// # use minijinja::Environment;
+    /// let mut env = Environment::new();
+    /// env.add_template("index.html", "Hello {{ name }}!").unwrap();
+    /// ```
+    ///
     /// Note that there are situations where the interface of this method is
     /// too restrictive.  For instance the environment itself does not permit
     /// any form of sensible dynamic template loading.
     #[cfg_attr(
-        feature = "source",
-        doc = "To address this restriction use [`set_source`](Self::set_source)."
+        feature = "loader",
+        doc = "To address this restriction use [`add_template_owned`](Self::add_template_owned)."
     )]
     pub fn add_template(&mut self, name: &'source str, source: &'source str) -> Result<(), Error> {
-        match self.templates {
-            Source::Borrowed(ref mut map, ref syntax) => {
-                let compiled_template = ok!(CompiledTemplate::from_name_and_source_with_syntax(
-                    name,
-                    source,
-                    syntax.clone()
-                ));
-                map.insert(name, Arc::new(compiled_template));
-                Ok(())
-            }
-            #[cfg(feature = "source")]
-            Source::Owned(ref mut src) => src.add_template(name, source),
+        #[cfg(feature = "loader")]
+        {
+            self.templates.source.add_template(name, source)
+        }
+        #[cfg(not(feature = "loader"))]
+        {
+            let compiled_template = ok!(CompiledTemplate::from_name_and_source_with_syntax(
+                name,
+                source,
+                self.templates.syntax_config.clone()
+            ));
+            self.templates.map.insert(name, Arc::new(compiled_template));
+            Ok(())
         }
     }
 
+    /// Adds a template without without borrowing.
+    ///
+    /// This lets you place an owned [`String`] in the environment rather than the
+    /// borrowed `&str`.
+    ///
+    /// ```
+    /// # use minijinja::Environment;
+    /// let mut env = Environment::new();
+    /// env.add_template_owned("index.html", "Hello {{ name }}!").unwrap();
+    /// ```
+    #[cfg(feature = "loader")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "loader")))]
+    pub fn add_template_owned<N, S>(&mut self, name: N, source: S) -> Result<(), Error>
+    where
+        N: Into<String>,
+        S: Into<String>,
+    {
+        self.templates.source.add_template(name, source)
+    }
+
+    /// Register a template loader as source of templates.
+    ///
+    /// When a template loader is registered, the environment gains the ability
+    /// to dynamically load templates.  The loader is invoked with the name of
+    /// the template.  If this template exists `Ok(Some(template_source))` has
+    /// to be returned, otherwise `Ok(None)`.  Once a template has been loaded
+    /// it's stored on the environment.  This means the loader is only invoked
+    /// once per template name.
+    ///
+    /// For loading templates from the file system, you can use the
+    /// [`path_loader`](crate::path_loader) function.
+    ///
+    /// # Example
+    ///
+    /// ```rust
+    /// # use minijinja::Environment;
+    /// fn create_env() -> Environment<'static> {
+    ///     let mut env = Environment::new();
+    ///     env.set_loader(|name| {
+    ///         if name == "layout.html" {
+    ///             Ok(Some("...".into()))
+    ///         } else {
+    ///             Ok(None)
+    ///         }
+    ///     });
+    ///     env
+    /// }
+    /// ```
+    #[cfg(feature = "loader")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "loader")))]
+    pub fn set_loader<F>(&mut self, f: F)
+    where
+        F: Fn(&str) -> Result<Option<String>, Error> + Send + Sync + 'static,
+    {
+        self.templates.source.set_loader(f);
+    }
+
     /// Removes a template by name.
     pub fn remove_template(&mut self, name: &str) {
-        match self.templates {
-            Source::Borrowed(ref mut map, _) => {
-                map.remove(name);
-            }
-            #[cfg(feature = "source")]
-            Source::Owned(ref mut source) => {
-                source.remove_template(name);
-            }
+        #[cfg(not(feature = "loader"))]
+        {
+            self.templates.map.remove(name);
+        }
+        #[cfg(feature = "loader")]
+        {
+            self.templates.source.remove_template(name);
+        }
+    }
+
+    /// Removes all stored templates.
+    pub fn clear_templates(&mut self) {
+        #[cfg(not(feature = "loader"))]
+        {
+            self.templates.map.clear();
+        }
+        #[cfg(feature = "loader")]
+        {
+            self.templates.source.clear_templates();
         }
     }
 
@@ -192,13 +270,19 @@ impl<'source> Environment<'source> {
     /// println!("{}", tmpl.render(context!{ name => "World" }).unwrap());
     /// ```
     pub fn get_template(&self, name: &str) -> Result<Template<'_, '_>, Error> {
-        let compiled = match &self.templates {
-            Source::Borrowed(ref map, _) => {
-                ok!(map.get(name).ok_or_else(|| Error::new_not_found(name)))
+        let compiled = ok!({
+            #[cfg(feature = "loader")]
+            {
+                self.templates.source.get_compiled_template(name)
+            }
+            #[cfg(not(feature = "loader"))]
+            {
+                self.templates
+                    .map
+                    .get(name)
+                    .ok_or_else(|| Error::new_not_found(name))
             }
-            #[cfg(feature = "source")]
-            Source::Owned(source) => ok!(source.get_compiled_template(name)),
-        };
+        });
         Ok(Template::new(
             self,
             CompiledTemplateRef::Borrowed(compiled),
@@ -244,22 +328,6 @@ impl<'source> Environment<'source> {
         self.template_from_named_str("<string>", source)
     }
 
-    /// Parses and renders a template from a string in one go.
-    ///
-    /// In some cases you really only need a template to be rendered once from
-    /// a string and returned.  The internal name of the template is `<string>`.
-    ///
-    /// This is an alias for [`template_from_str`](Self::template_from_str) paired with
-    /// [`render`](Template::render).
-    ///
-    /// **Note on values:** The [`Value`] type implements `Serialize` and can be
-    /// efficiently passed to render.  It does not undergo actual serialization.
-    pub fn render_str<S: Serialize>(&self, source: &str, ctx: S) -> Result<String, Error> {
-        // reduce total amount of code faling under mono morphization into
-        // this function, and share the rest in _eval.
-        ok!(self.template_from_str(source)).render(ctx)
-    }
-
     /// Parses and renders a template from a string in one go with name.
     ///
     /// Like [`render_str`](Self::render_str), but provide a name for the
@@ -289,6 +357,22 @@ impl<'source> Environment<'source> {
         ok!(self.template_from_named_str(name, source)).render(ctx)
     }
 
+    /// Parses and renders a template from a string in one go.
+    ///
+    /// In some cases you really only need a template to be rendered once from
+    /// a string and returned.  The internal name of the template is `<string>`.
+    ///
+    /// This is an alias for [`template_from_str`](Self::template_from_str) paired with
+    /// [`render`](Template::render).
+    ///
+    /// **Note on values:** The [`Value`] type implements `Serialize` and can be
+    /// efficiently passed to render.  It does not undergo actual serialization.
+    pub fn render_str<S: Serialize>(&self, source: &str, ctx: S) -> Result<String, Error> {
+        // reduce total amount of code faling under mono morphization into
+        // this function, and share the rest in _eval.
+        ok!(self.template_from_str(source)).render(ctx)
+    }
+
     /// Sets a new function to select the default auto escaping.
     ///
     /// This function is invoked when templates are loaded from the environment
@@ -430,12 +514,15 @@ impl<'source> Environment<'source> {
     #[cfg(feature = "custom_syntax")]
     #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
     pub fn set_syntax(&mut self, syntax: crate::custom_syntax::Syntax) -> Result<(), Error> {
-        match self.templates {
-            Source::Borrowed(_, ref mut syn) => *syn = ok!(syntax.compile()),
-            #[cfg(feature = "source")]
-            Source::Owned(ref mut source) => ok!(source.set_syntax(syntax)),
-        };
-        Ok(())
+        #[cfg(feature = "loader")]
+        {
+            self.templates.source.set_syntax(syntax)
+        }
+        #[cfg(not(feature = "loader"))]
+        {
+            self.templates.syntax_config = ok!(syntax.compile());
+            Ok(())
+        }
     }
 
     /// Returns the current syntax.
@@ -446,48 +533,13 @@ impl<'source> Environment<'source> {
     }
 
     fn _syntax_config(&self) -> &SyntaxConfig {
-        match self.templates {
-            Source::Borrowed(_, ref syn) => syn,
-            #[cfg(feature = "source")]
-            Source::Owned(ref source) => source._syntax_config(),
+        #[cfg(feature = "loader")]
+        {
+            self.templates.source._syntax_config()
         }
-    }
-
-    /// Sets the template source for the environment.
-    ///
-    /// This helps when working with dynamically loaded templates.  The
-    /// [`Source`](crate::source::Source) is consulted by the environment to
-    /// look up templates that are requested.  The source has the capabilities
-    /// to load templates with fewer lifetime restrictions and can also
-    /// load templates dynamically at runtime as requested.
-    ///
-    /// When a source is set already loaded templates in the environment are
-    /// discarded and replaced with the templates from the source.
-    ///
-    /// For more information see [`Source`](crate::source::Source).
-    #[cfg(feature = "source")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "source")))]
-    pub fn set_source(&mut self, source: crate::source::Source) {
-        self.templates = Source::Owned(source);
-    }
-
-    /// Returns the currently set source.
-    #[cfg(feature = "source")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "source")))]
-    pub fn source(&self) -> Option<&crate::source::Source> {
-        match self.templates {
-            Source::Borrowed(..) => None,
-            Source::Owned(ref source) => Some(source),
-        }
-    }
-
-    /// Returns the currently set source as mutable reference.
-    #[cfg(feature = "source")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "source")))]
-    pub fn source_mut(&mut self) -> Option<&mut crate::source::Source> {
-        match self.templates {
-            Source::Borrowed(..) => None,
-            Source::Owned(ref mut source) => Some(source),
+        #[cfg(not(feature = "loader"))]
+        {
+            &self.templates.syntax_config
         }
     }
 
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index 385b572f..0d9bf32e 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -159,7 +159,7 @@
 //!
 //! - `fuel`: enables the `fuel` feature which makes the engine track fuel consumption which
 //!   can be used to better protect against expensive templates.
-//! - `source`: enables the `Source` type which helps with dynamic loading of templates.
+//! - `loader`: enables owned and dynamic template loading of templates.
 //! - `speedups`: enables all speedups, in particular it turns on the `v_htmlescape` dependency
 //!   for faster HTML escapling.
 //! - `json`: When enabled the `tojson` filter is added as builtin filter as well as
@@ -207,9 +207,12 @@ pub mod value;
 #[cfg_attr(docsrs, doc(cfg(feature = "testutils")))]
 pub mod testutils;
 
-#[cfg(feature = "source")]
+#[cfg(feature = "loader")]
 mod source;
 
+#[cfg(feature = "loader")]
+pub use source::path_loader;
+
 #[cfg(feature = "custom_syntax")]
 mod custom_syntax;
 
@@ -227,9 +230,6 @@ pub use self::utils::{AutoEscape, HtmlEscape, UndefinedBehavior};
 #[cfg(feature = "custom_syntax")]
 pub use self::custom_syntax::Syntax;
 
-#[cfg(feature = "source")]
-pub use self::source::Source;
-
 pub use self::macros::__context;
 pub use self::vm::State;
 
diff --git a/minijinja/src/output.rs b/minijinja/src/output.rs
index 6fddf13a..6da71453 100644
--- a/minijinja/src/output.rs
+++ b/minijinja/src/output.rs
@@ -82,6 +82,7 @@ impl<'a> Output<'a> {
 
     /// Returns `true` if the output is discarding.
     #[inline(always)]
+    #[allow(unused)]
     pub(crate) fn is_discarding(&self) -> bool {
         matches!(self.capture_stack.last(), Some(None))
     }
diff --git a/minijinja/src/source.rs b/minijinja/src/source.rs
index 3bc03e75..685d59db 100644
--- a/minijinja/src/source.rs
+++ b/minijinja/src/source.rs
@@ -1,7 +1,8 @@
-use std::collections::HashMap;
+use std::collections::BTreeMap;
 use std::fmt;
 use std::fs;
 use std::io;
+use std::mem;
 use std::path::Path;
 use std::path::PathBuf;
 use std::sync::Arc;
@@ -18,23 +19,14 @@ use similar_asserts::assert_eq;
 
 type LoadFunc = dyn for<'a> Fn(&'a str) -> Result<String, Error> + Send + Sync;
 
-/// Utility for dynamic template loading.
+/// Internal utility for dynamic template loading.
 ///
 /// Because an [`Environment`](crate::Environment) holds a reference to the
 /// source lifetime it borrows templates from, it becomes very inconvenient when
 /// it is shared. This object provides a solution for such cases. First templates
-/// are loaded into the source, then it can be set as the "source" for an
-/// environment decouping the lifetimes.  Note that once a source has been added
-/// to an environment methods such as
-/// [`Environment::add_template`](crate::Environment::add_template) must no
-/// longer be used as otherwise the same lifetime concern arises.
-///
-/// Alternatively sources can also be used to implement completely dynamic template
-/// lookups by using [`with_loader`](Source::with_loader) in which case templates
-/// are loaded on first use.
+/// are loaded into the source to decouple the lifetimes from the environment.
 #[derive(Clone)]
-#[cfg_attr(docsrs, doc(cfg(feature = "source")))]
-pub struct Source {
+pub(crate) struct Source {
     backing: SourceBacking,
 }
 
@@ -46,14 +38,19 @@ enum SourceBacking {
         syntax: SyntaxConfig,
     },
     Static {
-        templates: HashMap<String, Arc<LoadedTemplate>>,
+        templates: BTreeMap<String, Arc<LoadedTemplate>>,
         syntax: SyntaxConfig,
     },
 }
 
 impl Default for Source {
     fn default() -> Source {
-        Source::new()
+        Source {
+            backing: SourceBacking::Static {
+                templates: Default::default(),
+                syntax: Default::default(),
+            },
+        }
     }
 }
 
@@ -87,32 +84,8 @@ impl fmt::Debug for LoadedTemplate {
 }
 
 impl Source {
-    /// Creates an empty source.
-    ///
-    /// ```rust
-    /// # use minijinja::{Source, Environment};
-    /// fn create_env() -> Environment<'static> {
-    ///     let mut env = Environment::new();
-    ///     let mut source = Source::new();
-    ///     source.add_template("index.html", "...").unwrap();
-    ///     env.set_source(source);
-    ///     env
-    /// }
-    /// ```
-    pub fn new() -> Source {
-        Source {
-            backing: SourceBacking::Static {
-                templates: HashMap::new(),
-                syntax: Default::default(),
-            },
-        }
-    }
-
     /// Sets the syntax for the source.
-    ///
-    /// See [`Syntax`](crate::Syntax) for more information.
     #[cfg(feature = "custom_syntax")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
     pub fn set_syntax(&mut self, new_syntax: crate::custom_syntax::Syntax) -> Result<(), Error> {
         match self.backing {
             SourceBacking::Dynamic { ref mut syntax, .. }
@@ -123,13 +96,6 @@ impl Source {
         Ok(())
     }
 
-    /// Returns the current syntax.
-    #[cfg(feature = "custom_syntax")]
-    #[cfg_attr(docsrs, doc(cfg(feature = "custom_syntax")))]
-    pub fn syntax(&self) -> &crate::custom_syntax::Syntax {
-        &self._syntax_config().syntax
-    }
-
     pub(crate) fn _syntax_config(&self) -> &SyntaxConfig {
         match &self.backing {
             SourceBacking::Dynamic { ref syntax, .. }
@@ -137,35 +103,26 @@ impl Source {
         }
     }
 
-    /// Creates a source with a dynamic loader.
-    ///
-    /// When a source was created with the loader, the source gains the ability
-    /// to dynamically load templates.  The loader is invoked with the name of
-    /// the template.  If this template exists `Ok(Some(template_source))` has
-    /// to be returned, otherwise `Ok(None)`.
-    ///
-    /// # Example
-    ///
-    /// ```rust
-    /// # use minijinja::{Source, Environment};
-    /// fn create_env() -> Environment<'static> {
-    ///     let mut env = Environment::new();
-    ///     env.set_source(Source::with_loader(|name| {
-    ///         if name == "layout.html" {
-    ///             Ok(Some("...".into()))
-    ///         } else {
-    ///             Ok(None)
-    ///         }
-    ///     }));
-    ///     env
-    /// }
-    /// ```
-    pub fn with_loader<F>(f: F) -> Source
+    /// Reconfigures the source with a new loader.
+    pub fn set_loader<F>(&mut self, f: F)
     where
         F: Fn(&str) -> Result<Option<String>, Error> + Send + Sync + 'static,
     {
-        Source {
-            backing: SourceBacking::Dynamic {
+        // Simple case: we already have dynamic backing, swap out the loader
+        if let SourceBacking::Dynamic { ref mut loader, .. } = self.backing {
+            *loader = Arc::new(move |name| match ok!(f(name)) {
+                Some(rv) => Ok(rv),
+                None => Err(Error::new_not_found(name)),
+            });
+
+        // complex case: we need to migrate static backing to dynamic backing.
+        // This requires some swapping hackery
+        } else if let SourceBacking::Static {
+            templates: old_templates,
+            syntax: old_syntax,
+        } = mem::replace(
+            &mut self.backing,
+            SourceBacking::Dynamic {
                 templates: MemoMap::new(),
                 loader: Arc::new(move |name| match ok!(f(name)) {
                     Some(rv) => Ok(rv),
@@ -173,41 +130,21 @@ impl Source {
                 }),
                 syntax: Default::default(),
             },
-        }
-    }
-
-    /// Creates a source that loads on demand from a given directory.
-    ///
-    /// This creates a source with a dynamic loader which looks up templates in the
-    /// given directory.  Templates that start with a dot (`.`) or are contained in
-    /// a folder starting with a dot cannot be loaded.
-    ///
-    /// # Example
-    ///
-    /// ```rust
-    /// # use minijinja::{Source, Environment};
-    /// fn create_env() -> Environment<'static> {
-    ///     let mut env = Environment::new();
-    ///     env.set_source(Source::from_path("path/to/templates"));
-    ///     env
-    /// }
-    /// ```
-    pub fn from_path<P: AsRef<Path>>(dir: P) -> Source {
-        let dir = dir.as_ref().to_path_buf();
-        Source::with_loader(move |name| {
-            let path = match safe_join(&dir, name) {
-                Some(path) => path,
-                None => return Ok(None),
-            };
-            match fs::read_to_string(path) {
-                Ok(result) => Ok(Some(result)),
-                Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
-                Err(err) => Err(
-                    Error::new(ErrorKind::InvalidOperation, "could not read template")
-                        .with_source(err),
-                ),
+        ) {
+            if let SourceBacking::Dynamic {
+                ref templates,
+                ref mut syntax,
+                ..
+            } = self.backing
+            {
+                for (key, value) in old_templates.into_iter() {
+                    templates.insert(key, value);
+                }
+                *syntax = old_syntax;
+            } else {
+                unreachable!();
             }
-        })
+        }
     }
 
     /// Adds a new template into the source.
@@ -289,6 +226,17 @@ impl Source {
                 .ok_or_else(|| Error::new_not_found(name)),
         }
     }
+
+    pub fn clear_templates(&mut self) {
+        match &mut self.backing {
+            SourceBacking::Dynamic { templates, .. } => {
+                templates.clear();
+            }
+            SourceBacking::Static { templates, .. } => {
+                templates.clear();
+            }
+        }
+    }
 }
 
 fn safe_join(base: &Path, template: &str) -> Option<PathBuf> {
@@ -302,24 +250,57 @@ fn safe_join(base: &Path, template: &str) -> Option<PathBuf> {
     Some(rv)
 }
 
+/// Helper to load templates from a given directory.
+///
+/// This creates a dynamic loader which looks up templates in the
+/// given directory.  Templates that start with a dot (`.`) or are contained in
+/// a folder starting with a dot cannot be loaded.
+///
+/// # Example
+///
+/// ```rust
+/// # use minijinja::{path_loader, Environment};
+/// fn create_env() -> Environment<'static> {
+///     let mut env = Environment::new();
+///     env.set_loader(path_loader("path/to/templates"));
+///     env
+/// }
+/// ```
+#[cfg_attr(docsrs, doc(cfg(feature = "loader")))]
+pub fn path_loader<'x, P: AsRef<Path> + 'x>(
+    dir: P,
+) -> impl for<'a> Fn(&'a str) -> Result<Option<String>, Error> + Send + Sync + 'static {
+    let dir = dir.as_ref().to_path_buf();
+    move |name| {
+        let path = match safe_join(&dir, name) {
+            Some(path) => path,
+            None => return Ok(None),
+        };
+        match fs::read_to_string(path) {
+            Ok(result) => Ok(Some(result)),
+            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
+            Err(err) => Err(
+                Error::new(ErrorKind::InvalidOperation, "could not read template").with_source(err),
+            ),
+        }
+    }
+}
+
 #[test]
 fn test_source_replace_static() {
-    let mut source = Source::new();
-    source.add_template("a", "1").unwrap();
-    source.add_template("a", "2").unwrap();
     let mut env = crate::Environment::new();
-    env.set_source(source);
+    env.add_template_owned("a", "1").unwrap();
+    env.add_template_owned("a", "2").unwrap();
     let rv = env.get_template("a").unwrap().render(()).unwrap();
     assert_eq!(rv, "2");
 }
 
 #[test]
 fn test_source_replace_dynamic() {
-    let mut source = Source::with_loader(|_| Ok(None));
-    source.add_template("a", "1").unwrap();
-    source.add_template("a", "2").unwrap();
     let mut env = crate::Environment::new();
-    env.set_source(source);
+    env.add_template("a", "1").unwrap();
+    env.add_template("a", "2").unwrap();
+    env.set_loader(|_| Ok(None));
     let rv = env.get_template("a").unwrap().render(()).unwrap();
     assert_eq!(rv, "2");
 }
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
index dad0380e..8a6e72a8 100644
--- a/minijinja/src/template.rs
+++ b/minijinja/src/template.rs
@@ -15,8 +15,7 @@ use crate::error::{attach_basic_debug_info, Error};
 use crate::output::{Output, WriteWrapper};
 use crate::utils::AutoEscape;
 use crate::value::{self, Value};
-use crate::vm::{prepare_blocks, Vm};
-use crate::{ErrorKind, State};
+use crate::vm::{prepare_blocks, State, Vm};
 
 /// Represents a handle to a template.
 ///
@@ -311,9 +310,10 @@ impl<'template, 'env> TemplateModule<'template, 'env> {
     #[cfg(feature = "macros")]
     #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
     pub fn call_macro(&self, name: &str, args: &[Value]) -> Result<String, Error> {
-        let f = ok!(self
-            .get_export(name)
-            .ok_or_else(|| Error::new(ErrorKind::UnknownFunction, "macro not found")));
+        let f = ok!(self.get_export(name).ok_or_else(|| Error::new(
+            crate::error::ErrorKind::UnknownFunction,
+            "macro not found"
+        )));
         f.call(&self.state, args).map(Into::into)
     }
 
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index 11787b50..5f6613aa 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -1,4 +1,4 @@
-use std::collections::{BTreeMap, BTreeSet};
+use std::collections::BTreeMap;
 use std::mem;
 use std::sync::atomic::{AtomicUsize, Ordering};
 use std::sync::{Arc, Mutex};
@@ -143,7 +143,7 @@ impl<'env> Vm<'env> {
                 auto_escape: state.auto_escape(),
                 instructions,
                 blocks: BTreeMap::default(),
-                loaded_templates: BTreeSet::new(),
+                loaded_templates: Default::default(),
                 #[cfg(feature = "macros")]
                 macros: state.macros.clone(),
                 #[cfg(feature = "fuel")]
diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs
index b82895f4..70c2f902 100644
--- a/minijinja/src/vm/state.rs
+++ b/minijinja/src/vm/state.rs
@@ -1,6 +1,5 @@
 use std::collections::{BTreeMap, BTreeSet};
 use std::fmt;
-use std::sync::Arc;
 
 use crate::compiler::instructions::Instructions;
 use crate::environment::Environment;
@@ -75,7 +74,7 @@ impl<'template, 'env> State<'template, 'env> {
             blocks,
             loaded_templates: BTreeSet::new(),
             #[cfg(feature = "macros")]
-            macros: Arc::new(Vec::new()),
+            macros: Default::default(),
             #[cfg(feature = "fuel")]
             fuel_tracker: env.fuel().map(FuelTracker::new),
         }


Test patch:
diff --git a/minijinja-py/tests/test_basic.py b/minijinja-py/tests/test_basic.py
index c20d4794..3ac37c52 100644
--- a/minijinja-py/tests/test_basic.py
+++ b/minijinja-py/tests/test_basic.py
@@ -111,10 +111,10 @@ def my_loader(name):
     assert called == ["index.html", "other.html"]
     env.loader = my_loader
     assert env.render_template("index.html") == "Hello from index.html"
-    assert called == ["index.html", "other.html", "index.html"]
+    assert called == ["index.html", "other.html"]
     env.reload()
     assert env.render_template("index.html") == "Hello from index.html"
-    assert called == ["index.html", "other.html", "index.html", "index.html"]
+    assert called == ["index.html", "other.html", "index.html"]
 
 
 def test_loader_reload():
diff --git a/minijinja/tests/test_source.rs b/minijinja/tests/test_source.rs
index 5b560d10..13acca5e 100644
--- a/minijinja/tests/test_source.rs
+++ b/minijinja/tests/test_source.rs
@@ -1,15 +1,13 @@
-#![cfg(feature = "source")]
+#![cfg(feature = "loader")]
 
-use minijinja::{Environment, Source};
+use minijinja::Environment;
 
 use similar_asserts::assert_eq;
 
 fn create_env() -> Environment<'static> {
-    let mut source = Source::new();
-    let template = String::from("Hello World!");
-    source.add_template("hello", template).unwrap();
     let mut env = Environment::new();
-    env.set_source(source);
+    let template = String::from("Hello World!");
+    env.add_template_owned("hello", template).unwrap();
     env
 }
 
@@ -22,14 +20,13 @@ fn test_basic() {
 
 #[test]
 fn test_dynamic() {
-    let mut source = Source::with_loader(|name| match name {
+    let mut env = Environment::new();
+    let template = String::from("Hello World 2!");
+    env.add_template_owned("hello2", template).unwrap();
+    env.set_loader(|name| match name {
         "hello" => Ok(Some("Hello World!".into())),
         _ => Ok(None),
     });
-    let template = String::from("Hello World 2!");
-    source.add_template("hello2", template).unwrap();
-    let mut env = Environment::new();
-    env.set_source(source);
     let t = env.get_template("hello").unwrap();
     assert_eq!(t.render(()).unwrap(), "Hello World!");
     let t = env.get_template("hello2").unwrap();