Repository: mitsuhiko/minijinja
Pull request #110 (2022-09-05 22:23:27)

Refactor engine internals
This is a major refactor for the engine internals to break things up more logically and to avoid the previous excessive use of macros.


Patch:
diff --git a/minijinja/src/compiler.rs b/minijinja/src/compiler.rs
index d80a951..ee4e8db 100644
--- a/minijinja/src/compiler.rs
+++ b/minijinja/src/compiler.rs
@@ -175,81 +175,17 @@ impl<'source> Compiler<'source> {
                 }
             }
             ast::Stmt::EmitExpr(expr) => {
-                self.set_location_from_span(expr.span());
-
-                // detect {{ super() }} and {{ loop() }} as special instructions
-                if let ast::Expr::Call(call) = &expr.expr {
-                    if let ast::Expr::Var(var) = &call.expr {
-                        if var.id == "super" && call.args.is_empty() {
-                            self.add(Instruction::FastSuper);
-                            return Ok(());
-                        }
-                        if var.id == "loop" && call.args.len() == 1 {
-                            self.compile_expr(&call.args[0])?;
-                            self.add(Instruction::FastRecurse);
-                            return Ok(());
-                        }
-                    }
-                }
-
-                self.compile_expr(&expr.expr)?;
-                self.add(Instruction::Emit);
+                self.compile_emit_expr(expr)?;
             }
             ast::Stmt::EmitRaw(raw) => {
                 self.set_location_from_span(raw.span());
                 self.add(Instruction::EmitRaw(raw.raw));
             }
             ast::Stmt::ForLoop(for_loop) => {
-                self.set_location_from_span(for_loop.span());
-
-                if let Some(ref filter_expr) = for_loop.filter_expr {
-                    // filter expressions work like a nested for loop without
-                    // the special loop variable that append into a new list
-                    // just outside of the loop.
-                    self.add(Instruction::BuildList(0));
-                    self.compile_expr(&for_loop.iter)?;
-                    self.start_for_loop(false, false);
-                    self.add(Instruction::DupTop);
-                    self.compile_assignment(&for_loop.target)?;
-                    self.compile_expr(filter_expr)?;
-                    self.start_if();
-                    self.add(Instruction::ListAppend);
-                    self.start_else();
-                    self.add(Instruction::DiscardTop);
-                    self.end_if();
-                    self.end_for_loop(false);
-                } else {
-                    self.compile_expr(&for_loop.iter)?;
-                }
-
-                self.start_for_loop(true, for_loop.recursive);
-                self.compile_assignment(&for_loop.target)?;
-                for node in &for_loop.body {
-                    self.compile_stmt(node)?;
-                }
-                self.end_for_loop(!for_loop.else_body.is_empty());
-                if !for_loop.else_body.is_empty() {
-                    self.start_if();
-                    for node in &for_loop.else_body {
-                        self.compile_stmt(node)?;
-                    }
-                    self.end_if();
-                }
+                self.compile_for_loop(for_loop)?;
             }
             ast::Stmt::IfCond(if_cond) => {
-                self.set_location_from_span(if_cond.span());
-                self.compile_expr(&if_cond.expr)?;
-                self.start_if();
-                for node in &if_cond.true_body {
-                    self.compile_stmt(node)?;
-                }
-                if !if_cond.false_body.is_empty() {
-                    self.start_else();
-                    for node in &if_cond.false_body {
-                        self.compile_stmt(node)?;
-                    }
-                }
-                self.end_if();
+                self.compile_if_stmt(if_cond)?;
             }
             ast::Stmt::WithBlock(with_block) => {
                 self.set_location_from_span(with_block.span());
@@ -281,18 +217,7 @@ impl<'source> Compiler<'source> {
                 self.compile_assignment(&set_block.target)?;
             }
             ast::Stmt::Block(block) => {
-                self.set_location_from_span(block.span());
-                let mut sub_compiler =
-                    Compiler::new(self.instructions.name(), self.instructions.source());
-                sub_compiler.set_line(self.current_line);
-                for node in &block.body {
-                    sub_compiler.compile_stmt(node)?;
-                }
-
-                let (instructions, blocks) = sub_compiler.finish();
-                self.blocks.extend(blocks.into_iter());
-                self.blocks.insert(block.name, instructions);
-                self.add(Instruction::CallBlock(block.name));
+                self.compile_block(block)?;
             }
             ast::Stmt::Extends(extends) => {
                 self.set_location_from_span(extends.span());
@@ -327,6 +252,103 @@ impl<'source> Compiler<'source> {
         Ok(())
     }
 
+    fn compile_block(&mut self, block: &ast::Spanned<ast::Block<'source>>) -> Result<(), Error> {
+        self.set_location_from_span(block.span());
+        let mut sub_compiler = Compiler::new(self.instructions.name(), self.instructions.source());
+        sub_compiler.set_line(self.current_line);
+        for node in &block.body {
+            sub_compiler.compile_stmt(node)?;
+        }
+        let (instructions, blocks) = sub_compiler.finish();
+        self.blocks.extend(blocks.into_iter());
+        self.blocks.insert(block.name, instructions);
+        self.add(Instruction::CallBlock(block.name));
+        Ok(())
+    }
+
+    fn compile_if_stmt(
+        &mut self,
+        if_cond: &ast::Spanned<ast::IfCond<'source>>,
+    ) -> Result<(), Error> {
+        self.set_location_from_span(if_cond.span());
+        self.compile_expr(&if_cond.expr)?;
+        self.start_if();
+        for node in &if_cond.true_body {
+            self.compile_stmt(node)?;
+        }
+        if !if_cond.false_body.is_empty() {
+            self.start_else();
+            for node in &if_cond.false_body {
+                self.compile_stmt(node)?;
+            }
+        }
+        self.end_if();
+        Ok(())
+    }
+
+    fn compile_emit_expr(
+        &mut self,
+        expr: &ast::Spanned<ast::EmitExpr<'source>>,
+    ) -> Result<(), Error> {
+        self.set_location_from_span(expr.span());
+        if let ast::Expr::Call(call) = &expr.expr {
+            if let ast::Expr::Var(var) = &call.expr {
+                if var.id == "super" && call.args.is_empty() {
+                    self.add(Instruction::FastSuper);
+                    return Ok(());
+                }
+                if var.id == "loop" && call.args.len() == 1 {
+                    self.compile_expr(&call.args[0])?;
+                    self.add(Instruction::FastRecurse);
+                    return Ok(());
+                }
+            }
+        }
+        self.compile_expr(&expr.expr)?;
+        self.add(Instruction::Emit);
+        Ok(())
+    }
+
+    fn compile_for_loop(
+        &mut self,
+        for_loop: &ast::Spanned<ast::ForLoop<'source>>,
+    ) -> Result<(), Error> {
+        self.set_location_from_span(for_loop.span());
+        if let Some(ref filter_expr) = for_loop.filter_expr {
+            // filter expressions work like a nested for loop without
+            // the special loop variable that append into a new list
+            // just outside of the loop.
+            self.add(Instruction::BuildList(0));
+            self.compile_expr(&for_loop.iter)?;
+            self.start_for_loop(false, false);
+            self.add(Instruction::DupTop);
+            self.compile_assignment(&for_loop.target)?;
+            self.compile_expr(filter_expr)?;
+            self.start_if();
+            self.add(Instruction::ListAppend);
+            self.start_else();
+            self.add(Instruction::DiscardTop);
+            self.end_if();
+            self.end_for_loop(false);
+        } else {
+            self.compile_expr(&for_loop.iter)?;
+        }
+        self.start_for_loop(true, for_loop.recursive);
+        self.compile_assignment(&for_loop.target)?;
+        for node in &for_loop.body {
+            self.compile_stmt(node)?;
+        }
+        self.end_for_loop(!for_loop.else_body.is_empty());
+        if !for_loop.else_body.is_empty() {
+            self.start_if();
+            for node in &for_loop.else_body {
+                self.compile_stmt(node)?;
+            }
+            self.end_if();
+        };
+        Ok(())
+    }
+
     /// Compiles an assignment expression.
     pub fn compile_assignment(&mut self, expr: &ast::Expr<'source>) -> Result<(), Error> {
         match expr {
@@ -366,35 +388,7 @@ impl<'source> Compiler<'source> {
                 });
             }
             ast::Expr::BinOp(c) => {
-                self.set_location_from_span(c.span());
-                let instr = match c.op {
-                    ast::BinOpKind::Eq => Instruction::Eq,
-                    ast::BinOpKind::Ne => Instruction::Ne,
-                    ast::BinOpKind::Lt => Instruction::Lt,
-                    ast::BinOpKind::Lte => Instruction::Lte,
-                    ast::BinOpKind::Gt => Instruction::Gt,
-                    ast::BinOpKind::Gte => Instruction::Gte,
-                    ast::BinOpKind::ScAnd | ast::BinOpKind::ScOr => {
-                        self.start_sc_bool();
-                        self.compile_expr(&c.left)?;
-                        self.sc_bool(matches!(c.op, ast::BinOpKind::ScAnd));
-                        self.compile_expr(&c.right)?;
-                        self.end_sc_bool();
-                        return Ok(());
-                    }
-                    ast::BinOpKind::Add => Instruction::Add,
-                    ast::BinOpKind::Sub => Instruction::Sub,
-                    ast::BinOpKind::Mul => Instruction::Mul,
-                    ast::BinOpKind::Div => Instruction::Div,
-                    ast::BinOpKind::FloorDiv => Instruction::IntDiv,
-                    ast::BinOpKind::Rem => Instruction::Rem,
-                    ast::BinOpKind::Pow => Instruction::Pow,
-                    ast::BinOpKind::Concat => Instruction::StringConcat,
-                    ast::BinOpKind::In => Instruction::In,
-                };
-                self.compile_expr(&c.left)?;
-                self.compile_expr(&c.right)?;
-                self.add(instr);
+                self.compile_bin_op(c)?;
             }
             ast::Expr::IfExpr(i) => {
                 self.set_location_from_span(i.span());
@@ -441,33 +435,7 @@ impl<'source> Compiler<'source> {
                 self.add(Instruction::GetItem);
             }
             ast::Expr::Call(c) => {
-                self.set_location_from_span(c.span());
-                match c.identify_call() {
-                    ast::CallType::Function(name) => {
-                        for arg in &c.args {
-                            self.compile_expr(arg)?;
-                        }
-                        self.add(Instruction::BuildList(c.args.len()));
-                        self.add(Instruction::CallFunction(name));
-                    }
-                    ast::CallType::Block(name) => {
-                        self.add(Instruction::BeginCapture);
-                        self.add(Instruction::CallBlock(name));
-                        self.add(Instruction::EndCapture);
-                    }
-                    ast::CallType::Method(expr, name) => {
-                        self.compile_expr(expr)?;
-                        for arg in &c.args {
-                            self.compile_expr(arg)?;
-                        }
-                        self.add(Instruction::BuildList(c.args.len()));
-                        self.add(Instruction::CallMethod(name));
-                    }
-                    ast::CallType::Object(expr) => {
-                        self.compile_expr(expr)?;
-                        self.add(Instruction::CallObject);
-                    }
-                }
+                self.compile_call(c)?;
             }
             ast::Expr::List(l) => {
                 self.set_location_from_span(l.span());
@@ -489,6 +457,70 @@ impl<'source> Compiler<'source> {
         Ok(())
     }
 
+    fn compile_call(&mut self, c: &ast::Spanned<ast::Call<'source>>) -> Result<(), Error> {
+        self.set_location_from_span(c.span());
+        match c.identify_call() {
+            ast::CallType::Function(name) => {
+                for arg in &c.args {
+                    self.compile_expr(arg)?;
+                }
+                self.add(Instruction::BuildList(c.args.len()));
+                self.add(Instruction::CallFunction(name));
+            }
+            ast::CallType::Block(name) => {
+                self.add(Instruction::BeginCapture);
+                self.add(Instruction::CallBlock(name));
+                self.add(Instruction::EndCapture);
+            }
+            ast::CallType::Method(expr, name) => {
+                self.compile_expr(expr)?;
+                for arg in &c.args {
+                    self.compile_expr(arg)?;
+                }
+                self.add(Instruction::BuildList(c.args.len()));
+                self.add(Instruction::CallMethod(name));
+            }
+            ast::CallType::Object(expr) => {
+                self.compile_expr(expr)?;
+                self.add(Instruction::CallObject);
+            }
+        };
+        Ok(())
+    }
+
+    fn compile_bin_op(&mut self, c: &ast::Spanned<ast::BinOp<'source>>) -> Result<(), Error> {
+        self.set_location_from_span(c.span());
+        let instr = match c.op {
+            ast::BinOpKind::Eq => Instruction::Eq,
+            ast::BinOpKind::Ne => Instruction::Ne,
+            ast::BinOpKind::Lt => Instruction::Lt,
+            ast::BinOpKind::Lte => Instruction::Lte,
+            ast::BinOpKind::Gt => Instruction::Gt,
+            ast::BinOpKind::Gte => Instruction::Gte,
+            ast::BinOpKind::ScAnd | ast::BinOpKind::ScOr => {
+                self.start_sc_bool();
+                self.compile_expr(&c.left)?;
+                self.sc_bool(matches!(c.op, ast::BinOpKind::ScAnd));
+                self.compile_expr(&c.right)?;
+                self.end_sc_bool();
+                return Ok(());
+            }
+            ast::BinOpKind::Add => Instruction::Add,
+            ast::BinOpKind::Sub => Instruction::Sub,
+            ast::BinOpKind::Mul => Instruction::Mul,
+            ast::BinOpKind::Div => Instruction::Div,
+            ast::BinOpKind::FloorDiv => Instruction::IntDiv,
+            ast::BinOpKind::Rem => Instruction::Rem,
+            ast::BinOpKind::Pow => Instruction::Pow,
+            ast::BinOpKind::Concat => Instruction::StringConcat,
+            ast::BinOpKind::In => Instruction::In,
+        };
+        self.compile_expr(&c.left)?;
+        self.compile_expr(&c.right)?;
+        self.add(instr);
+        Ok(())
+    }
+
     /// Converts the compiler into the instructions.
     pub fn finish(
         self,
diff --git a/minijinja/src/defaults.rs b/minijinja/src/defaults.rs
new file mode 100644
index 0000000..cf0e506
--- /dev/null
+++ b/minijinja/src/defaults.rs
@@ -0,0 +1,155 @@
+use std::collections::BTreeMap;
+use std::fmt;
+
+use crate::error::{Error, ErrorKind};
+use crate::filters::{self, BoxedFilter};
+use crate::output::Output;
+use crate::tests::BoxedTest;
+use crate::utils::{AutoEscape, HtmlEscape};
+use crate::value::{Value, ValueKind};
+
+fn write_with_html_escaping(out: &mut Output, value: &Value) -> fmt::Result {
+    if matches!(
+        value.kind(),
+        ValueKind::Undefined | ValueKind::None | ValueKind::Bool | ValueKind::Number
+    ) {
+        write!(out, "{}", value)
+    } else if let Some(s) = value.as_str() {
+        write!(out, "{}", HtmlEscape(s))
+    } else {
+        write!(out, "{}", HtmlEscape(&value.to_string()))
+    }
+}
+
+pub(crate) fn no_auto_escape(_: &str) -> AutoEscape {
+    AutoEscape::None
+}
+
+/// The default logic for auto escaping based on file extension.
+///
+/// * [`Html`](AutoEscape::Html): `.html`, `.htm`, `.xml`
+#[cfg_attr(
+    feature = "json",
+    doc = r" * [`Json`](AutoEscape::Json): `.json`, `.js`, `.yml`"
+)]
+/// * [`None`](AutoEscape::None): _all others_
+pub fn default_auto_escape_callback(name: &str) -> AutoEscape {
+    match name.rsplit('.').next() {
+        Some("html") | Some("htm") | Some("xml") => AutoEscape::Html,
+        #[cfg(feature = "json")]
+        Some("json") | Some("js") | Some("yaml") | Some("yml") => AutoEscape::Json,
+        _ => AutoEscape::None,
+    }
+}
+
+/// The default formatter.
+///
+/// This formatter takes a value and directly writes it into the output format
+/// while honoring the requested auto escape format of the output.  If the
+/// value is already marked as safe, it's handled as if no auto escaping
+/// was requested.
+///
+/// * [`Html`](AutoEscape::Html): performs HTML escaping
+#[cfg_attr(
+    feature = "json",
+    doc = r" * [`Json`](AutoEscape::Json): serializes values to JSON"
+)]
+/// * [`None`](AutoEscape::None): no escaping
+/// * [`Custom(..)`](AutoEscape::Custom): results in an error
+pub fn escape_formatter(out: &mut Output, value: &Value) -> Result<(), Error> {
+    match (value.is_safe(), out.auto_escape()) {
+        // safe values do not get escaped
+        (true, _) | (_, AutoEscape::None) => write!(out, "{}", value)?,
+        (false, AutoEscape::Html) => write_with_html_escaping(out, value)?,
+        #[cfg(feature = "json")]
+        (false, AutoEscape::Json) => {
+            let value = serde_json::to_string(&value).map_err(|err| {
+                Error::new(ErrorKind::BadSerialization, "unable to format to JSON").with_source(err)
+            })?;
+            write!(out, "{}", value)?
+        }
+        (false, AutoEscape::Custom(name)) => {
+            return Err(Error::new(
+                ErrorKind::ImpossibleOperation,
+                format!(
+                    "Default formatter does not know how to format to custom format '{}'",
+                    name
+                ),
+            ));
+        }
+    }
+    Ok(())
+}
+
+pub(crate) fn get_builtin_filters() -> BTreeMap<&'static str, filters::BoxedFilter> {
+    let mut rv = BTreeMap::new();
+    rv.insert("safe", BoxedFilter::new(filters::safe));
+    rv.insert("escape", BoxedFilter::new(filters::escape));
+    rv.insert("e", BoxedFilter::new(filters::escape));
+    #[cfg(feature = "builtins")]
+    {
+        rv.insert("lower", BoxedFilter::new(filters::lower));
+        rv.insert("upper", BoxedFilter::new(filters::upper));
+        rv.insert("title", BoxedFilter::new(filters::title));
+        rv.insert("replace", BoxedFilter::new(filters::replace));
+        rv.insert("length", BoxedFilter::new(filters::length));
+        rv.insert("count", BoxedFilter::new(filters::length));
+        rv.insert("dictsort", BoxedFilter::new(filters::dictsort));
+        rv.insert("items", BoxedFilter::new(filters::items));
+        rv.insert("reverse", BoxedFilter::new(filters::reverse));
+        rv.insert("trim", BoxedFilter::new(filters::trim));
+        rv.insert("join", BoxedFilter::new(filters::join));
+        rv.insert("default", BoxedFilter::new(filters::default));
+        rv.insert("round", BoxedFilter::new(filters::round));
+        rv.insert("abs", BoxedFilter::new(filters::abs));
+        rv.insert("first", BoxedFilter::new(filters::first));
+        rv.insert("last", BoxedFilter::new(filters::last));
+        rv.insert("d", BoxedFilter::new(filters::default));
+        rv.insert("list", BoxedFilter::new(filters::list));
+        rv.insert("bool", BoxedFilter::new(filters::bool));
+        rv.insert("batch", BoxedFilter::new(filters::batch));
+        rv.insert("slice", BoxedFilter::new(filters::slice));
+        #[cfg(feature = "json")]
+        {
+            rv.insert("tojson", BoxedFilter::new(filters::tojson));
+        }
+        #[cfg(feature = "urlencode")]
+        {
+            rv.insert("urlencode", BoxedFilter::new(filters::urlencode));
+        }
+    }
+    rv
+}
+
+pub(crate) fn get_builtin_tests() -> BTreeMap<&'static str, BoxedTest> {
+    #[allow(unused_mut)]
+    let mut rv = BTreeMap::new();
+    #[cfg(feature = "builtins")]
+    {
+        use crate::tests;
+        rv.insert("odd", BoxedTest::new(tests::is_odd));
+        rv.insert("even", BoxedTest::new(tests::is_even));
+        rv.insert("undefined", BoxedTest::new(tests::is_undefined));
+        rv.insert("defined", BoxedTest::new(tests::is_defined));
+        rv.insert("number", BoxedTest::new(tests::is_number));
+        rv.insert("string", BoxedTest::new(tests::is_string));
+        rv.insert("sequence", BoxedTest::new(tests::is_sequence));
+        rv.insert("mapping", BoxedTest::new(tests::is_mapping));
+        rv.insert("startingwith", BoxedTest::new(tests::is_startingwith));
+        rv.insert("endingwith", BoxedTest::new(tests::is_endingwith));
+    }
+    rv
+}
+
+pub(crate) fn get_globals() -> BTreeMap<&'static str, Value> {
+    #[allow(unused_mut)]
+    let mut rv = BTreeMap::new();
+    #[cfg(feature = "builtins")]
+    {
+        use crate::functions::{self, BoxedFunction};
+        rv.insert("range", BoxedFunction::new(functions::range).to_value());
+        rv.insert("dict", BoxedFunction::new(functions::dict).to_value());
+        rv.insert("debug", BoxedFunction::new(functions::debug).to_value());
+    }
+    rv
+}
diff --git a/minijinja/src/environment.rs b/minijinja/src/environment.rs
index 637a816..5a128ec 100644
--- a/minijinja/src/environment.rs
+++ b/minijinja/src/environment.rs
@@ -5,174 +5,15 @@ use std::sync::Arc;
 use serde::Serialize;
 
 use crate::compiler::Compiler;
