Skip to content

Commit 3885d10

Browse files
authored
Added argument splatting support (#642)
1 parent 92852d4 commit 3885d10

20 files changed

+492
-259
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ All notable changes to MiniJinja are documented here.
2929
- `minijinja-cli` now does not convert INI files to lowercase anymore. This was
3030
an unintended behavior. #633
3131
- Moved up MSRV to 1.63.0 due to indexmap. #635
32+
- Added argument splatting support (`*args` for variable args and `**kwargs`
33+
for keyword arguments) and fixed a bug where sometimes maps and keyword
34+
arguments were created in inverse order. #642
3235

3336
## 2.4.0
3437

minijinja/src/compiler/ast.rs

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,6 @@ pub enum Expr<'a> {
145145
Call(Spanned<Call<'a>>),
146146
List(Spanned<List<'a>>),
147147
Map(Spanned<Map<'a>>),
148-
Kwargs(Spanned<Kwargs<'a>>),
149148
}
150149

151150
#[cfg(feature = "internal_debug")]
@@ -165,7 +164,6 @@ impl<'a> fmt::Debug for Expr<'a> {
165164
Expr::Call(s) => fmt::Debug::fmt(s, f),
166165
Expr::List(s) => fmt::Debug::fmt(s, f),
167166
Expr::Map(s) => fmt::Debug::fmt(s, f),
168-
Expr::Kwargs(s) => fmt::Debug::fmt(s, f),
169167
}
170168
}
171169
}
@@ -186,7 +184,6 @@ impl<'a> Expr<'a> {
186184
Expr::Map(_) => "map literal",
187185
Expr::Test(_) => "test expression",
188186
Expr::Filter(_) => "filter expression",
189-
Expr::Kwargs(_) => "keyword arguments",
190187
}
191188
}
192189
}
@@ -444,7 +441,7 @@ pub struct IfExpr<'a> {
444441
pub struct Filter<'a> {
445442
pub name: &'a str,
446443
pub expr: Option<Expr<'a>>,
447-
pub args: Vec<Expr<'a>>,
444+
pub args: Vec<CallArg<'a>>,
448445
}
449446

450447
/// A test expression.
@@ -453,7 +450,7 @@ pub struct Filter<'a> {
453450
pub struct Test<'a> {
454451
pub name: &'a str,
455452
pub expr: Expr<'a>,
456-
pub args: Vec<Expr<'a>>,
453+
pub args: Vec<CallArg<'a>>,
457454
}
458455

459456
/// An attribute lookup expression.
@@ -477,7 +474,17 @@ pub struct GetItem<'a> {
477474
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
478475
pub struct Call<'a> {
479476
pub expr: Expr<'a>,
480-
pub args: Vec<Expr<'a>>,
477+
pub args: Vec<CallArg<'a>>,
478+
}
479+
480+
/// A call argument helper
481+
#[cfg_attr(feature = "internal_debug", derive(Debug))]
482+
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
483+
pub enum CallArg<'a> {
484+
Pos(Expr<'a>),
485+
Kwarg(&'a str, Expr<'a>),
486+
PosSplat(Expr<'a>),
487+
KwargSplat(Expr<'a>),
481488
}
482489

483490
/// Creates a list of values.
@@ -503,30 +510,6 @@ impl<'a> List<'a> {
503510
}
504511
}
505512

506-
/// Creates a map of kwargs
507-
#[cfg_attr(feature = "internal_debug", derive(Debug))]
508-
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
509-
pub struct Kwargs<'a> {
510-
pub pairs: Vec<(&'a str, Expr<'a>)>,
511-
}
512-
513-
impl<'a> Kwargs<'a> {
514-
pub fn as_const(&self) -> Option<Value> {
515-
if !self.pairs.iter().all(|x| matches!(x.1, Expr::Const(_))) {
516-
return None;
517-
}
518-
519-
let mut rv = value_map_with_capacity(self.pairs.len());
520-
for (key, value) in &self.pairs {
521-
if let Expr::Const(value) = value {
522-
rv.insert(Value::from(*key), value.value.clone());
523-
}
524-
}
525-
526-
Some(crate::value::Kwargs::wrap(rv))
527-
}
528-
}
529-
530513
/// Creates a map of values.
531514
#[cfg_attr(feature = "internal_debug", derive(Debug))]
532515
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]

