Repository: mitsuhiko/minijinja (language: rust)

Issue:
Move render_block and consorts onto TemplateModule
If template modules land (#265) then the existing block render methods should be exclusively available on the module.


Discussion / hints:

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04c971c3..c93449cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,12 @@ All notable changes to MiniJinja are documented here.
 
 ## 1.0.0
 
+- Added `Template::eval_to_module` and `TemplateModule`.  This replaces the
+  functionality of the previous `Template::render_block` which is now available
+  via the `TemplateModule`.  This functionality is closer to how how Jinja2
+  functions and leaves room for future expansion.  It also adds support for
+  accessing globals and macros from a template.  (#271)
+
 - Removed support for `State::current_call`.  This property wasn't too useful
   and unreliable.  Supporting it properly for nested invocations would require
   calls to take a mutable state or use interior mutability which did not seem
diff --git a/minijinja/src/compiler/instructions.rs b/minijinja/src/compiler/instructions.rs
index f958a4f4..26212c62 100644
--- a/minijinja/src/compiler/instructions.rs
+++ b/minijinja/src/compiler/instructions.rs
@@ -255,6 +255,15 @@ pub struct Instructions<'source> {
     source: &'source str,
 }
 
+pub(crate) static EMPTY_INSTRUCTIONS: Instructions<'static> = Instructions {
+    instructions: Vec::new(),
+    line_infos: Vec::new(),
+    #[cfg(feature = "debug")]
+    span_infos: Vec::new(),
+    name: "<unknown>",
+    source: "",
+};
+
 impl<'source> Instructions<'source> {
     /// Creates a new instructions object.
     pub fn new(name: &'source str, source: &'source str) -> Instructions<'source> {
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index e6e09c24..385b572f 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -221,7 +221,7 @@ pub use self::environment::Environment;
 pub use self::error::{Error, ErrorKind};
 pub use self::expression::Expression;
 pub use self::output::Output;
-pub use self::template::Template;
+pub use self::template::{Template, TemplateModule};
 pub use self::utils::{AutoEscape, HtmlEscape, UndefinedBehavior};
 
 #[cfg(feature = "custom_syntax")]
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
index c0945800..c4cf331d 100644
--- a/minijinja/src/template.rs
+++ b/minijinja/src/template.rs
@@ -14,6 +14,7 @@ use crate::output::{Output, WriteWrapper};
 use crate::utils::AutoEscape;
 use crate::value::{self, Value};
 use crate::vm::Vm;
+use crate::{ErrorKind, State};
 
 /// Represents a handle to a template.
 ///
@@ -95,44 +96,11 @@ impl<'env> Template<'env> {
             .map(|_| rv)
     }
 
-    /// Renders the template block into a string.
-    ///
-    /// This method works like [`render`](Self::render) but it only renders a specific
-    /// block in the template.  The first argument is the name of the block.
-    ///
-    /// This renders only the block `hi` in the template:
-    ///
-    /// ```
-    /// # use minijinja::{Environment, context};
-    /// # let mut env = Environment::new();
-    /// # env.add_template("hello", "{% block hi %}Hello {{ name }}!{% endblock %}").unwrap();
-    /// let tmpl = env.get_template("hello").unwrap();
-    /// println!("{}", tmpl.render_block("hi", context!(name => "John")).unwrap());
-    /// ```
-    ///
-    /// **Note on values:** The [`Value`] type implements `Serialize` and can be
-    /// efficiently passed to render.  It does not undergo actual serialization.
-    #[cfg(feature = "multi_template")]
-    pub fn render_block<S: Serialize>(&self, block: &str, ctx: S) -> Result<String, Error> {
-        // reduce total amount of code faling under mono morphization into
-        // this function, and share the rest in _render.
-        self._render_block(block, Value::from_serializable(&ctx))
-    }
-
-    #[cfg(feature = "multi_template")]
-    fn _render_block(&self, block: &str, root: Value) -> Result<String, Error> {
-        let mut rv = String::with_capacity(self.compiled.buffer_size_hint);
-        self._eval_block(block, root, &mut Output::with_string(&mut rv))
-            .map(|_| rv)
-    }
-
     /// Renders the template into a [`io::Write`].
     ///
     /// This works exactly like [`render`](Self::render) but instead writes the template
     /// as it's evaluating into a [`io::Write`].
     ///
-    /// This renders only the block `hi` in the template:
-    ///
     /// ```
     /// # use minijinja::{Environment, context};
     /// # let mut env = Environment::new();
@@ -155,59 +123,33 @@ impl<'env> Template<'env> {
         .map_err(|err| wrapper.take_err(err))
     }
 
-    /// Renders the template block into a [`io::Write`].
+    /// Evaluates the template into a [`TemplateModule`].
     ///
-    /// This works exactly like [`render_to_write`](Self::render_to_write) but renders
-    /// a single block.  The first argument is the name of the block.
+    /// This evaluates the template, discards the output and stores the final
+    /// state in the evaluated module.  From there global variables or blocks
+    /// can be accessed.  What this does is quite similar to how the engine
+    /// interally works with tempaltes that are extended or imported from.
     ///
-    /// ```
-    /// # use minijinja::{Environment, context};
-    /// # let mut env = Environment::new();
-    /// # env.add_template("hello", "{% block hi %}Hello {{ name }}!{% endblock %}").unwrap();
-    /// use std::io::stdout;
-    ///
-    /// let tmpl = env.get_template("hello").unwrap();
-    /// tmpl.render_block_to_write("hi", context!(name => "John"), &mut stdout()).unwrap();
-    /// ```
-    ///
-    /// **Note on values:** The [`Value`] type implements `Serialize` and can be
-    /// efficiently passed to render.  It does not undergo actual serialization.
-    #[cfg(feature = "multi_template")]
-    pub fn render_block_to_write<S: Serialize, W: io::Write>(
-        &self,
-        block: &str,
-        ctx: S,
-        w: W,
-    ) -> Result<(), Error> {
-        let mut wrapper = WriteWrapper { w, err: None };
-        self._eval_block(
-            block,
-            Value::from_serializable(&ctx),
-            &mut Output::with_write(&mut wrapper),
-        )
-        .map(|_| ())
-        .map_err(|err| wrapper.take_err(err))
-    }
-
-    fn _eval(&self, root: Value, out: &mut Output) -> Result<Option<Value>, Error> {
-        Vm::new(self.env).eval(
+    /// For more information see [`TemplateModule`].
+    pub fn eval_to_module<S: Serialize>(&self, ctx: S) -> Result<TemplateModule<'_, 'env>, Error> {
+        let root = Value::from_serializable(&ctx);
+        let mut out = Output::null();
+        let vm = Vm::new(self.env);
+        let state = ok!(vm.eval_to_module(
             &self.compiled.instructions,
             root,
             &self.compiled.blocks,
-            out,
+            &mut out,
             self.initial_auto_escape,
-        )
+        ));
+        Ok(TemplateModule {
+            template: self,
+            state,
+        })
     }
 
-    #[cfg(feature = "multi_template")]
-    fn _eval_block(
-        &self,
-        block: &str,
-        root: Value,
-        out: &mut Output,
-    ) -> Result<Option<Value>, Error> {
-        Vm::new(self.env).eval_block(
-            block,
+    fn _eval(&self, root: Value, out: &mut Output) -> Result<Option<Value>, Error> {
+        Vm::new(self.env).eval(
             &self.compiled.instructions,
             root,
             &self.compiled.blocks,
@@ -265,6 +207,107 @@ impl<'env> Template<'env> {
     }
 }
 
+/// Represents an evaluated template as module.
+///
+/// The engine internally sometimes treats templates as modules.  This happens
+/// for instance for template inheritance or when importing global variables or
+/// macros.  In the VM modules are virtual and never actually created, however
+/// for some advanced use cases the same logic is exposed via this type.
+///
+/// This lets you call macros or blocks of a template from outside of the
+/// template evaluation.  Because template modules hold mutable states, some
+/// of the methods take `&mut self`.  The template module internally retains the
+/// state of the execution and provides ways to extract some information from
+/// it.
+pub struct TemplateModule<'template: 'env, 'env> {
+    template: &'template Template<'env>,
+    state: State<'template, 'env>,
+}
+
+impl<'template, 'env> TemplateModule<'template, 'env> {
+    /// Returns a reference to the [`Template`] behind the module.
+    pub fn template(&self) -> &'template Template<'env> {
+        self.template
+    }
+
+    /// Returns the [`State`] of the module.
+    pub fn state(&self) -> &State<'template, 'env> {
+        &self.state
+    }
+
+    /// Renders a block with the given name into a string.
+    ///
+    /// This method works like [`render`](Template::render) but it only renders a specific
+    /// block in the template.  The first argument is the name of the block.
+    ///
+    /// This renders only the block `hi` in the template:
+    ///
+    /// ```
+    /// # use minijinja::{Environment, context};
+    /// # fn test() -> Result<(), minijinja::Error> {
+    /// # let mut env = Environment::new();
+    /// # env.add_template("hello", "{% block hi %}Hello {{ name }}!{% endblock %}")?;
+    /// let tmpl = env.get_template("hello")?;
+    /// let rv = tmpl
+    ///     .eval_to_module(context!(name => "John"))?
+    ///     .render_block("hi")?;
+    /// println!("{}", rv);
+    /// # Ok(()) }
+    /// ```
+    ///
+    /// Note that rendering a block is a stateful operation.  If an error
+    /// is returned the module has to be re-created as the internal state
+    /// can end up corrupted.
+    #[cfg(feature = "multi_template")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
+    pub fn render_block(&mut self, block: &str) -> Result<String, Error> {
+        let mut buf = String::new();
+        Vm::new(self.state.env)
+            .call_block(block, &mut self.state, &mut Output::with_string(&mut buf))
+            .map(|_| buf)
+    }
+
+    /// Renders a block with the given name into a [`io::Write`].
+    ///
+    /// For details see [`render_block`](Self::render_block).
+    #[cfg(feature = "multi_template")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "multi_template")))]
+    pub fn render_block_to_write<W>(&mut self, block: &str, w: W) -> Result<(), Error>
+    where
+        W: io::Write,
+    {
+        let mut wrapper = WriteWrapper { w, err: None };
+        Vm::new(self.state.env)
+            .call_block(
+                block,
+                &mut self.state,
+                &mut Output::with_write(&mut wrapper),
+            )
+            .map(|_| ())
+            .map_err(|err| wrapper.take_err(err))
+    }
+
+    /// Looks up a global macro and invokes it.
+    ///
+    /// This looks up a value like [`get_global`](Self::get_global) does and
+    /// invokes it.  It's not possible to call values directly under normal
+    /// circumstances as the state is not available that is necessary to
+    /// trigger a call.
+    #[cfg(feature = "macros")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
+    pub fn invoke_macro(&self, name: &str, args: &[Value]) -> Result<String, Error> {
+        let f = ok!(self
+            .get_global(name)
+            .ok_or_else(|| Error::new(ErrorKind::UnknownFunction, "macro not found")));
+        f.call(&self.state, args).map(Into::into)
+    }
+
+    /// Looks up a globally set variable in the template scope.
+    pub fn get_global(&self, name: &str) -> Option<Value> {
+        self.state.ctx.current_locals().get(name).cloned()
+    }
+}
+
 /// Represents a compiled template in memory.
 pub struct CompiledTemplate<'source> {
     /// The root instructions.
diff --git a/minijinja/src/vm/context.rs b/minijinja/src/vm/context.rs
index 2c0825b1..854de0ae 100644
--- a/minijinja/src/vm/context.rs
+++ b/minijinja/src/vm/context.rs
@@ -290,8 +290,14 @@ impl<'env> Context<'env> {
 
     /// Returns the current locals.
     #[track_caller]
+    pub fn current_locals(&self) -> &Locals<'env> {
+        &self.stack.last().unwrap().locals
+    }
+
+    /// Returns the current locals mutably.
+    #[track_caller]
     #[cfg(feature = "multi_template")]
-    pub fn current_locals(&mut self) -> &mut Locals<'env> {
+    pub fn current_locals_mut(&mut self) -> &mut Locals<'env> {
         &mut self.stack.last_mut().unwrap().locals
     }
 
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index bb5c2f93..c6b2cffd 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -43,9 +43,9 @@ pub struct Vm<'env> {
     env: &'env Environment<'env>,
 }
 
-fn prepare_blocks<'env, 'vm>(
-    blocks: &'vm BTreeMap<&'env str, Instructions<'env>>,
-) -> BTreeMap<&'env str, BlockStack<'vm, 'env>> {
+fn prepare_blocks<'env, 'template>(
+    blocks: &'template BTreeMap<&'env str, Instructions<'env>>,
+) -> BTreeMap<&'env str, BlockStack<'template, 'env>> {
     blocks
         .iter()
         .map(|(name, instr)| (*name, BlockStack::new(instr)))
@@ -96,17 +96,15 @@ impl<'env> Vm<'env> {
         )
     }
 
-    #[cfg(feature = "multi_template")]
-    /// Evaluates the given inputs and block
-    pub fn eval_block(
+    /// Evaluates the template as module
+    pub fn eval_to_module<'template>(
         &self,
-        block: &str,
-        instructions: &Instructions<'env>,
+        instructions: &'template Instructions<'env>,
         root: Value,
-        blocks: &BTreeMap<&'env str, Instructions<'env>>,
+        blocks: &'template BTreeMap<&'env str, Instructions<'env>>,
         out: &mut Output,
         auto_escape: AutoEscape,
-    ) -> Result<Option<Value>, Error> {
+    ) -> Result<State<'template, 'env>, Error> {
         let _guard = value_optimization();
         let mut state = State::new(
             self.env,
@@ -115,10 +113,8 @@ impl<'env> Vm<'env> {
             instructions,
             prepare_blocks(blocks),
         );
-        out.begin_capture(CaptureMode::Discard);
         self.eval_state(&mut state, out)?;
-        out.end_capture(state.auto_escape);
-        self.call_block(block, &mut state, out)
+        Ok(state)
     }
 
     /// Evaluate a macro in a state.
@@ -638,7 +634,7 @@ impl<'env> Vm<'env> {
                 }
                 #[cfg(feature = "multi_template")]
                 Instruction::ExportLocals => {
-                    let locals = state.ctx.current_locals();
+                    let locals = state.ctx.current_locals_mut();
                     let mut module = value_map_with_capacity(locals.len());
                     for (key, value) in locals.iter() {
                         module.insert((*key).into(), value.clone());
@@ -831,15 +827,14 @@ impl<'env> Vm<'env> {
     }
 
     #[cfg(feature = "multi_template")]
-    fn call_block(
+    pub(crate) fn call_block(
         &self,
-        name: &'env str,
+        name: &str,
         state: &mut State<'_, 'env>,
         out: &mut Output,
     ) -> Result<Option<Value>, Error> {
-        let old_block = state.current_block;
-        state.current_block = Some(name);
-        if let Some(block_stack) = state.blocks.get(name) {
+        if let Some((name, block_stack)) = state.blocks.get_key_value(name) {
+            let old_block = mem::replace(&mut state.current_block, Some(name));
             let old_instructions =
                 mem::replace(&mut state.instructions, block_stack.instructions());
             state.ctx.push_frame(Frame::default())?;
diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs
index 6170d99b..5d0ea8ef 100644
--- a/minijinja/src/vm/state.rs
+++ b/minijinja/src/vm/state.rs
@@ -24,22 +24,22 @@ use crate::vm::fuel::FuelTracker;
 /// **Notes on lifetimes:** the state object exposes some of the internal
 /// lifetimes through the type.  You should always elide these lifetimes
 /// as there might be lifetimes added or removed between releases.
-pub struct State<'vm, 'env> {
+pub struct State<'template, 'env> {
     pub(crate) env: &'env Environment<'env>,
     pub(crate) ctx: Context<'env>,
     pub(crate) current_block: Option<&'env str>,
     pub(crate) auto_escape: AutoEscape,
-    pub(crate) instructions: &'vm Instructions<'env>,
-    pub(crate) blocks: BTreeMap<&'env str, BlockStack<'vm, 'env>>,
+    pub(crate) instructions: &'template Instructions<'env>,
+    pub(crate) blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
     #[allow(unused)]
     pub(crate) loaded_templates: BTreeSet<&'env str>,
     #[cfg(feature = "macros")]
-    pub(crate) macros: std::sync::Arc<Vec<(&'vm Instructions<'env>, usize)>>,
+    pub(crate) macros: std::sync::Arc<Vec<(&'template Instructions<'env>, usize)>>,
     #[cfg(feature = "fuel")]
     pub(crate) fuel_tracker: Option<std::sync::Arc<FuelTracker>>,
 }
 
-impl<'vm, 'env> fmt::Debug for State<'vm, 'env> {
+impl<'template, 'env> fmt::Debug for State<'template, 'env> {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         let mut ds = f.debug_struct("State");
         ds.field("name", &self.instructions.name());
@@ -51,15 +51,15 @@ impl<'vm, 'env> fmt::Debug for State<'vm, 'env> {
     }
 }
 
-impl<'vm, 'env> State<'vm, 'env> {
+impl<'template, 'env> State<'template, 'env> {
     /// Creates a new state.
     pub(crate) fn new(
         env: &'env Environment,
         ctx: Context<'env>,
         auto_escape: AutoEscape,
-        instructions: &'vm Instructions<'env>,
-        blocks: BTreeMap<&'env str, BlockStack<'vm, 'env>>,
-    ) -> State<'vm, 'env> {
+        instructions: &'template Instructions<'env>,
+        blocks: BTreeMap<&'env str, BlockStack<'template, 'env>>,
+    ) -> State<'template, 'env> {
         State {
             env,
             ctx,
@@ -75,6 +75,18 @@ impl<'vm, 'env> State<'vm, 'env> {
         }
     }
 
+    /// Creates an empty state for an environment.
+    #[allow(unused)]
+    pub(crate) fn new_for_env(env: &'env Environment) -> State<'env, 'env> {
+        State::new(
+            env,
+            Context::default(),
+            AutoEscape::None,
+            &crate::compiler::instructions::EMPTY_INSTRUCTIONS,
+            BTreeMap::new(),
+        )
+    }
+
     /// Returns a reference to the current environment.
     #[inline(always)]
     pub fn env(&self) -> &Environment<'_> {
@@ -110,17 +122,6 @@ impl<'vm, 'env> State<'vm, 'env> {
         self.ctx.load(self.env, name)
     }
 
-    #[cfg(any(test, feature = "testutils"))]
-    pub(crate) fn with_dummy<R, F: FnOnce(&State) -> R>(env: &'env Environment<'env>, f: F) -> R {
-        f(&State::new(
-            env,
-            Context::default(),
-            AutoEscape::None,
-            &Instructions::new("<unknown>", ""),
-            BTreeMap::new(),
-        ))
-    }
-
     #[cfg(feature = "debug")]
     pub(crate) fn make_debug_info(
         &self,
@@ -161,20 +162,20 @@ impl<'a> ArgType<'a> for &State<'_, '_> {
 
 /// Tracks a block and it's parents for super.
 #[derive(Default)]
-pub(crate) struct BlockStack<'vm, 'env> {
-    instructions: Vec<&'vm Instructions<'env>>,
+pub(crate) struct BlockStack<'template, 'env> {
+    instructions: Vec<&'template Instructions<'env>>,
     depth: usize,
 }
 
-impl<'vm, 'env> BlockStack<'vm, 'env> {
-    pub fn new(instructions: &'vm Instructions<'env>) -> BlockStack<'vm, 'env> {
+impl<'template, 'env> BlockStack<'template, 'env> {
+    pub fn new(instructions: &'template Instructions<'env>) -> BlockStack<'template, 'env> {
         BlockStack {
             instructions: vec![instructions],
             depth: 0,
         }
     }
 
-    pub fn instructions(&self) -> &'vm Instructions<'env> {
+    pub fn instructions(&self) -> &'template Instructions<'env> {
         self.instructions.get(self.depth).copied().unwrap()
     }
 
@@ -193,7 +194,7 @@ impl<'vm, 'env> BlockStack<'vm, 'env> {
     }
 
     #[cfg(feature = "multi_template")]
-    pub fn append_instructions(&mut self, instructions: &'vm Instructions<'env>) {
+    pub fn append_instructions(&mut self, instructions: &'template Instructions<'env>) {
         self.instructions.push(instructions);
     }
 }


Test patch:
diff --git a/minijinja/src/testutils.rs b/minijinja/src/testutils.rs
index 77e6e29d..7b8bd70e 100644
--- a/minijinja/src/testutils.rs
+++ b/minijinja/src/testutils.rs
@@ -51,7 +51,8 @@ use crate::State;
 pub fn format(env: &Environment, value: Value) -> Result<String, Error> {
     let mut rv = String::new();
     let mut out = Output::with_string(&mut rv);
-    State::with_dummy(env, |state| env.format(&value, state, &mut out)).map(|_| rv)
+    env.format(&value, &State::new_for_env(env), &mut out)
+        .map(|_| rv)
 }
 
 /// Invokes a filter of an environment.
@@ -64,10 +65,11 @@ pub fn format(env: &Environment, value: Value) -> Result<String, Error> {
 /// ```
 #[cfg_attr(docsrs, doc(cfg(feature = "testutils")))]
 pub fn apply_filter(env: &Environment, filter: &str, args: &[Value]) -> Result<Value, Error> {
-    State::with_dummy(env, |state| match env.get_filter(filter) {
-        Some(filter) => filter.apply_to(state, args),
+    let state = State::new_for_env(env);
+    match env.get_filter(filter) {
+        Some(filter) => filter.apply_to(&state, args),
         None => Err(Error::from(ErrorKind::UnknownFilter)),
-    })
+    }
 }
 
 /// Invokes a test of an environment.
@@ -80,10 +82,11 @@ pub fn apply_filter(env: &Environment, filter: &str, args: &[Value]) -> Result<V
 /// ```
 #[cfg_attr(docsrs, doc(cfg(feature = "testutils")))]
 pub fn perform_test(env: &Environment, test: &str, args: &[Value]) -> Result<bool, Error> {
-    State::with_dummy(env, |state| match env.get_test(test) {
-        Some(test) => test.perform(state, args),
+    let state = State::new_for_env(env);
+    match env.get_test(test) {
+        Some(test) => test.perform(&state, args),
         None => Err(Error::from(ErrorKind::UnknownTest)),
-    })
+    }
 }
 
 /// Invokes a global function.
@@ -96,8 +99,9 @@ pub fn perform_test(env: &Environment, test: &str, args: &[Value]) -> Result<boo
 /// ```
 #[cfg_attr(docsrs, doc(cfg(feature = "testutils")))]
 pub fn invoke_global(env: &Environment, func: &str, args: &[Value]) -> Result<Value, Error> {
-    State::with_dummy(env, |state| match env.globals.get(func) {
-        Some(func) => func.call(state, args),
+    let state = State::new_for_env(env);
+    match env.globals.get(func) {
+        Some(func) => func.call(&state, args),
         None => Err(Error::from(ErrorKind::UnknownFunction)),
-    })
+    }
 }
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index e301387c..44b0aaee 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -108,7 +108,10 @@ fn test_vm_block_fragments() {
         } else {
             let template = env.get_template(filename).unwrap();
 
-            match template.render_block("fragment", &ctx) {
+            match template
+                .eval_to_module(&ctx)
+                .and_then(|mut x| x.render_block("fragment"))
+            {
                 Ok(mut rendered) => {
                     rendered.push('\n');
                     rendered
@@ -447,8 +450,36 @@ fn test_block_fragments() {
     let tmpl = env.get_template("demo").unwrap();
 
     let rv_a = tmpl.render(()).unwrap();
-    let rv_b = tmpl.render_block("foo", ()).unwrap();
+    let rv_b = tmpl
+        .eval_to_module(())
+        .unwrap()
+        .render_block("foo")
+        .unwrap();
 
     assert_eq!(rv_a, "I am outside the fragmentfooSo am I!");
     assert_eq!(rv_b, "foo");
 }
+
+#[test]
+fn test_module() {
+    let mut env = Environment::new();
+    env.add_template(
+        "foo.html",
+        r#"
+        {% set global = variable * 2 %}
+        {% macro something() %}{{ global }}{% endmacro %}
+        {% block baz %}[{{ global }}]{% endblock %}
+    "#,
+    )
+    .unwrap();
+    let template = env.get_template("foo.html").unwrap();
+    let mut module = template
+        .eval_to_module(context! {
+            variable => 23
+        })
+        .unwrap();
+    assert_eq!(module.get_global("range"), None);
+    assert_eq!(module.get_global("global"), Some(Value::from(23 * 2)));
+    assert_eq!(module.invoke_macro("something", &[]).unwrap(), "46");
+    assert_eq!(module.render_block("baz").unwrap(), "[46]");
+}