-use crate::error::Error;
-use crate::instructions::Instructions;
+use crate::error::{attach_basic_debug_info, Error};
+use crate::expression::Expression;
 use crate::output::Output;
-use crate::parser::{parse, parse_expr};
-use crate::utils::{AutoEscape, BTreeMapKeysDebug, HtmlEscape};
-use crate::value::{ArgType, FunctionArgs, FunctionResult, Value, ValueKind};
+use crate::parser::parse_expr;
+use crate::template::{CompiledTemplate, Template};
+use crate::utils::{AutoEscape, BTreeMapKeysDebug};
+use crate::value::{ArgType, FunctionArgs, FunctionResult, Value};
 use crate::vm::Vm;
-use crate::{filters, functions, tests, ErrorKind};
-
-#[cfg(test)]
-use similar_asserts::assert_eq;
-
-/// Represents a handle to a template.
-///
-/// Templates are stored in the [`Environment`] as bytecode instructions.  With the
-/// [`Environment::get_template`] method that is looked up and returned in form of
-/// this handle.  Such a template can be cheaply copied as it only holds references.
-///
-/// To render the [`render`](Template::render) method can be used.
-#[derive(Copy, Clone)]
-pub struct Template<'env> {
-    env: &'env Environment<'env>,
-    compiled: &'env CompiledTemplate<'env>,
-    initial_auto_escape: AutoEscape,
-}
-
-impl<'env> fmt::Debug for Template<'env> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut ds = f.debug_struct("Template");
-        ds.field("name", &self.name());
-        #[cfg(feature = "internal_debug")]
-        {
-            ds.field("instructions", &self.compiled.instructions);
-            ds.field("blocks", &self.compiled.blocks);
-        }
-        ds.field("initial_auto_escape", &self.initial_auto_escape);
-        ds.finish()
-    }
-}
-
-/// Represents a compiled template in memory.
-pub(crate) struct CompiledTemplate<'source> {
-    instructions: Instructions<'source>,
-    blocks: BTreeMap<&'source str, Instructions<'source>>,
-}
-
-impl<'source> CompiledTemplate<'source> {
-    fn eval(
-        &self,
-        env: &Environment,
-        root: Value,
-        out: &mut Output,
-    ) -> Result<Option<Value>, Error> {
-        let vm = Vm::new(env);
-        let blocks = &self.blocks;
-        vm.eval(&self.instructions, root, blocks, out)
-    }
-}
-
-impl<'env> fmt::Debug for CompiledTemplate<'env> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut ds = f.debug_struct("CompiledTemplate");
-        #[cfg(feature = "internal_debug")]
-        {
-            ds.field("instructions", &self.instructions);
-            ds.field("blocks", &self.blocks);
-        }
-        ds.finish()
-    }
-}
-
-fn attach_basic_debug_info<T>(rv: Result<T, Error>, source: &str) -> Result<T, Error> {
-    #[cfg(feature = "debug")]
-    {
-        match rv {
-            Ok(rv) => Ok(rv),
-            Err(mut err) => {
-                err.debug_info = Some(crate::error::DebugInfo {
-                    template_source: Some(source.to_string()),
-                    ..Default::default()
-                });
-                Err(err)
-            }
-        }
-    }
-    #[cfg(not(feature = "debug"))]
-    {
-        let _source = source;
-        rv
-    }
-}
-
-impl<'source> CompiledTemplate<'source> {
-    pub(crate) fn from_name_and_source(
-        name: &'source str,
-        source: &'source str,
-    ) -> Result<CompiledTemplate<'source>, Error> {
-        attach_basic_debug_info(Self::_from_name_and_source_impl(name, source), source)
-    }
-
-    fn _from_name_and_source_impl(
-        name: &'source str,
-        source: &'source str,
-    ) -> Result<CompiledTemplate<'source>, Error> {
-        let ast = parse(source, name)?;
-        let mut compiler = Compiler::new(name, source);
-        compiler.compile_stmt(&ast)?;
-        let (instructions, blocks) = compiler.finish();
-        Ok(CompiledTemplate {
-            blocks,
-            instructions,
-        })
-    }
-}
-
-impl<'env> Template<'env> {
-    /// Returns the name of the template.
-    pub fn name(&self) -> &str {
-        self.compiled.instructions.name()
-    }
-
-    /// Returns the source code of the template.
-    pub fn source(&self) -> &str {
-        self.compiled.instructions.source()
-    }
-
-    /// Renders the template into a string.
-    ///
-    /// The provided value is used as the initial context for the template.  It
-    /// can be any object that implements [`Serialize`](serde::Serialize).  You
-    /// can eiher create your own struct and derive `Serialize` for it or the
-    /// [`context!`](crate::context) macro can be used to create an ad-hoc context.
-    ///
-    /// ```
-    /// # use minijinja::{Environment, context};
-    /// # let mut env = Environment::new();
-    /// # env.add_template("hello", "Hello {{ name }}!").unwrap();
-    /// let tmpl = env.get_template("hello").unwrap();
-    /// println!("{}", tmpl.render(context!(name => "John")).unwrap());
-    /// ```
-    pub fn render<S: Serialize>(&self, ctx: S) -> Result<String, Error> {
-        // reduce total amount of code faling under mono morphization into
-        // this function, and share the rest in _eval.
-        self._render(Value::from_serializable(&ctx))
-    }
-
-    fn _render(&self, root: Value) -> Result<String, Error> {
-        let mut output = String::new();
-        let mut formatter = Output::string(&mut output, self.initial_auto_escape);
-        self.compiled.eval(self.env, root, &mut formatter)?;
-        Ok(output)
-    }
-
-    /// Returns the root instructions.
-    pub(crate) fn instructions(&self) -> &'env Instructions<'env> {
-        &self.compiled.instructions
-    }
-
-    /// Returns the blocks.
-    pub(crate) fn blocks(&self) -> &'env BTreeMap<&'env str, Instructions<'env>> {
-        &self.compiled.blocks
-    }
-
-    /// Returns the initial auto escape setting.
-    pub(crate) fn initial_auto_escape(&self) -> AutoEscape {
-        self.initial_auto_escape
-    }
-}
+use crate::{defaults, filters, functions, tests};
 
 type TemplateMap<'source> = BTreeMap<&'source str, Arc<CompiledTemplate<'source>>>;
 
@@ -247,61 +88,6 @@ impl<'source> fmt::Debug for Environment<'source> {
     }
 }
 
-/// A handle to a compiled expression.
-///
-/// An expression is created via the
-/// [`compile_expression`](Environment::compile_expression) method.  It provides
-/// a method to evaluate the expression and return the result as value object.
-/// This for instance can be used to evaluate simple expressions from user
-/// provided input to implement features such as dynamic filtering.
-///
-/// This is usually best paired with [`context`](crate::context!) to pass
-/// a single value to it.
-///
-/// # Example
-///
-/// ```rust
-/// # use minijinja::{Environment, context};
-/// let env = Environment::new();
-/// let expr = env.compile_expression("number > 10 and number < 20").unwrap();
-/// let rv = expr.eval(context!(number => 15)).unwrap();
-/// assert!(rv.is_true());
-/// ```
-pub struct Expression<'env, 'source> {
-    env: &'env Environment<'source>,
-    instructions: Instructions<'source>,
-}
-
-impl<'env, 'source> fmt::Debug for Expression<'env, 'source> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        f.debug_struct("Expression")
-            .field("env", &self.env)
-            .finish()
-    }
-}
-
-impl<'env, 'source> Expression<'env, 'source> {
-    /// Evaluates the expression with some context.
-    ///
-    /// The result of the expression is returned as [`Value`].
-    pub fn eval<S: Serialize>(&self, ctx: S) -> Result<Value, Error> {
-        // reduce total amount of code faling under mono morphization into
-        // this function, and share the rest in _eval.
-        self._eval(Value::from_serializable(&ctx))
-    }
-
-    fn _eval(&self, root: Value) -> Result<Value, Error> {
-        Ok(Vm::new(self.env)
-            .eval(
-                &self.instructions,
-                root,
-                &BTreeMap::new(),
-                &mut Output::null(),
-            )?
-            .unwrap())
-    }
-}
-
 impl<'source> Environment<'source> {
     /// Creates a new environment with sensible defaults.
     ///
@@ -312,11 +98,11 @@ impl<'source> Environment<'source> {
     pub fn new() -> Environment<'source> {
         Environment {
             templates: Source::Borrowed(Default::default()),
-            filters: filters::get_builtin_filters(),
-            tests: tests::get_builtin_tests(),
-            globals: functions::get_globals(),
-            default_auto_escape: Arc::new(default_auto_escape_callback),
-            formatter: Arc::new(escape_formatter),
+            filters: defaults::get_builtin_filters(),
+            tests: defaults::get_builtin_tests(),
+            globals: defaults::get_globals(),
+            default_auto_escape: Arc::new(defaults::default_auto_escape_callback),
+            formatter: Arc::new(defaults::escape_formatter),
             #[cfg(feature = "debug")]
             debug: cfg!(debug_assertions),
         }
@@ -332,8 +118,8 @@ impl<'source> Environment<'source> {
             filters: Default::default(),
             tests: Default::default(),
             globals: Default::default(),
-            default_auto_escape: Arc::new(no_auto_escape),
-            formatter: Arc::new(escape_formatter),
+            default_auto_escape: Arc::new(defaults::no_auto_escape),
+            formatter: Arc::new(defaults::escape_formatter),
             #[cfg(feature = "debug")]
             debug: cfg!(debug_assertions),
         }
@@ -396,11 +182,11 @@ impl<'source> Environment<'source> {
             #[cfg(feature = "source")]
             Source::Owned(source) => source.get_compiled_template(name)?,
         };
-        Ok(Template {
-            env: self,
+        Ok(Template::new(
+            self,
             compiled,
-            initial_auto_escape: self.get_initial_auto_escape(name),
-        })
+            self.get_initial_auto_escape(name),
+        ))
     }
 
     /// Parses and renders a template from a string in one go.
@@ -423,10 +209,10 @@ impl<'source> Environment<'source> {
     fn _render_str(&self, source: &str, root: Value) -> Result<String, Error> {
         let name = "<string>";
         let compiled = CompiledTemplate::from_name_and_source(name, source)?;
-        let mut output = String::new();
-        let mut formatter = Output::string(&mut output, self.get_initial_auto_escape(name));
-        compiled.eval(self, root, &mut formatter)?;
-        Ok(output)
+        let mut rv = String::new();
+        let mut out = Output::with_string(&mut rv, self.get_initial_auto_escape(name));
+        Vm::new(self).eval(&compiled.instructions, root, &compiled.blocks, &mut out)?;
+        Ok(rv)
     }
 
     /// Sets a new function to select the default auto escaping.
@@ -435,8 +221,8 @@ impl<'source> Environment<'source> {
     /// to determine the default auto escaping behavior.  The function is
     /// invoked with the name of the template and can make an initial auto
     /// escaping decision based on that.  The default implementation
-    /// ([`default_auto_escape_callback`]) turn on escaping depending on the file
-    /// extension.
+    /// ([`default_auto_escape_callback`](defaults::default_auto_escape_callback))
+    /// turn on escaping depending on the file extension.
     ///
     /// ```
     /// # use minijinja::{Environment, AutoEscape};
@@ -459,11 +245,12 @@ impl<'source> Environment<'source> {
     /// Sets a different formatter function.
     ///
     /// The formatter is invoked to format the given value into the provided
-    /// [`Output`].  The default implementation is the [`escape_formatter`].
+    /// [`Output`].  The default implementation is
+    /// [`escape_formatter`](defaults::escape_formatter).
     ///
     /// When implementing a custom formatter it depends on if auto escaping
     /// should be supported or not.  If auto escaping should be supported then
-    /// it's easiest to just wrap the default [`escape_formatter`].  The
+    /// it's easiest to just wrap the default formatter.  The
     /// following example swaps out `None` values before rendering for
     /// `Undefined` which renders as an empty string instead.
     ///
@@ -561,10 +348,7 @@ impl<'source> Environment<'source> {
         let mut compiler = Compiler::new("<expression>", expr);
         compiler.compile_expr(&ast)?;
         let (instructions, _) = compiler.finish();
-        Ok(Expression {
-            env: self,
-            instructions,
-        })
+        Ok(Expression::new(self, instructions))
     }
 
     /// Adds a new filter function.
@@ -658,140 +442,3 @@ impl<'source> Environment<'source> {
         (self.formatter)(out, value)
     }
 }
-
-pub fn write_with_html_escaping(out: &mut Output, value: &Value) -> fmt::Result {
-    if matches!(
-        value.kind(),
-        ValueKind::Undefined | ValueKind::None | ValueKind::Bool | ValueKind::Number
-    ) {
-        write!(out, "{}", value)
-    } else if let Some(s) = value.as_str() {
-        write!(out, "{}", HtmlEscape(s))
-    } else {
-        write!(out, "{}", HtmlEscape(&value.to_string()))
-    }
-}
-
-fn no_auto_escape(_: &str) -> AutoEscape {
-    AutoEscape::None
-}
-
-/// The default logic for auto escaping based on file extension.
-///
-/// * [`Html`](AutoEscape::Html): `.html`, `.htm`, `.xml`
-#[cfg_attr(
-    feature = "json",
-    doc = r" * [`Json`](AutoEscape::Json): `.json`, `.js`, `.yml`"
-)]
-/// * [`None`](AutoEscape::None): _all others_
-pub fn default_auto_escape_callback(name: &str) -> AutoEscape {
-    match name.rsplit('.').next() {
-        Some("html") | Some("htm") | Some("xml") => AutoEscape::Html,
-        #[cfg(feature = "json")]
-        Some("json") | Some("js") | Some("yaml") | Some("yml") => AutoEscape::Json,
-        _ => AutoEscape::None,
-    }
-}
-
-/// The default formatter.
-///
-/// This formatter takes a value and directly writes it into the output format
-/// while honoring the requested auto escape format of the output.  If the
-/// value is already marked as safe, it's handled as if no auto escaping
-/// was requested.
-///
-/// * [`Html`](AutoEscape::Html): performs HTML escaping
-#[cfg_attr(
-    feature = "json",
-    doc = r" * [`Json`](AutoEscape::Json): serializes values to JSON"
-)]
-/// * [`None`](AutoEscape::None): no escaping
-/// * [`Custom(..)`](AutoEscape::Custom): results in an error
-pub fn escape_formatter(out: &mut Output, value: &Value) -> Result<(), Error> {
-    match (value.is_safe(), out.auto_escape()) {
-        // safe values do not get escaped
-        (true, _) | (_, AutoEscape::None) => write!(out, "{}", value)?,
-        (false, AutoEscape::Html) => write_with_html_escaping(out, value)?,
-        #[cfg(feature = "json")]
-        (false, AutoEscape::Json) => {
-            let value = serde_json::to_string(&value).map_err(|err| {
-                Error::new(ErrorKind::BadSerialization, "unable to format to JSON").with_source(err)
-            })?;
-            write!(out, "{}", value)?
-        }
-        (false, AutoEscape::Custom(name)) => {
-            return Err(Error::new(
-                ErrorKind::ImpossibleOperation,
-                format!(
-                    "Default formatter does not know how to format to custom format '{}'",
-                    name
-                ),
-            ));
-        }
-    }
-    Ok(())
-}
-
-#[test]
-fn test_basic() {
-    use crate::value::Value;
-
-    let mut env = Environment::new();
-    env.add_template("test", "{% for x in seq %}[{{ x }}]{% endfor %}")
-        .unwrap();
-    let t = env.get_template("test").unwrap();
-    let mut ctx = BTreeMap::new();
-    ctx.insert("seq", Value::from((0..3).collect::<Vec<_>>()));
-    let rv = t.render(ctx).unwrap();
-    assert_eq!(rv, "[0][1][2]");
-}
-
-#[test]
-fn test_expression() {
-    let env = Environment::new();
-    let expr = env.compile_expression("foo + bar").unwrap();
-    let mut ctx = BTreeMap::new();
-    ctx.insert("foo", 42);
-    ctx.insert("bar", 23);
-    assert_eq!(expr.eval(&ctx).unwrap(), Value::from(65));
-}
-
-#[test]
-fn test_expression_lifetimes() {
-    let mut env = Environment::new();
-    let s = String::new();
-    env.add_template("test", &s).unwrap();
-    {
-        let x = String::from("1 + 1");
-        let expr = env.compile_expression(&x).unwrap();
-        assert_eq!(expr.eval(&()).unwrap().to_string(), "2");
-    }
-}
-
-#[test]
-fn test_clone() {
-    let mut env = Environment::new();
-    env.add_template("test", "a").unwrap();
-    let mut env2 = env.clone();
-    assert_eq!(env2.get_template("test").unwrap().render(&()).unwrap(), "a");
-    env2.add_template("test", "b").unwrap();
-    assert_eq!(env2.get_template("test").unwrap().render(&()).unwrap(), "b");
-    assert_eq!(env.get_template("test").unwrap().render(&()).unwrap(), "a");
-}
-
-#[test]
-fn test_globals() {
-    let mut env = Environment::new();
-    env.add_global("a", Value::from(42));
-    env.add_template("test", "{{ a }}").unwrap();
-    let tmpl = env.get_template("test").unwrap();
-    assert_eq!(tmpl.render(()).unwrap(), "42");
-}
-
-#[test]
-fn test_template_removal() {
-    let mut env = Environment::new();
-    env.add_template("test", "{{ a }}").unwrap();
-    env.remove_template("test");
-    assert!(env.get_template("test").is_err());
-}
diff --git a/minijinja/src/error.rs b/minijinja/src/error.rs
index 37e1e03..d0fb728 100644
--- a/minijinja/src/error.rs
+++ b/minijinja/src/error.rs
@@ -317,5 +317,26 @@ mod debug_info {
     }
 }
 
