parser: introduce askama_parser::expr::BinOp

This commit is contained in:
René Kijewski 2025-06-23 18:05:35 +02:00 committed by René Kijewski
parent edfa31f9cf
commit 9f882e2ca7
6 changed files with 184 additions and 168 deletions

View File

@ -581,9 +581,9 @@ fn is_copyable_within_op(expr: &Expr<'_>, within_op: bool) -> bool {
| Expr::NumLit(_, _)
| Expr::StrLit(_)
| Expr::CharLit(_)
| Expr::BinOp(_, _, _) => true,
| Expr::BinOp(_)
| Expr::Range(..) => true,
Expr::Unary(.., expr) => is_copyable_within_op(expr, true),
Expr::Range(..) => true,
// The result of a call likely doesn't need to be borrowed,
// as in that case the call is more likely to return a
// reference in the first place then.

View File

@ -75,7 +75,7 @@ impl<'a> Generator<'a, '_> {
ref generics,
}) => self.visit_filter(ctx, buf, name, arguments, generics, expr.span())?,
Expr::Unary(op, ref inner) => self.visit_unary(ctx, buf, op, inner)?,
Expr::BinOp(op, ref left, ref right) => self.visit_binop(ctx, buf, op, left, right)?,
Expr::BinOp(ref v) => self.visit_binop(ctx, buf, v.op, &v.lhs, &v.rhs)?,
Expr::Range(op, ref left, ref right) => {
self.visit_range(ctx, buf, op, left.as_deref(), right.as_deref())?
}
@ -113,9 +113,9 @@ impl<'a> Generator<'a, '_> {
expr: &WithSpan<'a, Expr<'a>>,
) -> Result<DisplayWrap, CompileError> {
match **expr {
Expr::BinOp(op @ ("||" | "&&"), ref left, _) => {
let ret = self.visit_expr(ctx, buf, left)?;
buf.write(format_args!(" {op} "));
Expr::BinOp(ref v) if matches!(v.op, "&&" | "||") => {
let ret = self.visit_expr(ctx, buf, &v.lhs)?;
buf.write(format_args!(" {} ", &v.op));
return Ok(ret);
}
Expr::Unary(op, ref inner) => {
@ -135,8 +135,8 @@ impl<'a> Generator<'a, '_> {
prev_display_wrap: DisplayWrap,
) -> Result<DisplayWrap, CompileError> {
match **expr {
Expr::BinOp("||" | "&&", _, ref right) => {
self.visit_condition(ctx, buf, right)?;
Expr::BinOp(ref v) if matches!(v.op, "&&" | "||") => {
self.visit_condition(ctx, buf, &v.rhs)?;
Ok(DisplayWrap::Unwrapped)
}
Expr::Unary(_, ref inner) => {
@ -160,10 +160,10 @@ impl<'a> Generator<'a, '_> {
buf.write('!');
self.visit_condition(ctx, buf, expr)?;
}
Expr::BinOp(op @ ("&&" | "||"), left, right) => {
self.visit_condition(ctx, buf, left)?;
buf.write(format_args!(" {op} "));
self.visit_condition(ctx, buf, right)?;
Expr::BinOp(v) if matches!(v.op, "&&" | "||") => {
self.visit_condition(ctx, buf, &v.lhs)?;
buf.write(format_args!(" {} ", v.op));
self.visit_condition(ctx, buf, &v.rhs)?;
}
Expr::Group(expr) => {
buf.write('(');

View File

@ -3,6 +3,7 @@ use std::collections::hash_map::{Entry, HashMap};
use std::fmt::{self, Debug, Write};
use std::mem;
use parser::expr::BinOp;
use parser::node::{
Call, Comment, Cond, CondTest, FilterBlock, If, Include, Let, Lit, Loop, Macro, Match,
Whitespace, Ws,
@ -232,15 +233,15 @@ impl<'a> Generator<'a, '_> {
}
}
Expr::Unary(_, _) => (EvaluatedResult::Unknown, WithSpan::new(expr, span)),
Expr::BinOp("&&", left, right) => {
Expr::BinOp(v) if v.op == "&&" => {
let (result_left, expr_left) =
self.evaluate_condition(*left, only_contains_is_defined);
self.evaluate_condition(v.lhs, only_contains_is_defined);
if result_left == EvaluatedResult::AlwaysFalse {
// The right side of the `&&` won't be evaluated, no need to go any further.
return (result_left, WithSpan::new(Expr::BoolLit(false), ""));
}
let (result_right, expr_right) =
self.evaluate_condition(*right, only_contains_is_defined);
self.evaluate_condition(v.rhs, only_contains_is_defined);
match (result_left, result_right) {
(EvaluatedResult::AlwaysTrue, EvaluatedResult::AlwaysTrue) => (
EvaluatedResult::AlwaysTrue,
@ -248,31 +249,25 @@ impl<'a> Generator<'a, '_> {
),
(_, EvaluatedResult::AlwaysFalse) => (
EvaluatedResult::AlwaysFalse,
WithSpan::new(
Expr::BinOp("&&", Box::new(expr_left), Box::new(expr_right)),
span,
),
bin_op(span, "&&", expr_left, expr_right),
),
(EvaluatedResult::AlwaysTrue, _) => (result_right, expr_right),
(_, EvaluatedResult::AlwaysTrue) => (result_left, expr_left),
_ => (
EvaluatedResult::Unknown,
WithSpan::new(
Expr::BinOp("&&", Box::new(expr_left), Box::new(expr_right)),
span,
),
bin_op(span, "&&", expr_left, expr_right),
),
}
}
Expr::BinOp("||", left, right) => {
Expr::BinOp(v) if v.op == "||" => {
let (result_left, expr_left) =
self.evaluate_condition(*left, only_contains_is_defined);
self.evaluate_condition(v.lhs, only_contains_is_defined);
if result_left == EvaluatedResult::AlwaysTrue {
// The right side of the `||` won't be evaluated, no need to go any further.
return (result_left, WithSpan::new(Expr::BoolLit(true), ""));
}
let (result_right, expr_right) =
self.evaluate_condition(*right, only_contains_is_defined);
self.evaluate_condition(v.rhs, only_contains_is_defined);
match (result_left, result_right) {
(EvaluatedResult::AlwaysFalse, EvaluatedResult::AlwaysFalse) => (
EvaluatedResult::AlwaysFalse,
@ -280,23 +275,17 @@ impl<'a> Generator<'a, '_> {
),
(_, EvaluatedResult::AlwaysTrue) => (
EvaluatedResult::AlwaysTrue,
WithSpan::new(
Expr::BinOp("||", Box::new(expr_left), Box::new(expr_right)),
span,
),
bin_op(span, "||", expr_left, expr_right),
),
(EvaluatedResult::AlwaysFalse, _) => (result_right, expr_right),
(_, EvaluatedResult::AlwaysFalse) => (result_left, expr_left),
_ => (
EvaluatedResult::Unknown,
WithSpan::new(
Expr::BinOp("||", Box::new(expr_left), Box::new(expr_right)),
span,
),
bin_op(span, "||", expr_left, expr_right),
),
}
}
Expr::BinOp(_, _, _) => {
Expr::BinOp(_) => {
*only_contains_is_defined = false;
(EvaluatedResult::Unknown, WithSpan::new(expr, span))
}
@ -386,14 +375,18 @@ impl<'a> Generator<'a, '_> {
// left expression has been handled but before the right expression is handled
// but this one should have access to the let-bound variable.
match &**expr {
Expr::BinOp(op @ ("||" | "&&"), ref left, ref right) => {
Expr::BinOp(v) if matches!(v.op, "||" | "&&") => {
let display_wrap =
this.visit_expr_first(ctx, &mut expr_buf, left)?;
this.visit_expr_first(ctx, &mut expr_buf, &v.lhs)?;
this.visit_target(buf, true, true, target);
this.visit_expr_not_first(ctx, &mut expr_buf, left, display_wrap)?;
buf.write(format_args!("= &{expr_buf}"));
buf.write(format_args!(" {op} "));
this.visit_condition(ctx, buf, right)?;
this.visit_expr_not_first(
ctx,
&mut expr_buf,
&v.lhs,
display_wrap,
)?;
buf.write(format_args!("= &{expr_buf} {} ", v.op));
this.visit_condition(ctx, buf, &v.rhs)?;
}
_ => {
let display_wrap =
@ -1392,6 +1385,15 @@ impl<'a> Generator<'a, '_> {
}
}
fn bin_op<'a>(
span: impl Into<Span<'a>>,
op: &'a str,
lhs: WithSpan<'a, Expr<'a>>,
rhs: WithSpan<'a, Expr<'a>>,
) -> WithSpan<'a, Expr<'a>> {
WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs, rhs })), span)
}
struct CondInfo<'a> {
cond: &'a WithSpan<'a, Cond<'a>>,
cond_expr: Option<WithSpan<'a, Expr<'a>>>,
@ -1645,7 +1647,7 @@ fn is_cacheable(expr: &WithSpan<'_, Expr<'_>>) -> bool {
Expr::Index(lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs),
Expr::Filter(Filter { arguments, .. }) => arguments.iter().all(is_cacheable),
Expr::Unary(_, arg) => is_cacheable(arg),
Expr::BinOp(_, lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs),
Expr::BinOp(v) => is_cacheable(&v.lhs) && is_cacheable(&v.rhs),
Expr::IsDefined(_) | Expr::IsNotDefined(_) => true,
Expr::Range(_, lhs, rhs) => {
lhs.as_ref().is_none_or(|v| is_cacheable(v))

View File

@ -19,21 +19,31 @@ use crate::{
macro_rules! expr_prec_layer {
( $name:ident, $inner:ident, $op:expr ) => {
fn $name(i: &mut &'a str, level: Level<'_>) -> ParseResult<'a, WithSpan<'a, Self>> {
let mut level_guard = level.guard();
let start = *i;
let mut expr = Self::$inner(i, level)?;
let mut i_before = *i;
let mut right = opt((ws($op), |i: &mut _| Self::$inner(i, level)));
while let Some((op, right)) = right.parse_next(i)? {
level_guard.nest(i_before)?;
i_before = *i;
expr = WithSpan::new(Self::BinOp(op, Box::new(expr), Box::new(right)), start);
}
Ok(expr)
expr_prec_layer(i, level, Expr::$inner, |i: &mut _| $op.parse_next(i))
}
};
}
fn expr_prec_layer<'a>(
i: &mut &'a str,
level: Level<'_>,
inner: fn(&mut &'a str, Level<'_>) -> ParseResult<'a, WithSpan<'a, Expr<'a>>>,
op: fn(&mut &'a str) -> ParseResult<'a>,
) -> ParseResult<'a, WithSpan<'a, Expr<'a>>> {
let start = *i;
let mut expr = inner(i, level)?;
let mut i_before = *i;
let mut level_guard = level.guard();
while let Some((op, rhs)) = opt((ws(op), |i: &mut _| inner(i, level))).parse_next(i)? {
level_guard.nest(i_before)?;
i_before = *i;
expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start);
}
Ok(expr)
}
#[derive(Clone, Copy, Default)]
struct Allowed {
underscore: bool,
@ -83,10 +93,14 @@ fn check_expr<'a>(expr: &WithSpan<'a, Expr<'a>>, allowed: Allowed) -> Result<(),
check_expr(elem, Allowed::default())
}
}
Expr::Index(elem1, elem2) | Expr::BinOp(_, elem1, elem2) => {
Expr::Index(elem1, elem2) => {
check_expr(elem1, Allowed::default())?;
check_expr(elem2, Allowed::default())
}
Expr::BinOp(v) => {
check_expr(&v.lhs, Allowed::default())?;
check_expr(&v.rhs, Allowed::default())
}
Expr::Range(_, elem1, elem2) => {
if let Some(elem1) = elem1 {
check_expr(elem1, Allowed::default())?;
@ -155,11 +169,7 @@ pub enum Expr<'a> {
As(Box<WithSpan<'a, Expr<'a>>>, &'a str),
NamedArgument(&'a str, Box<WithSpan<'a, Expr<'a>>>),
Unary(&'a str, Box<WithSpan<'a, Expr<'a>>>),
BinOp(
&'a str,
Box<WithSpan<'a, Expr<'a>>>,
Box<WithSpan<'a, Expr<'a>>>,
),
BinOp(Box<BinOp<'a>>),
Range(
&'a str,
Option<Box<WithSpan<'a, Expr<'a>>>>,
@ -186,6 +196,13 @@ pub enum Expr<'a> {
ArgumentPlaceholder,
}
#[derive(Clone, Debug, PartialEq)]
pub struct BinOp<'a> {
pub op: &'a str,
pub lhs: WithSpan<'a, Expr<'a>>,
pub rhs: WithSpan<'a, Expr<'a>>,
}
impl<'a> Expr<'a> {
pub(super) fn arguments(
i: &mut &'a str,
@ -320,7 +337,7 @@ impl<'a> Expr<'a> {
let Some((op, rhs)) = opt(right).parse_next(i)? else {
return Ok(expr);
};
let expr = WithSpan::new(Self::BinOp(op, Box::new(expr), Box::new(rhs)), start);
let expr = WithSpan::new(Expr::BinOp(Box::new(BinOp { op, lhs: expr, rhs })), start);
if let Some((op2, _)) = opt(right).parse_next(i)? {
return Err(ErrMode::Cut(ErrorContext::new(
@ -583,8 +600,8 @@ impl<'a> Expr<'a> {
match self {
Self::BoolLit(_) | Self::IsDefined(_) | Self::IsNotDefined(_) => true,
Self::Unary(_, expr) | Self::Group(expr) => expr.contains_bool_lit_or_is_defined(),
Self::BinOp("&&" | "||", left, right) => {
left.contains_bool_lit_or_is_defined() || right.contains_bool_lit_or_is_defined()
Self::BinOp(v) if matches!(v.op, "&&" | "||") => {
v.lhs.contains_bool_lit_or_is_defined() || v.rhs.contains_bool_lit_or_is_defined()
}
Self::NumLit(_, _)
| Self::StrLit(_)
@ -602,7 +619,7 @@ impl<'a> Expr<'a> {
| Self::Index(_, _)
| Self::Tuple(_)
| Self::Array(_)
| Self::BinOp(_, _, _)
| Self::BinOp(_)
| Self::Path(_)
| Self::Concat(_)
| Self::LetCond(_)

View File

@ -423,13 +423,13 @@ impl<'a> CondTest<'a> {
ws(|i: &mut _| {
let start = *i;
let mut expr = Expr::parse(i, s.level, false)?;
if let Expr::BinOp(_, _, ref mut right) = expr.inner {
if matches!(right.inner, Expr::Var("set" | "let")) {
if let Expr::BinOp(v) = &mut expr.inner {
if matches!(v.rhs.inner, Expr::Var("set" | "let")) {
let _level_guard = s.level.nest(i)?;
*i = right.span.as_suffix_of(start).unwrap();
*i = v.rhs.span.as_suffix_of(start).unwrap();
let start_span = Span::from(*i);
let new_right = Self::parse_cond(i, s)?;
right.inner = Expr::LetCond(Box::new(WithSpan::new(new_right, start_span)));
v.rhs.inner = Expr::LetCond(Box::new(WithSpan::new(new_right, start_span)));
}
}
Ok(expr)

View File

@ -41,6 +41,14 @@ fn int_lit(i: &str) -> Expr<'_> {
Expr::NumLit(i, Num::Int(i, None))
}
fn bin_op<'a>(
op: &'a str,
lhs: WithSpan<'a, Expr<'a>>,
rhs: WithSpan<'a, Expr<'a>>,
) -> WithSpan<'a, Expr<'a>> {
WithSpan::new_without_span(Expr::BinOp(Box::new(crate::expr::BinOp { op, lhs, rhs })))
}
#[test]
fn test_parse_filter() {
let syntax = Syntax::default();
@ -90,14 +98,11 @@ fn test_parse_filter() {
Ws(None, None),
WithSpan::no_span(Expr::Filter(Filter {
name: PathOrIdentifier::Identifier("abs"),
arguments: vec![WithSpan::no_span(Expr::Group(
WithSpan::no_span(Expr::BinOp(
"-",
WithSpan::no_span(int_lit("1")).into(),
WithSpan::no_span(int_lit("2")).into()
))
.into()
))],
arguments: vec![WithSpan::no_span(Expr::Group(Box::new(bin_op(
"-",
WithSpan::no_span(int_lit("1")),
WithSpan::no_span(int_lit("2")),
))))],
generics: vec![],
})),
)],
@ -445,16 +450,15 @@ fn test_precedence() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"==",
WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
WithSpan::no_span(Expr::Var("a")).into(),
WithSpan::no_span(Expr::Var("b")).into()
))
.into(),
WithSpan::no_span(Expr::Var("c")).into()
))
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
),
WithSpan::no_span(Expr::Var("c"))
)
)],
);
assert_eq!(
@ -463,26 +467,23 @@ fn test_precedence() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"-",
WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
WithSpan::no_span(Expr::Var("a")).into(),
WithSpan::no_span(Expr::BinOp(
WithSpan::no_span(Expr::Var("a")),
bin_op(
"*",
WithSpan::no_span(Expr::Var("b")).into(),
WithSpan::no_span(Expr::Var("c")).into()
))
.into()
))
.into(),
WithSpan::no_span(Expr::BinOp(
WithSpan::no_span(Expr::Var("b")),
WithSpan::no_span(Expr::Var("c"))
)
),
bin_op(
"/",
WithSpan::no_span(Expr::Var("d")).into(),
WithSpan::no_span(Expr::Var("e")).into()
))
.into(),
))
WithSpan::no_span(Expr::Var("d")),
WithSpan::no_span(Expr::Var("e"))
),
)
)],
);
assert_eq!(
@ -491,24 +492,22 @@ fn test_precedence() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"/",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"*",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Group(Box::new(WithSpan::no_span(
Expr::BinOp(
"+",
Box::new(WithSpan::no_span(Expr::Var("b"))),
Box::new(WithSpan::no_span(Expr::Var("c")))
)
)))))
))),
Box::new(WithSpan::no_span(Expr::Unary(
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Group(Box::new(bin_op(
"+",
WithSpan::no_span(Expr::Var("b")),
WithSpan::no_span(Expr::Var("c"))
))))
),
WithSpan::no_span(Expr::Unary(
"-",
Box::new(WithSpan::no_span(Expr::Var("d")))
)))
))
))
)
)],
);
assert_eq!(
@ -517,23 +516,23 @@ fn test_precedence() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"||",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"||",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::BinOp(
WithSpan::no_span(Expr::Var("a")),
bin_op(
"&&",
Box::new(WithSpan::no_span(Expr::Var("b"))),
Box::new(WithSpan::no_span(Expr::Var("c")))
))),
))),
Box::new(WithSpan::no_span(Expr::BinOp(
WithSpan::no_span(Expr::Var("b")),
WithSpan::no_span(Expr::Var("c"))
),
),
bin_op(
"&&",
Box::new(WithSpan::no_span(Expr::Var("d"))),
Box::new(WithSpan::no_span(Expr::Var("e")))
))),
))
WithSpan::no_span(Expr::Var("d")),
WithSpan::no_span(Expr::Var("e"))
),
)
)],
);
}
@ -547,15 +546,15 @@ fn test_associativity() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Var("b")))
))),
Box::new(WithSpan::no_span(Expr::Var("c")))
))
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
),
WithSpan::no_span(Expr::Var("c"))
)
)],
);
assert_eq!(
@ -564,15 +563,15 @@ fn test_associativity() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"*",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"*",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Var("b")))
))),
Box::new(WithSpan::no_span(Expr::Var("c")))
))
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
),
WithSpan::no_span(Expr::Var("c"))
)
)],
);
assert_eq!(
@ -581,15 +580,15 @@ fn test_associativity() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"&&",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"&&",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Var("b")))
))),
Box::new(WithSpan::no_span(Expr::Var("c")))
))
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
),
WithSpan::no_span(Expr::Var("c"))
)
)],
);
assert_eq!(
@ -598,19 +597,19 @@ fn test_associativity() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"-",
Box::new(WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Var("b")))
))),
Box::new(WithSpan::no_span(Expr::Var("c")))
))),
Box::new(WithSpan::no_span(Expr::Var("d")))
))
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
),
WithSpan::no_span(Expr::Var("c"))
),
WithSpan::no_span(Expr::Var("d"))
)
)],
);
}
@ -639,12 +638,10 @@ fn test_odd_calls() {
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::Call {
path: Box::new(WithSpan::no_span(Expr::Group(Box::new(WithSpan::no_span(
Expr::BinOp(
"+",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Var("b")))
)
path: Box::new(WithSpan::no_span(Expr::Group(Box::new(bin_op(
"+",
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Var("b"))
))))),
args: vec![WithSpan::no_span(Expr::Var("c"))],
generics: vec![],
@ -657,15 +654,15 @@ fn test_odd_calls() {
.nodes,
vec![Node::Expr(
Ws(None, None),
WithSpan::no_span(Expr::BinOp(
bin_op(
"+",
Box::new(WithSpan::no_span(Expr::Var("a"))),
Box::new(WithSpan::no_span(Expr::Call {
WithSpan::no_span(Expr::Var("a")),
WithSpan::no_span(Expr::Call {
path: Box::new(WithSpan::no_span(Expr::Var("b"))),
args: vec![WithSpan::no_span(Expr::Var("c"))],
generics: vec![],
})),
)),
}),
),
)],
);
assert_eq!(