Skip to content

Commit

Permalink
Added support for slicing (#120)
Browse files Browse the repository at this point in the history
  • Loading branch information
mitsuhiko committed Sep 17, 2022
1 parent 258544f commit 0e1d206
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 9 deletions.
11 changes: 11 additions & 0 deletions minijinja/src/compiler/ast.rs
Expand Up @@ -91,6 +91,7 @@ impl<'a> fmt::Debug for Stmt<'a> {
pub enum Expr<'a> {
Var(Spanned<Var<'a>>),
Const(Spanned<Const>),
Slice(Spanned<Slice<'a>>),
UnaryOp(Spanned<UnaryOp<'a>>),
BinOp(Spanned<BinOp<'a>>),
IfExpr(Spanned<IfExpr<'a>>),
Expand All @@ -109,6 +110,7 @@ impl<'a> fmt::Debug for Expr<'a> {
match self {
Expr::Var(s) => fmt::Debug::fmt(s, f),
Expr::Const(s) => fmt::Debug::fmt(s, f),
Expr::Slice(s) => fmt::Debug::fmt(s, f),
Expr::UnaryOp(s) => fmt::Debug::fmt(s, f),
Expr::BinOp(s) => fmt::Debug::fmt(s, f),
Expr::IfExpr(s) => fmt::Debug::fmt(s, f),
Expand Down Expand Up @@ -228,6 +230,15 @@ pub struct Const {
pub value: Value,
}

/// Represents a slice.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub struct Slice<'a> {
pub expr: Expr<'a>,
pub start: Option<Expr<'a>>,
pub stop: Option<Expr<'a>>,
pub step: Option<Expr<'a>>,
}

/// A kind of unary operator.
#[cfg_attr(feature = "internal_debug", derive(Debug))]
pub enum UnaryOpKind {
Expand Down
21 changes: 21 additions & 0 deletions minijinja/src/compiler/codegen.rs
Expand Up @@ -402,6 +402,27 @@ impl<'source> CodeGenerator<'source> {
self.set_line_from_span(v.span());
self.add(Instruction::LoadConst(v.value.clone()));
}
ast::Expr::Slice(s) => {
self.push_span(s.span());
self.compile_expr(&s.expr)?;
if let Some(ref start) = s.start {
self.compile_expr(start)?;
} else {
self.add(Instruction::LoadConst(Value::from(0)));
}
if let Some(ref stop) = s.stop {
self.compile_expr(stop)?;
} else {
self.add(Instruction::LoadConst(Value::from(())));
}
if let Some(ref step) = s.step {
self.compile_expr(step)?;
} else {
self.add(Instruction::LoadConst(Value::from(1)));
}
self.add(Instruction::Slice);
self.pop_span();
}
ast::Expr::UnaryOp(c) => {
self.set_line_from_span(c.span());
self.compile_expr(&c.expr)?;
Expand Down
3 changes: 3 additions & 0 deletions minijinja/src/compiler/instructions.rs
Expand Up @@ -31,6 +31,9 @@ pub enum Instruction<'source> {
/// Looks up an item.
GetItem,

/// Performs a slice operation.
Slice,

/// Loads a constant value.
LoadConst(Value),

Expand Down
55 changes: 47 additions & 8 deletions minijinja/src/compiler/parser.rs
Expand Up @@ -315,15 +315,54 @@ impl<'a> Parser<'a> {
}
Some((Token::BracketOpen, _)) => {
self.stream.next()?;
let subscript_expr = self.parse_expr()?;

let mut start = None;
let mut stop = None;
let mut step = None;
let mut is_slice = false;

if !matches!(self.stream.current()?, Some((Token::Colon, _))) {
start = Some(self.parse_expr()?);
}
if matches!(self.stream.current()?, Some((Token::Colon, _))) {
is_slice = true;
self.stream.next()?;
if !matches!(
self.stream.current()?,
Some((Token::BracketClose | Token::Colon, _))
) {
stop = Some(self.parse_expr()?);
}
if matches!(self.stream.current()?, Some((Token::Colon, _))) {
self.stream.next()?;
if !matches!(self.stream.current()?, Some((Token::BracketClose, _))) {
step = Some(self.parse_expr()?);
}
}
}
expect_token!(self, Token::BracketClose, "`]`")?;
expr = ast::Expr::GetItem(Spanned::new(
ast::GetItem {
expr,
subscript_expr,
},
self.stream.expand_span(span),
));

if !is_slice {
expr = ast::Expr::GetItem(Spanned::new(
ast::GetItem {
expr,
subscript_expr: start.ok_or_else(|| {
Error::new(ErrorKind::SyntaxError, "empty subscript")
})?,
},
self.stream.expand_span(span),
));
} else {
expr = ast::Expr::Slice(Spanned::new(
ast::Slice {
expr,
start,
stop,
step,
},
self.stream.expand_span(span),
));
}
}
Some((Token::ParenOpen, _)) => {
let args = self.parse_args()?;
Expand Down
2 changes: 2 additions & 0 deletions minijinja/src/syntax.rs
Expand Up @@ -136,6 +136,8 @@
//! which are treated like a dict syntax. Eg: `foo(a=1, b=2)` is the same as
//! `foo({"a": 1, "b": 2})`.
//! - ``.`` / ``[]``: Get an attribute of an object.
//! - ``[start:stop]`` / ``[start:stop:step]``: slices a list or string. All three expressions
//! are optional (`start`, `stop`, `step`).
//!
//! ### If Expressions
//!
Expand Down
69 changes: 68 additions & 1 deletion minijinja/src/value/ops.rs
@@ -1,4 +1,4 @@
use std::convert::TryFrom;
use std::convert::{TryFrom, TryInto};
use std::fmt::Write;

use crate::error::{Error, ErrorKind};
Expand Down Expand Up @@ -48,6 +48,73 @@ pub fn coerce(a: &Value, b: &Value) -> Option<CoerceResult> {
}
}

fn get_offset_and_len<F: FnOnce() -> usize>(
start: i64,
stop: Option<i64>,
end: F,
) -> (usize, usize) {
if start < 0 || stop.map_or(true, |x| x < 0) {
let end = end();
let start = if start < 0 {
(end as i64 + start) as usize
} else {
start as usize
};
let stop = match stop {
None => end,
Some(x) if x < 0 => (end as i64 + x) as usize,
Some(x) => x as usize,
};
(start, stop.saturating_sub(start))
} else {
(
start as usize,
(stop.unwrap() as usize).saturating_sub(start as usize),
)
}
}

pub fn slice(value: Value, start: Value, stop: Value, step: Value) -> Result<Value, Error> {
let start: i64 = if start.is_none() {
0
} else {
start.try_into()?
};
let stop: Option<i64> = if stop.is_none() {
None
} else {
Some(stop.try_into()?)
};
let step = if step.is_none() {
1
} else {
u64::try_from(step)? as usize
};

if let Some(s) = value.as_str() {
let (start, len) = get_offset_and_len(start, stop, || s.chars().count());
return Ok(Value::from(
s.chars()
.skip(start)
.take(len)
.step_by(step)
.collect::<String>(),
));
}

let slice = value.as_slice()?;
let (start, len) = get_offset_and_len(start, stop, || slice.len());
Ok(Value::from(
slice
.iter()
.skip(start)
.take(len)
.step_by(step)
.cloned()
.collect::<Vec<_>>(),
))
}

fn int_as_value(val: i128) -> Value {
if val as i64 as i128 == val {
(val as i64).into()
Expand Down
7 changes: 7 additions & 0 deletions minijinja/src/vm/mod.rs
Expand Up @@ -146,6 +146,13 @@ impl<'env> Vm<'env> {
let value = stack.pop();
stack.push(try_ctx!(value.get_item(&attr)));
}
Instruction::Slice => {
let step = stack.pop();
let stop = stack.pop();
let start = stack.pop();
let value = stack.pop();
stack.push(try_ctx!(ops::slice(value, start, stop, step)));
}
Instruction::LoadConst(value) => {
stack.push(value.clone());
}
Expand Down
17 changes: 17 additions & 0 deletions minijinja/tests/inputs/slicing.txt
@@ -0,0 +1,17 @@
{
"hello": "Hello World",
"intrange": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
}
---
{{ hello[:] }}
{{ hello[1:] }}
{{ hello[1:-1] }}
{{ hello[::2] }}
{{ hello[2:10] }}
{{ hello[2:10:2] }}
{{ intrange[:] }}
{{ intrange[1:] }}
{{ intrange[1:-1] }}
{{ intrange[::2] }}
{{ intrange[2:10] }}
{{ intrange[2:10:2] }}
31 changes: 31 additions & 0 deletions minijinja/tests/snapshots/test_templates__vm@slicing.txt.snap
@@ -0,0 +1,31 @@
---
source: minijinja/tests/test_templates.rs
description: "{{ hello[:] }}\n{{ hello[1:] }}\n{{ hello[1:-1] }}\n{{ hello[::2] }}\n{{ hello[2:10] }}\n{{ hello[2:10:2] }}\n{{ intrange[:] }}\n{{ intrange[1:] }}\n{{ intrange[1:-1] }}\n{{ intrange[::2] }}\n{{ intrange[2:10] }}\n{{ intrange[2:10:2] }}"
info:
hello: Hello World
intrange:
- 0
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
input_file: minijinja/tests/inputs/slicing.txt
---
Hello World
ello World
ello Worl
HloWrd
llo Worl
loWr
[0, 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]
[0, 2, 4, 6, 8]
[2, 3, 4, 5, 6, 7, 8, 9]
[2, 4, 6, 8]

0 comments on commit 0e1d206

Please sign in to comment.