+pub fn attach_basic_debug_info<T>(rv: Result<T, Error>, source: &str) -> Result<T, Error> {
+    #[cfg(feature = "debug")]
+    {
+        match rv {
+            Ok(rv) => Ok(rv),
+            Err(mut err) => {
+                err.debug_info = Some(crate::error::DebugInfo {
+                    template_source: Some(source.to_string()),
+                    ..Default::default()
+                });
+                Err(err)
+            }
+        }
+    }
+    #[cfg(not(feature = "debug"))]
+    {
+        let _source = source;
+        rv
+    }
+}
+
 #[cfg(feature = "debug")]
 pub(crate) use self::debug_info::*;
diff --git a/minijinja/src/expression.rs b/minijinja/src/expression.rs
new file mode 100644
index 0000000..54c9a4b
--- /dev/null
+++ b/minijinja/src/expression.rs
@@ -0,0 +1,73 @@
+use std::collections::BTreeMap;
+use std::fmt;
+
+use serde::Serialize;
+
+use crate::environment::Environment;
+use crate::error::Error;
+use crate::instructions::Instructions;
+use crate::output::Output;
+use crate::value::Value;
+use crate::vm::Vm;
+
+/// A handle to a compiled expression.
+///
+/// An expression is created via the
+/// [`compile_expression`](Environment::compile_expression) method.  It provides
+/// a method to evaluate the expression and return the result as value object.
+/// This for instance can be used to evaluate simple expressions from user
+/// provided input to implement features such as dynamic filtering.
+///
+/// This is usually best paired with [`context`](crate::context!) to pass
+/// a single value to it.
+///
+/// # Example
+///
+/// ```rust
+/// # use minijinja::{Environment, context};
+/// let env = Environment::new();
+/// let expr = env.compile_expression("number > 10 and number < 20").unwrap();
+/// let rv = expr.eval(context!(number => 15)).unwrap();
+/// assert!(rv.is_true());
+/// ```
+pub struct Expression<'env, 'source> {
+    env: &'env Environment<'source>,
+    instructions: Instructions<'source>,
+}
+
+impl<'env, 'source> fmt::Debug for Expression<'env, 'source> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("Expression")
+            .field("env", &self.env)
+            .finish()
+    }
+}
+
+impl<'env, 'source> Expression<'env, 'source> {
+    pub fn new(
+        env: &'env Environment<'source>,
+        instructions: Instructions<'source>,
+    ) -> Expression<'env, 'source> {
+        Expression { env, instructions }
+    }
+
+    /// Evaluates the expression with some context.
+    ///
+    /// The result of the expression is returned as [`Value`].
+    pub fn eval<S: Serialize>(&self, ctx: S) -> Result<Value, Error> {
+        // reduce total amount of code faling under mono morphization into
+        // this function, and share the rest in _eval.
+        self._eval(Value::from_serializable(&ctx))
+    }
+
+    fn _eval(&self, root: Value) -> Result<Value, Error> {
+        Ok(Vm::new(self.env)
+            .eval(
+                &self.instructions,
+                root,
+                &BTreeMap::new(),
+                &mut Output::null(),
+            )?
+            .expect("expression evaluation did not leave value on stack"))
+    }
+}
diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs
index c208388..1e1d13a 100644
--- a/minijinja/src/filters.rs
+++ b/minijinja/src/filters.rs
@@ -55,10 +55,9 @@
 //! this module.  Note though that these functions are not to be
 //! called from Rust code as their exact interface (arguments and return types)
 //! might change from one MiniJinja version to another.
-use std::collections::BTreeMap;
 use std::sync::Arc;
 
-use crate::environment::escape_formatter;
+use crate::defaults::escape_formatter;
 use crate::error::Error;
 use crate::output::Output;
 use crate::value::{ArgType, FunctionArgs, FunctionResult, Value};
@@ -158,46 +157,6 @@ impl BoxedFilter {
     }
 }
 
-pub(crate) fn get_builtin_filters() -> BTreeMap<&'static str, BoxedFilter> {
-    let mut rv = BTreeMap::new();
-    rv.insert("safe", BoxedFilter::new(safe));
-    rv.insert("escape", BoxedFilter::new(escape));
-    rv.insert("e", BoxedFilter::new(escape));
-    #[cfg(feature = "builtins")]
-    {
-        rv.insert("lower", BoxedFilter::new(lower));
-        rv.insert("upper", BoxedFilter::new(upper));
-        rv.insert("title", BoxedFilter::new(title));
-        rv.insert("replace", BoxedFilter::new(replace));
-        rv.insert("length", BoxedFilter::new(length));
-        rv.insert("count", BoxedFilter::new(length));
-        rv.insert("dictsort", BoxedFilter::new(dictsort));
-        rv.insert("items", BoxedFilter::new(items));
-        rv.insert("reverse", BoxedFilter::new(reverse));
-        rv.insert("trim", BoxedFilter::new(trim));
-        rv.insert("join", BoxedFilter::new(join));
-        rv.insert("default", BoxedFilter::new(default));
-        rv.insert("round", BoxedFilter::new(round));
-        rv.insert("abs", BoxedFilter::new(abs));
-        rv.insert("first", BoxedFilter::new(first));
-        rv.insert("last", BoxedFilter::new(last));
-        rv.insert("d", BoxedFilter::new(default));
-        rv.insert("list", BoxedFilter::new(list));
-        rv.insert("bool", BoxedFilter::new(bool));
-        rv.insert("batch", BoxedFilter::new(batch));
-        rv.insert("slice", BoxedFilter::new(slice));
-        #[cfg(feature = "json")]
-        {
-            rv.insert("tojson", BoxedFilter::new(tojson));
-        }
-        #[cfg(feature = "urlencode")]
-        {
-            rv.insert("urlencode", BoxedFilter::new(urlencode));
-        }
-    }
-    rv
-}
-
 /// Marks a value as safe.  This converts it into a string.
 ///
 /// When a value is marked as safe, no further auto escaping will take place.
@@ -227,7 +186,7 @@ pub fn escape(state: &State, v: Value) -> Result<Value, Error> {
         other => other,
     };
     let mut out = String::new();
-    let mut formatter = Output::string(&mut out, auto_escape);
+    let mut formatter = Output::with_string(&mut out, auto_escape);
     escape_formatter(&mut formatter, &v)?;
     Ok(Value::from_safe_string(out))
 }
@@ -795,19 +754,14 @@ mod builtins {
         }
 
         let env = crate::Environment::new();
-        let state = State {
-            env: &env,
-            ctx: crate::vm::Context::default(),
-            current_block: None,
-            name: "<unknown>",
-            out: &mut Output::null(),
-        };
-        let bx = BoxedFilter::new(test);
-        assert_eq!(
-            bx.apply_to(&state, &Value::from(23), &[Value::from(42)][..])
-                .unwrap(),
-            Value::from(65)
-        );
+        State::with_dummy(&env, |state| {
+            let bx = BoxedFilter::new(test);
+            assert_eq!(
+                bx.apply_to(state, &Value::from(23), &[Value::from(42)][..])
+                    .unwrap(),
+                Value::from(65)
+            );
+        });
     }
 
     #[test]
@@ -821,38 +775,32 @@ mod builtins {
         }
 
         let env = crate::Environment::new();
-        let mut out = Output::null();
-        let state = State {
-            env: &env,
-            ctx: crate::vm::Context::default(),
-            current_block: None,
-            name: "<unknown>",
-            out: &mut out,
-        };
-        let bx = BoxedFilter::new(add);
-        assert_eq!(
-            bx.apply_to(&state, &Value::from(23), &[Value::from(42)][..])
+        State::with_dummy(&env, |state| {
+            let bx = BoxedFilter::new(add);
+            assert_eq!(
+                bx.apply_to(state, &Value::from(23), &[Value::from(42)][..])
+                    .unwrap(),
+                Value::from(65)
+            );
+            assert_eq!(
+                bx.apply_to(
+                    state,
+                    &Value::from(23),
+                    &[Value::from(42), Value::UNDEFINED][..]
+                )
                 .unwrap(),
-            Value::from(65)
-        );
-        assert_eq!(
-            bx.apply_to(
-                &state,
-                &Value::from(23),
-                &[Value::from(42), Value::UNDEFINED][..]
-            )
-            .unwrap(),
-            Value::from(65)
-        );
-        assert_eq!(
-            bx.apply_to(
-                &state,
-                &Value::from(23),
-                &[Value::from(42), Value::from(1)][..]
-            )
-            .unwrap(),
-            Value::from(66)
-        );
+                Value::from(65)
+            );
+            assert_eq!(
+                bx.apply_to(
+                    state,
+                    &Value::from(23),
+                    &[Value::from(42), Value::from(1)][..]
+                )
+                .unwrap(),
+                Value::from(66)
+            );
+        });
     }
 }
 
diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs
index 1f836ea..425b2c9 100644
--- a/minijinja/src/functions.rs
+++ b/minijinja/src/functions.rs
@@ -48,7 +48,6 @@
 //! this module.  Note though that these functions are not to be
 //! called from Rust code as their exact interface (arguments and return types)
 //! might change from one MiniJinja version to another.
-use std::collections::BTreeMap;
 use std::fmt;
 use std::sync::Arc;
 
@@ -179,22 +178,12 @@ impl Object for BoxedFunction {
     }
 }
 
-pub(crate) fn get_globals() -> BTreeMap<&'static str, Value> {
-    #[allow(unused_mut)]
-    let mut rv = BTreeMap::new();
-    #[cfg(feature = "builtins")]
-    {
-        rv.insert("range", BoxedFunction::new(range).to_value());
-        rv.insert("dict", BoxedFunction::new(dict).to_value());
-        rv.insert("debug", BoxedFunction::new(debug).to_value());
-    }
-    rv
-}
-
 #[cfg(feature = "builtins")]
 mod builtins {
     use super::*;
 
+    use std::collections::BTreeMap;
+
     use crate::error::ErrorKind;
     use crate::value::ValueKind;
 
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index 7b8c3b0..64f553f 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -129,13 +129,16 @@ mod key;
 
 mod ast;
 mod compiler;
+mod defaults;
 mod environment;
 mod error;
+mod expression;
 mod instructions;
 mod lexer;
 mod macros;
 mod output;
 mod parser;
+mod template;
 mod tokens;
 mod utils;
 mod vm;
@@ -149,11 +152,12 @@ pub mod value;
 #[cfg(feature = "source")]
 mod source;
 
-pub use self::environment::{
-    default_auto_escape_callback, escape_formatter, Environment, Expression, Template,
-};
+pub use self::defaults::{default_auto_escape_callback, escape_formatter};
+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::utils::{AutoEscape, HtmlEscape};
 
 #[cfg(feature = "source")]
@@ -183,6 +187,6 @@ pub mod machinery {
     use crate::{AutoEscape, Output};
 
     pub fn make_string_formatter(s: &mut String, auto_escape: AutoEscape) -> Output<'_> {
-        Output::string(s, auto_escape)
+        Output::with_string(s, auto_escape)
     }
 }
diff --git a/minijinja/src/output.rs b/minijinja/src/output.rs
index be3b997..e822602 100644
--- a/minijinja/src/output.rs
+++ b/minijinja/src/output.rs
@@ -1,4 +1,4 @@
-use std::fmt::{self, Arguments};
+use std::fmt;
 
 use crate::utils::AutoEscape;
 use crate::value::Value;
@@ -14,6 +14,11 @@ use crate::value::Value;
 /// escaping request.  The output itself however does not have functionality to
 /// escape by itself.
 pub struct Output<'a> {
+    // Note on type design: this type partially exists so that in the future non
+    // string outputs can be implemented.  Right now only the null writer exists
+    // as alternative which is also infallible.  If writing to io::Write should
+    // be added, then errors would need to be collected out of bounds as
+    // fmt::Error has no support for carrying actual IO errors.
     w: &'a mut (dyn fmt::Write + 'a),
     capture_stack: Vec<String>,
     pub(crate) auto_escape: AutoEscape,
