Skip to content

Commit

Permalink
Adds support for tuple unpacking in With blocks (#66)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Jun 16, 2022
1 parent 180b097 commit 9617ec8
Show file tree
Hide file tree
Showing 12 changed files with 106 additions and 47 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to MiniJinja are documented here.

# 0.16.0

- Added support for unpacking in `with` blocks. (#65)

# 0.15.0

- Bumped minimum version requirement to 1.43.
Expand Down
2 changes: 1 addition & 1 deletion minijinja/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ pub struct IfCond<'a> {
/// A with block.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct WithBlock<'a> {
pub assignments: Vec<(&'a str, Expr<'a>)>,
pub assignments: Vec<(Expr<'a>, Expr<'a>)>,
pub body: Vec<Stmt<'a>>,
}

Expand Down
5 changes: 2 additions & 3 deletions minijinja/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,11 @@ impl<'source> Compiler<'source> {
}
ast::Stmt::WithBlock(with_block) => {
self.set_location_from_span(with_block.span());
self.add(Instruction::PushWith);
for (target, expr) in &with_block.assignments {
self.add(Instruction::LoadConst(Value::from(*target)));
self.compile_expr(expr)?;
self.compile_assignment(target)?;
}
self.add(Instruction::BuildMap(with_block.assignments.len()));
self.add(Instruction::PushContext);
for node in &with_block.body {
self.compile_stmt(node)?;
}
Expand Down
8 changes: 4 additions & 4 deletions minijinja/src/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ pub enum Instruction<'source> {
/// The argument are loop flags.
PushLoop(u8),

/// Pushes a value as context layer.
PushContext,
/// Starts a with block.
PushWith,

/// Does a single loop iteration
///
Expand Down Expand Up @@ -229,7 +229,7 @@ impl<'source> fmt::Debug for Instruction<'source> {
loop_var, recursive
)
}
Instruction::PushContext => write!(f, "PUSH_CONTEXT"),
Instruction::PushWith => write!(f, "PUSH_WITH"),
Instruction::Iterate(t) => write!(f, "ITERATE (exit to {:>05x})", t),
Instruction::PopFrame => write!(f, "POP_FRAME"),
Instruction::Jump(t) => write!(f, "JUMP (to {:>05x})", t),
Expand Down Expand Up @@ -349,7 +349,7 @@ impl<'source> Instructions<'source> {
| Instruction::StoreLocal(name)
| Instruction::CallFunction(name) => *name,
Instruction::PushLoop(flags) if flags & LOOP_FLAG_WITH_LOOP_VAR != 0 => "loop",
Instruction::PushLoop(_) | Instruction::PushContext => break,
Instruction::PushLoop(_) | Instruction::PushWith => break,
_ => continue,
};
if !rv.contains(&name) {
Expand Down
4 changes: 2 additions & 2 deletions minijinja/src/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ pub fn find_undeclared_variables(source: &str) -> Result<HashSet<String>, Error>
}
ast::Stmt::WithBlock(stmt) => {
state.push();
for (name, expr) in &stmt.assignments {
state.assign(name);
for (target, expr) in &stmt.assignments {
assign_nested(target, state);
visit_expr(expr, state);
}
stmt.body.iter().for_each(|x| walk(x, state));
Expand Down
10 changes: 7 additions & 3 deletions minijinja/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -699,9 +699,13 @@ impl<'a> Parser<'a> {
if !assignments.is_empty() {
expect_token!(self, Token::Comma, "comma")?;
}
let target = match self.parse_assign_name()? {
ast::Expr::Var(var) => var.id,
_ => panic!("unexpected node from assignment parse"),
let target = if matches!(self.stream.current()?, Some((Token::ParenOpen, _))) {
self.stream.next()?;
let assign = self.parse_assignment()?;
expect_token!(self, Token::ParenClose, "`)`")?;
assign
} else {
self.parse_assign_name()?
};
expect_token!(self, Token::Assign, "assignment operator")?;
let expr = self.parse_expr()?;
Expand Down
8 changes: 8 additions & 0 deletions minijinja/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,14 @@
//! foo is not visible here any longer
//! ```
//!
//! Multiple variables can be set at once and unpacking is supported:
//!
//! ```jinja
//! {% with a = 1, (b, c) = [2, 3] %}
//! {{ a }}, {{ b }}, {{ c }} (outputs 1, 2, 3)
//! {% endwith %}
//! ```
//!
//! ## `{% filter %}`
//!
//! Filter sections allow you to apply regular [filters](crate::filters) on a
Expand Down
79 changes: 50 additions & 29 deletions minijinja/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,11 @@ impl fmt::Display for LoopState {
}
}

type Locals<'env> = BTreeMap<&'env str, Value>;

#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Loop<'env> {
locals: BTreeMap<&'env str, Value>,
locals: Locals<'env>,
with_loop_var: bool,
recurse_jump_target: Option<usize>,
// if we're popping the frame, do we want to jump somewhere? The
Expand All @@ -107,13 +109,18 @@ pub struct Loop<'env> {
controller: RcType<LoopState>,
}

#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct With<'env> {
locals: Locals<'env>,
}

pub enum Frame<'env, 'vm> {
// This layer dispatches to another context
Chained { base: &'vm Context<'env, 'vm> },
// this layer isolates
Isolate { value: Value },
// this layer shadows another one
Merge { value: Value },
// this layer is a with statement.
With(With<'env>),
// this layer is a for loop
Loop(Loop<'env>),
}
Expand All @@ -124,7 +131,11 @@ impl<'env, 'vm> fmt::Debug for Frame<'env, 'vm> {
match self {
Self::Chained { base } => fmt::Debug::fmt(base, f),
Self::Isolate { value } => fmt::Debug::fmt(value, f),
Self::Merge { value } => fmt::Debug::fmt(value, f),
Self::With(w) => {
let mut m = f.debug_map();
m.entries(w.locals.iter());
m.finish()
}
Self::Loop(l) => {
let mut m = f.debug_map();
m.entries(l.locals.iter());
Expand Down Expand Up @@ -180,7 +191,10 @@ impl<'env, 'vm> Context<'env, 'vm> {
// recurse
Frame::Chained { base } => return base.freeze(env),
Frame::Isolate { value } => (value, false),
Frame::Merge { value } => (value, true),
Frame::With(With { locals }) => {
rv.extend(locals.iter().map(|(k, v)| (*k, v.clone())));
continue;
}
Frame::Loop(Loop {
locals,
controller,
Expand Down Expand Up @@ -208,21 +222,40 @@ impl<'env, 'vm> Context<'env, 'vm> {

/// Stores a variable in the context.
pub fn store(&mut self, key: &'env str, value: Value) {
self.current_loop()
.expect("can only assign to loop but not inside a loop")
.locals
.insert(key, value);
if let Frame::Loop(Loop { locals, .. }) | Frame::With(With { locals }) =
self.stack.last_mut().expect("cannot store on empty stack")
{
locals.insert(key, value);
} else {
panic!("can only assign to a loop or with statement")
}
}

/// Looks up a variable in the context.
pub fn load(&self, env: &Environment, key: &str) -> Option<Value> {
for ctx in self.stack.iter().rev() {
let (lookup_base, cont) = match ctx {
match ctx {
// if we hit a chain frame we dispatch there and never
// recurse
Frame::Chained { base } => return base.load(env, key),
Frame::Isolate { value } => (value, false),
Frame::Merge { value } => (value, true),
Frame::Isolate { value } => {
let rv = value.get_attr(key);
if let Ok(rv) = rv {
if !rv.is_undefined() {
return Some(rv);
}
}
if let Some(value) = env.get_global(key) {
return Some(value);
}
break;
}
Frame::With(With { locals }) => {
if let Some(value) = locals.get(key) {
return Some(value.clone());
}
continue;
}
Frame::Loop(Loop {
locals,
controller,
Expand All @@ -237,19 +270,6 @@ impl<'env, 'vm> Context<'env, 'vm> {
continue;
}
};

let rv = lookup_base.get_attr(key);
if let Ok(rv) = rv {
if !rv.is_undefined() {
return Some(rv);
}
}
if !cont {
if let Some(value) = env.get_global(key) {
return Some(value);
}
break;
}
}
None
}
Expand Down Expand Up @@ -683,9 +703,10 @@ impl<'env> Vm<'env> {
let a = stack.pop();
stack.push(try_ctx!(value::neg(&a)));
}
Instruction::PushContext => {
let value = stack.pop();
state.ctx.push_frame(Frame::Merge { value });
Instruction::PushWith => {
state.ctx.push_frame(Frame::With(With {
locals: Locals::new(),
}));
}
Instruction::PopFrame => {
if let Frame::Loop(mut loop_ctx) = state.ctx.pop_frame() {
Expand All @@ -710,7 +731,7 @@ impl<'env> Vm<'env> {
.map_or(0, |x| x.controller.depth + 1);
let recursive = *flags & LOOP_FLAG_RECURSIVE != 0;
state.ctx.push_frame(Frame::Loop(Loop {
locals: BTreeMap::new(),
locals: Locals::new(),
iterator,
with_loop_var: *flags & LOOP_FLAG_WITH_LOOP_VAR != 0,
recurse_jump_target: if recursive { Some(pc) } else { None },
Expand Down
10 changes: 10 additions & 0 deletions minijinja/tests/inputs/with.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
foo: 42
bar: 23
other: 11
tuple: [1, 2, [3]]
tuple2: [[1], 2, 3]
---
{% with a=foo, b=bar %}
{{ a }}|{{ b }}|{{ other }}
{% endwith %}

{% with (a, b, (c,)) = tuple %}
{{ a }}|{{ b }}|{{ c }}
{% endwith %}

{% with ((a,), b, c) = tuple2 %}
{{ a }}|{{ b }}|{{ c }}
{% endwith %}
13 changes: 9 additions & 4 deletions minijinja/tests/snapshots/test_parser__parser@with.txt.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,24 @@
source: minijinja/tests/test_parser.rs
expression: "&ast"
input_file: minijinja/tests/parser-inputs/with.txt

---
Ok(
Template {
children: [
WithBlock {
assignments: [
(
"a",
Var {
id: "a",
} @ 1:8-1:9,
Var {
id: "foo",
} @ 1:10-1:13,
),
(
"b",
Var {
id: "b",
} @ 1:15-1:16,
Var {
id: "bar",
} @ 1:17-1:20,
Expand Down Expand Up @@ -50,7 +53,9 @@ Ok(
WithBlock {
assignments: [
(
"a",
Var {
id: "a",
} @ 5:8-5:9,
Var {
id: "foo",
} @ 5:10-5:13,
Expand Down
9 changes: 8 additions & 1 deletion minijinja/tests/snapshots/test_templates__vm@with.txt.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@
source: minijinja/tests/test_templates.rs
expression: "&rendered"
input_file: minijinja/tests/inputs/with.txt

---

42|23|11



1|2|3



1|2|3


1 change: 1 addition & 0 deletions minijinja/tests/test_templates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fn test_vm() {

env.add_template(filename, iter.next().unwrap()).unwrap();
let template = env.get_template(filename).unwrap();
dbg!(&template);

let mut rendered = match template.render(ctx) {
Ok(rendered) => rendered,
Expand Down

0 comments on commit 9617ec8

Please sign in to comment.