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

Added support for slicing #120

Merged
merged 1 commit into from Sep 17, 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
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]