@@ -22,7 +27,8 @@ pub struct Output<'a> {
 pub struct NullWriter;
 
 impl<'a> Output<'a> {
-    pub(crate) fn string(buf: &'a mut String, auto_escape: AutoEscape) -> Self {
+    /// Creates an output writing to a string.
+    pub(crate) fn with_string(buf: &'a mut String, auto_escape: AutoEscape) -> Self {
         Self {
             w: buf,
             capture_stack: Vec::new(),
@@ -30,6 +36,7 @@ impl<'a> Output<'a> {
         }
     }
 
+    /// Creates a null output that writes nowhere.
     pub(crate) fn null() -> Self {
         static mut NULL_WRITER: NullWriter = NullWriter;
         Self {
@@ -40,10 +47,12 @@ impl<'a> Output<'a> {
         }
     }
 
+    /// Begins capturing into a string.
     pub(crate) fn begin_capture(&mut self) {
         self.capture_stack.push(String::new());
     }
 
+    /// Ends capturing and returns the captured string as value.
     pub(crate) fn end_capture(&mut self) -> Value {
         let captured = self.capture_stack.pop().unwrap();
         if !matches!(self.auto_escape, AutoEscape::None) {
@@ -74,7 +83,7 @@ impl<'a> Output<'a> {
 
     /// Writes some formatted information into this instance.
     #[inline]
-    pub fn write_fmt(&mut self, a: Arguments<'_>) -> fmt::Result {
+    pub fn write_fmt(&mut self, a: fmt::Arguments<'_>) -> fmt::Result {
         self.target().write_fmt(a)
     }
 }
diff --git a/minijinja/src/source.rs b/minijinja/src/source.rs
index 79282d2..a861d88 100644
--- a/minijinja/src/source.rs
+++ b/minijinja/src/source.rs
@@ -7,8 +7,8 @@ use std::sync::Arc;
 use memo_map::MemoMap;
 use self_cell::self_cell;
 
-use crate::environment::CompiledTemplate;
 use crate::error::{Error, ErrorKind};
+use crate::template::CompiledTemplate;
 
 #[cfg(test)]
 use similar_asserts::assert_eq;
diff --git a/minijinja/src/template.rs b/minijinja/src/template.rs
new file mode 100644
index 0000000..7baf02e
--- /dev/null
+++ b/minijinja/src/template.rs
@@ -0,0 +1,154 @@
+use std::collections::BTreeMap;
+use std::fmt;
+
+use serde::Serialize;
+
+use crate::compiler::Compiler;
+use crate::environment::Environment;
+use crate::error::{attach_basic_debug_info, Error};
+use crate::instructions::Instructions;
+use crate::output::Output;
+use crate::parser::parse;
+use crate::utils::AutoEscape;
+use crate::value::Value;
+use crate::vm::Vm;
+
+/// Represents a handle to a template.
+///
+/// Templates are stored in the [`Environment`] as bytecode instructions.  With the
+/// [`Environment::get_template`] method that is looked up and returned in form of
+/// this handle.  Such a template can be cheaply copied as it only holds references.
+///
+/// To render the [`render`](Template::render) method can be used.
+#[derive(Copy, Clone)]
+pub struct Template<'env> {
+    env: &'env Environment<'env>,
+    compiled: &'env CompiledTemplate<'env>,
+    initial_auto_escape: AutoEscape,
+}
+
+impl<'env> fmt::Debug for Template<'env> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut ds = f.debug_struct("Template");
+        ds.field("name", &self.name());
+        #[cfg(feature = "internal_debug")]
+        {
+            ds.field("instructions", &self.compiled.instructions);
+            ds.field("blocks", &self.compiled.blocks);
+        }
+        ds.field("initial_auto_escape", &self.initial_auto_escape);
+        ds.finish()
+    }
+}
+
+impl<'env> Template<'env> {
+    pub(crate) fn new(
+        env: &'env Environment<'env>,
+        compiled: &'env CompiledTemplate<'env>,
+        initial_auto_escape: AutoEscape,
+    ) -> Template<'env> {
+        Template {
+            env,
+            compiled,
+            initial_auto_escape,
+        }
+    }
+
+    /// Returns the name of the template.
+    pub fn name(&self) -> &str {
+        self.compiled.instructions.name()
+    }
+
+    /// Returns the source code of the template.
+    pub fn source(&self) -> &str {
+        self.compiled.instructions.source()
+    }
+
+    /// Renders the template into a string.
+    ///
+    /// The provided value is used as the initial context for the template.  It
+    /// can be any object that implements [`Serialize`](serde::Serialize).  You
+    /// can eiher create your own struct and derive `Serialize` for it or the
+    /// [`context!`](crate::context) macro can be used to create an ad-hoc context.
+    ///
+    /// ```
+    /// # use minijinja::{Environment, context};
+    /// # let mut env = Environment::new();
+    /// # env.add_template("hello", "Hello {{ name }}!").unwrap();
+    /// let tmpl = env.get_template("hello").unwrap();
+    /// println!("{}", tmpl.render(context!(name => "John")).unwrap());
+    /// ```
+    pub fn render<S: Serialize>(&self, ctx: S) -> Result<String, Error> {
+        // reduce total amount of code faling under mono morphization into
+        // this function, and share the rest in _eval.
+        self._render(Value::from_serializable(&ctx))
+    }
+
+    fn _render(&self, root: Value) -> Result<String, Error> {
+        let mut rv = String::new();
+        let mut out = Output::with_string(&mut rv, self.initial_auto_escape);
+        Vm::new(self.env).eval(
+            &self.compiled.instructions,
+            root,
+            &self.compiled.blocks,
+            &mut out,
+        )?;
+        Ok(rv)
+    }
+
+    /// Returns the root instructions.
+    pub(crate) fn instructions(&self) -> &'env Instructions<'env> {
+        &self.compiled.instructions
+    }
+
+    /// Returns the blocks.
+    pub(crate) fn blocks(&self) -> &'env BTreeMap<&'env str, Instructions<'env>> {
+        &self.compiled.blocks
+    }
+
+    /// Returns the initial auto escape setting.
+    pub(crate) fn initial_auto_escape(&self) -> AutoEscape {
+        self.initial_auto_escape
+    }
+}
+
+/// Represents a compiled template in memory.
+pub(crate) struct CompiledTemplate<'source> {
+    pub instructions: Instructions<'source>,
+    pub blocks: BTreeMap<&'source str, Instructions<'source>>,
+}
+
+impl<'env> fmt::Debug for CompiledTemplate<'env> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut ds = f.debug_struct("CompiledTemplate");
+        #[cfg(feature = "internal_debug")]
+        {
+            ds.field("instructions", &self.instructions);
+            ds.field("blocks", &self.blocks);
+        }
+        ds.finish()
+    }
+}
+
+impl<'source> CompiledTemplate<'source> {
+    pub(crate) fn from_name_and_source(
+        name: &'source str,
+        source: &'source str,
+    ) -> Result<CompiledTemplate<'source>, Error> {
+        attach_basic_debug_info(Self::_from_name_and_source_impl(name, source), source)
+    }
+
+    fn _from_name_and_source_impl(
+        name: &'source str,
+        source: &'source str,
+    ) -> Result<CompiledTemplate<'source>, Error> {
+        let ast = parse(source, name)?;
+        let mut compiler = Compiler::new(name, source);
+        compiler.compile_stmt(&ast)?;
+        let (instructions, blocks) = compiler.finish();
+        Ok(CompiledTemplate {
+            blocks,
+            instructions,
+        })
+    }
+}
diff --git a/minijinja/src/vm.rs b/minijinja/src/vm.rs
deleted file mode 100644
index 5e407c6..0000000
--- a/minijinja/src/vm.rs
+++ /dev/null
@@ -1,1031 +0,0 @@
-use std::collections::{BTreeMap, HashSet};
-use std::fmt;
-use std::sync::atomic::{AtomicUsize, Ordering};
-use std::sync::{Arc, Mutex};
-
-use crate::environment::Environment;
-use crate::error::{Error, ErrorKind};
-use crate::instructions::{
-    Instruction, Instructions, LOOP_FLAG_RECURSIVE, LOOP_FLAG_WITH_LOOP_VAR,
-};
-use crate::key::Key;
-use crate::output::Output;
-use crate::value::{self, ops, Object, Value, ValueIterator, ValueRepr};
-use crate::AutoEscape;
-
-pub struct LoopState {
-    len: usize,
-    idx: AtomicUsize,
-    depth: usize,
-    last_changed_value: Mutex<Option<Vec<Value>>>,
-}
-
-impl fmt::Debug for LoopState {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut s = f.debug_struct("LoopState");
-        for attr in self.attributes() {
-            s.field(attr, &self.get_attr(attr).unwrap());
-        }
-        s.finish()
-    }
-}
-
-impl Object for LoopState {
-    fn attributes(&self) -> &[&str] {
-        &[
-            "index0",
-            "index",
-            "length",
-            "revindex",
-            "revindex0",
-            "first",
-            "last",
-            "depth",
-            "depth0",
-        ][..]
-    }
-
-    fn get_attr(&self, name: &str) -> Option<Value> {
-        let idx = self.idx.load(Ordering::Relaxed) as u64;
-        let len = self.len as u64;
-        match name {
-            "index0" => Some(Value::from(idx)),
-            "index" => Some(Value::from(idx + 1)),
-            "length" => Some(Value::from(len)),
-            "revindex" => Some(Value::from(len.saturating_sub(idx))),
-            "revindex0" => Some(Value::from(len.saturating_sub(idx).saturating_sub(1))),
-            "first" => Some(Value::from(idx == 0)),
-            "last" => Some(Value::from(len == 0 || idx == len - 1)),
-            "depth" => Some(Value::from(self.depth + 1)),
-            "depth0" => Some(Value::from(self.depth)),
-            _ => None,
-        }
-    }
-
-    fn call(&self, _state: &State, _args: &[Value]) -> Result<Value, Error> {
-        Err(Error::new(
-            ErrorKind::ImpossibleOperation,
-            "loop cannot be called if reassigned to different variable",
-        ))
-    }
-
-    fn call_method(&self, _state: &State, name: &str, args: &[Value]) -> Result<Value, Error> {
-        if name == "changed" {
-            let mut last_changed_value = self.last_changed_value.lock().unwrap();
-            let value = args.to_owned();
-            let changed = last_changed_value.as_ref() != Some(&value);
-            if changed {
-                *last_changed_value = Some(value);
-                Ok(Value::from(true))
-            } else {
-                Ok(Value::from(false))
-            }
-        } else if name == "cycle" {
-            let idx = self.idx.load(Ordering::Relaxed);
-            match args.get(idx % args.len()) {
-                Some(arg) => Ok(arg.clone()),
-                None => Ok(Value::UNDEFINED),
-            }
-        } else {
-            Err(Error::new(
-                ErrorKind::ImpossibleOperation,
-                format!("loop object has no method named {}", name),
-            ))
-        }
-    }
-}
-
-impl fmt::Display for LoopState {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(
-            f,
-            "<loop {}/{}>",
-            self.idx.load(Ordering::Relaxed),
-            self.len
-        )
-    }
-}
-
-type Locals<'env> = BTreeMap<&'env str, Value>;
-
-#[cfg_attr(feature = "internal_debug", derive(Debug))]
-pub struct Loop {
-    with_loop_var: bool,
-    recurse_jump_target: Option<usize>,
-    // if we're popping the frame, do we want to jump somewhere?  The
-    // first item is the target jump instruction, the second argument
-    // tells us if we need to end capturing.
-    current_recursion_jump: Option<(usize, bool)>,
-    iterator: ValueIterator,
-    controller: Arc<LoopState>,
-}
-
-#[cfg_attr(feature = "internal_debug", derive(Debug))]
-pub enum FrameBase<'env, 'vm> {
-    None,
-    Context(&'vm Context<'env, 'vm>),
-    Value(Value),
-}
-
-pub struct Frame<'env, 'vm> {
-    locals: Locals<'env>,
-    base: FrameBase<'env, 'vm>,
-    current_loop: Option<Loop>,
-}
-
-impl<'env, 'vm> Default for Frame<'env, 'vm> {
-    fn default() -> Frame<'env, 'vm> {
-        Frame::new(FrameBase::None)
-    }
-}
-
-impl<'env, 'vm> Frame<'env, 'vm> {
-    pub fn new(base: FrameBase<'env, 'vm>) -> Frame<'env, 'vm> {
-        Frame {
-            locals: Locals::new(),
-            base,
-            current_loop: None,
-        }
-    }
-}
-
-#[cfg(feature = "internal_debug")]
-impl<'env, 'vm> fmt::Debug for Frame<'env, 'vm> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut m = f.debug_map();
-        m.entries(self.locals.iter());
-        if let Some(Loop { ref controller, .. }) = self.current_loop {
-            m.entry(&"loop", controller);
-        }
-        if let FrameBase::Value(ref value) = self.base {
-            m.entries(value.iter_as_str_map());
-        }
-        m.finish()
-    }
-}
-
-#[cfg_attr(feature = "internal_debug", derive(Debug))]
-pub struct Stack {
-    values: Vec<Value>,
-}
-
-impl Stack {
-    pub fn push(&mut self, arg: Value) {
-        self.values.push(arg);
-    }
-
-    pub fn pop(&mut self) -> Value {
-        self.values.pop().expect("stack was empty")
-    }
-
-    pub fn try_pop(&mut self) -> Option<Value> {
-        self.values.pop()
-    }
-
-    pub fn peek(&self) -> &Value {
-        self.values.last().expect("stack was empty")
-    }
-}
-
-#[derive(Default)]
-pub struct Context<'env, 'vm> {
-    stack: Vec<Frame<'env, 'vm>>,
-}
-
-impl<'env, 'vm> fmt::Debug for Context<'env, 'vm> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        fn dump<'a>(
-            m: &mut std::fmt::DebugMap,
-            seen: &mut HashSet<&'a str>,
-            ctx: &'a Context<'a, 'a>,
-        ) -> fmt::Result {
-            for frame in ctx.stack.iter().rev() {
-                for (key, value) in frame.locals.iter() {
-                    if !seen.contains(key) {
-                        seen.insert(*key);
-                        m.entry(key, value);
-                    }
-                }
-
-                if let Some(ref l) = frame.current_loop {
-                    if l.with_loop_var && !seen.contains(&"loop") {
-                        seen.insert("loop");
-                        m.entry(&"loop", &l.controller);
-                    }
-                }
-
-                match frame.base {
-                    FrameBase::Context(ctx) => {
-                        dump(m, seen, ctx)?;
-                    }
-                    FrameBase::Value(ref value) => {
-                        for (key, value) in value.iter_as_str_map() {
-                            if !seen.contains(key) {
-                                seen.insert(key);
-                                m.entry(&key, &value);
-                            }
-                        }
-                    }
-                    FrameBase::None => continue,
-                }
-            }
-            Ok(())
-        }
-
-        let mut m = f.debug_map();
-        let mut seen = HashSet::new();
-        dump(&mut m, &mut seen, self)?;
-        m.finish()
-    }
-}
-
-impl<'env, 'vm> Context<'env, 'vm> {
-    /// Freezes the context.
-    ///
-    /// This implementation is not particularly beautiful and highly inefficient.
-    /// Since it's only used for the debug support changing this is not too
-    /// critical.
-    #[cfg(feature = "debug")]
-    fn freeze<'a>(&'a self, env: &'a Environment) -> Locals {
-        let mut rv = Locals::new();
-
-        rv.extend(env.globals.iter().map(|(k, v)| (*k, v.clone())));
-
-        for frame in self.stack.iter().rev() {
-            // look at locals first
-            rv.extend(frame.locals.iter().map(|(k, v)| (*k, v.clone())));
-
-            // if we are a loop, check if we are looking up the special loop var.
-            if let Some(ref l) = frame.current_loop {
-                if l.with_loop_var {
-                    rv.insert("loop", Value::from_rc_object(l.controller.clone()));
-                }
-            }
-
-            match frame.base {
-                FrameBase::Context(ctx) => {
-                    rv.extend(ctx.freeze(env));
-                }
-                FrameBase::Value(ref value) => {
-                    rv.extend(value.iter_as_str_map());
-                }
-                FrameBase::None => continue,
-            }
-        }
-
-        rv
-    }
-
-    /// Stores a variable in the context.
-    pub fn store(&mut self, key: &'env str, value: Value) {
-        self.stack
-            .last_mut()
-            .expect("cannot store on empty stack")
-            .locals
-            .insert(key, value);
-    }
-
-    /// Looks up a variable in the context.
-    pub fn load(&self, env: &Environment, key: &str) -> Option<Value> {
-        for frame in self.stack.iter().rev() {
-            // look at locals first
-            if let Some(value) = frame.locals.get(key) {
-                if !value.is_undefined() {
-                    return Some(value.clone());
-                }
-            }
-
-            // if we are a loop, check if we are looking up the special loop var.
-            if let Some(ref l) = frame.current_loop {
-                if l.with_loop_var && key == "loop" {
-                    return Some(Value::from_rc_object(l.controller.clone()));
-                }
-            }
-
-            match frame.base {
-                FrameBase::Context(ctx) => return ctx.load(env, key),
-                FrameBase::Value(ref value) => {
-                    let rv = value.get_attr(key);
-                    if let Ok(rv) = rv {
-                        if !rv.is_undefined() {
-                            return Some(rv);
-                        }
-                    }
-                    if let Some(value) = env.get_global(key) {
-                        return Some(value);
-                    }
-                }
-                FrameBase::None => continue,
-            }
-        }
-        None
-    }
-
-    /// Pushes a new layer.
-    pub fn push_frame(&mut self, layer: Frame<'env, 'vm>) {
-        self.stack.push(layer);
-    }
-
-    /// Pops the topmost layer.
-    pub fn pop_frame(&mut self) -> Frame {
-        self.stack.pop().expect("pop from empty context stack")
-    }
-
-    /// Returns the current innermost loop.
-    pub fn current_loop(&mut self) -> Option<&mut Loop> {
-        self.stack
-            .iter_mut()
-            .rev()
-            .filter_map(|x| x.current_loop.as_mut())
-            .next()
-    }
-}
-
-fn process_err(mut err: Error, pc: usize, _state: &State, instructions: &Instructions) -> Error {
-    if let Some(lineno) = instructions.get_line(pc) {
-        err.set_location(instructions.name(), lineno);
-    }
-    #[cfg(feature = "debug")]
-    {
-        if _state.env.debug() && err.debug_info.is_none() {
-            err.debug_info = Some(_state.make_debug_info(pc, instructions));
-        }
-    }
-    err
-}
-
-/// Provides access to the current execution state of the engine.
-///
-/// A read only reference is passed to filter functions and similar objects to
-/// allow limited interfacing with the engine.  The state is useful to look up
-/// information about the engine in filter, test or global functions.  It not
-/// only provides access to the template environment but also the context
-/// variables of the engine, the current auto escaping behavior as well as the
-/// auto escape flag.
-pub struct State<'vm, 'env, 'out, 'buf> {
-    pub(crate) env: &'env Environment<'env>,
-    pub(crate) ctx: Context<'env, 'vm>,
-    pub(crate) name: &'env str,
-    pub(crate) current_block: Option<&'env str>,
-    pub(crate) out: &'out mut Output<'buf>,
-}
-
-impl<'vm, 'env, 'out, 'buf> fmt::Debug for State<'vm, 'env, 'out, 'buf> {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        let mut ds = f.debug_struct("State");
-        ds.field("name", &self.name);
-        ds.field("current_block", &self.current_block);
-        ds.field("auto_escape", &self.out.auto_escape);
-        ds.field("ctx", &self.ctx);
-        ds.field("env", &self.env);
-        ds.finish()
-    }
-}
-
-impl<'vm, 'env, 'out, 'buf> State<'vm, 'env, 'out, 'buf> {
-    /// Returns a reference to the current environment.
-    pub fn env(&self) -> &Environment<'env> {
-        self.env
-    }
-
-    /// Returns the name of the current template.
-    pub fn name(&self) -> &str {
-        self.name
-    }
-
-    /// Returns the current value of the auto escape flag.
-    pub fn auto_escape(&self) -> AutoEscape {
-        self.out.auto_escape
-    }
-
-    /// Returns the name of the innermost block.
-    pub fn current_block(&self) -> Option<&str> {
-        self.current_block
-    }
-
-    /// Looks up a variable by name in the context.
-    pub fn lookup(&self, name: &str) -> Option<Value> {
-        self.ctx.load(self.env(), name)
-    }
-
-    pub(crate) fn apply_filter(
-        &self,
-        name: &str,
-        value: &Value,
-        args: &[Value],
-    ) -> Result<Value, Error> {
-        if let Some(filter) = self.env().get_filter(name) {
-            filter.apply_to(self, value, args)
-        } else {
-            Err(Error::new(
-                ErrorKind::UnknownFilter,
-                format!("filter {} is unknown", name),
-            ))
-        }
-    }
-
-    pub(crate) fn perform_test(
-        &self,
-        name: &str,
-        value: &Value,
-        args: &[Value],
-    ) -> Result<bool, Error> {
-        if let Some(test) = self.env().get_test(name) {
-            test.perform(self, value, args)
-        } else {
-            Err(Error::new(
-                ErrorKind::UnknownTest,
-                format!("test {} is unknown", name),
-            ))
-        }
-    }
-
-    #[cfg(feature = "debug")]
-    fn make_debug_info(
-        &self,
-        pc: usize,
-        instructions: &Instructions<'_>,
-    ) -> crate::error::DebugInfo {
-        let referenced_names = instructions.get_referenced_names(pc);
-        crate::error::DebugInfo {
-            template_source: Some(instructions.source().to_string()),
-            context: Some(Value::from(self.ctx.freeze(self.env))),
-            referenced_names: Some(referenced_names.iter().map(|x| x.to_string()).collect()),
-        }
-    }
-}
-
-/// Helps to evaluate something.
-#[cfg_attr(feature = "internal_debug", derive(Debug))]
-pub struct Vm<'env> {
-    env: &'env Environment<'env>,
-}
-
-impl<'env> Vm<'env> {
-    /// Creates a new VM.
-    pub fn new(env: &'env Environment<'env>) -> Vm<'env> {
-        Vm { env }
-    }
-
-    /// Evaluates the given inputs
-    pub fn eval(
-        &self,
-        instructions: &Instructions<'env>,
-        root: Value,
-        blocks: &BTreeMap<&'env str, Instructions<'env>>,
-        out: &mut Output,
-    ) -> Result<Option<Value>, Error> {
-        let mut ctx = Context::default();
-        ctx.push_frame(Frame::new(FrameBase::Value(root)));
-        let mut referenced_blocks = BTreeMap::new();
-        for (&name, instr) in blocks.iter() {
-            referenced_blocks.insert(name, vec![instr]);
-        }
-        let mut state = State {
-            env: self.env,
-            ctx,
-            current_block: None,
-            name: instructions.name(),
-            out,
-        };
-        value::with_value_optimization(|| {
-            self.eval_state(&mut state, instructions, referenced_blocks)
-        })
-    }
-
-    /// This is the actual evaluation loop that works with a specific context.
-    fn eval_state(
-        &self,
-        state: &mut State<'_, 'env, '_, '_>,
-        mut instructions: &Instructions<'env>,
-        mut blocks: BTreeMap<&'env str, Vec<&'_ Instructions<'env>>>,
-    ) -> Result<Option<Value>, Error> {
-        let initial_auto_escape = state.out.auto_escape;
-        let mut stack = Stack { values: Vec::new() };
-        let mut auto_escape_stack = vec![];
-        let mut block_stack = vec![];
-        let mut next_loop_recursion_jump = None;
-        let mut pc = 0;
-
-        macro_rules! bail {
-            ($err:expr) => {{
-                return Err(process_err($err, pc, state, instructions));
-            }};
-        }
-
-        macro_rules! try_ctx {
-            ($expr:expr) => {
-                match $expr {
-                    Ok(rv) => rv,
-                    Err(err) => bail!(err),
-                }
-            };
-        }
-
-        macro_rules! func_binop {
-            ($method:ident) => {{
-                let b = stack.pop();
-                let a = stack.pop();
-                stack.push(try_ctx!(ops::$method(&a, &b)));
-            }};
-        }
-
-        macro_rules! op_binop {
-            ($op:tt) => {{
-                let b = stack.pop();
-                let a = stack.pop();
-                stack.push(Value::from(a $op b));
-            }};
-        }
-
-        macro_rules! sub_eval {
-            ($instructions:expr) => {{
-                self.sub_eval(state, $instructions, blocks.clone(), state.current_block)?
-            }};
-        }
-
-        macro_rules! super_block {
-            ($capture:expr) => {
-                let mut inner_blocks = blocks.clone();
-                let name = match state.current_block {
-                    Some(name) => name,
-                    None => {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "cannot super outside of block",
-                        ));
-                    }
-                };
-                if let Some(layers) = inner_blocks.get_mut(name) {
-                    layers.remove(0);
-                    let instructions = layers.first().unwrap();
-                    if $capture {
-                        state.out.begin_capture();
-                    }
-                    sub_eval!(instructions);
-                    if $capture {
-                        stack.push(state.out.end_capture());
-                    }
-                } else {
-                    panic!("attempted to super unreferenced block");
-                }
-            };
-        }
-
-        macro_rules! recurse_loop {
-            ($capture:expr) => {
-                if let Some(loop_ctx) = state.ctx.current_loop() {
-                    if let Some(recurse_jump_target) = loop_ctx.recurse_jump_target {
-                        // the way this works is that we remember the next instruction
-                        // as loop exit jump target.  Whenever a loop is pushed, it
-                        // memorizes the value in `next_loop_iteration_jump` to jump
-                        // to.
-                        next_loop_recursion_jump = Some((pc + 1, $capture));
-                        if $capture {
-                            state.out.begin_capture();
-                        }
-                        pc = recurse_jump_target;
-                        continue;
-                    } else {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "cannot recurse outside of recursive loop"
-                        ));
-                    }
-                } else {
-                    bail!(Error::new(
-                        ErrorKind::ImpossibleOperation,
-                        "cannot recurse outside of loop"
-                    ));
-                }
-            };
-        }
-
-        while let Some(instr) = instructions.get(pc) {
-            match instr {
-                Instruction::EmitRaw(val) => {
-                    // this only produces a format error, no need to attach
-                    // location information.
-                    write!(state.out, "{}", val)?;
-                }
-                Instruction::Emit => {
-                    try_ctx!(self.env.format(&stack.pop(), state.out));
-                }
-                Instruction::StoreLocal(name) => {
-                    state.ctx.store(name, stack.pop());
-                }
-                Instruction::Lookup(name) => {
-                    stack.push(state.ctx.load(self.env, name).unwrap_or(Value::UNDEFINED));
-                }
-                Instruction::GetAttr(name) => {
-                    let value = stack.pop();
-                    stack.push(try_ctx!(value.get_attr(name)));
-                }
-                Instruction::GetItem => {
-                    let attr = stack.pop();
-                    let value = stack.pop();
-                    stack.push(try_ctx!(value.get_item(&attr)));
-                }
-                Instruction::LoadConst(value) => {
-                    stack.push(value.clone());
-                }
-                Instruction::BuildMap(pair_count) => {
-                    let mut map = BTreeMap::new();
-                    for _ in 0..*pair_count {
-                        let value = stack.pop();
-                        let key: Key = try_ctx!(stack.pop().try_into_key());
-                        map.insert(key, value);
-                    }
-                    stack.push(Value::from(map));
-                }
-                Instruction::BuildList(count) => {
-                    let mut v = Vec::with_capacity(*count);
-                    for _ in 0..*count {
-                        v.push(stack.pop());
-                    }
-                    v.reverse();
-                    stack.push(Value(ValueRepr::Seq(Arc::new(v))));
-                }
-                Instruction::UnpackList(count) => {
-                    let top = stack.pop();
-                    let v = try_ctx!(top.as_slice().map_err(|e| {
-                        Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "cannot unpack: not a sequence",
-                        )
-                        .with_source(e)
-                    }));
-                    if v.len() != *count {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            format!(
-                                "cannot unpack: sequence of wrong length (expected {}, got {})",
-                                *count,
-                                v.len()
-                            )
-                        ));
-                    }
-                    for value in v.iter().rev() {
-                        stack.push(value.clone());
-                    }
-                }
-                Instruction::ListAppend => {
-                    let item = stack.pop();
-                    if let ValueRepr::Seq(mut v) = stack.pop().0 {
-                        Arc::make_mut(&mut v).push(item);
-                        stack.push(Value(ValueRepr::Seq(v)))
-                    } else {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "cannot append to non-list"
-                        ));
-                    }
-                }
-                Instruction::Add => func_binop!(add),
-                Instruction::Sub => func_binop!(sub),
-                Instruction::Mul => func_binop!(mul),
-                Instruction::Div => func_binop!(div),
-                Instruction::IntDiv => func_binop!(int_div),
-                Instruction::Rem => func_binop!(rem),
-                Instruction::Pow => func_binop!(pow),
-                Instruction::Eq => op_binop!(==),
-                Instruction::Ne => op_binop!(!=),
-                Instruction::Gt => op_binop!(>),
-                Instruction::Gte => op_binop!(>=),
-                Instruction::Lt => op_binop!(<),
-                Instruction::Lte => op_binop!(<=),
-                Instruction::Not => {
-                    let a = stack.pop();
-                    stack.push(Value::from(!a.is_true()));
-                }
-                Instruction::StringConcat => {
-                    let a = stack.pop();
-                    let b = stack.pop();
-                    stack.push(ops::string_concat(b, &a));
-                }
-                Instruction::In => {
-                    let container = stack.pop();
-                    let value = stack.pop();
-                    stack.push(try_ctx!(ops::contains(&container, &value)));
-                }
-                Instruction::Neg => {
-                    let a = stack.pop();
-                    stack.push(try_ctx!(ops::neg(&a)));
-                }
-                Instruction::PushWith => {
-                    state.ctx.push_frame(Frame::new(FrameBase::None));
-                }
-                Instruction::PopFrame => {
-                    if let Some(mut loop_ctx) = state.ctx.pop_frame().current_loop {
-                        if let Some((target, end_capture)) = loop_ctx.current_recursion_jump.take()
-                        {
-                            pc = target;
-                            if end_capture {
-                                stack.push(state.out.end_capture());
-                            }
-                            continue;
-                        }
-                    }
-                }
-                Instruction::PushLoop(flags) => {
-                    let iterable = stack.pop();
-                    let iterator = try_ctx!(iterable.try_iter());
-                    let len = iterator.len();
-                    let depth = state
-                        .ctx
-                        .current_loop()
-                        .filter(|x| x.recurse_jump_target.is_some())
-                        .map_or(0, |x| x.controller.depth + 1);
-                    let recursive = *flags & LOOP_FLAG_RECURSIVE != 0;
-                    state.ctx.push_frame(Frame {
-                        current_loop: Some(Loop {
-                            iterator,
-                            with_loop_var: *flags & LOOP_FLAG_WITH_LOOP_VAR != 0,
-                            recurse_jump_target: if recursive { Some(pc) } else { None },
-                            current_recursion_jump: next_loop_recursion_jump.take(),
-                            controller: Arc::new(LoopState {
-                                idx: AtomicUsize::new(!0usize),
-                                len,
-                                depth,
-                                last_changed_value: Mutex::default(),
-                            }),
-                        }),
-                        ..Frame::default()
-                    });
-                }
-                Instruction::Iterate(jump_target) => {
-                    let l = state.ctx.current_loop().expect("not inside a loop");
-                    l.controller.idx.fetch_add(1, Ordering::Relaxed);
-                    match l.iterator.next() {
-                        Some(item) => {
-                            stack.push(item);
-                        }
-                        None => {
-                            pc = *jump_target;
-                            continue;
-                        }
-                    };
-                }
-                Instruction::Jump(jump_target) => {
-                    pc = *jump_target;
-                    continue;
-                }
-                Instruction::JumpIfFalse(jump_target) => {
-                    let value = stack.pop();
-                    if !value.is_true() {
-                        pc = *jump_target;
-                        continue;
-                    }
-                }
-                Instruction::JumpIfFalseOrPop(jump_target) => {
-                    if !stack.peek().is_true() {
-                        pc = *jump_target;
-                        continue;
-                    } else {
-                        stack.pop();
-                    }
-                }
-                Instruction::JumpIfTrueOrPop(jump_target) => {
-                    if stack.peek().is_true() {
-                        pc = *jump_target;
-                        continue;
-                    } else {
-                        stack.pop();
-                    }
-                }
-                Instruction::CallBlock(name) => {
-                    block_stack.push(state.current_block);
-                    state.current_block = Some(name);
-                    if let Some(layers) = blocks.get(name) {
-                        let instructions = layers.first().unwrap();
-                        sub_eval!(instructions);
-                    } else {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            "tried to invoke unknown block"
-                        ));
-                    }
-                    state.current_block = block_stack.pop().unwrap();
-                }
-                Instruction::LoadBlocks => {
-                    let name = stack.pop();
-                    let tmpl = try_ctx!(name
-                        .as_str()
-                        .ok_or_else(|| {
-                            Error::new(
-                                ErrorKind::ImpossibleOperation,
-                                "template name was not a string",
-                            )
-                        })
-                        .and_then(|name| self.env.get_template(name)));
-
-                    // first load the blocks
-                    for (name, instr) in tmpl.blocks().iter() {
-                        blocks.entry(name).or_insert_with(Vec::new).push(instr);
-                    }
-
-                    // then replace the instructions and set the pc to 0 again.
-                    // this effectively means that the template engine will now
-                    // execute the extended template's code instead.  From this
-                    // there is no way back.
-                    instructions = tmpl.instructions();
-                    state.name = instructions.name();
-                    pc = 0;
-                    continue;
-                }
-                Instruction::Include(ignore_missing) => {
-                    let name = stack.pop();
-                    try_ctx!(self.perform_include(name, state, *ignore_missing));
-                }
-                Instruction::PushAutoEscape => {
-                    let value = stack.pop();
-                    auto_escape_stack.push(state.out.auto_escape);
-                    state.out.auto_escape = match (value.as_str(), value == Value::from(true)) {
-                        (Some("html"), _) => AutoEscape::Html,
-                        #[cfg(feature = "json")]
-                        (Some("json"), _) => AutoEscape::Json,
-                        (Some("none"), _) | (None, false) => AutoEscape::None,
-                        (None, true) => {
-                            if matches!(initial_auto_escape, AutoEscape::None) {
-                                AutoEscape::Html
-                            } else {
-                                initial_auto_escape
-                            }
-                        }
-                        _ => {
-                            bail!(Error::new(
-                                ErrorKind::ImpossibleOperation,
-                                "invalid value to autoescape tag",
-                            ));
-                        }
-                    };
-                }
-                Instruction::PopAutoEscape => {
-                    state.out.auto_escape = auto_escape_stack.pop().unwrap();
-                }
-                Instruction::BeginCapture => {
-                    state.out.begin_capture();
-                }
-                Instruction::EndCapture => {
-                    stack.push(state.out.end_capture());
-                }
-                Instruction::ApplyFilter(name) => {
-                    let top = stack.pop();
-                    let args = try_ctx!(top.as_slice());
-                    let value = stack.pop();
-                    stack.push(try_ctx!(state.apply_filter(name, &value, args)));
-                }
-                Instruction::PerformTest(name) => {
-                    let top = stack.pop();
-                    let args = try_ctx!(top.as_slice());
-                    let value = stack.pop();
-                    stack.push(Value::from(
-                        try_ctx!(state.perform_test(name, &value, args)),
-                    ));
-                }
-                Instruction::CallFunction(function_name) => {
-                    let top = stack.pop();
-                    let args = try_ctx!(top.as_slice());
-                    // super is a special function reserved for super-ing into blocks.
-                    if *function_name == "super" {
-                        if !args.is_empty() {
-                            bail!(Error::new(
-                                ErrorKind::ImpossibleOperation,
-                                "super() takes no arguments",
-                            ));
-                        }
-                        super_block!(true);
-                    // loop is a special name which when called recurses the current loop.
-                    } else if *function_name == "loop" {
-                        if args.len() != 1 {
-                            bail!(Error::new(
-                                ErrorKind::ImpossibleOperation,
-                                format!("loop() takes one argument, got {}", args.len())
-                            ));
-                        }
-                        stack.push(args[0].clone());
-                        recurse_loop!(true);
-                    } else if let Some(func) = state.ctx.load(self.env, function_name) {
-                        stack.push(try_ctx!(func.call(state, args)));
-                    } else {
-                        bail!(Error::new(
-                            ErrorKind::ImpossibleOperation,
-                            format!("unknown function {}", function_name),
-                        ));
-                    }
-                }
-                Instruction::CallMethod(name) => {
-                    let top = stack.pop();
-                    let args = try_ctx!(top.as_slice());
-                    let obj = stack.pop();
-                    stack.push(try_ctx!(obj.call_method(state, name, args)));
-                }
-                Instruction::CallObject => {
-                    let top = stack.pop();
-                    let args = try_ctx!(top.as_slice());
-                    let obj = stack.pop();
-                    stack.push(try_ctx!(obj.call(state, args)));
-                }
-                Instruction::DupTop => {
-                    stack.push(stack.peek().clone());
-                }
-                Instruction::DiscardTop => {
-                    stack.pop();
-                }
-                Instruction::FastSuper => {
-                    super_block!(false);
-                }
-                Instruction::FastRecurse => {
-                    recurse_loop!(false);
-                }
-            }
-            pc += 1;
-        }
-
-        Ok(stack.try_pop())
-    }
-
-    fn perform_include(
-        &self,
-        name: Value,
-        state: &mut State<'_, 'env, '_, '_>,
-        ignore_missing: bool,
-    ) -> Result<(), Error> {
-        let choices = if let ValueRepr::Seq(ref choices) = name.0 {
-            &choices[..]
-        } else {
-            std::slice::from_ref(&name)
-        };
-        let mut templates_tried = vec![];
-        for name in choices {
-            let name = name.as_str().ok_or_else(|| {
-                Error::new(
-                    ErrorKind::ImpossibleOperation,
-                    "template name was not a string",
-                )
-            })?;
-            let tmpl = match self.env.get_template(name) {
-                Ok(tmpl) => tmpl,
-                Err(err) => {
-                    if err.kind() == ErrorKind::TemplateNotFound {
-                        templates_tried.push(name);
-                    } else {
-                        return Err(err);
-                    }
-                    continue;
-                }
-            };
-            let instructions = tmpl.instructions();
-            let mut referenced_blocks = BTreeMap::new();
-            for (&name, instr) in tmpl.blocks().iter() {
-                referenced_blocks.insert(name, vec![instr]);
-            }
-            let original_escape = state.out.auto_escape;
-            state.out.auto_escape = tmpl.initial_auto_escape();
-            self.sub_eval(state, instructions, referenced_blocks, None)?;
-            state.out.auto_escape = original_escape;
-            return Ok(());
-        }
-        if !templates_tried.is_empty() && !ignore_missing {
-            Err(if templates_tried.len() == 1 {
-                Error::new(
-                    ErrorKind::TemplateNotFound,
-                    format!(
-                        "tried to include non-existing template {:?}",
-                        templates_tried[0]
-                    ),
-                )
-            } else {
-                Error::new(
-                    ErrorKind::TemplateNotFound,
-                    format!(
-                        "tried to include one of multiple templates, none of which existed {:?}",
-                        templates_tried
-                    ),
-                )
-            })
-        } else {
-            Ok(())
-        }
-    }
-
-    fn sub_eval(
-        &self,
-        state: &mut State<'_, 'env, '_, '_>,
-        instructions: &Instructions<'env>,
-        blocks: BTreeMap<&'env str, Vec<&'_ Instructions<'env>>>,
-        current_block: Option<&str>,
-    ) -> Result<(), Error> {
-        let mut sub_context = Context::default();
-        sub_context.push_frame(Frame::new(FrameBase::Context(&state.ctx)));
-        let mut sub_state = State {
-            env: self.env,
-            ctx: sub_context,
-            current_block,
-            name: instructions.name(),
-            out: state.out,
-        };
-        self.eval_state(&mut sub_state, instructions, blocks)?;
-        Ok(())
-    }
-}
diff --git a/minijinja/src/vm/context.rs b/minijinja/src/vm/context.rs
new file mode 100644
index 0000000..d4e3e19
--- /dev/null
+++ b/minijinja/src/vm/context.rs
@@ -0,0 +1,234 @@
+use std::collections::{BTreeMap, HashSet};
+use std::fmt;
+
+use crate::environment::Environment;
+use crate::value::Value;
+use crate::vm::forloop::ForLoop;
+
+type Locals<'env> = BTreeMap<&'env str, Value>;
+
+#[cfg_attr(feature = "internal_debug", derive(Debug))]
+pub(crate) enum FrameBase<'env, 'vm> {
+    None,
+    Context(&'vm Context<'env, 'vm>),
+    Value(Value),
+}
+
+pub(crate) struct Frame<'env, 'vm> {
+    pub(crate) locals: Locals<'env>,
+    pub(crate) base: FrameBase<'env, 'vm>,
+    pub(crate) current_loop: Option<ForLoop>,
+}
+
+impl<'env, 'vm> Default for Frame<'env, 'vm> {
+    fn default() -> Frame<'env, 'vm> {
+        Frame::new(FrameBase::None)
+    }
+}
+
+impl<'env, 'vm> Frame<'env, 'vm> {
+    pub fn new(base: FrameBase<'env, 'vm>) -> Frame<'env, 'vm> {
+        Frame {
+            locals: Locals::new(),
+            base,
+            current_loop: None,
+        }
+    }
+}
+
+#[cfg(feature = "internal_debug")]
+impl<'env, 'vm> fmt::Debug for Frame<'env, 'vm> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut m = f.debug_map();
+        m.entries(self.locals.iter());
+        if let Some(ForLoop {
+            state: ref controller,
+            ..
+        }) = self.current_loop
+        {
+            m.entry(&"loop", controller);
+        }
+        if let FrameBase::Value(ref value) = self.base {
+            m.entries(value.iter_as_str_map());
+        }
+        m.finish()
+    }
+}
+
+#[cfg_attr(feature = "internal_debug", derive(Debug))]
+#[derive(Default)]
+pub(crate) struct Stack {
+    values: Vec<Value>,
+}
+
+impl Stack {
+    pub fn push(&mut self, arg: Value) {
+        self.values.push(arg);
+    }
+
+    pub fn pop(&mut self) -> Value {
+        self.values.pop().expect("stack was empty")
+    }
+
+    pub fn try_pop(&mut self) -> Option<Value> {
+        self.values.pop()
+    }
+
+    pub fn peek(&self) -> &Value {
+        self.values.last().expect("stack was empty")
+    }
+}
+
+#[derive(Default)]
+pub(crate) struct Context<'env, 'vm> {
+    stack: Vec<Frame<'env, 'vm>>,
+}
+
+impl<'env, 'vm> fmt::Debug for Context<'env, 'vm> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fn dump<'a>(
+            m: &mut std::fmt::DebugMap,
+            seen: &mut HashSet<&'a str>,
+            ctx: &'a Context<'a, 'a>,
+        ) -> fmt::Result {
+            for frame in ctx.stack.iter().rev() {
+                for (key, value) in frame.locals.iter() {
+                    if !seen.contains(key) {
+                        seen.insert(*key);
+                        m.entry(key, value);
+                    }
+                }
+
+                if let Some(ref l) = frame.current_loop {
+                    if l.with_loop_var && !seen.contains(&"loop") {
+                        seen.insert("loop");
+                        m.entry(&"loop", &l.state);
+                    }
+                }
+
+                match frame.base {
+                    FrameBase::Context(ctx) => {
+                        dump(m, seen, ctx)?;
+                    }
+                    FrameBase::Value(ref value) => {
+                        for (key, value) in value.iter_as_str_map() {
+                            if !seen.contains(key) {
+                                seen.insert(key);
+                                m.entry(&key, &value);
+                            }
+                        }
+                    }
+                    FrameBase::None => continue,
+                }
+            }
+            Ok(())
+        }
+
+        let mut m = f.debug_map();
+        let mut seen = HashSet::new();
+        dump(&mut m, &mut seen, self)?;
+        m.finish()
+    }
+}
+
+impl<'env, 'vm> Context<'env, 'vm> {
+    /// Freezes the context.
+    ///
+    /// This implementation is not particularly beautiful and highly inefficient.
+    /// Since it's only used for the debug support changing this is not too
+    /// critical.
+    #[cfg(feature = "debug")]
+    pub fn freeze<'a>(&'a self, env: &'a Environment) -> Locals {
+        let mut rv = Locals::new();
+
+        rv.extend(env.globals.iter().map(|(k, v)| (*k, v.clone())));
+
+        for frame in self.stack.iter().rev() {
+            // look at locals first
+            rv.extend(frame.locals.iter().map(|(k, v)| (*k, v.clone())));
+
+            // if we are a loop, check if we are looking up the special loop var.
+            if let Some(ref l) = frame.current_loop {
+                if l.with_loop_var {
+                    rv.insert("loop", Value::from_rc_object(l.state.clone()));
+                }
+            }
+
+            match frame.base {
+                FrameBase::Context(ctx) => {
+                    rv.extend(ctx.freeze(env));
+                }
+                FrameBase::Value(ref value) => {
+                    rv.extend(value.iter_as_str_map());
+                }
+                FrameBase::None => continue,
+            }
+        }
+
+        rv
+    }
+
+    /// Stores a variable in the context.
+    pub fn store(&mut self, key: &'env str, value: Value) {
+        self.stack
+            .last_mut()
+            .expect("cannot store on empty stack")
+            .locals
+            .insert(key, value);
+    }
+
+    /// Looks up a variable in the context.
+    pub fn load(&self, env: &Environment, key: &str) -> Option<Value> {
+        for frame in self.stack.iter().rev() {
+            // look at locals first
+            if let Some(value) = frame.locals.get(key) {
+                if !value.is_undefined() {
+                    return Some(value.clone());
+                }
+            }
+
+            // if we are a loop, check if we are looking up the special loop var.
+            if let Some(ref l) = frame.current_loop {
+                if l.with_loop_var && key == "loop" {
+                    return Some(Value::from_rc_object(l.state.clone()));
+                }
+            }
+
+            match frame.base {
+                FrameBase::Context(ctx) => return ctx.load(env, key),
+                FrameBase::Value(ref value) => {
+                    let rv = value.get_attr(key);
+                    if let Ok(rv) = rv {
+                        if !rv.is_undefined() {
+                            return Some(rv);
+                        }
+                    }
+                    if let Some(value) = env.get_global(key) {
+                        return Some(value);
+                    }
+                }
+                FrameBase::None => continue,
+            }
+        }
+        None
+    }
+
+    /// Pushes a new layer.
+    pub fn push_frame(&mut self, layer: Frame<'env, 'vm>) {
+        self.stack.push(layer);
+    }
+
+    /// Pops the topmost layer.
+    pub fn pop_frame(&mut self) -> Frame {
+        self.stack.pop().expect("pop from empty context stack")
+    }
+
+    /// Returns the current innermost loop.
+    pub fn current_loop(&mut self) -> Option<&mut ForLoop> {
+        self.stack
+            .iter_mut()
+            .rev()
+            .filter_map(|x| x.current_loop.as_mut())
+            .next()
+    }
+}
diff --git a/minijinja/src/vm/forloop.rs b/minijinja/src/vm/forloop.rs
new file mode 100644
index 0000000..3fc2fd7
--- /dev/null
+++ b/minijinja/src/vm/forloop.rs
@@ -0,0 +1,112 @@
+use std::fmt;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::{Arc, Mutex};
+
+use crate::error::{Error, ErrorKind};
+use crate::value::{Object, Value, ValueIterator};
+use crate::vm::state::State;
+
+pub(crate) struct LoopState {
+    pub len: usize,
+    pub idx: AtomicUsize,
+    pub depth: usize,
+    pub last_changed_value: Mutex<Option<Vec<Value>>>,
+}
+
+impl fmt::Debug for LoopState {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut s = f.debug_struct("LoopState");
+        for attr in self.attributes() {
+            s.field(attr, &self.get_attr(attr).unwrap());
+        }
+        s.finish()
+    }
+}
+
+impl Object for LoopState {
+    fn attributes(&self) -> &[&str] {
+        &[
+            "index0",
+            "index",
+            "length",
+            "revindex",
+            "revindex0",
+            "first",
+            "last",
+            "depth",
+            "depth0",
+        ][..]
+    }
+
+    fn get_attr(&self, name: &str) -> Option<Value> {
+        let idx = self.idx.load(Ordering::Relaxed) as u64;
+        let len = self.len as u64;
+        match name {
+            "index0" => Some(Value::from(idx)),
+            "index" => Some(Value::from(idx + 1)),
+            "length" => Some(Value::from(len)),
+            "revindex" => Some(Value::from(len.saturating_sub(idx))),
+            "revindex0" => Some(Value::from(len.saturating_sub(idx).saturating_sub(1))),
+            "first" => Some(Value::from(idx == 0)),
+            "last" => Some(Value::from(len == 0 || idx == len - 1)),
+            "depth" => Some(Value::from(self.depth + 1)),
+            "depth0" => Some(Value::from(self.depth)),
+            _ => None,
+        }
+    }
+
+    fn call(&self, _state: &State, _args: &[Value]) -> Result<Value, Error> {
+        Err(Error::new(
+            ErrorKind::ImpossibleOperation,
+            "loop cannot be called if reassigned to different variable",
+        ))
+    }
+
+    fn call_method(&self, _state: &State, name: &str, args: &[Value]) -> Result<Value, Error> {
+        if name == "changed" {
+            let mut last_changed_value = self.last_changed_value.lock().unwrap();
+            let value = args.to_owned();
+            let changed = last_changed_value.as_ref() != Some(&value);
+            if changed {
+                *last_changed_value = Some(value);
+                Ok(Value::from(true))
+            } else {
+                Ok(Value::from(false))
+            }
+        } else if name == "cycle" {
+            let idx = self.idx.load(Ordering::Relaxed);
+            match args.get(idx % args.len()) {
+                Some(arg) => Ok(arg.clone()),
+                None => Ok(Value::UNDEFINED),
+            }
+        } else {
+            Err(Error::new(
+                ErrorKind::ImpossibleOperation,
+                format!("loop object has no method named {}", name),
+            ))
+        }
+    }
+}
+
+impl fmt::Display for LoopState {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(
+            f,
+            "<loop {}/{}>",
+            self.idx.load(Ordering::Relaxed),
+            self.len
+        )
+    }
+}
+
+#[cfg_attr(feature = "internal_debug", derive(Debug))]
+pub(crate) struct ForLoop {
+    pub(crate) with_loop_var: bool,
+    pub(crate) recurse_jump_target: Option<usize>,
+    // if we're popping the frame, do we want to jump somewhere?  The
+    // first item is the target jump instruction, the second argument
+    // tells us if we need to end capturing.
+    pub(crate) current_recursion_jump: Option<(usize, bool)>,
+    pub(crate) iterator: ValueIterator,
+    pub(crate) state: Arc<LoopState>,
+}
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
new file mode 100644
index 0000000..3b1135c
--- /dev/null
+++ b/minijinja/src/vm/mod.rs
@@ -0,0 +1,635 @@
+use std::collections::BTreeMap;
+use std::sync::atomic::{AtomicUsize, Ordering};
+use std::sync::{Arc, Mutex};
+
+use crate::environment::Environment;
+use crate::error::{Error, ErrorKind};
+use crate::instructions::{
+    Instruction, Instructions, LOOP_FLAG_RECURSIVE, LOOP_FLAG_WITH_LOOP_VAR,
+};
+use crate::key::Key;
+use crate::output::Output;
+use crate::utils::AutoEscape;
+use crate::value::{self, ops, Value, ValueRepr};
+use crate::vm::context::{Context, Frame, FrameBase, Stack};
+use crate::vm::forloop::{ForLoop, LoopState};
+
+pub use crate::vm::state::State;
+
+mod context;
+mod forloop;
+mod state;
+
+/// Helps to evaluate something.
+#[cfg_attr(feature = "internal_debug", derive(Debug))]
+pub struct Vm<'env> {
+    env: &'env Environment<'env>,
+}
+
+impl<'env> Vm<'env> {
+    /// Creates a new VM.
+    pub fn new(env: &'env Environment<'env>) -> Vm<'env> {
+        Vm { env }
+    }
+
+    /// Evaluates the given inputs
+    pub fn eval(
+        &self,
+        instructions: &Instructions<'env>,
+        root: Value,
+        blocks: &BTreeMap<&'env str, Instructions<'env>>,
+        out: &mut Output,
+    ) -> Result<Option<Value>, Error> {
+        let mut ctx = Context::default();
+        ctx.push_frame(Frame::new(FrameBase::Value(root)));
+        let mut referenced_blocks = BTreeMap::new();
+        for (&name, instr) in blocks.iter() {
+            referenced_blocks.insert(name, vec![instr]);
+        }
+        value::with_value_optimization(|| {
+            self.eval_state(&mut State {
+                env: self.env,
+                ctx,
+                current_block: None,
+                instructions,
+                out,
+                blocks: referenced_blocks,
+            })
+        })
+    }
+
+    /// This is the actual evaluation loop that works with a specific context.
+    fn eval_state(&self, state: &mut State<'_, 'env, '_, '_>) -> Result<Option<Value>, Error> {
+        let initial_auto_escape = state.out.auto_escape;
+        let mut stack = Stack::default();
+        let mut auto_escape_stack = vec![];
+        let mut next_loop_recursion_jump = None;
+        let mut pc = 0;
+
+        macro_rules! bail {
+            ($err:expr) => {{
+                return Err(process_err($err, pc, state));
+            }};
+        }
+
+        macro_rules! try_ctx {
+            ($expr:expr) => {
+                match $expr {
+                    Ok(rv) => rv,
+                    Err(err) => bail!(err),
+                }
+            };
+        }
+
+        macro_rules! func_binop {
+            ($method:ident) => {{
+                let b = stack.pop();
+                let a = stack.pop();
+                stack.push(try_ctx!(ops::$method(&a, &b)));
+            }};
+        }
+
+        macro_rules! op_binop {
+            ($op:tt) => {{
+                let b = stack.pop();
+                let a = stack.pop();
+                stack.push(Value::from(a $op b));
+            }};
+        }
+
+        macro_rules! recurse_loop {
+            ($capture:expr) => {{
+                let jump_target = try_ctx!(self.prepare_loop_recursion(state));
+                // the way this works is that we remember the next instruction
+                // as loop exit jump target.  Whenever a loop is pushed, it
+                // memorizes the value in `next_loop_iteration_jump` to jump
+                // to.
+                next_loop_recursion_jump = Some((pc + 1, $capture));
+                if $capture {
+                    state.out.begin_capture();
+                }
+                pc = jump_target;
+                continue;
+            }};
+        }
+
+        while let Some(instr) = state.instructions.get(pc) {
+            match instr {
+                Instruction::EmitRaw(val) => {
+                    // this only produces a format error, no need to attach
+                    // location information.
+                    write!(state.out, "{}", val)?;
+                }
+                Instruction::Emit => {
+                    try_ctx!(self.env.format(&stack.pop(), state.out));
+                }
+                Instruction::StoreLocal(name) => {
+                    state.ctx.store(name, stack.pop());
+                }
+                Instruction::Lookup(name) => {
+                    stack.push(state.ctx.load(self.env, name).unwrap_or(Value::UNDEFINED));
+                }
+                Instruction::GetAttr(name) => {
+                    let value = stack.pop();
+                    stack.push(try_ctx!(value.get_attr(name)));
+                }
+                Instruction::GetItem => {
+                    let attr = stack.pop();
+                    let value = stack.pop();
+                    stack.push(try_ctx!(value.get_item(&attr)));
+                }
+                Instruction::LoadConst(value) => {
+                    stack.push(value.clone());
+                }
+                Instruction::BuildMap(pair_count) => {
+                    let mut map = BTreeMap::new();
+                    for _ in 0..*pair_count {
+                        let value = stack.pop();
+                        let key: Key = try_ctx!(stack.pop().try_into_key());
+                        map.insert(key, value);
+                    }
+                    stack.push(Value::from(map));
+                }
+                Instruction::BuildList(count) => {
+                    let mut v = Vec::with_capacity(*count);
+                    for _ in 0..*count {
+                        v.push(stack.pop());
+                    }
+                    v.reverse();
+                    stack.push(Value(ValueRepr::Seq(Arc::new(v))));
+                }
+                Instruction::UnpackList(count) => {
+                    try_ctx!(self.unpack_list(&mut stack, count));
+                }
+                Instruction::ListAppend => {
+                    let item = stack.pop();
+                    if let ValueRepr::Seq(mut v) = stack.pop().0 {
+                        Arc::make_mut(&mut v).push(item);
+                        stack.push(Value(ValueRepr::Seq(v)))
+                    } else {
+                        bail!(Error::new(
+                            ErrorKind::ImpossibleOperation,
+                            "cannot append to non-list"
+                        ));
+                    }
+                }
+                Instruction::Add => func_binop!(add),
+                Instruction::Sub => func_binop!(sub),
+                Instruction::Mul => func_binop!(mul),
+                Instruction::Div => func_binop!(div),
+                Instruction::IntDiv => func_binop!(int_div),
+                Instruction::Rem => func_binop!(rem),
+                Instruction::Pow => func_binop!(pow),
+                Instruction::Eq => op_binop!(==),
+                Instruction::Ne => op_binop!(!=),
+                Instruction::Gt => op_binop!(>),
+                Instruction::Gte => op_binop!(>=),
+                Instruction::Lt => op_binop!(<),
+                Instruction::Lte => op_binop!(<=),
+                Instruction::Not => {
+                    let a = stack.pop();
+                    stack.push(Value::from(!a.is_true()));
+                }
+                Instruction::StringConcat => {
+                    let a = stack.pop();
+                    let b = stack.pop();
+                    stack.push(ops::string_concat(b, &a));
+                }
+                Instruction::In => {
+                    let container = stack.pop();
+                    let value = stack.pop();
+                    stack.push(try_ctx!(ops::contains(&container, &value)));
+                }
+                Instruction::Neg => {
+                    let a = stack.pop();
+                    stack.push(try_ctx!(ops::neg(&a)));
+                }
+                Instruction::PushWith => {
+                    state.ctx.push_frame(Frame::new(FrameBase::None));
+                }
+                Instruction::PopFrame => {
+                    if let Some(mut loop_ctx) = state.ctx.pop_frame().current_loop {
+                        if let Some((target, end_capture)) = loop_ctx.current_recursion_jump.take()
+                        {
+                            pc = target;
+                            if end_capture {
+                                stack.push(state.out.end_capture());
+                            }
+                            continue;
+                        }
+                    }
+                }
+                Instruction::PushLoop(flags) => {
+                    let iterable = stack.pop();
+                    try_ctx!(self.push_loop(
+                        state,
+                        iterable,
+                        *flags,
+                        pc,
+                        next_loop_recursion_jump.take()
+                    ));
+                }
+                Instruction::Iterate(jump_target) => {
+                    let l = state.ctx.current_loop().expect("not inside a loop");
+                    l.state.idx.fetch_add(1, Ordering::Relaxed);
+                    match l.iterator.next() {
+                        Some(item) => stack.push(item),
+                        None => {
+                            pc = *jump_target;
+                            continue;
+                        }
+                    };
+                }
+                Instruction::Jump(jump_target) => {
+                    pc = *jump_target;
+                    continue;
+                }
+                Instruction::JumpIfFalse(jump_target) => {
+                    let value = stack.pop();
+                    if !value.is_true() {
+                        pc = *jump_target;
+                        continue;
+                    }
+                }
+                Instruction::JumpIfFalseOrPop(jump_target) => {
+                    if !stack.peek().is_true() {
+                        pc = *jump_target;
+                        continue;
+                    } else {
+                        stack.pop();
+                    }
+                }
+                Instruction::JumpIfTrueOrPop(jump_target) => {
+                    if stack.peek().is_true() {
+                        pc = *jump_target;
+                        continue;
+                    } else {
+                        stack.pop();
+                    }
+                }
+                Instruction::CallBlock(name) => {
+                    let old_block = state.current_block;
+                    state.current_block = Some(name);
+                    if let Some(layers) = state.blocks.get(name) {
+                        let instructions = layers.first().unwrap();
+                        try_ctx!(self.sub_eval(state, instructions, state.blocks.clone()));
+                    } else {
+                        bail!(Error::new(
+                            ErrorKind::ImpossibleOperation,
+                            "tried to invoke unknown block"
+                        ));
+                    }
+                    state.current_block = old_block;
+                }
+                Instruction::LoadBlocks => {
+                    let name = stack.pop();
+                    try_ctx!(self.load_blocks(name, state));
+
+                    // then replace the instructions and set the pc to 0 again.
+                    // this effectively means that the template engine will now
+                    // execute the extended template's code instead.  From this
+                    // there is no way back.
+                    pc = 0;
+                    continue;
+                }
+                Instruction::Include(ignore_missing) => {
+                    let name = stack.pop();
+                    try_ctx!(self.perform_include(name, state, *ignore_missing));
+                }
+                Instruction::PushAutoEscape => {
+                    let value = stack.pop();
+                    auto_escape_stack.push(state.out.auto_escape);
+                    state.out.auto_escape =
+                        try_ctx!(self.derive_auto_escape(value, initial_auto_escape));
+                }
+                Instruction::PopAutoEscape => {
+                    state.out.auto_escape = auto_escape_stack.pop().unwrap();
+                }
+                Instruction::BeginCapture => {
+                    state.out.begin_capture();
+                }
+                Instruction::EndCapture => {
+                    stack.push(state.out.end_capture());
+                }
+                Instruction::ApplyFilter(name) => {
+                    let top = stack.pop();
+                    let args = try_ctx!(top.as_slice());
+                    let value = stack.pop();
+                    stack.push(try_ctx!(state.apply_filter(name, &value, args)));
+                }
+                Instruction::PerformTest(name) => {
+                    let top = stack.pop();
+                    let args = try_ctx!(top.as_slice());
+                    let value = stack.pop();
+                    stack.push(Value::from(
+                        try_ctx!(state.perform_test(name, &value, args)),
+                    ));
+                }
+                Instruction::CallFunction(function_name) => {
+                    let top = stack.pop();
+                    let args = try_ctx!(top.as_slice());
+                    // super is a special function reserved for super-ing into blocks.
+                    if *function_name == "super" {
+                        if !args.is_empty() {
+                            bail!(Error::new(
+                                ErrorKind::ImpossibleOperation,
+                                "super() takes no arguments",
+                            ));
+                        }
+                        stack.push(try_ctx!(self.perform_super(state, true)));
+                    // loop is a special name which when called recurses the current loop.
+                    } else if *function_name == "loop" {
+                        if args.len() != 1 {
+                            bail!(Error::new(
+                                ErrorKind::ImpossibleOperation,
+                                format!("loop() takes one argument, got {}", args.len())
+                            ));
+                        }
+                        stack.push(args[0].clone());
+                        recurse_loop!(true);
+                    } else if let Some(func) = state.ctx.load(self.env, function_name) {
+                        stack.push(try_ctx!(func.call(state, args)));
+                    } else {
+                        bail!(Error::new(
+                            ErrorKind::ImpossibleOperation,
+                            format!("unknown function {}", function_name),
+                        ));
+                    }
+                }
+                Instruction::CallMethod(name) => {
+                    let top = stack.pop();
+                    let args = try_ctx!(top.as_slice());
+                    let obj = stack.pop();
+                    stack.push(try_ctx!(obj.call_method(state, name, args)));
+                }
+                Instruction::CallObject => {
+                    let top = stack.pop();
+                    let args = try_ctx!(top.as_slice());
+                    let obj = stack.pop();
+                    stack.push(try_ctx!(obj.call(state, args)));
+                }
+                Instruction::DupTop => {
+                    stack.push(stack.peek().clone());
+                }
+                Instruction::DiscardTop => {
+                    stack.pop();
+                }
+                Instruction::FastSuper => {
+                    try_ctx!(self.perform_super(state, false));
+                }
+                Instruction::FastRecurse => {
+                    recurse_loop!(false);
+                }
+            }
+            pc += 1;
+        }
+
+        Ok(stack.try_pop())
+    }
+
+    fn perform_include(
+        &self,
+        name: Value,
+        state: &mut State<'_, 'env, '_, '_>,
+        ignore_missing: bool,
+    ) -> Result<(), Error> {
+        let choices = if let ValueRepr::Seq(ref choices) = name.0 {
+            &choices[..]
+        } else {
+            std::slice::from_ref(&name)
+        };
+        let mut templates_tried = vec![];
+        for name in choices {
+            let name = name.as_str().ok_or_else(|| {
+                Error::new(
+                    ErrorKind::ImpossibleOperation,
+                    "template name was not a string",
+                )
+            })?;
+            let tmpl = match self.env.get_template(name) {
+                Ok(tmpl) => tmpl,
+                Err(err) => {
+                    if err.kind() == ErrorKind::TemplateNotFound {
+                        templates_tried.push(name);
+                    } else {
+                        return Err(err);
+                    }
+                    continue;
+                }
+            };
+            let instructions = tmpl.instructions();
+            let mut referenced_blocks = BTreeMap::new();
+            for (&name, instr) in tmpl.blocks().iter() {
+                referenced_blocks.insert(name, vec![instr]);
+            }
+            let original_escape = state.out.auto_escape;
+            state.out.auto_escape = tmpl.initial_auto_escape();
+            self.sub_eval(state, instructions, referenced_blocks)?;
+            state.out.auto_escape = original_escape;
+            return Ok(());
+        }
+        if !templates_tried.is_empty() && !ignore_missing {
+            Err(Error::new(
+                ErrorKind::TemplateNotFound,
+                if templates_tried.len() == 1 {
+                    format!(
+                        "tried to include non-existing template {:?}",
+                        templates_tried[0]
+                    )
+                } else {
+                    format!(
+                        "tried to include one of multiple templates, none of which existed {:?}",
+                        templates_tried
+                    )
+                },
+            ))
+        } else {
+            Ok(())
+        }
+    }
+
+    fn perform_super(
+        &self,
+        state: &mut State<'_, 'env, '_, '_>,
+        capture: bool,
+    ) -> Result<Value, Error> {
+        let mut inner_blocks = state.blocks.clone();
+        let name = match state.current_block {
+            Some(name) => name,
+            None => {
+                return Err(Error::new(
+                    ErrorKind::ImpossibleOperation,
+                    "cannot super outside of block",
+                ));
+            }
+        };
+
+        if let Some(layers) = inner_blocks.get_mut(name) {
+            layers.remove(0);
+            let instructions = layers.first().unwrap();
+            if capture {
+                state.out.begin_capture();
+            }
+            self.sub_eval(state, instructions, state.blocks.clone())?;
+            if capture {
+                Ok(state.out.end_capture())
+            } else {
+                Ok(Value::UNDEFINED)
+            }
+        } else {
+            panic!("attempted to super unreferenced block");
+        }
+    }
+
+    fn prepare_loop_recursion(&self, state: &mut State) -> Result<usize, Error> {
+        if let Some(loop_ctx) = state.ctx.current_loop() {
+            if let Some(recurse_jump_target) = loop_ctx.recurse_jump_target {
+                Ok(recurse_jump_target)
+            } else {
+                Err(Error::new(
+                    ErrorKind::ImpossibleOperation,
+                    "cannot recurse outside of recursive loop",
+                ))
+            }
+        } else {
+            Err(Error::new(
+                ErrorKind::ImpossibleOperation,
+                "cannot recurse outside of loop",
+            ))
+        }
+    }
+
+    fn load_blocks(&self, name: Value, state: &mut State<'_, 'env, '_, '_>) -> Result<(), Error> {
+        let tmpl = name
+            .as_str()
+            .ok_or_else(|| {
+                Error::new(
+                    ErrorKind::ImpossibleOperation,
+                    "template name was not a string",
+                )
+            })
+            .and_then(|name| self.env.get_template(name))?;
+        for (name, instr) in tmpl.blocks().iter() {
+            state
+                .blocks
+                .entry(name)
+                .or_insert_with(Vec::new)
+                .push(instr);
+        }
+        state.instructions = tmpl.instructions();
+        Ok(())
+    }
+
+    fn derive_auto_escape(
+        &self,
+        value: Value,
+        initial_auto_escape: AutoEscape,
+    ) -> Result<AutoEscape, Error> {
+        match (value.as_str(), value == Value::from(true)) {
+            (Some("html"), _) => Ok(AutoEscape::Html),
+            #[cfg(feature = "json")]
+            (Some("json"), _) => Ok(AutoEscape::Json),
+            (Some("none"), _) | (None, false) => Ok(AutoEscape::None),
+            (None, true) => Ok(if matches!(initial_auto_escape, AutoEscape::None) {
+                AutoEscape::Html
+            } else {
+                initial_auto_escape
+            }),
+            _ => Err(Error::new(
+                ErrorKind::ImpossibleOperation,
+                "invalid value to autoescape tag",
+            )),
+        }
+    }
+
+    fn push_loop(
+        &self,
+        state: &mut State<'_, 'env, '_, '_>,
+        iterable: Value,
+        flags: u8,
+        pc: usize,
+        next_loop_recursion_jump: Option<(usize, bool)>,
+    ) -> Result<(), Error> {
+        let iterator = iterable.try_iter()?;
+        let len = iterator.len();
+        let depth = state
+            .ctx
+            .current_loop()
+            .filter(|x| x.recurse_jump_target.is_some())
+            .map_or(0, |x| x.state.depth + 1);
+        let recursive = flags & LOOP_FLAG_RECURSIVE != 0;
+        state.ctx.push_frame(Frame {
+            current_loop: Some(ForLoop {
+                iterator,
+                with_loop_var: flags & LOOP_FLAG_WITH_LOOP_VAR != 0,
+                recurse_jump_target: if recursive { Some(pc) } else { None },
+                current_recursion_jump: next_loop_recursion_jump,
+                state: Arc::new(LoopState {
+                    idx: AtomicUsize::new(!0usize),
+                    len,
+                    depth,
+                    last_changed_value: Mutex::default(),
+                }),
+            }),
+            ..Frame::default()
+        });
+        Ok(())
+    }
+
+    fn unpack_list(&self, stack: &mut Stack, count: &usize) -> Result<(), Error> {
+        let top = stack.pop();
+        let v = top.as_slice().map_err(|e| {
+            Error::new(
+                ErrorKind::ImpossibleOperation,
+                "cannot unpack: not a sequence",
+            )
+            .with_source(e)
+        })?;
+        if v.len() != *count {
+            return Err(Error::new(
+                ErrorKind::ImpossibleOperation,
+                format!(
+                    "cannot unpack: sequence of wrong length (expected {}, got {})",
+                    *count,
+                    v.len()
+                ),
+            ));
+        }
+        for value in v.iter().rev() {
+            stack.push(value.clone());
+        }
+        Ok(())
+    }
+
+    fn sub_eval(
+        &self,
+        state: &mut State<'_, 'env, '_, '_>,
+        instructions: &Instructions<'env>,
+        blocks: BTreeMap<&'env str, Vec<&'_ Instructions<'env>>>,
+    ) -> Result<(), Error> {
+        let mut sub_context = Context::default();
+        sub_context.push_frame(Frame::new(FrameBase::Context(&state.ctx)));
+        self.eval_state(&mut State {
+            env: self.env,
+            ctx: sub_context,
+            current_block: state.current_block,
+            out: state.out,
+            instructions,
+            blocks,
+        })?;
+        Ok(())
+    }
+}
+
+fn process_err(mut err: Error, pc: usize, state: &State) -> Error {
+    if let Some(lineno) = state.instructions.get_line(pc) {
+        err.set_location(state.instructions.name(), lineno);
+    }
+    #[cfg(feature = "debug")]
+    {
+        if state.env.debug() && err.debug_info.is_none() {
+            err.debug_info = Some(state.make_debug_info(pc, state.instructions));
+        }
+    }
+    err
+}
diff --git a/minijinja/src/vm/state.rs b/minijinja/src/vm/state.rs
new file mode 100644
index 0000000..6eaca71
--- /dev/null
+++ b/minijinja/src/vm/state.rs
@@ -0,0 +1,128 @@
+use std::collections::BTreeMap;
+use std::fmt;
+
+use crate::environment::Environment;
+use crate::error::{Error, ErrorKind};
+use crate::instructions::Instructions;
+use crate::output::Output;
+use crate::value::Value;
+use crate::vm::context::Context;
+use crate::AutoEscape;
+
+/// Provides access to the current execution state of the engine.
+///
+/// A read only reference is passed to filter functions and similar objects to
+/// allow limited interfacing with the engine.  The state is useful to look up
+/// information about the engine in filter, test or global functions.  It not
+/// only provides access to the template environment but also the context
+/// variables of the engine, the current auto escaping behavior as well as the
+/// auto escape flag.
+///
+/// **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, 'out, 'buf> {
+    pub(crate) env: &'env Environment<'env>,
+    pub(crate) ctx: Context<'env, 'vm>,
+    pub(crate) current_block: Option<&'env str>,
+    pub(crate) out: &'out mut Output<'buf>,
+    pub(crate) instructions: &'vm Instructions<'env>,
+    pub(crate) blocks: BTreeMap<&'env str, Vec<&'vm Instructions<'env>>>,
+}
+
+impl<'vm, 'env, 'out, 'buf> fmt::Debug for State<'vm, 'env, 'out, 'buf> {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let mut ds = f.debug_struct("State");
+        ds.field("name", &self.instructions.name());
+        ds.field("current_block", &self.current_block);
+        ds.field("auto_escape", &self.out.auto_escape);
+        ds.field("ctx", &self.ctx);
+        ds.field("env", &self.env);
+        ds.finish()
+    }
+}
+
+impl<'vm, 'env, 'out, 'buf> State<'vm, 'env, 'out, 'buf> {
+    /// Returns a reference to the current environment.
+    pub fn env(&self) -> &Environment<'_> {
+        self.env
+    }
+
+    /// Returns the name of the current template.
+    pub fn name(&self) -> &str {
+        self.instructions.name()
+    }
+
+    /// Returns the current value of the auto escape flag.
+    pub fn auto_escape(&self) -> AutoEscape {
+        self.out.auto_escape
+    }
+
+    /// Returns the name of the innermost block.
+    pub fn current_block(&self) -> Option<&str> {
+        self.current_block
+    }
+
+    /// Looks up a variable by name in the context.
+    pub fn lookup(&self, name: &str) -> Option<Value> {
+        self.ctx.load(self.env(), name)
+    }
+
+    #[cfg(test)]
+    pub fn with_dummy<R, F: FnOnce(&State) -> R>(env: &'env Environment<'env>, f: F) -> R {
+        f(&State {
+            env,
+            ctx: crate::vm::Context::default(),
+            current_block: None,
+            out: &mut Output::null(),
+            instructions: &Instructions::new("<unknown>", ""),
+            blocks: BTreeMap::default(),
+        })
+    }
+
+    pub(crate) fn apply_filter(
+        &self,
+        name: &str,
+        value: &Value,
+        args: &[Value],
+    ) -> Result<Value, Error> {
+        if let Some(filter) = self.env.get_filter(name) {
+            filter.apply_to(self, value, args)
+        } else {
+            Err(Error::new(
+                ErrorKind::UnknownFilter,
+                format!("filter {} is unknown", name),
+            ))
+        }
+    }
+
+    pub(crate) fn perform_test(
+        &self,
+        name: &str,
+        value: &Value,
+        args: &[Value],
+    ) -> Result<bool, Error> {
+        if let Some(test) = self.env.get_test(name) {
+            test.perform(self, value, args)
+        } else {
+            Err(Error::new(
+                ErrorKind::UnknownTest,
+                format!("test {} is unknown", name),
+            ))
+        }
+    }
+
+    #[cfg(feature = "debug")]
+    pub(crate) fn make_debug_info(
+        &self,
+        pc: usize,
+        instructions: &Instructions<'_>,
+    ) -> crate::error::DebugInfo {
+        let referenced_names = instructions.get_referenced_names(pc);
+        crate::error::DebugInfo {
+            template_source: Some(instructions.source().to_string()),
+            context: Some(Value::from(self.ctx.freeze(self.env))),
+            referenced_names: Some(referenced_names.iter().map(|x| x.to_string()).collect()),
+        }
+    }
+}


