Repository: mitsuhiko/minijinja
Pull request #799 (2025-06-03 14:33:33)

Allow negative arguments to range function
Does what it says on the tin. I added a few relevant snapshot tests, too.

This was motivated by running into this in the wild, when evaluating a chat template from Qwen3.
(See https://github.com/nobodywho-ooo/nobodywho/issues/154 for vaguely relevant context).

mitsuhiko: Will fix this after I unbreak master.  Refs #801

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d943cc9..cdf9066 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@ All notable changes to MiniJinja are documented here.
   strings in the `|join` filter.  This was inconsistent with
   Jinja2 and the filter itself for undefineds in other
   positions.  #794
+* Allow negative arguments to `range` function and change
+  range to `isize`.  #799
+* Allow `isize` as argument type.  #799
 
 ## 2.10.2
 
diff --git a/minijinja/src/functions.rs b/minijinja/src/functions.rs
index 7bbaa13..5b0d18e 100644
--- a/minijinja/src/functions.rs
+++ b/minijinja/src/functions.rs
@@ -299,6 +299,8 @@ impl Object for BoxedFunction {
 
 #[cfg(feature = "builtins")]
 mod builtins {
+    use std::cmp::Ordering;
+
     use super::*;
 
     use crate::error::ErrorKind;
@@ -321,8 +323,8 @@ mod builtins {
     ///
     /// This function will refuse to create ranges over 10.000 items.
     #[cfg_attr(docsrs, doc(cfg(feature = "builtins")))]
-    pub fn range(lower: u32, upper: Option<u32>, step: Option<u32>) -> Result<Value, Error> {
-        fn to_result<I: ExactSizeIterator<Item = u32> + Send + Sync + Clone + 'static>(
+    pub fn range(lower: isize, upper: Option<isize>, step: Option<isize>) -> Result<Value, Error> {
+        fn to_result<I: ExactSizeIterator<Item = isize> + Send + Sync + Clone + 'static>(
             i: I,
         ) -> Result<Value, Error> {
             if i.len() > 100000 {
@@ -339,17 +341,34 @@ mod builtins {
             Some(upper) => lower..upper,
             None => 0..lower,
         };
-        if let Some(step) = step {
-            if step == 0 {
-                Err(Error::new(
-                    ErrorKind::InvalidOperation,
-                    "cannot create range with step of 0",
-                ))
-            } else {
-                to_result(rng.step_by(step as usize))
+
+        let Some(step) = step else {
+            return to_result(rng);
+        };
+
+        match step.cmp(&0) {
+            Ordering::Equal => Err(Error::new(
+                ErrorKind::InvalidOperation,
+                "cannot create range with step of 0",
+            )),
+            Ordering::Greater => to_result(rng.step_by(step as usize)),
+            Ordering::Less => {
+                // handle negative steps
+                debug_assert!(step < 0);
+                let (start, end) = match upper {
+                    Some(upper) => (lower, upper),
+                    None => (0, lower),
+                };
+
+                let len = if start <= end {
+                    0
+                } else {
+                    ((start - end + (-step) - 1) / (-step)) as usize
+                };
+
+                let iter = (0..len).map(move |i| start + (i as isize) * step);
+                to_result(iter)
             }
-        } else {
-            to_result(rng)
         }
     }
 
diff --git a/minijinja/src/value/argtypes.rs b/minijinja/src/value/argtypes.rs
index c117a2c..4356e27 100644
--- a/minijinja/src/value/argtypes.rs
+++ b/minijinja/src/value/argtypes.rs
@@ -432,6 +432,7 @@ primitive_int_try_from!(i32);
 primitive_int_try_from!(i64);
 primitive_int_try_from!(i128);
 primitive_int_try_from!(usize);
+primitive_int_try_from!(isize);
 
 primitive_try_from!(bool, {
     ValueRepr::Bool(val) => val,


Test patch:
diff --git a/minijinja/tests/inputs/functions.txt b/minijinja/tests/inputs/functions.txt
index 733a402..1445f7e 100644
--- a/minijinja/tests/inputs/functions.txt
+++ b/minijinja/tests/inputs/functions.txt
@@ -2,4 +2,8 @@
 ---
 short-range: {{ range(10) }}
 range-is-iterable: {{ range(10) is iterable }}
-range-is-not-a-sequence: {{ range(10) is not sequence }}
\ No newline at end of file
+range-is-not-a-sequence: {{ range(10) is not sequence }}
+negative-range: {{ range(-5, 5) }}
+negative-start: {{ range(-3, 3, 2) }}
+negative-step: {{ range(5, -5, -2) }}
+negative-step-off: {{ range(5, -4, -2) }}
diff --git a/minijinja/tests/snapshots/test_templates__vm@functions.txt.snap b/minijinja/tests/snapshots/test_templates__vm@functions.txt.snap
index 62e0a08..7bdd7fd 100644
--- a/minijinja/tests/snapshots/test_templates__vm@functions.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@functions.txt.snap
@@ -1,9 +1,13 @@
 ---
 source: minijinja/tests/test_templates.rs
-description: "short-range: {{ range(10) }}\nrange-is-iterable: {{ range(10) is iterable }}\nrange-is-not-a-sequence: {{ range(10) is not sequence }}"
+description: "short-range: {{ range(10) }}\nrange-is-iterable: {{ range(10) is iterable }}\nrange-is-not-a-sequence: {{ range(10) is not sequence }}\nnegative-range: {{ range(-5, 5) }}\nnegative-start: {{ range(-3, 3, 2) }}\nnegative-step: {{ range(5, -5, -2) }}\nnegative-step-off: {{ range(5, -4, -2) }}"
 info: {}
 input_file: minijinja/tests/inputs/functions.txt
 ---
 short-range: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 range-is-iterable: true
 range-is-not-a-sequence: true
+negative-range: [-5, -4, -3, -2, -1, 0, 1, 2, 3, 4]
+negative-start: [-3, -1, 1]
+negative-step: [5, 3, 1, -1, -3]
+negative-step-off: [5, 3, 1, -1, -3]