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

Add support for break and continue #558

Merged
merged 5 commits into from
Aug 14, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

All notable changes to MiniJinja are documented here.

## 2.1.3
## 2.2.0

- Fixes a bug where some enums did not deserialize correctly when
used with `ViaDeserialize`. #554
- Implemented `IntoDeserializer` for `Value` and `&Value`. #555
- Added `filesizeformat` to minijinja-contrib. #556
- Added support for the `loop_controls` feature which adds
`{% break %}` and `{% continue %}`. #558

## 2.1.2

Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
DOC_FEATURES=loader,json,urlencode,custom_syntax,fuel
TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax,deserialization,serde
TEST_FEATURES=unstable_machinery,builtins,loader,json,urlencode,debug,internal_debug,macros,multi_template,adjacent_loop_items,custom_syntax,deserialization,serde,loop_controls

.PHONY: all
all: test
Expand Down Expand Up @@ -40,7 +40,7 @@ snapshot-tests:
run-tests:
@rustup component add rustfmt 2> /dev/null
@echo "CARGO TESTS"
@cd minijinja; cargo test --features=json,urlencode,internal_debug
@cd minijinja; cargo test --features=json,urlencode,internal_debug,loop_controls
@echo "CARGO TEST SPEEDUPS"
@cd minijinja; cargo test --no-default-features --features=speedups,$(TEST_FEATURES)
@echo "CARGO CHECK NO_DEFAULT_FEATURES"
Expand Down
1 change: 1 addition & 0 deletions minijinja-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ minijinja = { version = "=2.1.2", path = "../minijinja", features = [
"fuel",
"unstable_machinery",
"custom_syntax",
"loop_controls"
] }
minijinja-contrib = { version = "=2.1.2", optional = true, path = "../minijinja-contrib", features = ["pycompat", "datetime", "timezone", "rand"] }
rustyline = { version = "12.0.0", optional = true }
Expand Down
1 change: 1 addition & 0 deletions minijinja/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ builtins = []
macros = []
multi_template = []
adjacent_loop_items = []
loop_controls = []
fuel = []

# Extra Filters
Expand Down
20 changes: 20 additions & 0 deletions minijinja/src/compiler/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ pub enum Stmt<'a> {
Macro(Spanned<Macro<'a>>),
#[cfg(feature = "macros")]
CallBlock(Spanned<CallBlock<'a>>),
#[cfg(feature = "loop_controls")]
Continue(Spanned<Continue>),
#[cfg(feature = "loop_controls")]
Break(Spanned<Break>),
Do(Spanned<Do<'a>>),
}

Expand Down Expand Up @@ -111,6 +115,10 @@ impl<'a> fmt::Debug for Stmt<'a> {
Stmt::Macro(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "macros")]
Stmt::CallBlock(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Continue(s) => fmt::Debug::fmt(s, f),
#[cfg(feature = "loop_controls")]
Stmt::Break(s) => fmt::Debug::fmt(s, f),
Stmt::Do(s) => fmt::Debug::fmt(s, f),
}
}
Expand Down Expand Up @@ -298,6 +306,18 @@ pub struct CallBlock<'a> {
pub macro_decl: Spanned<Macro<'a>>,
}

/// Continue
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Continue;

/// Break
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg(feature = "loop_controls")]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
pub struct Break;

/// A call block
#[cfg_attr(feature = "internal_debug", derive(Debug))]
#[cfg_attr(feature = "unstable_machinery_serde", derive(serde::Serialize))]
Expand Down
58 changes: 40 additions & 18 deletions minijinja/src/compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn get_local_id<'source>(ids: &mut BTreeMap<&'source str, LocalId>, name: &'sour
/// jump targets.
enum PendingBlock {
Branch(usize),
Loop(usize),
Loop(usize, Vec<usize>),
ScBool(Vec<usize>),
}

Expand Down Expand Up @@ -135,29 +135,30 @@ impl<'source> CodeGenerator<'source> {
flags |= LOOP_FLAG_RECURSIVE;
}
self.add(Instruction::PushLoop(flags));
let iter_instr = self.add(Instruction::Iterate(!0));
self.pending_block.push(PendingBlock::Loop(iter_instr));
let instr = self.add(Instruction::Iterate(!0));
self.pending_block.push(PendingBlock::Loop(instr, vec![]));
}