Test patch:
diff --git a/minijinja/src/tests.rs b/minijinja/src/tests.rs
index 13fbcfd..98738bc 100644
--- a/minijinja/src/tests.rs
+++ b/minijinja/src/tests.rs
@@ -55,7 +55,6 @@
 //! this module.  Note though that these functions are not to be
 //! called from Rust code as their exact interface (arguments and return types)
 //! might change from one MiniJinja version to another.
-use std::collections::BTreeMap;
 use std::sync::Arc;
 
 use crate::error::Error;
@@ -180,25 +179,6 @@ impl BoxedTest {
     }
 }
 
-pub(crate) fn get_builtin_tests() -> BTreeMap<&'static str, BoxedTest> {
-    #[allow(unused_mut)]
-    let mut rv = BTreeMap::new();
-    #[cfg(feature = "builtins")]
-    {
-        rv.insert("odd", BoxedTest::new(is_odd));
-        rv.insert("even", BoxedTest::new(is_even));
-        rv.insert("undefined", BoxedTest::new(is_undefined));
-        rv.insert("defined", BoxedTest::new(is_defined));
-        rv.insert("number", BoxedTest::new(is_number));
-        rv.insert("string", BoxedTest::new(is_string));
-        rv.insert("sequence", BoxedTest::new(is_sequence));
-        rv.insert("mapping", BoxedTest::new(is_mapping));
-        rv.insert("startingwith", BoxedTest::new(is_startingwith));
-        rv.insert("endingwith", BoxedTest::new(is_endingwith));
-    }
-    rv
-}
-
 #[cfg(feature = "builtins")]
 mod builtins {
     use super::*;
@@ -274,17 +254,12 @@ mod builtins {
         }
 
         let env = crate::Environment::new();
-        let state = State {
-            env: &env,
-            ctx: crate::vm::Context::default(),
-            current_block: None,
-            name: "<unknown>",
-            out: &mut crate::Output::null(),
-        };
-        let bx = BoxedTest::new(test);
-        assert!(bx
-            .perform(&state, &Value::from(23), &[Value::from(23)][..])
-            .unwrap());
+        State::with_dummy(&env, |state| {
+            let bx = BoxedTest::new(test);
+            assert!(bx
+                .perform(state, &Value::from(23), &[Value::from(23)][..])
+                .unwrap());
+        });
     }
 }
 
