Repository: mitsuhiko/minijinja (language: rust)

Issue:
Consider implementing loop.previtem and loop.nextitem
These two special loop variables are currently not supported but both very useful. `loop.previtem` points to the last value of the iteration, `loop.nextitem` to the next one.


Discussion / hints:
This is implementable but requires a mutex on the loop controller which might not be worth it.

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0080170f..2451264a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ All notable changes to MiniJinja are documented here.
 - Added `none` test. (#185)
 - Improved various issues with whitespace trimming. (#187)
 - `Value::default` now returns `Undefined` rather than `None`.
+- Added support for `loop.previtem` and `loop.nextitem`. (#188)
 
 # 0.28.0
 
diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md
index 65fcdaf6..a990d025 100644
--- a/COMPATIBILITY.md
+++ b/COMPATIBILITY.md
@@ -66,12 +66,7 @@ forms of auto escaping as well.
 
 ### `{% for %}`
 
-`for` works is at feature parity with Jinja2 with the exception of the
-missing support for `loop.previtem` and `loop.nexitem`.  Supporting this
-is possible but has not been implemented yet because it requires taking
-a mutex for each loop iteration.
-
-**Tracking issue:** [#47](https://github.com/mitsuhiko/minijinja/issues/47)
+`for` has feature parity with Jinja2.
 
 ### `{% if %}`
 
diff --git a/Makefile b/Makefile
index 087d6aab..4759972b 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 DOC_FEATURES=source,json,urlencode
-TEST_FEATURES=unstable_machinery,builtins,source,json,urlencode,debug,internal_debug,macros,multi-template
+TEST_FEATURES=unstable_machinery,builtins,source,json,urlencode,debug,internal_debug,macros,multi-template,adjacent-loop-items
 
 all: test
 
diff --git a/benchmarks/Cargo.toml b/benchmarks/Cargo.toml
index 94020aec..ebbd06fd 100644
--- a/benchmarks/Cargo.toml
+++ b/benchmarks/Cargo.toml
@@ -9,7 +9,7 @@ speedups = ["minijinja/speedups"]
 [dependencies]
 handlebars = "4.3.4"
 liquid = "0.26.0"
-minijinja = { path = "../minijinja", features = ["unstable_machinery"] }
+minijinja = { path = "../minijinja", default-features = false, features = ["unstable_machinery", "multi-template", "builtins"] }
 serde = { version = "1.0.144", features = ["derive"] }
 serde_json = "1.0.85"
 tera = "1.17.0"
diff --git a/examples/minimal/README.md b/examples/minimal/README.md
index c1444b3f..791d2e18 100644
--- a/examples/minimal/README.md
+++ b/examples/minimal/README.md
@@ -4,5 +4,6 @@ An example using MiniJinja without default features.
 
 ```console
 $ cargo run
-Hello World!
+1. Hello John!
+2. Hello Peter!
 ```
diff --git a/examples/minimal/src/hello.txt b/examples/minimal/src/hello.txt
new file mode 100644
index 00000000..f69a1f37
--- /dev/null
+++ b/examples/minimal/src/hello.txt
@@ -0,0 +1,3 @@
+{% for name in names %}
+{{- loop.index }}. Hello {{ name }}!
+{% endfor %}
diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs
index 29d839f9..0b18c68b 100644
--- a/examples/minimal/src/main.rs
+++ b/examples/minimal/src/main.rs
@@ -2,7 +2,11 @@ use minijinja::{context, Environment};
 
 fn main() {
     let mut env = Environment::new();
-    env.add_template("hello.txt", "Hello {{ name }}!").unwrap();
+    env.add_template("hello.txt", include_str!("hello.txt"))
+        .unwrap();
     let tmpl = env.get_template("hello.txt").unwrap();
-    println!("{}", tmpl.render(context!(name => "World")).unwrap());
+    println!(
+        "{}",
+        tmpl.render(context!(names => ["John", "Peter"])).unwrap()
+    );
 }
diff --git a/minijinja/Cargo.toml b/minijinja/Cargo.toml
index 19d1535e..7e82950a 100644
--- a/minijinja/Cargo.toml
+++ b/minijinja/Cargo.toml
@@ -17,7 +17,7 @@ features = ["source", "json", "urlencode"]
 rustdoc-args = ["--cfg", "docsrs", "--html-in-header", "doc-header.html"]
 
 [features]
-default = ["builtins", "debug", "key_interning", "deserialization", "macros", "multi-template"]
+default = ["builtins", "debug", "key_interning", "deserialization", "macros", "multi-template", "adjacent-loop-items"]
 
 # API features
 preserve_order = ["indexmap"]
@@ -34,6 +34,7 @@ speedups = ["v_htmlescape", "key_interning"]
 builtins = []
 macros = []
 multi-template = []
+adjacent-loop-items = []
 fuel = []
 
 # Extra Filters
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index a30d0c6f..36b50419 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -142,6 +142,9 @@
 //!   - `multi-template`: when removed the templates related to imports and extends
 //!     are removed (`{% from %}`, `{% import %}`, `{% include %}`, and `{% extends %}`
 //!     as well as `{% block %}`).
+//!   - `adjacent-loop-items`: when removed the `previtem` and `nextitem` attributes of
+//!     the `loop` object no longer exist.  Removing this feature can provide faster
+//!     template execution when a lot of loops are involved.
 //!   - `unicode`: when added unicode identifiers are supported.  Without this features
 //!     only ASCII identifiers can be used for variable names and attributes.
 //!
diff --git a/minijinja/src/syntax.rs b/minijinja/src/syntax.rs
index a6c26e25..47ae41fe 100644
--- a/minijinja/src/syntax.rs
+++ b/minijinja/src/syntax.rs
@@ -203,6 +203,8 @@
 //! - `loop.cycle`: A helper function to cycle between a list of sequences. See the explanation below.
 //! - `loop.depth`: Indicates how deep in a recursive loop the rendering currently is. Starts at level 1
 //! - `loop.depth0`: Indicates how deep in a recursive loop the rendering currently is. Starts at level 0
+//! - `loop.previtem`: The item from the previous iteration of the loop. `Undefined` during the first iteration.
+//! - `loop.nextitem`: The item from the previous iteration of the loop. `Undefined` during the last iteration.
 //! - `loop.changed(...args)`: Returns true if the passed values have changed since the last time it was called with the same arguments.
 //! - `loop.cycle(...args)`: Returns a value from the passed sequence in a cycle.
 //!
@@ -268,6 +270,10 @@
 //! </ul>
 //! ```
 //!
+//! **Special note:** the `previtem` and `nextitem` attributes are available by default
+//! but can be disabled by removing the `adjacent-loop-items` crate feature.  Removing
+//! these attributes can provide meaningful speedups for templates with a lot of loops.
+//!
 //! ## `{% if %}`
 //!
 //! The `if` statement is comparable with the Python if statement. In the simplest form,
diff --git a/minijinja/src/vm/loop_object.rs b/minijinja/src/vm/loop_object.rs
index 86b0b661..7522f609 100644
--- a/minijinja/src/vm/loop_object.rs
+++ b/minijinja/src/vm/loop_object.rs
@@ -10,6 +10,8 @@ pub(crate) struct Loop {
     pub len: usize,
     pub idx: AtomicUsize,
     pub depth: usize,
+    #[cfg(feature = "adjacent-loop-items")]
+    pub value_triple: Mutex<(Option<Value>, Option<Value>, Option<Value>)>,
     pub last_changed_value: Mutex<Option<Vec<Value>>>,
 }
 
@@ -74,6 +76,10 @@ impl StructObject for Loop {
                 "last",
                 "depth",
                 "depth0",
+                #[cfg(feature = "adjacent-loop-items")]
+                "previtem",
+                #[cfg(feature = "adjacent-loop-items")]
+                "nextitem",
             ][..],
         )
     }
@@ -97,6 +103,24 @@ impl StructObject for Loop {
             "last" => Some(Value::from(len == 0 || idx == len - 1)),
             "depth" => Some(Value::from(self.depth + 1)),
             "depth0" => Some(Value::from(self.depth)),
+            #[cfg(feature = "adjacent-loop-items")]
+            "previtem" => Some(
+                self.value_triple
+                    .lock()
+                    .unwrap()
+                    .0
+                    .clone()
+                    .unwrap_or(Value::UNDEFINED),
+            ),
+            #[cfg(feature = "adjacent-loop-items")]
+            "nextitem" => Some(
+                self.value_triple
+                    .lock()
+                    .unwrap()
+                    .2
+                    .clone()
+                    .unwrap_or(Value::UNDEFINED),
+            ),
             _ => None,
         }
     }
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index 576e2e20..9d7ef343 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -372,7 +372,22 @@ impl<'env> Vm<'env> {
                 Instruction::Iterate(jump_target) => {
                     let l = state.ctx.current_loop().unwrap();
                     l.object.idx.fetch_add(1, Ordering::Relaxed);
-                    match l.iterator.next() {
+
+                    let next = {
+                        #[cfg(feature = "adjacent-loop-items")]
+                        {
+                            let mut triple = l.object.value_triple.lock().unwrap();
+                            triple.0 = triple.1.take();
+                            triple.1 = triple.2.take();
+                            triple.2 = l.iterator.next();
+                            triple.1.clone()
+                        }
+                        #[cfg(not(feature = "adjacent-loop-items"))]
+                        {
+                            l.iterator.next()
+                        }
+                    };
+                    match next {
                         Some(item) => stack.push(item),
                         None => {
                             pc = *jump_target;
@@ -822,7 +837,8 @@ impl<'env> Vm<'env> {
         pc: usize,
         current_recursion_jump: Option<(usize, bool)>,
     ) -> Result<(), Error> {
-        let iterator = ok!(iterable.try_iter_owned());
+        #[allow(unused_mut)]
+        let mut iterator = ok!(iterable.try_iter_owned());
         let len = iterator.len();
         let depth = state
             .ctx
@@ -833,7 +849,6 @@ impl<'env> Vm<'env> {
         let with_loop_var = flags & LOOP_FLAG_WITH_LOOP_VAR != 0;
         ok!(state.ctx.push_frame(Frame {
             current_loop: Some(LoopState {
-                iterator,
                 with_loop_var,
                 recurse_jump_target: if recursive { Some(pc) } else { None },
                 current_recursion_jump,
@@ -841,8 +856,11 @@ impl<'env> Vm<'env> {
                     idx: AtomicUsize::new(!0usize),
                     len,
                     depth,
+                    #[cfg(feature = "adjacent-loop-items")]
+                    value_triple: Mutex::new((None, None, iterator.next())),
                     last_changed_value: Mutex::default(),
                 }),
+                iterator,
             }),
             ..Frame::default()
         }));


Test patch:
diff --git a/minijinja/tests/inputs/loop_var.txt b/minijinja/tests/inputs/loop_var.txt
index 24bf69c3..96e6af41 100644
--- a/minijinja/tests/inputs/loop_var.txt
+++ b/minijinja/tests/inputs/loop_var.txt
@@ -14,4 +14,6 @@
     revindex={{ loop.revindex }}
     revindex0={{ loop.revindex0 }}
     cycle={{ loop.cycle("odd", "even") }}
+    previtem={{ loop.previtem }}
+    nextitem={{ loop.nextitem }}
 {% endfor %}
diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
index 76c13eef..fff72a55 100644
--- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
@@ -24,6 +24,8 @@ State {
             last: true,
             depth: 1,
             depth0: 0,
+            previtem: Undefined,
+            nextitem: Undefined,
         },
         "f": minijinja::functions::builtins::range,
         "upper": 1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_nested_subtraction.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_nested_subtraction.txt.snap
index 3b49784f..9b11acbc 100644
--- a/minijinja/tests/snapshots/test_templates__vm@err_bad_nested_subtraction.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_nested_subtraction.txt.snap
@@ -36,6 +36,8 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_test.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_test.txt.snap
index 467804d0..7f339666 100644
--- a/minijinja/tests/snapshots/test_templates__vm@err_bad_test.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_test.txt.snap
@@ -38,6 +38,8 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_test_arguments.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_test_arguments.txt.snap
index 8466e8ac..171d5ba6 100644
--- a/minijinja/tests/snapshots/test_templates__vm@err_bad_test_arguments.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_test_arguments.txt.snap
@@ -37,6 +37,8 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_in_include.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_in_include.txt.snap
index 07491531..cb335e4b 100644
--- a/minijinja/tests/snapshots/test_templates__vm@err_in_include.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@err_in_include.txt.snap
@@ -43,6 +43,8 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@loop-recursion-error.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop-recursion-error.txt.snap
index 576e3ad6..621947f5 100644
--- a/minijinja/tests/snapshots/test_templates__vm@loop-recursion-error.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@loop-recursion-error.txt.snap
@@ -34,6 +34,8 @@ Referenced variables: {
         last: false,
         depth: 500,
         depth0: 499,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap
index f6eb726a..8c2e0fa7 100644
--- a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking.txt.snap
@@ -37,6 +37,8 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: 2,
     },
     seq: [
         1,
diff --git a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking_wrong_len.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking_wrong_len.txt.snap
index 66a80aec..cca9a0d9 100644
--- a/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking_wrong_len.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@loop_bad_unpacking_wrong_len.txt.snap
@@ -43,6 +43,12 @@ Referenced variables: {
         last: false,
         depth: 1,
         depth0: 0,
+        previtem: Undefined,
+        nextitem: [
+            2,
+            3,
+            4,
+        ],
     },
     seq: [
         [
diff --git a/minijinja/tests/snapshots/test_templates__vm@loop_var.txt.snap b/minijinja/tests/snapshots/test_templates__vm@loop_var.txt.snap
index 6b91b7dd..01d120d7 100644
--- a/minijinja/tests/snapshots/test_templates__vm@loop_var.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@loop_var.txt.snap
@@ -1,6 +1,6 @@
 ---
 source: minijinja/tests/test_templates.rs
-description: "{% for item in seq %}\n  {{ item }} ({{ loop.index }} of {{ loop.length }})\n    first={{ loop.first }}\n    last={{ loop.last }}\n    revindex={{ loop.revindex }}\n    revindex0={{ loop.revindex0 }}\n    cycle={{ loop.cycle(\"odd\", \"even\") }}\n{% endfor %}"
+description: "{% for item in seq %}\n  {{ item }} ({{ loop.index }} of {{ loop.length }})\n    first={{ loop.first }}\n    last={{ loop.last }}\n    revindex={{ loop.revindex }}\n    revindex0={{ loop.revindex0 }}\n    cycle={{ loop.cycle(\"odd\", \"even\") }}\n    previtem={{ loop.previtem }}\n    nextitem={{ loop.nextitem }}\n{% endfor %}"
 info:
   seq:
     - a
@@ -16,6 +16,8 @@ input_file: minijinja/tests/inputs/loop_var.txt
     revindex=4
     revindex0=3
     cycle=odd
+    previtem=
+    nextitem=b
 
   b (2 of 4)
     first=false
@@ -23,6 +25,8 @@ input_file: minijinja/tests/inputs/loop_var.txt
     revindex=3
     revindex0=2
     cycle=even
+    previtem=a
+    nextitem=c
 
   c (3 of 4)
     first=false
@@ -30,6 +34,8 @@ input_file: minijinja/tests/inputs/loop_var.txt
     revindex=2
     revindex0=1
     cycle=odd
+    previtem=b
+    nextitem=d
 
   d (4 of 4)
     first=false
@@ -37,5 +43,7 @@ input_file: minijinja/tests/inputs/loop_var.txt
     revindex=1
     revindex0=0
     cycle=even
+    previtem=c
+    nextitem=