From 2912ec92f83357e6909f13674529546fa73878ac Mon Sep 17 00:00:00 2001 From: Dirkjan Ochtman Date: Sat, 1 Jul 2023 16:08:44 +0200 Subject: derive: move generator-specific methods out of Expr --- askama_derive/src/generator.rs | 73 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 71 insertions(+), 2 deletions(-) (limited to 'askama_derive/src/generator.rs') diff --git a/askama_derive/src/generator.rs b/askama_derive/src/generator.rs index f17b6f6..e2d657f 100644 --- a/askama_derive/src/generator.rs +++ b/askama_derive/src/generator.rs @@ -1277,7 +1277,7 @@ impl<'a> Generator<'a> { }; let id = match expr_cache.entry(expression.clone()) { - Entry::Occupied(e) if s.is_cacheable() => *e.get(), + Entry::Occupied(e) if is_cacheable(s) => *e.get(), e => { let id = self.named; self.named += 1; @@ -1596,7 +1596,7 @@ impl<'a> Generator<'a> { buf.write(", "); } - let borrow = !arg.is_copyable(); + let borrow = !is_copyable(arg); if borrow { buf.write("&("); } @@ -2124,6 +2124,75 @@ impl MapChain<'_, &str, LocalMeta> { } } +/// Returns `true` if enough assumptions can be made, +/// to determine that `self` is copyable. +fn is_copyable(expr: &Expr<'_>) -> bool { + is_copyable_within_op(expr, false) +} + +fn is_copyable_within_op(expr: &Expr<'_>, within_op: bool) -> bool { + use Expr::*; + match expr { + BoolLit(_) | NumLit(_) | StrLit(_) | CharLit(_) => true, + Unary(.., expr) => is_copyable_within_op(expr, true), + BinOp(_, lhs, rhs) => is_copyable_within_op(lhs, true) && is_copyable_within_op(rhs, true), + 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. + Call(..) | Path(..) => true, + // If the `expr` is within a `Unary` or `BinOp` then + // an assumption can be made that the operand is copy. + // If not, then the value is moved and adding `.clone()` + // will solve that issue. However, if the operand is + // implicitly borrowed, then it's likely not even possible + // to get the template to compile. + _ => within_op && is_attr_self(expr), + } +} + +/// Returns `true` if this is an `Attr` where the `obj` is `"self"`. +pub(crate) fn is_attr_self(expr: &Expr<'_>) -> bool { + match expr { + Expr::Attr(obj, _) if matches!(obj.as_ref(), Expr::Var("self")) => true, + Expr::Attr(obj, _) if matches!(obj.as_ref(), Expr::Attr(..)) => is_attr_self(expr), + _ => false, + } +} + +/// Returns `true` if the outcome of this expression may be used multiple times in the same +/// `write!()` call, without evaluating the expression again, i.e. the expression should be +/// side-effect free. +pub(crate) fn is_cacheable(expr: &Expr<'_>) -> bool { + match expr { + // Literals are the definition of pure: + Expr::BoolLit(_) => true, + Expr::NumLit(_) => true, + Expr::StrLit(_) => true, + Expr::CharLit(_) => true, + // fmt::Display should have no effects: + Expr::Var(_) => true, + Expr::Path(_) => true, + // Check recursively: + Expr::Array(args) => args.iter().all(is_cacheable), + Expr::Attr(lhs, _) => is_cacheable(lhs), + Expr::Index(lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs), + Expr::Filter(_, args) => args.iter().all(is_cacheable), + Expr::Unary(_, arg) => is_cacheable(arg), + Expr::BinOp(_, lhs, rhs) => is_cacheable(lhs) && is_cacheable(rhs), + Expr::Range(_, lhs, rhs) => { + lhs.as_ref().map_or(true, |v| is_cacheable(v)) + && rhs.as_ref().map_or(true, |v| is_cacheable(v)) + } + Expr::Group(arg) => is_cacheable(arg), + Expr::Tuple(args) => args.iter().all(is_cacheable), + // We have too little information to tell if the expression is pure: + Expr::Call(_, _) => false, + Expr::RustMacro(_, _) => false, + Expr::Try(_) => false, + } +} + fn median(sizes: &mut [usize]) -> usize { sizes.sort_unstable(); if sizes.len() % 2 == 1 { -- cgit