Repository: mitsuhiko/minijinja
Pull request #642 (2024-11-10 12:18:08)

Added argument splatting support
Unlike Jinja2, MiniJinja did not support splatting.  You can now call functions with `*args` and `**kwargs`.

```jinja
{{ f(1, 2, *[3, 4], *[5, 6]) }}
{{ f(1, 2, *[3, 4], *[5, 6], seven=8, **{"nine": 10), **dict(eleven=12) }}
```

This also fixes a bug where maps were sometimes created in reverse order.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e6f931..6546004 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,9 @@ All notable changes to MiniJinja are documented here.
 - `minijinja-cli` now does not convert INI files to lowercase anymore.  This was
   an unintended behavior.  #633
 - Moved up MSRV to 1.63.0 due to indexmap.  #635
+- Added argument splatting support (`*args` for variable args and `**kwargs`
+  for keyword arguments) and fixed a bug where sometimes maps and keyword
+  arguments were created in inverse order.  #642
 
 ## 2.4.0
 
diff --git a/README.md b/README.md
index 0bd4e7e..2a29de2 100644
--- a/README.md
+++ b/README.md
@@ -104,6 +104,8 @@ the engine is used so you can see how it's utilized:
 * Data and Processing:
   * **[Cube](https://cube.dev/docs/product/data-modeling/dynamic/jinja)** uses it [for data modelling](https://github.com/cube-js/cube/tree/db11c121c77c663845242366d3d972b9bc30ae54/packages/cubejs-backend-native/src/template/mj_value)
   * **[PRQL](https://prql-lang.org/)** uses it [to handle DBT style pipelines](https://github.com/PRQL/prql/blob/59fb3cc4b9b6c9e195c928b1ba1134e2c5706ea3/prqlc/prqlc/src/cli/jinja.rs#L21)
+  * **[qsv](https://qsv.dathere.com)** uses it [to render templates from CSV files](https://github.com/jqnatividad/qsv/blob/master/src/cmd/template.rs#L2) and to [construct payloads to post to web services](https://github.com/jqnatividad/qsv/blob/master/src/cmd/fetchpost.rs#L3).
+
 
 ## Getting Help
 
diff --git a/UPDATING.md b/UPDATING.md
index 80059de..5011247 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -1,7 +1,7 @@
 # Updating to MiniJinja 2
 
 MiniJinja 2.0 is a major update to MiniJinja that changes a lot of core
-internals and cleans up some APIs.  In particular it resolves somes limitations
+internals and cleans up some APIs.  In particular it resolves some limitations
 in the engine in relation to working with dynamic objects, unlocks potentials
 for future performance improvements and enhancements.  This document helps with
 upgrading to that version.
@@ -93,12 +93,12 @@ In a nutshell:
 
 When working with objects of an unknown type, you can use the new `DynObject`
 struct which is a type erased box over `Arc<Object>`.  `Value::as_object` now
-returns an `Option<&DynObject>` compared to previosly an `Option<&dyn Object>`
+returns an `Option<&DynObject>` compared to previously an `Option<&dyn Object>`
 as an example.  The `DynObject` can be cheaply cloned which bumps the reference
 count.
 
 On the value type, the object related APIs were changed a bit to better
-accomodate for the new trait:
+accommodate for the new trait:
 
 * `Value::as_struct` was removed, use `Value::as_object` instead.
 * `Value::as_seq` was removed, use `Value::as_object` instead.
@@ -313,7 +313,7 @@ impl Object for Markdown {
 
 ## Stack Ref
 
-The old `minijinja-stack-ref` module was removed as it can no longer accomodate the
+The old `minijinja-stack-ref` module was removed as it can no longer accommodate the
 new object model.  However that module largely is no longer useful as the new object
 system is powerful enough to support it _for the most part_.  While it's not possible
 any more to return references to objects on the stack, you can now trivially work with
diff --git a/minijinja-cabi/include/minijinja.h b/minijinja-cabi/include/minijinja.h
index f7f381a..b312912 100644
--- a/minijinja-cabi/include/minijinja.h
+++ b/minijinja-cabi/include/minijinja.h
@@ -288,7 +288,7 @@ MINIJINJA_API struct mj_value mj_value_get_by_index(struct mj_value value, uint6
 MINIJINJA_API struct mj_value mj_value_get_by_str(struct mj_value value, const char *key);
 
 /*
- Looks up an element by a vaue
+ Looks up an element by a value
  */
 MINIJINJA_API struct mj_value mj_value_get_by_value(struct mj_value value, struct mj_value key);
 
diff --git a/minijinja-cabi/src/value.rs b/minijinja-cabi/src/value.rs
index 3738e93..03d2a73 100644
--- a/minijinja-cabi/src/value.rs
+++ b/minijinja-cabi/src/value.rs
@@ -40,7 +40,7 @@ where
 #[repr(C)]
 pub struct mj_value {
     // Motivation on the size here: The size of `Value` is really not
-    // known and since cbindgen has no way to guarnatee us a matching
+    // known and since cbindgen has no way to guarantee us a matching
     // size we have to be creative.  The dominating type size wise is
     // most likely going to be SmallStr which is a u8+[u8; 22] plus the
     // enum discriminant (u8).
@@ -335,7 +335,7 @@ ffi_fn! {
 }
 
 ffi_fn! {
-    /// Looks up an element by a vaue
+    /// Looks up an element by a value
     unsafe fn mj_value_get_by_value(_scope, value: mj_value, key: mj_value) -> mj_value {
         value.get_item(&key as &Value).unwrap_or_default().into()
     }
diff --git a/minijinja-cli/src/command.rs b/minijinja-cli/src/command.rs
index bee8e32..7432230 100644
--- a/minijinja-cli/src/command.rs
+++ b/minijinja-cli/src/command.rs
@@ -279,7 +279,7 @@ pub(super) fn make_command() -> Command {
                 .long_help("\
                     Dump internals of a template to stdout.\n\n\
                     \
-                    This feature is primarily useful to debug what is going on in a MiniJinja tempalte. \
+                    This feature is primarily useful to debug what is going on in a MiniJinja template. \
                     'instructions' will dump out the bytecode that the engine generated, 'ast' dumps out \
                     the AST in a text only format and 'tokens' will print a line per token of the template \
                     after lexing.")
diff --git a/minijinja-cli/src/long_help.txt b/minijinja-cli/src/long_help.txt
index 33d3b21..f221b8a 100644
--- a/minijinja-cli/src/long_help.txt
+++ b/minijinja-cli/src/long_help.txt
@@ -2,7 +2,7 @@ Most of the functionality is handled via options, but there are two positional a
 
 Various file formats are supported for the template context, the exact formats depend on the features enabled at compilation time.
 
-Configuration is loaded from $HOME/minijinja.toml and environment variables, before being overridden by command line options.  The environment variables are documented with the options that they correspond to.  Note that flags (boolen values) are reconfigured with true/false or 1/0 respectively.  For instance --no-include corresponds to MINIJINJA_INCLUDE=false.  Not all options can be configured from environment variables or config options.
+Configuration is loaded from $HOME/minijinja.toml and environment variables, before being overridden by command line options.  The environment variables are documented with the options that they correspond to.  Note that flags (boolean values) are reconfigured with true/false or 1/0 respectively.  For instance --no-include corresponds to MINIJINJA_INCLUDE=false.  Not all options can be configured from environment variables or config options.
 
 Examples:
 
@@ -14,4 +14,4 @@ Examples:
 
     minijinja-cli --template="Hello {{ name }}!" -Dname=World
 
-    minijinja-cli --expr "1 + 1"
\ No newline at end of file
+    minijinja-cli --expr "1 + 1"
diff --git a/minijinja-embed/src/lib.rs b/minijinja-embed/src/lib.rs
index 319edeb..aa719fa 100644
--- a/minijinja-embed/src/lib.rs
+++ b/minijinja-embed/src/lib.rs
@@ -59,8 +59,8 @@ use std::path::Path;
 /// # }
 /// ```
 ///
-/// To embed diffent folders, alternative bundle names can be provided.
-/// Also tempaltes can be filtered down by extension to avoid accidentally
+/// To embed different folders, alternative bundle names can be provided.
+/// Also templates can be filtered down by extension to avoid accidentally
 /// including unexpected templates.
 ///
 /// ```rust
diff --git a/minijinja/src/compiler/ast.rs b/minijinja/src/compiler/ast.rs
index dd4b58f..38a36c5 100644
--- a/minijinja/src/compiler/ast.rs
+++ b/minijinja/src/compiler/ast.rs
@@ -145,7 +145,6 @@ pub enum Expr<'a> {
     Call(Spanned<Call<'a>>),
     List(Spanned<List<'a>>),
     Map(Spanned<Map<'a>>),
-    Kwargs(Spanned<Kwargs<'a>>),
 }
 
 #[cfg(feature = "internal_debug")]
@@ -165,7 +164,6 @@ impl<'a> fmt::Debug for Expr<'a> {
             Expr::Call(s) => fmt::Debug::fmt(s, f),
             Expr::List(s) => fmt::Debug::fmt(s, f),
             Expr::Map(s) => fmt::Debug::fmt(s, f),
-            Expr::Kwargs(s) => fmt::Debug::fmt(s, f),
         }
     }
 }
@@ -186,7 +184,6 @@ impl<'a> Expr<'a> {
             Expr::Map(_) => "map literal",
             Expr::Test(_) => "test expression",
             Expr::Filter(_) => "filter expression",
-            Expr::Kwargs(_) => "keyword arguments",
         }
     }
 }
@@ -444,7 +441,7 @@ pub struct IfExpr<'a> {
 pub struct Filter<'a> {
     pub name: &'a str,
     pub expr: Option<Expr<'a>>,
-    pub args: Vec<Expr<'a>>,
+    pub args: Vec<CallArg<'a>>,
 }
 
 /// A test expression.
@@ -453,7 +450,7 @@ pub struct Filter<'a> {
 pub struct Test<'a> {
     pub name: &'a str,
     pub expr: Expr<'a>,
-    pub args: Vec<Expr<'a>>,
+    pub args: Vec<CallArg<'a>>,
 }
 
 /// An attribute lookup expression.
@@ -477,7 +474,17 @@ pub struct GetItem<'a> {
 #[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
 pub struct Call<'a> {
     pub expr: Expr<'a>,
-    pub args: Vec<Expr<'a>>,
+    pub args: Vec<CallArg<'a>>,
+}
+
+/// A call argument helper
+#[cfg_attr(feature = "internal_debug", derive(Debug))]
+#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
+pub enum CallArg<'a> {
+    Pos(Expr<'a>),
+    Kwarg(&'a str, Expr<'a>),
+    PosSplat(Expr<'a>),
+    KwargSplat(Expr<'a>),
 }
 
 /// Creates a list of values.
@@ -503,30 +510,6 @@ impl<'a> List<'a> {
     }
 }
 
-/// Creates a map of kwargs
-#[cfg_attr(feature = "internal_debug", derive(Debug))]
-#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
-pub struct Kwargs<'a> {
-    pub pairs: Vec<(&'a str, Expr<'a>)>,
-}
-
-impl<'a> Kwargs<'a> {
-    pub fn as_const(&self) -> Option<Value> {
-        if !self.pairs.iter().all(|x| matches!(x.1, Expr::Const(_))) {
-            return None;
-        }
-
-        let mut rv = value_map_with_capacity(self.pairs.len());
-        for (key, value) in &self.pairs {
-            if let Expr::Const(value) = value {
-                rv.insert(Value::from(*key), value.value.clone());
-            }
-        }
-
-        Some(crate::value::Kwargs::wrap(rv))
-    }
-}
-
 /// Creates a map of values.
 #[cfg_attr(feature = "internal_debug", derive(Debug))]
 #[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
diff --git a/minijinja/src/compiler/codegen.rs b/minijinja/src/compiler/codegen.rs
index f5550d9..f45e5a5 100644
--- a/minijinja/src/compiler/codegen.rs
+++ b/minijinja/src/compiler/codegen.rs
@@ -7,7 +7,7 @@ use crate::compiler::instructions::{
 use crate::compiler::tokens::Span;
 use crate::output::CaptureMode;
 use crate::value::ops::neg;
-use crate::value::Value;
+use crate::value::{Kwargs, Value, ValueMap};
 
 #[cfg(test)]
 use similar_asserts::assert_eq;
@@ -503,7 +503,7 @@ impl<'source> CodeGenerator<'source> {
                         self.add_with_span(Instruction::FastSuper, call.span());
                         return;
                     } else if name == "loop" && call.args.len() == 1 {
-                        self.compile_expr(&call.args[0]);
+                        self.compile_call_args(std::slice::from_ref(&call.args[0]), 0, None);
                         self.add(Instruction::FastRecurse);
                         return;
                     }
@@ -525,7 +525,7 @@ impl<'source> CodeGenerator<'source> {
 
         // filter expressions work like a nested for loop without
         // the special loop variable. in one loop, the condition is checked and
-        // passing items accumlated into a list. in the second, that list is
+        // passing items accumulated into a list. in the second, that list is
         // iterated over normally
         if let Some(ref filter_expr) = for_loop.filter_expr {
             self.add(Instruction::LoadConst(Value::from(0usize)));
@@ -660,21 +660,17 @@ impl<'source> CodeGenerator<'source> {
                 if let Some(ref expr) = f.expr {
                     self.compile_expr(expr);
                 }
-                for arg in &f.args {
-                    self.compile_expr(arg);
-                }
+                let arg_count = self.compile_call_args(&f.args, 1, None);
                 let local_id = get_local_id(&mut self.filter_local_ids, f.name);
-                self.add(Instruction::ApplyFilter(f.name, f.args.len() + 1, local_id));
+                self.add(Instruction::ApplyFilter(f.name, arg_count, local_id));
                 self.pop_span();
             }
             ast::Expr::Test(f) => {
                 self.push_span(f.span());
                 self.compile_expr(&f.expr);
-                for arg in &f.args {
-                    self.compile_expr(arg);
-                }
+                let arg_count = self.compile_call_args(&f.args, 1, None);
                 let local_id = get_local_id(&mut self.test_local_ids, f.name);
-                self.add(Instruction::PerformTest(f.name, f.args.len() + 1, local_id));
+                self.add(Instruction::PerformTest(f.name, arg_count, local_id));
                 self.pop_span();
             }
             ast::Expr::GetAttr(g) => {
@@ -717,18 +713,6 @@ impl<'source> CodeGenerator<'source> {
                     self.add(Instruction::BuildMap(m.keys.len()));
                 }
             }
-            ast::Expr::Kwargs(m) => {
-                if let Some(val) = m.as_const() {
-                    self.add(Instruction::LoadConst(val));
-                } else {
-                    self.set_line_from_span(m.span());
-                    for (key, value) in &m.pairs {
-                        self.add(Instruction::LoadConst(Value::from(*key)));
-                        self.compile_expr(value);
-                    }
-                    self.add(Instruction::BuildKwargs(m.pairs.len()));
-                }
-            }
         }
     }
 
@@ -740,7 +724,7 @@ impl<'source> CodeGenerator<'source> {
         self.push_span(c.span());
         match c.identify_call() {
             ast::CallType::Function(name) => {
-                let arg_count = self.compile_call_args(&c.args, caller);
+                let arg_count = self.compile_call_args(&c.args, 0, caller);
                 self.add(Instruction::CallFunction(name, arg_count));
             }
             #[cfg(feature = "multi_template")]
@@ -751,13 +735,13 @@ impl<'source> CodeGenerator<'source> {
             }
             ast::CallType::Method(expr, name) => {
                 self.compile_expr(expr);
-                let arg_count = self.compile_call_args(&c.args, caller);
-                self.add(Instruction::CallMethod(name, arg_count + 1));
+                let arg_count = self.compile_call_args(&c.args, 1, caller);
+                self.add(Instruction::CallMethod(name, arg_count));
             }
             ast::CallType::Object(expr) => {
                 self.compile_expr(expr);
-                let arg_count = self.compile_call_args(&c.args, caller);
-                self.add(Instruction::CallObject(arg_count + 1));
+                let arg_count = self.compile_call_args(&c.args, 1, caller);
+                self.add(Instruction::CallObject(arg_count));
             }
         };
         self.pop_span();
@@ -765,57 +749,112 @@ impl<'source> CodeGenerator<'source> {
 
     fn compile_call_args(
         &mut self,
-        args: &[ast::Expr<'source>],
+        args: &[ast::CallArg<'source>],
+        extra_args: usize,
         caller: Option<&Caller<'source>>,
-    ) -> usize {
-        match caller {
-            // we can conditionally compile the caller part here since this will
-            // nicely call through for non macro builds
-            #[cfg(feature = "macros")]
-            Some(caller) => self.compile_call_args_with_caller(args, caller),
-            _ => {
-                for arg in args {
-                    self.compile_expr(arg);
+    ) -> Option<u16> {
+        let mut pending_args = extra_args;
+        let mut num_args_batches = 0;
+        let mut has_kwargs = caller.is_some();
+        let mut static_kwargs = caller.is_none();
+
+        for arg in args {
+            match arg {
+                ast::CallArg::Pos(expr) => {
+                    self.compile_expr(expr);
+                    pending_args += 1;
+                }
+                ast::CallArg::PosSplat(expr) => {
+                    if pending_args > 0 {
+                        self.add(Instruction::BuildList(Some(pending_args)));
+                        pending_args = 0;
+                        num_args_batches += 1;
+                    }
+                    self.compile_expr(expr);
+                    num_args_batches += 1;
+                }
+                ast::CallArg::Kwarg(_, expr) => {
+                    if !matches!(expr, ast::Expr::Const(_)) {
+                        static_kwargs = false;
+                    }
+                    has_kwargs = true;
+                }
+                ast::CallArg::KwargSplat(_) => {
+                    static_kwargs = false;
+                    has_kwargs = true;
                 }
-                args.len()
             }
         }
-    }
 
-    #[cfg(feature = "macros")]
-    fn compile_call_args_with_caller(
-        &mut self,
-        args: &[ast::Expr<'source>],
-        caller: &Caller<'source>,
-    ) -> usize {
-        let mut injected_caller = false;
+        if has_kwargs {
+            let mut pending_kwargs = 0;
+            let mut num_kwargs_batches = 0;
+            let mut collected_kwargs = ValueMap::new();
+            for arg in args {
+                match arg {
+                    ast::CallArg::Kwarg(key, value) => {
+                        if static_kwargs {
+                            if let ast::Expr::Const(c) = value {
+                                collected_kwargs.insert(Value::from(*key), c.value.clone());
+                            } else {
+                                unreachable!();
+                            }
+                        } else {
+                            self.add(Instruction::LoadConst(Value::from(*key)));
+                            self.compile_expr(value);
+                            pending_kwargs += 1;
+                        }
+                    }
+                    ast::CallArg::KwargSplat(expr) => {
+                        if pending_kwargs > 0 {
+                            self.add(Instruction::BuildKwargs(pending_kwargs));
+                            num_kwargs_batches += 1;
+                            pending_kwargs = 0;
+                        }
+                        self.compile_expr(expr);
+                        num_kwargs_batches += 1;
+                    }
+                    ast::CallArg::Pos(_) | ast::CallArg::PosSplat(_) => {}
+                }
+            }
 
-        // try to add the caller to already existing keyword arguments.
-        for arg in args {
-            if let ast::Expr::Kwargs(ref m) = arg {
-                self.set_line_from_span(m.span());
-                for (key, value) in &m.pairs {
-                    self.add(Instruction::LoadConst(Value::from(*key)));
-                    self.compile_expr(value);
-                }
-                self.add(Instruction::LoadConst(Value::from("caller")));
-                self.compile_macro_expression(caller);
-                self.add(Instruction::BuildKwargs(m.pairs.len() + 1));
-                injected_caller = true;
+            if !collected_kwargs.is_empty() {
+                self.add(Instruction::LoadConst(Kwargs::wrap(collected_kwargs)));
             } else {
-                self.compile_expr(arg);
+                // The conditions above guarantee that if we collect static kwargs
+                // we cannot enter this block (single kwargs batch, no caller).
+
+                #[cfg(feature = "macros")]
+                {
+                    if let Some(caller) = caller {
+                        self.add(Instruction::LoadConst(Value::from("caller")));
+                        self.compile_macro_expression(caller);
+                        pending_kwargs += 1
+                    }
+                }
+                if num_kwargs_batches > 0 {
+                    if pending_kwargs > 0 {
+                        self.add(Instruction::BuildKwargs(pending_kwargs));
+                        num_kwargs_batches += 1;
+                    }
+                    self.add(Instruction::MergeKwargs(num_kwargs_batches));
+                } else {
+                    self.add(Instruction::BuildKwargs(pending_kwargs));
+                }
             }
+            pending_args += 1;
         }
 
-        // if there are no keyword args so far, create a new kwargs object
-        // and add caller to that.
-        if !injected_caller {
-            self.add(Instruction::LoadConst(Value::from("caller")));
-            self.compile_macro_expression(caller);
-            self.add(Instruction::BuildKwargs(1));
-            args.len() + 1
+        if num_args_batches > 0 {
+            if pending_args > 0 {
+                self.add(Instruction::BuildList(Some(pending_args)));
+                num_args_batches += 1;
+            }
+            self.add(Instruction::UnpackLists(num_args_batches));
+            None
         } else {
-            args.len()
+            assert!(pending_args as u16 as usize == pending_args);
+            Some(pending_args as u16)
         }
     }
 
diff --git a/minijinja/src/compiler/instructions.rs b/minijinja/src/compiler/instructions.rs
index 56dc1a7..dd7fb28 100644
--- a/minijinja/src/compiler/instructions.rs
+++ b/minijinja/src/compiler/instructions.rs
@@ -60,12 +60,18 @@ pub enum Instruction<'source> {
     /// Builds a kwargs map of the last n pairs on the stack.
     BuildKwargs(usize),
 
+    /// Merges N kwargs maps on the list into one.
+    MergeKwargs(usize),
+
     /// Builds a list of the last n pairs on the stack.
     BuildList(Option<usize>),
 
     /// Unpacks a list into N stack items.
     UnpackList(usize),
 
+    /// Unpacks N lists onto the stack and pushes the number of items there were unpacked.
+    UnpackLists(usize),
+
     /// Add the top two values
     Add,
 
@@ -122,10 +128,10 @@ pub enum Instruction<'source> {
     In,
 
     /// Apply a filter.
-    ApplyFilter(&'source str, usize, LocalId),
+    ApplyFilter(&'source str, Option<u16>, LocalId),
 
     /// Perform a filter.
-    PerformTest(&'source str, usize, LocalId),
+    PerformTest(&'source str, Option<u16>, LocalId),
 
     /// Emit the stack top as output
     Emit,
@@ -175,13 +181,13 @@ pub enum Instruction<'source> {
     EndCapture,
 
     /// Calls a global function
-    CallFunction(&'source str, usize),
+    CallFunction(&'source str, Option<u16>),
 
     /// Calls a method
-    CallMethod(&'source str, usize),
+    CallMethod(&'source str, Option<u16>),
 
     /// Calls an object
-    CallObject(usize),
+    CallObject(Option<u16>),
 
     /// Duplicates the top item
     DupTop,
diff --git a/minijinja/src/compiler/meta.rs b/minijinja/src/compiler/meta.rs
index 2f5915a..2566159 100644
--- a/minijinja/src/compiler/meta.rs
+++ b/minijinja/src/compiler/meta.rs
@@ -81,6 +81,15 @@ fn tracker_visit_macro<'a>(m: &ast::Macro<'a>, state: &mut AssignmentTracker<'a>
     m.body.iter().for_each(|node| track_walk(node, state));
 }
 
+fn tracker_visit_callarg<'a>(callarg: &ast::CallArg<'a>, state: &mut AssignmentTracker<'a>) {
+    match callarg {
+        ast::CallArg::Pos(expr)
+        | ast::CallArg::Kwarg(_, expr)
+        | ast::CallArg::PosSplat(expr)
+        | ast::CallArg::KwargSplat(expr) => tracker_visit_expr(expr, state),
+    }
+}
+
 fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a>) {
     match expr {
         ast::Expr::Var(var) => {
@@ -108,11 +117,15 @@ fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a
         }
         ast::Expr::Filter(expr) => {
             tracker_visit_expr_opt(&expr.expr, state);
-            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+            expr.args
+                .iter()
+                .for_each(|x| tracker_visit_callarg(x, state));
         }
         ast::Expr::Test(expr) => {
             tracker_visit_expr(&expr.expr, state);
-            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+            expr.args
+                .iter()
+                .for_each(|x| tracker_visit_callarg(x, state));
         }
         ast::Expr::GetAttr(expr) => {
             // if we are tracking nested, we check if we have a chain of attribute
@@ -157,17 +170,15 @@ fn tracker_visit_expr<'a>(expr: &ast::Expr<'a>, state: &mut AssignmentTracker<'a
         }
         ast::Expr::Call(expr) => {
             tracker_visit_expr(&expr.expr, state);
-            expr.args.iter().for_each(|x| tracker_visit_expr(x, state));
+            expr.args
+                .iter()
+                .for_each(|x| tracker_visit_callarg(x, state));
         }
         ast::Expr::List(expr) => expr.items.iter().for_each(|x| tracker_visit_expr(x, state)),
         ast::Expr::Map(expr) => expr.keys.iter().zip(expr.values.iter()).for_each(|(k, v)| {
             tracker_visit_expr(k, state);
             tracker_visit_expr(v, state);
         }),
-        ast::Expr::Kwargs(expr) => expr
-            .pairs
-            .iter()
-            .for_each(|(_, v)| tracker_visit_expr(v, state)),
     }
 }
 
@@ -265,7 +276,7 @@ fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
             stmt.call
                 .args
                 .iter()
-                .for_each(|x| tracker_visit_expr(x, state));
+                .for_each(|x| tracker_visit_callarg(x, state));
             tracker_visit_macro(&stmt.macro_decl, state);
         }
         #[cfg(feature = "loop_controls")]
@@ -275,7 +286,7 @@ fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
             stmt.call
                 .args
                 .iter()
-                .for_each(|x| tracker_visit_expr(x, state));
+                .for_each(|x| tracker_visit_callarg(x, state));
         }
     }
 }
diff --git a/minijinja/src/compiler/parser.rs b/minijinja/src/compiler/parser.rs
index ae9a06e..9228e68 100644
--- a/minijinja/src/compiler/parser.rs
+++ b/minijinja/src/compiler/parser.rs
@@ -523,7 +523,7 @@ impl<'a> Parser<'a> {
                         let span = self.stream.current_span();
                         let mut expr = ok!(self.parse_unary_only());
                         expr = ok!(self.parse_postfix(expr, span));
-                        vec![expr]
+                        vec![ast::CallArg::Pos(expr)]
                     } else {
                         Vec::new()
                     };
@@ -547,49 +547,76 @@ impl<'a> Parser<'a> {
         Ok(expr)
     }
 
-    fn parse_args(&mut self) -> Result<Vec<ast::Expr<'a>>, Error> {
+    fn parse_args(&mut self) -> Result<Vec<ast::CallArg<'a>>, Error> {
         let mut args = Vec::new();
         let mut first_span = None;
-        let mut kwargs = Vec::new();
+        let mut has_kwargs = false;
+
+        enum ArgType {
+            Regular,
+            Splat,
+            KwargsSplat,
+        }
 
         expect_token!(self, Token::ParenOpen, "`(`");
         loop {
             if skip_token!(self, Token::ParenClose) {
                 break;
             }
-            if !args.is_empty() || !kwargs.is_empty() {
+            if !args.is_empty() || has_kwargs {
                 expect_token!(self, Token::Comma, "`,`");
                 if skip_token!(self, Token::ParenClose) {
                     break;
                 }
             }
+
+            let arg_type = if skip_token!(self, Token::Pow) {
+                ArgType::KwargsSplat
+            } else if skip_token!(self, Token::Mul) {
+                ArgType::Splat
+            } else {
+                ArgType::Regular
+            };
+
             let expr = ok!(self.parse_expr());
 
-            // keyword argument
-            match expr {
-                ast::Expr::Var(ref var) if skip_token!(self, Token::Assign) => {
-                    if first_span.is_none() {
-                        first_span = Some(var.span());
+            match arg_type {
+                ArgType::Regular => {
+                    // keyword argument
+                    match expr {
+                        ast::Expr::Var(ref var) if skip_token!(self, Token::Assign) => {
+                            if first_span.is_none() {
+                                first_span = Some(var.span());
+                            }
+                            has_kwargs = true;
+                            args.push(ast::CallArg::Kwarg(var.id, ok!(self.parse_expr_noif())));
+                        }
+                        _ if has_kwargs => {
+                            return Err(syntax_error(Cow::Borrowed(
+                                "non-keyword arg after keyword arg",
+                            )));
+                        }
+                        _ => {
+                            args.push(ast::CallArg::Pos(expr));
+                        }
                     }
-                    kwargs.push((var.id, ok!(self.parse_expr_noif())));
                 }
-                _ if !kwargs.is_empty() => {
-                    return Err(syntax_error(Cow::Borrowed(
-                        "non-keyword arg after keyword arg",
-                    )));
+                ArgType::Splat => {
+                    args.push(ast::CallArg::PosSplat(expr));
                 }
-                _ => {
-                    args.push(expr);
+                ArgType::KwargsSplat => {
+                    args.push(ast::CallArg::KwargSplat(expr));
+                    has_kwargs = true;
                 }
             }
-        }
 
-        if !kwargs.is_empty() {
-            args.push(ast::Expr::Kwargs(ast::Spanned::new(
-                ast::Kwargs { pairs: kwargs },
-                self.stream.expand_span(first_span.unwrap()),
-            )));
-        };
+            // Set an arbitrary limit of max function parameters.  This is done
+            // in parts because the opcodes can only express 2**16 as argument
+            // count.
+            if args.len() > 2000 {
+                syntax_error!("Too many aguments in function call")
+            }
+        }
 
         Ok(args)
     }
diff --git a/minijinja/src/filters.rs b/minijinja/src/filters.rs
index d324c93..535c0f4 100644
--- a/minijinja/src/filters.rs
+++ b/minijinja/src/filters.rs
@@ -1384,7 +1384,7 @@ mod builtins {
     /// The attribute can use dot notation for nested access, like `"address.city"``.
     /// The values are sorted first so only one group is returned for each unique value.
     /// The attribute can be passed as first argument or as keyword argument named
-    /// `atttribute`.
+    /// `attribute`.
     ///
     /// For example, a list of User objects with a city attribute can be
     /// rendered in groups. In this example, grouper refers to the city value of
diff --git a/minijinja/src/lib.rs b/minijinja/src/lib.rs
index 0557aef..775ae39 100644
--- a/minijinja/src/lib.rs
+++ b/minijinja/src/lib.rs
@@ -250,7 +250,7 @@ pub use self::value::Value;
 pub use self::macros::__context;
 pub use self::vm::State;
 
-// fowards compatibility
+// forwards compatibility
 #[cfg(not(feature = "serde"))]
 const _: () = {
     #[deprecated(
diff --git a/minijinja/src/value/mod.rs b/minijinja/src/value/mod.rs
index 1fec131..95e577c 100644
--- a/minijinja/src/value/mod.rs
+++ b/minijinja/src/value/mod.rs
@@ -1315,7 +1315,7 @@ impl Value {
     /// * undefined or none: value returned unchanged.
     /// * string and bytes: returns a reversed version of that value
     /// * iterables: returns a reversed version of the iterable.  If the iterable is not
-    ///   reversable itself, it consumes it and then reverses it.
+    ///   reversible itself, it consumes it and then reverses it.
     pub fn reverse(&self) -> Result<Value, Error> {
         match self.0 {
             ValueRepr::Undefined | ValueRepr::None => Some(self.clone()),
diff --git a/minijinja/src/value/object.rs b/minijinja/src/value/object.rs
index b2fe3b9..a77aa49 100644
--- a/minijinja/src/value/object.rs
+++ b/minijinja/src/value/object.rs
@@ -196,7 +196,7 @@ pub trait Object: fmt::Debug + Send + Sync {
     ///
     /// By default the length is taken by calling [`enumerate`](Self::enumerate) and
     /// inspecting the [`Enumerator`].  This means that in order to determine
-    /// the length, an iteration is started.  If you this is a problem for your
+    /// the length, an iteration is started.  If you think this is a problem for your
     /// uses, you can manually implement this.  This might for instance be
     /// needed if your type can only be iterated over once.
     fn enumerator_len(self: &Arc<Self>) -> Option<usize> {
diff --git a/minijinja/src/value/type_erase.rs b/minijinja/src/value/type_erase.rs
index b966d29..6850f70 100644
--- a/minijinja/src/value/type_erase.rs
+++ b/minijinja/src/value/type_erase.rs
@@ -76,7 +76,7 @@ macro_rules! type_erase {
                     }
                 )*
 
-                /// Returns the type name of the conrete underlying type.
+                /// Returns the type name of the concrete underlying type.
                 $v fn type_name(&self) -> &'static str {
                     (vt(self).__type_name)()
                 }
diff --git a/minijinja/src/vm/context.rs b/minijinja/src/vm/context.rs
index 5902b75..b0f9577 100644
--- a/minijinja/src/vm/context.rs
+++ b/minijinja/src/vm/context.rs
@@ -109,7 +109,11 @@ impl Stack {
         self.values[start..].reverse();
     }
 
-    pub fn slice_top(&mut self, n: usize) -> &[Value] {
+    pub fn get_call_args(&mut self, n: Option<u16>) -> &[Value] {
+        let n = match n {
+            Some(n) => n as usize,
+            None => self.pop().as_usize().unwrap(),
+        };
         &self.values[self.values.len() - n..]
     }
 
diff --git a/minijinja/src/vm/mod.rs b/minijinja/src/vm/mod.rs
index d6776e0..4119b3f 100644
--- a/minijinja/src/vm/mod.rs
+++ b/minijinja/src/vm/mod.rs
@@ -11,7 +11,9 @@ use crate::error::{Error, ErrorKind};
 use crate::output::{CaptureMode, Output};
 use crate::utils::{untrusted_size_hint, AutoEscape, UndefinedBehavior};
 use crate::value::namespace_object::Namespace;
-use crate::value::{ops, value_map_with_capacity, value_optimization, Kwargs, Value};
+use crate::value::{
+    ops, value_map_with_capacity, value_optimization, Kwargs, ObjectRepr, Value, ValueMap,
+};
 use crate::vm::context::{Frame, LoopState, Stack};
 use crate::vm::loop_object::Loop;
 use crate::vm::state::BlockStack;
@@ -367,22 +369,53 @@ impl<'env> Vm<'env> {
                 }
                 Instruction::BuildMap(pair_count) => {
                     let mut map = value_map_with_capacity(*pair_count);
+                    stack.reverse_top(*pair_count * 2);
                     for _ in 0..*pair_count {
-                        let value = stack.pop();
                         let key = stack.pop();
+                        let value = stack.pop();
                         map.insert(key, value);
                     }
                     stack.push(Value::from_object(map))
                 }
                 Instruction::BuildKwargs(pair_count) => {
                     let mut map = value_map_with_capacity(*pair_count);
+                    stack.reverse_top(*pair_count * 2);
                     for _ in 0..*pair_count {
-                        let value = stack.pop();
                         let key = stack.pop();
+                        let value = stack.pop();
                         map.insert(key, value);
                     }
                     stack.push(Kwargs::wrap(map))
                 }
+                Instruction::MergeKwargs(count) => {
+                    let mut kwargs_sources = Vec::new();
+                    for _ in 0..*count {
+                        kwargs_sources.push(stack.pop());
+                    }
+                    kwargs_sources.reverse();
+                    let values: &[Value] = &kwargs_sources;
+                    let mut rv = ValueMap::new();
+                    for value in values {
+                        ctx_ok!(self.env.undefined_behavior().assert_iterable(value));
+                        let iter = ctx_ok!(value
+                            .as_object()
+                            .filter(|x| x.repr() == ObjectRepr::Map)
+                            .and_then(|x| x.try_iter_pairs())
+                            .ok_or_else(|| {
+                                Error::new(
+                                    ErrorKind::InvalidOperation,
+                                    format!(
+                                        "attempted to apply keyword arguments from non map (got {})",
+                                        value.kind()
+                                    ),
+                                )
+                            }));
+                        for (key, value) in iter {
+                            rv.insert(key, value);
+                        }
+                    }
+                    stack.push(Kwargs::wrap(rv));
+                }
                 Instruction::BuildList(n) => {
                     let count = n.unwrap_or_else(|| stack.pop().try_into().unwrap());
                     let mut v = Vec::with_capacity(untrusted_size_hint(count));
@@ -395,6 +428,20 @@ impl<'env> Vm<'env> {
                 Instruction::UnpackList(count) => {
                     ctx_ok!(self.unpack_list(&mut stack, *count));
                 }
+                Instruction::UnpackLists(count) => {
+                    let mut lists = Vec::new();
+                    for _ in 0..*count {
+                        lists.push(stack.pop());
+                    }
+                    let mut len = 0;
+                    for list in lists.into_iter().rev() {
+                        for item in ctx_ok!(list.try_iter()) {
+                            stack.push(item);
+                            len += 1;
+                        }
+                    }
+                    stack.push(Value::from(len));
+                }
                 Instruction::Add => func_binop!(add),
                 Instruction::Sub => func_binop!(sub),
                 Instruction::Mul => func_binop!(mul),
@@ -541,9 +588,10 @@ impl<'env> Vm<'env> {
                                 format!("filter {name} is unknown"),
                             )
                         }));
-                    let args = stack.slice_top(*arg_count);
+                    let args = stack.get_call_args(*arg_count);
+                    let arg_count = args.len();
                     a = ctx_ok!(filter.apply_to(state, args));
-                    stack.drop_top(*arg_count);
+                    stack.drop_top(arg_count);
                     stack.push(a);
                 }
                 Instruction::PerformTest(name, arg_count, local_id) => {
@@ -553,53 +601,58 @@ impl<'env> Vm<'env> {
                     .ok_or_else(|| {
                         Error::new(ErrorKind::UnknownTest, format!("test {name} is unknown"))
                     }));
-                    let args = stack.slice_top(*arg_count);
+                    let args = stack.get_call_args(*arg_count);
+                    let arg_count = args.len();
                     let rv = ctx_ok!(test.perform(state, args));
-                    stack.drop_top(*arg_count);
+                    stack.drop_top(arg_count);
                     stack.push(Value::from(rv));
                 }
                 Instruction::CallFunction(name, arg_count) => {
+                    let args = stack.get_call_args(*arg_count);
                     // super is a special function reserved for super-ing into blocks.
-                    if *name == "super" {
-                        if *arg_count != 0 {
+                    let rv = if *name == "super" {
+                        if !args.is_empty() {
                             bail!(Error::new(
                                 ErrorKind::InvalidOperation,
                                 "super() takes no arguments",
                             ));
                         }
-                        stack.push(ctx_ok!(self.perform_super(state, out, true)));
+                        ctx_ok!(self.perform_super(state, out, true))
                     // loop is a special name which when called recurses the current loop.
                     } else if *name == "loop" {
-                        if *arg_count != 1 {
+                        if args.len() != 1 {
                             bail!(Error::new(
                                 ErrorKind::InvalidOperation,
-                                format!("loop() takes one argument, got {}", *arg_count)
+                                "loop() takes one argument"
                             ));
                         }
-                        // leave the one argument on the stack for the recursion
+                        // leave the one argument on the stack for the recursion.  The
+                        // recurse_loop! macro itself will perform a jump and not return here.
                         recurse_loop!(true);
                     } else if let Some(func) = state.lookup(name) {
-                        let args = stack.slice_top(*arg_count);
-                        a = ctx_ok!(func.call(state, args));
-                        stack.drop_top(*arg_count);
-                        stack.push(a);
+                        ctx_ok!(func.call(state, args))
                     } else {
                         bail!(Error::new(
                             ErrorKind::UnknownFunction,
                             format!("{name} is unknown"),
                         ));
-                    }
+                    };
+                    let arg_count = args.len();
+                    stack.drop_top(arg_count);
+                    stack.push(rv);
                 }
                 Instruction::CallMethod(name, arg_count) => {
-                    let args = stack.slice_top(*arg_count);
+                    let args = stack.get_call_args(*arg_count);
+                    let arg_count = args.len();
                     a = ctx_ok!(args[0].call_method(state, name, &args[1..]));
-                    stack.drop_top(*arg_count);
+                    stack.drop_top(arg_count);
                     stack.push(a);
                 }
                 Instruction::CallObject(arg_count) => {
-                    let args = stack.slice_top(*arg_count);
+                    let args = stack.get_call_args(*arg_count);
+                    let arg_count = args.len();
                     a = ctx_ok!(args[0].call(state, &args[1..]));
-                    stack.drop_top(*arg_count);
+                    stack.drop_top(arg_count);
                     stack.push(a);
                 }
                 Instruction::DupTop => {


Test patch:
diff --git a/minijinja-cli/tests/snapshots/test_basic__long_help.snap b/minijinja-cli/tests/snapshots/test_basic__long_help.snap
index 4f6bbca..2c885ed 100644
--- a/minijinja-cli/tests/snapshots/test_basic__long_help.snap
+++ b/minijinja-cli/tests/snapshots/test_basic__long_help.snap
@@ -20,7 +20,7 @@ features enabled at compilation time.
 
 Configuration is loaded from $HOME/minijinja.toml and environment variables, before being overridden
 by command line options.  The environment variables are documented with the options that they
-correspond to.  Note that flags (boolen values) are reconfigured with true/false or 1/0
+correspond to.  Note that flags (boolean values) are reconfigured with true/false or 1/0
 respectively.  For instance --no-include corresponds to MINIJINJA_INCLUDE=false.  Not all options
 can be configured from environment variables or config options.
 
@@ -293,7 +293,7 @@ Advanced:
       --dump <KIND>
           Dump internals of a template to stdout.
           
-          This feature is primarily useful to debug what is going on in a MiniJinja tempalte.
+          This feature is primarily useful to debug what is going on in a MiniJinja template.
           'instructions' will dump out the bytecode that the engine generated, 'ast' dumps out the
           AST in a text only format and 'tokens' will print a line per token of the template after
           lexing.
diff --git a/minijinja/tests/inputs/calls-and-literals.txt b/minijinja/tests/inputs/calls-and-literals.txt
new file mode 100644
index 0000000..354628b
--- /dev/null
+++ b/minijinja/tests/inputs/calls-and-literals.txt
@@ -0,0 +1,15 @@
+{
+    "one": 1,
+    "two": 2,
+    "nine": 9
+}
+---
+{{ get_args(1, 2, 3, 4, 5, 6, 7, 8, 9) }}
+{{ get_args(1, 2, 3, *[4, 5, 6], 7, 8, 9) }}
+{{ get_args(one, 2, 3, 4, 5, 6, 7, 8, 9) }}
+{{ get_args(1, 2, 3, *[4, 5, 6], 7, 8, nine) }}
+{{ get_args(1, 2, one=1, two=two) }}
+{{ get_args(1, 2, one=1, **dict(two=two)) }}
+{{ get_args(1, *[2], one=1, **dict(two=two)) }}
+{{ get_args(1, 2, *[3], *[4], **{8: 8, 9: 9})}}
+{{ get_args(1, 2, *[3], *[4], **{8: 8, nine: nine})}}
\ No newline at end of file
diff --git a/minijinja/tests/inputs/err_bad_args_unpack.txt b/minijinja/tests/inputs/err_bad_args_unpack.txt
new file mode 100644
index 0000000..496abcb
--- /dev/null
+++ b/minijinja/tests/inputs/err_bad_args_unpack.txt
@@ -0,0 +1,3 @@
+{}
+---
+{{ get_args(*true) }}
diff --git a/minijinja/tests/inputs/err_bad_kwargs_unpack.txt b/minijinja/tests/inputs/err_bad_kwargs_unpack.txt
new file mode 100644
index 0000000..5efa105
--- /dev/null
+++ b/minijinja/tests/inputs/err_bad_kwargs_unpack.txt
@@ -0,0 +1,3 @@
+{}
+---
+{{ get_args(**[1, 2, 3]) }}
diff --git a/minijinja/tests/snapshots/test_parser__parser@call.txt.snap b/minijinja/tests/snapshots/test_parser__parser@call.txt.snap
index 6b8429f..9cbdd37 100644
--- a/minijinja/tests/snapshots/test_parser__parser@call.txt.snap
+++ b/minijinja/tests/snapshots/test_parser__parser@call.txt.snap
@@ -26,12 +26,16 @@ Ok(
                         name: "cycle",
                     } @ 2:3-2:13,
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 2:14-2:15,
-                        Const {
-                            value: 2,
-                        } @ 2:17-2:18,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 2:14-2:15,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 2:17-2:18,
+                        ),
                     ],
                 } @ 2:7-2:19,
             } @ 2:0-2:19,
@@ -58,28 +62,28 @@ Ok(
                         id: "foo",
                     } @ 4:3-4:6,
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 4:7-4:8,
-                        Const {
-                            value: 2,
-                        } @ 4:10-4:11,
-                        Kwargs {
-                            pairs: [
-                                (
-                                    "a",
-                                    Const {
-                                        value: 3,
-                                    } @ 4:15-4:16,
-                                ),
-                                (
-                                    "b",
-                                    Const {
-                                        value: 4,
-                                    } @ 4:20-4:21,
-                                ),
-                            ],
-                        } @ 4:13-4:22,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 4:7-4:8,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 4:10-4:11,
+                        ),
+                        Kwarg(
+                            "a",
+                            Const {
+                                value: 3,
+                            } @ 4:15-4:16,
+                        ),
+                        Kwarg(
+                            "b",
+                            Const {
+                                value: 4,
+                            } @ 4:20-4:21,
+                        ),
                     ],
                 } @ 4:3-4:22,
             } @ 4:0-4:22,
@@ -92,12 +96,16 @@ Ok(
                         id: "trailing",
                     } @ 5:3-5:11,
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 5:12-5:13,
-                        Const {
-                            value: 2,
-                        } @ 5:15-5:16,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 5:12-5:13,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 5:15-5:16,
+                        ),
                     ],
                 } @ 5:3-5:18,
             } @ 5:0-5:18,
@@ -110,22 +118,22 @@ Ok(
                         id: "trailing_kwarg",
                     } @ 6:3-6:17,
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 6:18-6:19,
-                        Const {
-                            value: 2,
-                        } @ 6:21-6:22,
-                        Kwargs {
-                            pairs: [
-                                (
-                                    "a",
-                                    Const {
-                                        value: 3,
-                                    } @ 6:26-6:27,
-                                ),
-                            ],
-                        } @ 6:24-6:29,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 6:18-6:19,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 6:21-6:22,
+                        ),
+                        Kwarg(
+                            "a",
+                            Const {
+                                value: 3,
+                            } @ 6:26-6:27,
+                        ),
                     ],
                 } @ 6:3-6:29,
             } @ 6:0-6:29,
diff --git a/minijinja/tests/snapshots/test_parser__parser@filter.txt.snap b/minijinja/tests/snapshots/test_parser__parser@filter.txt.snap
index d61a733..57425eb 100644
--- a/minijinja/tests/snapshots/test_parser__parser@filter.txt.snap
+++ b/minijinja/tests/snapshots/test_parser__parser@filter.txt.snap
@@ -18,12 +18,16 @@ Ok(
                                 } @ 1:3-1:6,
                             ),
                             args: [
-                                Const {
-                                    value: 1,
-                                } @ 1:11-1:12,
-                                Const {
-                                    value: 2,
-                                } @ 1:14-1:15,
+                                Pos(
+                                    Const {
+                                        value: 1,
+                                    } @ 1:11-1:12,
+                                ),
+                                Pos(
+                                    Const {
+                                        value: 2,
+                                    } @ 1:14-1:15,
+                                ),
                             ],
                         } @ 1:7-1:16,
                     ),
@@ -42,12 +46,16 @@ Ok(
                         } @ 2:3-2:6,
                     ),
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 2:11-2:12,
-                        Const {
-                            value: 2,
-                        } @ 2:14-2:15,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 2:11-2:12,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 2:14-2:15,
+                        ),
                     ],
                 } @ 2:7-2:17,
             } @ 2:0-2:17,
@@ -63,22 +71,22 @@ Ok(
                         } @ 3:3-3:6,
                     ),
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 3:11-3:12,
-                        Const {
-                            value: 2,
-                        } @ 3:14-3:15,
-                        Kwargs {
-                            pairs: [
-                                (
-                                    "a",
-                                    Const {
-                                        value: 1,
-                                    } @ 3:19-3:20,
-                                ),
-                            ],
-                        } @ 3:17-3:21,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 3:11-3:12,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 3:14-3:15,
+                        ),
+                        Kwarg(
+                            "a",
+                            Const {
+                                value: 1,
+                            } @ 3:19-3:20,
+                        ),
                     ],
                 } @ 3:7-3:21,
             } @ 3:0-3:21,