minijinja/src/compiler/codegen.rs

Lines changed: 106 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ use crate::compiler::instructions::{
77
use crate::compiler::tokens::Span;
88
use crate::output::CaptureMode;
99
use crate::value::ops::neg;
10-
use crate::value::Value;
10+
use crate::value::{Kwargs, Value, ValueMap};
1111

1212
#[cfg(test)]
1313
use similar_asserts::assert_eq;
@@ -503,7 +503,7 @@ impl<'source> CodeGenerator<'source> {
503503
self.add_with_span(Instruction::FastSuper, call.span());
504504
return;
505505
} else if name == "loop" && call.args.len() == 1 {
506-
self.compile_expr(&call.args[0]);
506+
self.compile_call_args(std::slice::from_ref(&call.args[0]), 0, None);
507507
self.add(Instruction::FastRecurse);
508508
return;
509509
}
@@ -660,21 +660,17 @@ impl<'source> CodeGenerator<'source> {
660660
if let Some(ref expr) = f.expr {
661661
self.compile_expr(expr);
662662
}
663-
for arg in &f.args {
664-
self.compile_expr(arg);
665-
}
663+
let arg_count = self.compile_call_args(&f.args, 1, None);
666664
let local_id = get_local_id(&mut self.filter_local_ids, f.name);
667-
self.add(Instruction::ApplyFilter(f.name, f.args.len() + 1, local_id));
665+
self.add(Instruction::ApplyFilter(f.name, arg_count, local_id));
668666
self.pop_span();
669667
}
670668
ast::Expr::Test(f) => {
671669
self.push_span(f.span());
672670
self.compile_expr(&f.expr);
673-
for arg in &f.args {
674-
self.compile_expr(arg);
675-
}
671+
let arg_count = self.compile_call_args(&f.args, 1, None);
676672
let local_id = get_local_id(&mut self.test_local_ids, f.name);
677-
self.add(Instruction::PerformTest(f.name, f.args.len() + 1, local_id));
673+
self.add(Instruction::PerformTest(f.name, arg_count, local_id));
678674
self.pop_span();
679675
}
680676
ast::Expr::GetAttr(g) => {
@@ -717,18 +713,6 @@ impl<'source> CodeGenerator<'source> {
717713
self.add(Instruction::BuildMap(m.keys.len()));
718714
}
719715
}
720-
ast::Expr::Kwargs(m) => {
721-
if let Some(val) = m.as_const() {
722-
self.add(Instruction::LoadConst(val));
723-
} else {
724-
self.set_line_from_span(m.span());
725-
for (key, value) in &m.pairs {
726-
self.add(Instruction::LoadConst(Value::from(*key)));
727-
self.compile_expr(value);
728-
}
729-
self.add(Instruction::BuildKwargs(m.pairs.len()));
730-
}
731-
}
732716
}
733717
}
734718

@@ -740,7 +724,7 @@ impl<'source> CodeGenerator<'source> {
740724
self.push_span(c.span());
741725
match c.identify_call() {
742726
ast::CallType::Function(name) => {
743-
let arg_count = self.compile_call_args(&c.args, caller);
727+
let arg_count = self.compile_call_args(&c.args, 0, caller);
744728
self.add(Instruction::CallFunction(name, arg_count));
745729
}
746730
#[cfg(feature = "multi_template")]
@@ -751,71 +735,126 @@ impl<'source> CodeGenerator<'source> {
751735
}
752736
ast::CallType::Method(expr, name) => {
753737
self.compile_expr(expr);
754-
let arg_count = self.compile_call_args(&c.args, caller);
755-
self.add(Instruction::CallMethod(name, arg_count + 1));
738+
let arg_count = self.compile_call_args(&c.args, 1, caller);
739+
self.add(Instruction::CallMethod(name, arg_count));
756740
}
757741
ast::CallType::Object(expr) => {
758742
self.compile_expr(expr);
759-
let arg_count = self.compile_call_args(&c.args, caller);
760-
self.add(Instruction::CallObject(arg_count + 1));
743+
let arg_count = self.compile_call_args(&c.args, 1, caller);
744+
self.add(Instruction::CallObject(arg_count));
761745
}
762746
};
763747
self.pop_span();
764748
}
765749