diff --git a/minijinja/tests/test_environment.rs b/minijinja/tests/test_environment.rs
new file mode 100644
index 0000000..0c66598
--- /dev/null
+++ b/minijinja/tests/test_environment.rs
@@ -0,0 +1,68 @@
+use std::collections::BTreeMap;
+
+use similar_asserts::assert_eq;
+
+use minijinja::value::Value;
+use minijinja::Environment;
+
+#[test]
+fn test_basic() {
+    let mut env = Environment::new();
+    env.add_template("test", "{% for x in seq %}[{{ x }}]{% endfor %}")
+        .unwrap();
+    let t = env.get_template("test").unwrap();
+    let mut ctx = BTreeMap::new();
+    ctx.insert("seq", Value::from((0..3).collect::<Vec<_>>()));
+    let rv = t.render(ctx).unwrap();
+    assert_eq!(rv, "[0][1][2]");
+}
+
+#[test]
+fn test_expression() {
+    let env = Environment::new();
+    let expr = env.compile_expression("foo + bar").unwrap();
+    let mut ctx = BTreeMap::new();
+    ctx.insert("foo", 42);
+    ctx.insert("bar", 23);
+    assert_eq!(expr.eval(&ctx).unwrap(), Value::from(65));
+}
+
+#[test]
+fn test_expression_lifetimes() {
+    let mut env = Environment::new();
+    let s = String::new();
+    env.add_template("test", &s).unwrap();
+    {
+        let x = String::from("1 + 1");
+        let expr = env.compile_expression(&x).unwrap();
+        assert_eq!(expr.eval(&()).unwrap().to_string(), "2");
+    }
+}
+
+#[test]
+fn test_clone() {
+    let mut env = Environment::new();
+    env.add_template("test", "a").unwrap();
+    let mut env2 = env.clone();
+    assert_eq!(env2.get_template("test").unwrap().render(&()).unwrap(), "a");
+    env2.add_template("test", "b").unwrap();
+    assert_eq!(env2.get_template("test").unwrap().render(&()).unwrap(), "b");
+    assert_eq!(env.get_template("test").unwrap().render(&()).unwrap(), "a");
+}
+
+#[test]
+fn test_globals() {
+    let mut env = Environment::new();
+    env.add_global("a", Value::from(42));
+    env.add_template("test", "{{ a }}").unwrap();
+    let tmpl = env.get_template("test").unwrap();
+    assert_eq!(tmpl.render(()).unwrap(), "42");
+}
+
+#[test]
+fn test_template_removal() {
+    let mut env = Environment::new();
+    env.add_template("test", "{{ a }}").unwrap();
+    env.remove_template("test");
+    assert!(env.get_template("test").is_err());
+}
diff --git a/minijinja/tests/test_vm.rs b/minijinja/tests/test_vm.rs
index 71a2253..2c0e07f 100644
--- a/minijinja/tests/test_vm.rs
+++ b/minijinja/tests/test_vm.rs
@@ -10,14 +10,15 @@ use similar_asserts::assert_eq;
 pub fn simple_eval<S: serde::Serialize>(
     instructions: &Instructions<'_>,
     ctx: S,
-    output: &mut String,
-) -> Result<Option<Value>, Error> {
+) -> Result<String, Error> {
     let env = Environment::new();
     let empty_blocks = BTreeMap::new();
     let vm = Vm::new(&env);
     let root = Value::from_serializable(&ctx);
-    let mut formatter = make_string_formatter(output, AutoEscape::None);
-    vm.eval(instructions, root, &empty_blocks, &mut formatter)
+    let mut rv = String::new();
+    let mut output = make_string_formatter(&mut rv, AutoEscape::None);
+    vm.eval(instructions, root, &empty_blocks, &mut output)?;
+    Ok(rv)
 }
 
 #[test]