@@ -94,22 +102,22 @@ Ok(
                         } @ 4:3-4:6,
                     ),
                     args: [
-                        Const {
-                            value: 1,
-                        } @ 4:11-4:12,
-                        Const {
-                            value: 2,
-                        } @ 4:14-4:15,
-                        Kwargs {
-                            pairs: [
-                                (
-                                    "a",
-                                    Const {
-                                        value: 1,
-                                    } @ 4:19-4:20,
-                                ),
-                            ],
-                        } @ 4:17-4:22,
+                        Pos(
+                            Const {
+                                value: 1,
+                            } @ 4:11-4:12,
+                        ),
+                        Pos(
+                            Const {
+                                value: 2,
+                            } @ 4:14-4:15,
+                        ),
+                        Kwarg(
+                            "a",
+                            Const {
+                                value: 1,
+                            } @ 4:19-4:20,
+                        ),
                     ],
                 } @ 4:7-4:22,
             } @ 4:0-4:22,
diff --git a/minijinja/tests/snapshots/test_parser__parser@filter_block.txt.snap b/minijinja/tests/snapshots/test_parser__parser@filter_block.txt.snap
index 293a4b2..ed3d557 100644
--- a/minijinja/tests/snapshots/test_parser__parser@filter_block.txt.snap
+++ b/minijinja/tests/snapshots/test_parser__parser@filter_block.txt.snap
@@ -20,12 +20,16 @@ Ok(
                                 } @ 1:10-1:13,
                             ),
                             args: [
-                                Const {
-                                    value: 1,
-                                } @ 1:18-1:19,
-                                Const {
-                                    value: 2,
-                                } @ 1:21-1:22,
+                                Pos(
+                                    Const {
+                                        value: 1,
+                                    } @ 1:18-1:19,
+                                ),
+                                Pos(
+                                    Const {
+                                        value: 2,
+                                    } @ 1:21-1:22,
+                                ),
                             ],
                         } @ 1:14-1:23,
                     ),