766750
fn compile_call_args(
767751
&mut self,
768-
args: &[ast::Expr<'source>],
752+
args: &[ast::CallArg<'source>],
753+
extra_args: usize,
769754
caller: Option<&Caller<'source>>,
770-
) -> usize {
771-
match caller {
772-
// we can conditionally compile the caller part here since this will
773-
// nicely call through for non macro builds
774-
#[cfg(feature = "macros")]
775-
Some(caller) => self.compile_call_args_with_caller(args, caller),
776-
_ => {
777-
for arg in args {
778-
self.compile_expr(arg);
755+
) -> Option<u16> {
756+
let mut pending_args = extra_args;
757+
let mut num_args_batches = 0;
758+
let mut has_kwargs = caller.is_some();
759+
let mut static_kwargs = caller.is_none();
760+
761+
for arg in args {
762+
match arg {
763+
ast::CallArg::Pos(expr) => {
764+
self.compile_expr(expr);
765+
pending_args += 1;
766+
}
767+
ast::CallArg::PosSplat(expr) => {
768+
if pending_args > 0 {
769+
self.add(Instruction::BuildList(Some(pending_args)));
770+
pending_args = 0;
771+
num_args_batches += 1;
772+
}
773+
self.compile_expr(expr);
774+
num_args_batches += 1;
775+
}
776+
ast::CallArg::Kwarg(_, expr) => {
777+
if !matches!(expr, ast::Expr::Const(_)) {
778+
static_kwargs = false;
779+
}
780+
has_kwargs = true;
781+
}
782+
ast::CallArg::KwargSplat(_) => {
783+
static_kwargs = false;
784+
has_kwargs = true;
779785
}
780-
args.len()
781786
}
782787
}
783-
}
784788

785-
#[cfg(feature = "macros")]
786-
fn compile_call_args_with_caller(
787-
&mut self,
788-
args: &[ast::Expr<'source>],
789-
caller: &Caller<'source>,
790-
) -> usize {
791-
let mut injected_caller = false;
789+
if has_kwargs {
790+
let mut pending_kwargs = 0;
791+
let mut num_kwargs_batches = 0;
792+
let mut collected_kwargs = ValueMap::new();
793+
for arg in args {
794+
match arg {
795+
ast::CallArg::Kwarg(key, value) => {
796+
if static_kwargs {
797+
if let ast::Expr::Const(c) = value {
798+
collected_kwargs.insert(Value::from(*key), c.value.clone());
799+
} else {
800+
unreachable!();
801+
}
802+
} else {
803+
self.add(Instruction::LoadConst(Value::from(*key)));
804+
self.compile_expr(value);
805+
pending_kwargs += 1;
806+
}
807+
}
808+
ast::CallArg::KwargSplat(expr) => {
809+
if pending_kwargs > 0 {
810+
self.add(Instruction::BuildKwargs(pending_kwargs));
811+
num_kwargs_batches += 1;
812+
pending_kwargs = 0;
813+
}
814+
self.compile_expr(expr);
815+
num_kwargs_batches += 1;
816+
}
817+
ast::CallArg::Pos(_) | ast::CallArg::PosSplat(_) => {}
818+
}
819+
}
792820

793-
// try to add the caller to already existing keyword arguments.
794-
for arg in args {
795-
if let ast::Expr::Kwargs(ref m) = arg {
796-
self.set_line_from_span(m.span());
797-
for (key, value) in &m.pairs {
798-
self.add(Instruction::LoadConst(Value::from(*key)));
799-
self.compile_expr(value);
800-
}
801-
self.add(Instruction::LoadConst(Value::from("caller")));
802-
self.compile_macro_expression(caller);
803-
self.add(Instruction::BuildKwargs(m.pairs.len() + 1));
804-
injected_caller = true;
821+
if !collected_kwargs.is_empty() {
822+
self.add(Instruction::LoadConst(Kwargs::wrap(collected_kwargs)));
805823
} else {
806-
self.compile_expr(arg);
824+
// The conditions above guarantee that if we collect static kwargs
825+
// we cannot enter this block (single kwargs batch, no caller).
826+
827+
#[cfg(feature = "macros")]
828+
{
829+
if let Some(caller) = caller {
830+
self.add(Instruction::LoadConst(Value::from("caller")));
831+
self.compile_macro_expression(caller);
832+
pending_kwargs += 1
833+
}
834+
}
835+
if num_kwargs_batches > 0 {
836+
if pending_kwargs > 0 {
837+
self.add(Instruction::BuildKwargs(pending_kwargs));
838+
num_kwargs_batches += 1;
839+
}
840+
self.add(Instruction::MergeKwargs(num_kwargs_batches));
841+
} else {
842+
self.add(Instruction::BuildKwargs(pending_kwargs));
843+
}
807844
}
845+
pending_args += 1;
808846
}
809847

810-
// if there are no keyword args so far, create a new kwargs object
811-
// and add caller to that.
812-
if !injected_caller {
813-
self.add(Instruction::LoadConst(Value::from("caller")));
814-
self.compile_macro_expression(caller);
815-
self.add(Instruction::BuildKwargs(1));
816-
args.len() + 1
848+
if num_args_batches > 0 {
849+
if pending_args > 0 {
850+
self.add(Instruction::BuildList(Some(pending_args)));
851+
num_args_batches += 1;
852+
}
853+
self.add(Instruction::UnpackLists(num_args_batches));
854+
None
817855
} else {
818-
args.len()
856+
assert!(pending_args as u16 as usize == pending_args);
857+
Some(pending_args as u16)
819858
}
820859
}
821860

minijinja/src/compiler/instructions.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,18 @@ pub enum Instruction<'source> {
6060
/// Builds a kwargs map of the last n pairs on the stack.
6161
BuildKwargs(usize),
6262

63+
/// Merges N kwargs maps on the list into one.
64+
MergeKwargs(usize),
65+
6366
/// Builds a list of the last n pairs on the stack.
6467
BuildList(Option<usize>),
6568

6669
/// Unpacks a list into N stack items.
6770
UnpackList(usize),
6871

72+
/// Unpacks N lists onto the stack and pushes the number of items there were unpacked.
73+
UnpackLists(usize),
74+
6975
/// Add the top two values
7076
Add,
7177

@@ -122,10 +128,10 @@ pub enum Instruction<'source> {
122128
In,
123129

124130
/// Apply a filter.
125-
ApplyFilter(&'source str, usize, LocalId),
131+
ApplyFilter(&'source str, Option<u16>, LocalId),
126132

127133
/// Perform a filter.
128-
PerformTest(&'source str, usize, LocalId),
134+
PerformTest(&'source str, Option<u16>, LocalId),
129135

130136
/// Emit the stack top as output
131137
Emit,
@@ -175,13 +181,13 @@ pub enum Instruction<'source> {
175181
EndCapture,
176182

177183
/// Calls a global function
178-
CallFunction(&'source str, usize),
184+
CallFunction(&'source str, Option<u16>),
179185

180186
/// Calls a method
181-
CallMethod(&'source str, usize),
187+
CallMethod(&'source str, Option<u16>),
182188

183189
/// Calls an object
184-
CallObject(usize),
190+
CallObject(Option<u16>),
185191

186192
/// Duplicates the top item
187193
DupTop,

0 commit comments

Comments
 (0)