/// Ends the open for loop
pub fn end_for_loop(&mut self, push_did_not_iterate: bool) {
match self.pending_block.pop() {
Some(PendingBlock::Loop(iter_instr)) => {
self.add(Instruction::Jump(iter_instr));
let loop_end = self.next_instruction();
if push_did_not_iterate {
self.add(Instruction::PushDidNotIterate);
};
self.add(Instruction::PopFrame);
if let Some(Instruction::Iterate(ref mut jump_target)) =
self.instructions.get_mut(iter_instr)
{
*jump_target = loop_end;
} else {
unreachable!();
if let Some(PendingBlock::Loop(iter_instr, breaks)) = self.pending_block.pop() {
self.add(Instruction::Jump(iter_instr));
let loop_end = self.next_instruction();
if push_did_not_iterate {
self.add(Instruction::PushDidNotIterate);
};
self.add(Instruction::PopFrame);
for instr in breaks.into_iter().chain(Some(iter_instr)) {
match self.instructions.get_mut(instr) {
Some(Instruction::Iterate(ref mut jump_target))
| Some(Instruction::Jump(ref mut jump_target)) => {
*jump_target = loop_end;
}
_ => unreachable!(),
}
}
_ => unreachable!(),
} else {
unreachable!()
}
}

Expand Down Expand Up @@ -347,6 +348,27 @@ impl<'source> CodeGenerator<'source> {
ast::Stmt::CallBlock(call_block) => {
self.compile_call_block(call_block);
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(cont) => {
self.set_line_from_span(cont.span());
for pending_block in self.pending_block.iter().rev() {
if let PendingBlock::Loop(instr, _) = pending_block {
self.add(Instruction::Jump(*instr));
break;
}
}
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Break(brk) => {
self.set_line_from_span(brk.span());
let instr = self.add(Instruction::Jump(0));
for pending_block in self.pending_block.iter_mut().rev() {
if let PendingBlock::Loop(_, ref mut breaks) = pending_block {
breaks.push(instr);
break;
}
}
}
ast::Stmt::Do(do_tag) => {
self.compile_do(do_tag);
}
Expand Down
2 changes: 2 additions & 0 deletions minijinja/src/compiler/meta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ fn track_walk<'a>(node: &ast::Stmt<'a>, state: &mut AssignmentTracker<'a>) {
.for_each(|x| tracker_visit_expr(x, state));
tracker_visit_macro(&stmt.macro_decl, state);
}
#[cfg(feature = "loop_controls")]
ast::Stmt::Continue(_) | ast::Stmt::Break(_) => {}
ast::Stmt::Do(stmt) => {
tracker_visit_expr(&stmt.call.expr, state);
stmt.call
Expand Down
23 changes: 23 additions & 0 deletions minijinja/src/compiler/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ struct Parser<'a> {
#[allow(unused)]
in_macro: bool,
#[allow(unused)]
in_loop: bool,
#[allow(unused)]
blocks: BTreeSet<&'a str>,
depth: usize,
}
Expand Down Expand Up @@ -236,6 +238,7 @@ impl<'a> Parser<'a> {
Parser {
stream: TokenStream::new(source, in_expr, syntax_config, whitespace_config),
in_macro: false,
in_loop: false,
blocks: BTreeSet::new(),
depth: 0,
}
Expand Down Expand Up @@ -692,6 +695,20 @@ impl<'a> Parser<'a> {
"macro" => ast::Stmt::Macro(respan!(ok!(self.parse_macro()))),
#[cfg(feature = "macros")]
"call" => ast::Stmt::CallBlock(respan!(ok!(self.parse_call_block()))),
#[cfg(feature = "loop_controls")]
"continue" => {
if !self.in_loop {
syntax_error!("'continue' must be placed inside a loop");
}
ast::Stmt::Continue(respan!(ast::Continue))
}
#[cfg(feature = "loop_controls")]
"break" => {
if !self.in_loop {
syntax_error!("'break' must be placed inside a loop");
}
ast::Stmt::Break(respan!(ast::Break))
}
"do" => ast::Stmt::Do(respan!(ok!(self.parse_do()))),
name => syntax_error!("unknown statement {}", name),
})
Expand Down Expand Up @@ -758,6 +775,7 @@ impl<'a> Parser<'a> {
}

fn parse_for_stmt(&mut self) -> Result<ast::ForLoop<'a>, Error> {
let old_in_loop = std::mem::replace(&mut self.in_loop, true);
let target = ok!(self.parse_assignment());
expect_token!(self, Token::Ident("in"), "in");
let iter = ok!(self.parse_expr_noif());
Expand All @@ -776,6 +794,7 @@ impl<'a> Parser<'a> {
Vec::new()
};
ok!(self.stream.next());
self.in_loop = old_in_loop;
Ok(ast::ForLoop {
target,
iter,
Expand Down Expand Up @@ -872,6 +891,7 @@ impl<'a> Parser<'a> {
if self.in_macro {
syntax_error!("block tags in macros are not allowed");
}
let old_in_loop = std::mem::replace(&mut self.in_loop, false);
let (name, _) = expect_token!(self, Token::Ident(name) => name, "identifier");
if !self.blocks.insert(name) {
syntax_error!("block '{}' defined twice", name);
Expand All @@ -891,6 +911,7 @@ impl<'a> Parser<'a> {
}
ok!(self.stream.next());
}
self.in_loop = old_in_loop;

Ok(ast::Block { name, body })
}
Expand Down Expand Up @@ -1035,13 +1056,15 @@ impl<'a> Parser<'a> {
name: Option<&'a str>,
) -> Result<ast::Macro<'a>, Error> {
expect_token!(self, Token::BlockEnd, "end of block");
let old_in_loop = std::mem::replace(&mut self.in_loop, false);
let old_in_macro = std::mem::replace(&mut self.in_macro, true);
let body = ok!(self.subparse(&|tok| match tok {
Token::Ident("endmacro") if name.is_some() => true,
Token::Ident("endcall") if name.is_none() => true,
_ => false,
}));
self.in_macro = old_in_macro;
self.in_loop = old_in_loop;
ok!(self.stream.next());
Ok(ast::Macro {
name: name.unwrap_or("caller"),
Expand Down
2 changes: 2 additions & 0 deletions minijinja/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@
//! - `json`: When enabled the `tojson` filter is added as builtin filter as well as
//! the ability to auto escape via `AutoEscape::Json`.
//! - `urlencode`: When enabled the `urlencode` filter is added as builtin filter.
//! - `loop_controls`: enables the `{% break %}` and `{% continue %}` loop control flow
//! tags.
//!
//! Performance and memory related features:
//!
Expand Down
25 changes: 25 additions & 0 deletions minijinja/src/syntax.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
//! - [`{% do %}`](#-do-)
//! - [`{% autoescape %}`](#-autoescape-)
//! - [`{% raw %}`](#-raw-)
//! - [`{% break %} / {% continue %}`](#-break----continue-)
#![cfg_attr(
feature = "custom_syntax",
doc = "- [Custom Delimiters](#custom-delimiters)"
Expand Down Expand Up @@ -721,6 +722,30 @@
//! {% endraw %}
//! ```
//!
//! ## `{% break %}` / `{% continue %}`
//!
//! If MiniJinja was compiled with the `loop_controls` feature, it’s possible to
//! use `break`` and `continue`` in loops. When break is reached, the loop is
//! terminated; if continue is reached, the processing is stopped and continues
//! with the next iteration.
//!
//! Here’s a loop that skips every second item:
//!
//! ```jinja
//! {% for user in users %}
//! {%- if loop.index is even %}{% continue %}{% endif %}
//! ...
//! {% endfor %}
//! ```
//!
//! Likewise, a loop that stops processing after the 10th iteration:
//!
//! ```jinja
//! {% for user in users %}
//! {%- if loop.index >= 10 %}{% break %}{% endif %}
//! {%- endfor %}
//! ```
//!
#![cfg_attr(
feature = "custom_syntax",
doc = r###"
Expand Down
3 changes: 3 additions & 0 deletions minijinja/tests/inputs/err_toplevel_break.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{}
---
{% break %}
3 changes: 3 additions & 0 deletions minijinja/tests/inputs/err_toplevel_continue.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{}
---
{% continue %}
7 changes: 7 additions & 0 deletions minijinja/tests/inputs/loop_controls.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{}
---
{% for item in range(10) -%}
{%- if item is odd %}{% continue %}{% endif %}
{%- if item > 5 %}{% break %}{% endif %}
{{- item }}
{%- endfor %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: minijinja/tests/test_templates.rs
description: "{% break %}"
info: {}
input_file: minijinja/tests/inputs/err_toplevel_break.txt
---
!!!SYNTAX ERROR!!!

Error {
kind: SyntaxError,
detail: "'break' must be placed inside a loop",
name: "err_toplevel_break.txt",
line: 1,
}

syntax error: 'break' must be placed inside a loop (in err_toplevel_break.txt:1)
--------------------------- err_toplevel_break.txt ----------------------------
1 > {% break %}
i ^^^^^ syntax error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No referenced variables
-------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
source: minijinja/tests/test_templates.rs
description: "{% continue %}"
info: {}
input_file: minijinja/tests/inputs/err_toplevel_continue.txt
---
!!!SYNTAX ERROR!!!

Error {
kind: SyntaxError,
detail: "'continue' must be placed inside a loop",
name: "err_toplevel_continue.txt",
line: 1,
}

syntax error: 'continue' must be placed inside a loop (in err_toplevel_continue.txt:1)
-------------------------- err_toplevel_continue.txt --------------------------
1 > {% continue %}
i ^^^^^^^^ syntax error
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
No referenced variables
-------------------------------------------------------------------------------
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
source: minijinja/tests/test_templates.rs
description: "{% for item in range(10) -%}\n {%- if item is odd %}{% continue %}{% endif %}\n {%- if item > 5 %}{% break %}{% endif %}\n {{- item }}\n{%- endfor %}"
info: {}
input_file: minijinja/tests/inputs/loop_controls.txt
---
024