@@ -35,8 +36,7 @@ fn test_loop() {
     c.end_for_loop(false);
     c.add(Instruction::EmitRaw("!"));
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, ctx, &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ctx).unwrap();
 
     assert_eq!(output, "123456789!");
 }
@@ -55,8 +55,7 @@ fn test_if() {
         c.add(Instruction::EmitRaw("false"));
         c.end_if();
 
-        let mut output = String::new();
-        simple_eval(&c.finish().0, ctx, &mut output).unwrap();
+        let output = simple_eval(&c.finish().0, ctx).unwrap();
 
         assert_eq!(output, expectation);
     }
@@ -81,8 +80,7 @@ fn test_if_branches() {
     c.end_if();
     c.end_if();
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, ctx, &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ctx).unwrap();
 
     assert_eq!(output, "yes");
 }
@@ -96,8 +94,6 @@ fn test_basic() {
     ctx.insert("a", Value::from(42));
     ctx.insert("b", Value::from(23));
 
-    let mut output = String::new();
-
     let mut i = Instructions::new("", "");
     i.add(Instruction::EmitRaw("Hello "));
     i.add(Instruction::Lookup("user"));
@@ -109,8 +105,7 @@ fn test_basic() {
     i.add(Instruction::Neg);
     i.add(Instruction::Emit);
 
-    simple_eval(&i, ctx, &mut output).unwrap();
-
+    let output = simple_eval(&i, ctx).unwrap();
     assert_eq!(output, "Hello Peter-65");
 }
 
@@ -128,7 +123,7 @@ fn test_error_info() {
     ctx.insert("a_string", Value::from("foo"));
     ctx.insert("an_int", Value::from(42));
 
-    let err = simple_eval(&c.finish().0, ctx, &mut String::new()).unwrap_err();
+    let err = simple_eval(&c.finish().0, ctx).unwrap_err();
     assert_eq!(err.name(), Some("hello.html"));
     assert_eq!(err.line(), Some(2));
 }
@@ -141,8 +136,7 @@ fn test_op_eq() {
     c.add(Instruction::Eq);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 
     let mut c = Compiler::new("hello.html", "");
@@ -151,8 +145,7 @@ fn test_op_eq() {
     c.add(Instruction::Eq);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 }
 
@@ -164,8 +157,7 @@ fn test_op_ne() {
     c.add(Instruction::Ne);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -174,8 +166,7 @@ fn test_op_ne() {
     c.add(Instruction::Ne);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 }
 
@@ -187,8 +178,7 @@ fn test_op_lt() {
     c.add(Instruction::Lt);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -197,8 +187,7 @@ fn test_op_lt() {
     c.add(Instruction::Lt);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 }
 
@@ -210,8 +199,7 @@ fn test_op_gt() {
     c.add(Instruction::Gt);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -220,8 +208,7 @@ fn test_op_gt() {
     c.add(Instruction::Gt);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 }
 
@@ -233,8 +220,7 @@ fn test_op_lte() {
     c.add(Instruction::Lte);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -243,8 +229,7 @@ fn test_op_lte() {
     c.add(Instruction::Lte);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 }
 
@@ -256,8 +241,7 @@ fn test_op_gte() {
     c.add(Instruction::Gte);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -266,8 +250,7 @@ fn test_op_gte() {
     c.add(Instruction::Gte);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 }
 
@@ -278,8 +261,7 @@ fn test_op_not() {
     c.add(Instruction::Not);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "true");
 
     let mut c = Compiler::new("<unkown>", "");
@@ -287,8 +269,7 @@ fn test_op_not() {
     c.add(Instruction::Not);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "false");
 }
 
@@ -300,8 +281,7 @@ fn test_string_concat() {
     c.add(Instruction::StringConcat);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "foo42");
 }
 
@@ -313,7 +293,6 @@ fn test_unpacking() {
     c.add(Instruction::StringConcat);
     c.add(Instruction::Emit);
 
-    let mut output = String::new();
-    simple_eval(&c.finish().0, (), &mut output).unwrap();
+    let output = simple_eval(&c.finish().0, ()).unwrap();
     assert_eq!(output, "foobar");
 }