Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds support for tuple unpacking in With blocks #66

Merged
merged 3 commits into from
Jun 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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