diff --git a/minijinja/tests/snapshots/test_templates__vm@calls-and-literals.txt.snap b/minijinja/tests/snapshots/test_templates__vm@calls-and-literals.txt.snap
new file mode 100644
index 0000000..260995d
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@calls-and-literals.txt.snap
@@ -0,0 +1,18 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ get_args(1, 2, 3, 4, 5, 6, 7, 8, 9) }}\n{{ get_args(1, 2, 3, *[4, 5, 6], 7, 8, 9) }}\n{{ get_args(one, 2, 3, 4, 5, 6, 7, 8, 9) }}\n{{ get_args(1, 2, 3, *[4, 5, 6], 7, 8, nine) }}\n{{ get_args(1, 2, one=1, two=two) }}\n{{ get_args(1, 2, one=1, **dict(two=two)) }}\n{{ get_args(1, *[2], one=1, **dict(two=two)) }}\n{{ get_args(1, 2, *[3], *[4], **{8: 8, 9: 9})}}\n{{ get_args(1, 2, *[3], *[4], **{8: 8, nine: nine})}}"
+info:
+  one: 1
+  two: 2
+  nine: 9
+input_file: minijinja/tests/inputs/calls-and-literals.txt
+---
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2, 3, 4, 5, 6, 7, 8, 9]
+[1, 2, {"one": 1, "two": 2}]
+[1, 2, {"one": 1, "two": 2}]
+[1, 2, {"one": 1, "two": 2}]
+[1, 2, 3, 4, {8: 8, 9: 9}]
+[1, 2, 3, 4, {8: 8, 9: 9}]
diff --git a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
index 007fdcb..c5b7b04 100644
--- a/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
+++ b/minijinja/tests/snapshots/test_templates__vm@debug.txt.snap
@@ -26,6 +26,7 @@ State {
         globals: {
             "debug": minijinja::functions::builtins::debug,
             "dict": minijinja::functions::builtins::dict,
+            "get_args": test_templates::test_vm::{{closure}}::{{closure}},
             "namespace": minijinja::functions::builtins::namespace,
             "range": minijinja::functions::builtins::range,
         },
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_args_unpack.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_args_unpack.txt.snap
new file mode 100644
index 0000000..16ec4c5
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_args_unpack.txt.snap
@@ -0,0 +1,22 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ get_args(*true) }}"
+info: {}
+input_file: minijinja/tests/inputs/err_bad_args_unpack.txt
+---
+!!!ERROR!!!
+
+Error {
+    kind: InvalidOperation,
+    detail: "bool is not iterable",
+    name: "err_bad_args_unpack.txt",
+    line: 1,
+}
+
+invalid operation: bool is not iterable (in err_bad_args_unpack.txt:1)
+--------------------------- err_bad_args_unpack.txt ---------------------------
+   1 > {{ get_args(*true) }}
+     i    ^^^^^^^^^^^^^^^ invalid operation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+No referenced variables
+-------------------------------------------------------------------------------
diff --git a/minijinja/tests/snapshots/test_templates__vm@err_bad_kwargs_unpack.txt.snap b/minijinja/tests/snapshots/test_templates__vm@err_bad_kwargs_unpack.txt.snap
new file mode 100644
index 0000000..16e6a91
--- /dev/null
+++ b/minijinja/tests/snapshots/test_templates__vm@err_bad_kwargs_unpack.txt.snap
@@ -0,0 +1,22 @@
+---
+source: minijinja/tests/test_templates.rs
+description: "{{ get_args(**[1, 2, 3]) }}"
+info: {}
+input_file: minijinja/tests/inputs/err_bad_kwargs_unpack.txt
+---
+!!!ERROR!!!
+
+Error {
+    kind: InvalidOperation,
+    detail: "attempted to apply keyword arguments from non map (got sequence)",
+    name: "err_bad_kwargs_unpack.txt",
+    line: 1,
+}
+
+invalid operation: attempted to apply keyword arguments from non map (got sequence) (in err_bad_kwargs_unpack.txt:1)
+-------------------------- err_bad_kwargs_unpack.txt --------------------------
+   1 > {{ get_args(**[1, 2, 3]) }}
+     i    ^^^^^^^^^^^^^^^^^^^^^ invalid operation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+No referenced variables
+-------------------------------------------------------------------------------
diff --git a/minijinja/tests/test_templates.rs b/minijinja/tests/test_templates.rs
index f3fe5d7..22c7fdd 100644
--- a/minijinja/tests/test_templates.rs
+++ b/minijinja/tests/test_templates.rs
@@ -11,7 +11,7 @@ use std::sync::Arc;
 use std::{env, fs};
 
 use insta::assert_snapshot;
-use minijinja::value::{Enumerator, Object, ObjectRepr, Value};
+use minijinja::value::{Enumerator, Object, ObjectRepr, Rest, Value};
 use minijinja::{context, render, Environment, Error, ErrorKind, State};
 
 use similar_asserts::assert_eq;
@@ -36,6 +36,9 @@ fn test_vm() {
         let contents = std::fs::read_to_string(path).unwrap();
         let mut iter = contents.splitn(2, "\n---\n");
         let mut env = Environment::new();
+        env.add_function("get_args", |args: Rest<Value>| -> Value {
+            Value::from(args.0)
+        });
         let ctx: Value = serde_json::from_str(iter.next().unwrap()).unwrap();
 
         for (path, source) in &refs {
diff --git a/minijinja/tests/test_vm.rs b/minijinja/tests/test_vm.rs
index f13ebac..19d6cdb 100644
--- a/minijinja/tests/test_vm.rs
+++ b/minijinja/tests/test_vm.rs
@@ -307,7 +307,7 @@ fn test_call_object() {
         42 + a
     })));
     c.add(Instruction::LoadConst(Value::from(23i32)));
-    c.add(Instruction::CallObject(2));
+    c.add(Instruction::CallObject(Some(2)));
     c.add(Instruction::Emit);
 
     let output = simple_eval(&c.finish().0, ()).unwrap();