runjucks_core/
renderer.rs

1//! Walks [`crate::ast::Node`] trees and produces output strings using an [`crate::Environment`] and JSON context.
2
3use crate::ast::{
4    BinOp, Expr, ForVars, MacroDef, MacroParam, Node, SwitchCase, UnaryOp,
5};
6use crate::environment::Environment;
7use crate::errors::{Result, RunjucksError};
8use crate::globals::{
9    parse_cycler_id, parse_joiner_id, CyclerState, JoinerState, RJ_CALLABLE,
10};
11use crate::loader::TemplateLoader;
12use crate::render_common::{
13    add_like_js, apply_builtin_filter_chain_on_cow_value, as_number, collect_attr_chain_from_getattr,
14    compare_values, eval_in, is_test_parts, is_truthy, iterable_empty, iterable_from_value,
15    jinja_slice_array, json_num, peel_builtin_upper_lower_length_chain, ExtendsLayout, Iterable,
16};
17use crate::value::{is_undefined_value, mark_safe, undefined_value};
18use ahash::AHashMap;
19use rand::rngs::SmallRng;
20use rand::SeedableRng;
21use serde_json::{json, Map, Value};
22use std::borrow::Cow;
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25
26/// Nunjucks-style frame stack: inner frames shadow outer; `set` updates the innermost existing binding.
27///
28/// Values are stored as [`Arc`] so repeated reads and shallow copies of bindings can share the same
29/// [`Value`] allocation when the stack is cloned or merged (see [`Self::flatten`]).
30///
31/// Frame maps use [`ahash::AHashMap`] for faster string-key lookup on hot paths (many distinct variables).
32#[derive(Debug, Clone)]
33pub struct CtxStack {
34    pub(crate) frames: Vec<AHashMap<String, Arc<Value>>>,
35    /// Incremented on any binding change (frames, `set`, `set_local`, `loop` injection).
36    /// Used to reuse merged extension context snapshots when the stack is unchanged.
37    revision: u64,
38}
39
40impl CtxStack {
41    pub fn from_root(root: Map<String, Value>) -> Self {
42        let mapped: AHashMap<String, Arc<Value>> =
43            root.into_iter().map(|(k, v)| (k, Arc::new(v))).collect();
44        Self {
45            frames: vec![mapped],
46            revision: 0,
47        }
48    }
49
50    #[inline]
51    pub(crate) fn bump_revision(&mut self) {
52        self.revision = self.revision.wrapping_add(1);
53    }
54
55    /// Monotonic counter; changes whenever template bindings or frames change.
56    #[inline]
57    pub fn revision(&self) -> u64 {
58        self.revision
59    }
60
61    pub fn push_frame(&mut self) {
62        self.frames.push(AHashMap::new());
63        self.bump_revision();
64    }
65
66    pub fn pop_frame(&mut self) {
67        if self.frames.len() > 1 {
68            self.frames.pop();
69            self.bump_revision();
70        }
71    }
72
73    /// Borrows the innermost binding for `name` across frames (template context shadows outer).
74    pub fn get_ref(&self, name: &str) -> Option<&Value> {
75        for f in self.frames.iter().rev() {
76            if let Some(v) = f.get(name) {
77                return Some(v.as_ref());
78            }
79        }
80        None
81    }
82
83    pub fn get(&self, name: &str) -> Value {
84        self.get_ref(name).cloned().unwrap_or(Value::Null)
85    }
86
87    pub fn defined(&self, name: &str) -> bool {
88        self.frames.iter().rev().any(|f| f.contains_key(name))
89    }
90
91    pub fn set(&mut self, name: &str, value: Value) {
92        let arc = Arc::new(value);
93        for f in self.frames.iter_mut().rev() {
94            if f.contains_key(name) {
95                f.insert(name.to_string(), arc);
96                self.bump_revision();
97                return;
98            }
99        }
100        if let Some(inner) = self.frames.last_mut() {
101            inner.insert(name.to_string(), arc);
102            self.bump_revision();
103        }
104    }
105
106    /// Assign in the innermost frame only (for `for` / `loop.*` bindings so inner loops can shadow).
107    pub fn set_local(&mut self, name: &str, value: Value) {
108        if let Some(inner) = self.frames.last_mut() {
109            inner.insert(name.to_string(), Arc::new(value));
110            self.bump_revision();
111        }
112    }
113
114    /// Outer frames first, then inner overwrites — snapshot for macro bodies.
115    pub fn flatten(&self) -> Map<String, Value> {
116        let cap: usize = self.frames.iter().map(|f| f.len()).sum();
117        let mut m = Map::with_capacity(cap);
118        for f in &self.frames {
119            for (k, v) in f {
120                m.insert(k.clone(), v.as_ref().clone());
121            }
122        }
123        m
124    }
125}
126
127/// One active `{% call %}`: body to render for `caller()` / `caller(args…)`, plus optional formal parameters.
128#[derive(Clone)]
129pub struct CallerFrame {
130    pub body: Vec<Node>,
131    pub params: Vec<MacroParam>,
132}
133
134/// Per-render state: optional loader, include cycle stack, macro scopes, and block inheritance for `extends`.
135pub struct RenderState<'a> {
136    pub loader: Option<&'a (dyn TemplateLoader + Send + Sync)>,
137    pub stack: Vec<String>,
138    pub macro_scopes: Vec<HashMap<String, MacroDef>>,
139    /// `{% import "x" as ns %}` — macros callable as `ns.macro_name()`.
140    pub macro_namespaces: HashMap<String, HashMap<String, MacroDef>>,
141    /// Top-level `{% set %}` exports from each `import … as ns` namespace (`ns.name`): single- and
142    /// multi-target `=` forms (same value per target) and block `{% set x %}…{% endset %}`, evaluated
143    /// in source order. Also used with `macro_namespaces` for resolving `ns.*`.
144    pub macro_namespace_values: HashMap<String, HashMap<String, Value>>,
145    /// Per-block inheritance: innermost template first (child → parent → …) for `{{ super() }}`.
146    pub block_chains: Option<HashMap<String, Vec<Vec<Node>>>>,
147    /// When rendering a block layer, `Some((block_name, layer_index))` for `super()` resolution.
148    pub super_context: Option<(String, usize)>,
149    /// Innermost `{% call %}` frame for `caller()` / `caller(args…)` inside macro execution.
150    pub caller_stack: Vec<CallerFrame>,
151    /// Stateful `cycler(...)` instances (index matches handle object).
152    pub cyclers: Vec<CyclerState>,
153    /// Stateful `joiner(...)` instances.
154    pub joiners: Vec<JoinerState>,
155    /// PRNG for `| random` (seed from [`Environment::random_seed`] when set).
156    pub rng: SmallRng,
157    /// Cached `stack.flatten()` for [`Node::ExtensionTag`] when [`CtxStack::revision`] matches.
158    extension_context_cache: Option<(u64, Value)>,
159}
160
161impl<'a> RenderState<'a> {
162    pub fn new(
163        loader: Option<&'a (dyn TemplateLoader + Send + Sync)>,
164        rng_seed: Option<u64>,
165    ) -> Self {
166        let rng = match rng_seed {
167            Some(s) => SmallRng::seed_from_u64(s),
168            None => SmallRng::from_entropy(),
169        };
170        Self {
171            loader,
172            stack: Vec::new(),
173            macro_scopes: Vec::new(),
174            macro_namespaces: HashMap::new(),
175            macro_namespace_values: HashMap::new(),
176            block_chains: None,
177            super_context: None,
178            caller_stack: Vec::new(),
179            cyclers: Vec::new(),
180            joiners: Vec::new(),
181            rng,
182            extension_context_cache: None,
183        }
184    }
185
186    pub fn push_template(&mut self, name: &str) -> Result<()> {
187        if self.stack.iter().any(|s| s == name) {
188            return Err(RunjucksError::new(format!(
189                "circular template reference: {name}"
190            )));
191        }
192        self.stack.push(name.to_string());
193        Ok(())
194    }
195
196    pub fn pop_template(&mut self) {
197        self.stack.pop();
198    }
199
200    pub fn push_macros(&mut self, defs: HashMap<String, MacroDef>) {
201        self.macro_scopes.push(defs);
202    }
203
204    pub fn pop_macros(&mut self) {
205        self.macro_scopes.pop();
206    }
207
208    pub fn lookup_macro(&self, name: &str) -> Option<&MacroDef> {
209        for scope in self.macro_scopes.iter().rev() {
210            if let Some(m) = scope.get(name) {
211                return Some(m);
212            }
213        }
214        None
215    }
216
217    pub fn lookup_namespaced_macro(&self, ns: &str, macro_name: &str) -> Option<&MacroDef> {
218        self.macro_namespaces
219            .get(ns)
220            .and_then(|m| m.get(macro_name))
221    }
222
223    pub fn lookup_namespaced_value(&self, ns: &str, name: &str) -> Option<&Value> {
224        self.macro_namespace_values
225            .get(ns)
226            .and_then(|m| m.get(name))
227    }
228}
229
230/// Renders `root` to a string using `env` and `ctx_stack`.
231pub fn render(
232    env: &Environment,
233    loader: Option<&(dyn TemplateLoader + Send + Sync)>,
234    root: &Node,
235    ctx_stack: &mut CtxStack,
236) -> Result<String> {
237    let mut state = RenderState::new(loader, env.random_seed);
238    render_entry(env, &mut state, root, ctx_stack)
239}
240
241/// Entry: handle `{% extends %}` child templates, otherwise normal render.
242pub fn render_entry(
243    env: &Environment,
244    state: &mut RenderState<'_>,
245    root: &Node,
246    ctx_stack: &mut CtxStack,
247) -> Result<String> {
248    if let Some((parent_expr, blocks)) = extract_layout_if_any(root)? {
249        let parent_name =
250            crate::value::value_to_string(&eval_to_value(env, state, &parent_expr, ctx_stack)?);
251        render_extends(env, state, &parent_name, blocks, ctx_stack)
252    } else {
253        render_with_state(env, state, root, ctx_stack)
254    }
255}
256
257pub(crate) fn extract_layout_if_any(root: &Node) -> Result<Option<ExtendsLayout>> {
258    let Node::Root(children) = root else {
259        return Ok(None);
260    };
261    let mut idx = 0usize;
262    while idx < children.len() {
263        match &children[idx] {
264            Node::Text(s) if s.trim().is_empty() => idx += 1,
265            Node::Extends { parent } => {
266                let parent = parent.clone();
267                let mut blocks = HashMap::new();
268                for n in children.iter().skip(idx + 1) {
269                    match n {
270                        Node::Block { name, body } => {
271                            blocks.insert(name.clone(), body.clone());
272                        }
273                        Node::Text(s) if s.chars().all(|c| c.is_whitespace()) => {}
274                        Node::MacroDef(_) => {}
275                        _ => {
276                            return Err(RunjucksError::new(
277                                "invalid content in template with `extends` (only `block` allowed)",
278                            ));
279                        }
280                    }
281                }
282                return Ok(Some((parent, blocks)));
283            }
284            _ => return Ok(None),
285        }
286    }
287    Ok(None)
288}
289
290pub(crate) fn collect_blocks_in_root(root: &Node) -> HashMap<String, Vec<Node>> {
291    let Node::Root(children) = root else {
292        return HashMap::new();
293    };
294    let mut m = HashMap::new();
295    for n in children {
296        if let Node::Block { name, body } = n {
297            m.insert(name.clone(), body.clone());
298        }
299    }
300    m
301}
302
303pub(crate) fn extends_parent_expr(root: &Node) -> Option<&Expr> {
304    let Node::Root(children) = root else {
305        return None;
306    };
307    for n in children {
308        if let Node::Extends { parent } = n {
309            return Some(parent);
310        }
311    }
312    None
313}
314
315/// Block bodies from innermost (overriding child) to outermost for each block name.
316#[allow(clippy::too_many_arguments)]
317fn build_block_chains(
318    parent_name: &str,
319    parent_ast: &Node,
320    immediate_child_overrides: &HashMap<String, Vec<Node>>,
321    loader: &(dyn TemplateLoader + Send + Sync),
322    visited: &mut HashSet<String>,
323    env: &Environment,
324    state: &mut RenderState<'_>,
325    ctx_stack: &mut CtxStack,
326) -> Result<HashMap<String, Vec<Vec<Node>>>> {
327    if !visited.insert(parent_name.to_string()) {
328        return Err(RunjucksError::new(format!(
329            "circular `{{% extends %}}` involving `{parent_name}`"
330        )));
331    }
332
333    let result = (|| {
334        let local_blocks = collect_blocks_in_root(parent_ast);
335        let inherited: HashMap<String, Vec<Vec<Node>>> =
336            if let Some(gp_expr) = extends_parent_expr(parent_ast) {
337                let gp_name =
338                    crate::value::value_to_string(&eval_to_value(env, state, gp_expr, ctx_stack)?);
339                let gp_ast = env.load_and_parse_named(&gp_name, loader)?;
340                build_block_chains(
341                    &gp_name,
342                    gp_ast.as_ref(),
343                    &local_blocks,
344                    loader,
345                    visited,
346                    env,
347                    state,
348                    ctx_stack,
349                )?
350            } else {
351                HashMap::new()
352            };
353
354        let mut all_names: HashSet<String> = immediate_child_overrides.keys().cloned().collect();
355        all_names.extend(local_blocks.keys().cloned());
356        all_names.extend(inherited.keys().cloned());
357
358        let mut out = HashMap::new();
359        for name in all_names {
360            let mut chain: Vec<Vec<Node>> = Vec::new();
361            if let Some(c) = immediate_child_overrides.get(&name) {
362                chain.push(c.clone());
363            }
364            if let Some(rest) = inherited.get(&name) {
365                chain.extend(rest.iter().cloned());
366            } else if let Some(l) = local_blocks.get(&name) {
367                chain.push(l.clone());
368            }
369            if !chain.is_empty() {
370                out.insert(name, chain);
371            }
372        }
373        Ok(out)
374    })();
375
376    visited.remove(parent_name);
377    result
378}
379
380fn render_extends(
381    env: &Environment,
382    state: &mut RenderState<'_>,
383    parent_name: &str,
384    blocks: HashMap<String, Vec<Node>>,
385    ctx_stack: &mut CtxStack,
386) -> Result<String> {
387    let loader = state
388        .loader
389        .ok_or_else(|| RunjucksError::new("`extends` requires a template loader"))?;
390    let parent_ast = env.load_and_parse_named(parent_name, loader)?;
391    state.push_template(parent_name)?;
392    let mut visited = HashSet::new();
393    let chains = build_block_chains(
394        parent_name,
395        parent_ast.as_ref(),
396        &blocks,
397        loader,
398        &mut visited,
399        env,
400        state,
401        ctx_stack,
402    )?;
403    let prev_chains = state.block_chains.take();
404    state.block_chains = Some(chains);
405    let out = render_with_state(env, state, parent_ast.as_ref(), ctx_stack)?;
406    state.block_chains = prev_chains;
407    state.pop_template();
408    Ok(out)
409}
410
411fn render_with_state(
412    env: &Environment,
413    state: &mut RenderState<'_>,
414    root: &Node,
415    ctx_stack: &mut CtxStack,
416) -> Result<String> {
417    render_node(env, state, root, ctx_stack)
418}
419
420/// Top-level `{% macro %}` definitions only (Nunjucks `getExported` surface for macro libraries).
421pub(crate) fn collect_top_level_macros(root: &Node) -> HashMap<String, MacroDef> {
422    let mut m = HashMap::new();
423    let Node::Root(children) = root else {
424        return m;
425    };
426    for n in children {
427        if let Node::MacroDef(def) = n {
428            m.insert(def.name.clone(), def.clone());
429        }
430    }
431    m
432}
433
434/// Top-level `{% set … %}` forms that participate in `{% import %}` / `{% from %}` exports (same
435/// order as source; mirrors [`Node::Set`] rendering for multi-target and block capture).
436pub(crate) enum TopLevelSetExport {
437    /// `{% set a = expr %}`, `{% set a, b = expr %}` (same value cloned to every target).
438    FromExpr { targets: Vec<String>, expr: Expr },
439    /// `{% set name %}…{% endset %}` (parser allows only one target for block form).
440    FromBlock { target: String, body: Vec<Node> },
441}
442
443pub(crate) fn collect_top_level_set_exports(root: &Node) -> Vec<TopLevelSetExport> {
444    let mut out = Vec::new();
445    let Node::Root(children) = root else {
446        return out;
447    };
448    for n in children {
449        match n {
450            Node::Set {
451                targets,
452                value: Some(expr),
453                body: None,
454            } if !targets.is_empty() => {
455                out.push(TopLevelSetExport::FromExpr {
456                    targets: targets.clone(),
457                    expr: expr.clone(),
458                });
459            }
460            Node::Set {
461                targets,
462                value: None,
463                body: Some(body),
464            } if targets.len() == 1 => {
465                out.push(TopLevelSetExport::FromBlock {
466                    target: targets[0].clone(),
467                    body: body.clone(),
468                });
469            }
470            _ => {}
471        }
472    }
473    out
474}
475
476/// Evaluates exported top-level assignments (`getExported`) with Nunjucks-style context:
477/// `with context` → parent context; omitted or `without context` → isolated root (globals still resolve via [`Environment`].
478fn eval_exported_top_level_sets(
479    env: &Environment,
480    state: &mut RenderState<'_>,
481    root: &Node,
482    ctx_stack: &mut CtxStack,
483    with_context: Option<bool>,
484) -> Result<HashMap<String, Value>> {
485    let mut out = HashMap::new();
486    let exports = collect_top_level_set_exports(root);
487    let mut import_stack = if matches!(with_context, Some(true)) {
488        CtxStack::from_root(ctx_stack.flatten())
489    } else {
490        CtxStack::from_root(Map::new())
491    };
492    for ex in exports {
493        match ex {
494            TopLevelSetExport::FromExpr { targets, expr } => {
495                let v = eval_to_value(env, state, &expr, &mut import_stack)?;
496                for t in &targets {
497                    import_stack.set(t, v.clone());
498                }
499                for t in &targets {
500                    out.insert(t.clone(), v.clone());
501                }
502            }
503            TopLevelSetExport::FromBlock { target, body } => {
504                let s = render_children(env, state, &body, &mut import_stack)?;
505                let val = Value::String(s);
506                import_stack.set(&target, val.clone());
507                out.insert(target, val);
508            }
509        }
510    }
511    Ok(out)
512}
513
514/// Detects `{% import "x" %}` / `{% from "x" %}` cycles using **string-literal** paths only (matches
515/// typical macro libraries; dynamic names are not traced here).
516/// Detects `{% extends "x" %}` cycles using **string-literal** parents only (same idea as
517/// [`scan_literal_import_graph`]; dynamic `{% extends expr %}` is checked at render time).
518pub(crate) fn scan_literal_extends_graph(
519    env: &Environment,
520    state: &mut RenderState<'_>,
521    root: &Node,
522    loader: &(dyn TemplateLoader + Send + Sync),
523) -> Result<()> {
524    let Some(expr) = extends_parent_expr(root) else {
525        return Ok(());
526    };
527    let Expr::Literal(Value::String(path)) = expr else {
528        return Ok(());
529    };
530    state.push_template(path)?;
531    let nested = env.load_and_parse_named(path, loader)?;
532    let r = scan_literal_extends_graph(env, state, nested.as_ref(), loader);
533    state.pop_template();
534    r
535}
536
537pub(crate) fn scan_literal_import_graph(
538    env: &Environment,
539    state: &mut RenderState<'_>,
540    root: &Node,
541    loader: &(dyn TemplateLoader + Send + Sync),
542) -> Result<()> {
543    let Node::Root(children) = root else {
544        return Ok(());
545    };
546    for n in children {
547        let template_expr = match n {
548            Node::Import { template, .. } | Node::FromImport { template, .. } => template,
549            _ => continue,
550        };
551        let Expr::Literal(Value::String(path)) = template_expr else {
552            continue;
553        };
554        state.push_template(path)?;
555        let nested = env.load_and_parse_named(path, loader)?;
556        scan_literal_import_graph(env, state, nested.as_ref(), loader)?;
557        state.pop_template();
558    }
559    Ok(())
560}
561
562fn render_node(
563    env: &Environment,
564    state: &mut RenderState<'_>,
565    n: &Node,
566    stack: &mut CtxStack,
567) -> Result<String> {
568    match n {
569        Node::Root(nodes) => {
570            let mut defs = HashMap::new();
571            for n in nodes.iter() {
572                if let Node::MacroDef(m) = n {
573                    defs.insert(m.name.clone(), m.clone());
574                }
575            }
576            let had_macros = !defs.is_empty();
577            let scope_base = state.macro_scopes.len();
578            if had_macros {
579                state.push_macros(defs);
580            }
581            let mut out = String::new();
582            out.reserve(nodes.len().saturating_mul(32));
583            for child in nodes.iter() {
584                if matches!(child, Node::MacroDef(_) | Node::Extends { .. }) {
585                    continue;
586                }
587                out.push_str(&render_node(env, state, child, stack)?);
588            }
589            while state.macro_scopes.len() > scope_base {
590                state.pop_macros();
591            }
592            Ok(out)
593        }
594        Node::Text(s) => Ok(s.to_string()),
595        Node::Output(exprs) => render_output(env, state, exprs, stack),
596        Node::If { branches } => {
597            for br in branches {
598                if let Some(cond) = &br.cond {
599                    if !is_truthy(&eval_to_value(env, state, cond, stack)?) {
600                        continue;
601                    }
602                }
603                return render_children(env, state, &br.body, stack);
604            }
605            Ok(String::new())
606        }
607        Node::Switch {
608            expr,
609            cases,
610            default_body,
611        } => render_switch(env, state, expr, cases, default_body.as_deref(), stack),
612        Node::For {
613            vars,
614            iter,
615            body,
616            else_body,
617        } => render_for(env, state, vars, iter, body, else_body.as_deref(), stack),
618        Node::Set {
619            targets,
620            value,
621            body,
622        } => {
623            if let Some(expr) = value {
624                let v = eval_to_value(env, state, expr, stack)?;
625                for t in targets {
626                    stack.set(t, v.clone());
627                }
628            } else if let Some(nodes) = body {
629                let s = render_children(env, state, nodes, stack)?;
630                if let Some(t) = targets.first() {
631                    stack.set(t, Value::String(s));
632                }
633            }
634            Ok(String::new())
635        }
636        Node::Include {
637            template,
638            ignore_missing,
639            with_context,
640        } => {
641            let loader = state
642                .loader
643                .ok_or_else(|| RunjucksError::new("`include` requires a template loader"))?;
644            let name = crate::value::value_to_string(&eval_to_value(env, state, template, stack)?);
645            let included = match env.load_and_parse_named(&name, loader) {
646                Ok(ast) => ast,
647                Err(_) if *ignore_missing => return Ok(String::new()),
648                Err(e) => return Err(e),
649            };
650            state.push_template(&name)?;
651            let out = if matches!(with_context, Some(false)) {
652                let mut isolated = CtxStack::from_root(Map::new());
653                render_entry(env, state, included.as_ref(), &mut isolated)?
654            } else {
655                render_entry(env, state, included.as_ref(), stack)?
656            };
657            state.pop_template();
658            Ok(out)
659        }
660        Node::Import {
661            template,
662            alias,
663            with_context,
664        } => {
665            let loader = state
666                .loader
667                .ok_or_else(|| RunjucksError::new("`import` requires a template loader"))?;
668            let name = crate::value::value_to_string(&eval_to_value(env, state, template, stack)?);
669            let imported = env.load_and_parse_named(&name, loader)?;
670            state.push_template(&name)?;
671            scan_literal_import_graph(env, state, imported.as_ref(), loader)?;
672            let defs = collect_top_level_macros(imported.as_ref());
673            let exported_sets =
674                eval_exported_top_level_sets(env, state, imported.as_ref(), stack, *with_context)?;
675            state.pop_template();
676            state.macro_namespaces.insert(alias.clone(), defs);
677            state
678                .macro_namespace_values
679                .insert(alias.clone(), exported_sets);
680            Ok(String::new())
681        }
682        Node::FromImport {
683            template,
684            names,
685            with_context,
686        } => {
687            let loader = state
688                .loader
689                .ok_or_else(|| RunjucksError::new("`from` requires a template loader"))?;
690            let name = crate::value::value_to_string(&eval_to_value(env, state, template, stack)?);
691            let imported = env.load_and_parse_named(&name, loader)?;
692            state.push_template(&name)?;
693            scan_literal_import_graph(env, state, imported.as_ref(), loader)?;
694            let defs = collect_top_level_macros(imported.as_ref());
695            let exported_sets =
696                eval_exported_top_level_sets(env, state, imported.as_ref(), stack, *with_context)?;
697            state.pop_template();
698            let mut scope = HashMap::new();
699            for (export_name, alias_opt) in names {
700                let local = alias_opt.as_ref().unwrap_or(export_name);
701                if let Some(mdef) = defs.get(export_name) {
702                    scope.insert(local.clone(), mdef.clone());
703                } else if let Some(v) = exported_sets.get(export_name) {
704                    stack.set(local, v.clone());
705                } else {
706                    return Err(RunjucksError::new(format!("cannot import '{export_name}'")));
707                }
708            }
709            state.push_macros(scope);
710            Ok(String::new())
711        }
712        Node::Extends { .. } => Err(RunjucksError::new(
713            "`extends` is only valid at the top level of a loaded template",
714        )),
715        Node::Block { name, body } => {
716            let to_render: Vec<Node> = if let Some(ref chains) = state.block_chains {
717                chains
718                    .get(name)
719                    .and_then(|ch| ch.first().cloned())
720                    .unwrap_or_else(|| body.clone())
721            } else {
722                body.clone()
723            };
724            let prev_super = state.super_context.take();
725            state.super_context = Some((name.clone(), 0));
726            let out = render_children(env, state, &to_render, stack);
727            state.super_context = prev_super;
728            out
729        }
730        Node::FilterBlock { name, args, body } => {
731            #[cfg(feature = "async")]
732            if env.async_custom_filters.contains_key(name) {
733                return Err(RunjucksError::new(format!(
734                    "`{name}` is an async filter and can only be used with `renderStringAsync()` or `renderTemplateAsync()`"
735                )));
736            }
737            let s = render_children(env, state, body, stack)?;
738            let arg_vals: Vec<Value> = args
739                .iter()
740                .map(|a| eval_to_value(env, state, a, stack))
741                .collect::<Result<_>>()?;
742            let v = crate::filters::apply_builtin(
743                env,
744                &mut state.rng,
745                name,
746                &Value::String(s),
747                &arg_vals,
748            )?;
749            let out = crate::value::value_to_string(&v);
750            if env.autoescape && !crate::value::is_marked_safe(&v) {
751                Ok(crate::filters::escape_html(&out))
752            } else {
753                Ok(out)
754            }
755        }
756        Node::CallBlock {
757            caller_params,
758            callee,
759            body,
760        } => {
761            let Expr::Call {
762                callee: macro_target,
763                args,
764                kwargs,
765            } = callee
766            else {
767                return Err(RunjucksError::new(
768                    "`{% call %}` expects a macro call expression such as `wrap()` or `ns.wrap()`",
769                ));
770            };
771            let arg_vals: Vec<Value> = args
772                .iter()
773                .map(|a| eval_to_value(env, state, a, stack))
774                .collect::<Result<_>>()?;
775            let kw_vals: Vec<(String, Value)> = kwargs
776                .iter()
777                .map(|(k, e)| Ok((k.clone(), eval_to_value(env, state, e, stack)?)))
778                .collect::<Result<_>>()?;
779            let mdef = if let Expr::Variable(name) = macro_target.as_ref() {
780                state
781                    .lookup_macro(name)
782                    .cloned()
783                    .ok_or_else(|| RunjucksError::new(format!("unknown macro `{name}`")))?
784            } else if let Expr::GetAttr { base, attr } = macro_target.as_ref() {
785                if let Expr::Variable(ns) = base.as_ref() {
786                    state
787                        .lookup_namespaced_macro(ns, attr)
788                        .cloned()
789                        .ok_or_else(|| RunjucksError::new(format!("unknown macro `{ns}.{attr}`")))?
790                } else {
791                    return Err(RunjucksError::new(
792                        "`{% call %}` only supports simple macro or `namespace.macro()` calls",
793                    ));
794                }
795            } else {
796                return Err(RunjucksError::new(
797                    "`{% call %}` only supports simple macro or `namespace.macro()` calls",
798                ));
799            };
800            let frame = CallerFrame {
801                body: body.clone(),
802                params: caller_params.clone(),
803            };
804            state.caller_stack.push(frame);
805            let module_closure_owned =
806                if let Expr::GetAttr { base, attr: _ } = macro_target.as_ref() {
807                    if let Expr::Variable(ns) = base.as_ref() {
808                        state.macro_namespace_values.get(ns).cloned()
809                    } else {
810                        None
811                    }
812                } else {
813                    None
814                };
815            let res = render_macro_body(
816                env,
817                state,
818                &mdef,
819                &arg_vals,
820                &kw_vals,
821                stack,
822                module_closure_owned.as_ref(),
823            );
824            state.caller_stack.pop();
825            res
826        }
827        Node::ExtensionTag {
828            extension_name,
829            args,
830            body,
831            ..
832        } => {
833            let handler = env.custom_extensions.get(extension_name).ok_or_else(|| {
834                RunjucksError::new(format!("unknown extension `{extension_name}`"))
835            })?;
836            let rev = stack.revision();
837            let ctx_for_handler = match state.extension_context_cache.take() {
838                Some((r, v)) if r == rev => v,
839                _ => Value::Object(stack.flatten()),
840            };
841            let body_s = if let Some(nodes) = body {
842                Some(render_children(env, state, nodes, stack)?)
843            } else {
844                None
845            };
846            let out = handler(&ctx_for_handler, args.as_str(), body_s)?;
847            state.extension_context_cache = Some((rev, ctx_for_handler));
848            Ok(if env.autoescape {
849                crate::filters::escape_html(&out)
850            } else {
851                out
852            })
853        }
854        Node::AsyncEach { .. } => Err(RunjucksError::new(
855            "`{% asyncEach %}` requires async render mode; use `renderStringAsync()` or `renderTemplateAsync()`",
856        )),
857        Node::AsyncAll { .. } => Err(RunjucksError::new(
858            "`{% asyncAll %}` requires async render mode; use `renderStringAsync()` or `renderTemplateAsync()`",
859        )),
860        Node::IfAsync { .. } => Err(RunjucksError::new(
861            "`{% ifAsync %}` requires async render mode; use `renderStringAsync()` or `renderTemplateAsync()`",
862        )),
863        Node::MacroDef(_) => Ok(String::new()),
864    }
865}
866
867fn render_switch(
868    env: &Environment,
869    state: &mut RenderState<'_>,
870    disc_expr: &Expr,
871    cases: &[SwitchCase],
872    default_body: Option<&[Node]>,
873    stack: &mut CtxStack,
874) -> Result<String> {
875    let disc = eval_to_value(env, state, disc_expr, stack)?;
876    let mut start = None;
877    for (i, c) in cases.iter().enumerate() {
878        if eval_to_value(env, state, &c.cond, stack)? == disc {
879            start = Some(i);
880            break;
881        }
882    }
883    let mut acc = String::new();
884    if let Some(mut idx) = start {
885        loop {
886            let body = &cases[idx].body;
887            acc.push_str(&render_children(env, state, body, stack)?);
888            if !body.is_empty() {
889                return Ok(acc);
890            }
891            idx += 1;
892            if idx >= cases.len() {
893                break;
894            }
895        }
896    }
897    if let Some(db) = default_body {
898        acc.push_str(&render_children(env, state, db, stack)?);
899    }
900    Ok(acc)
901}
902
903fn inject_loop(stack: &mut CtxStack, i: usize, len: usize) {
904    crate::render_common::inject_loop(&mut stack.frames, i, len);
905    stack.bump_revision();
906}
907
908fn render_for(
909    env: &Environment,
910    state: &mut RenderState<'_>,
911    vars: &ForVars,
912    iter_expr: &Expr,
913    body: &[Node],
914    else_body: Option<&[Node]>,
915    stack: &mut CtxStack,
916) -> Result<String> {
917    let v = eval_to_value(env, state, iter_expr, stack)?;
918    let it = iterable_from_value(v);
919    if iterable_empty(&it) {
920        return if let Some(eb) = else_body {
921            render_children(env, state, eb, stack)
922        } else {
923            Ok(String::new())
924        };
925    }
926
927    stack.push_frame();
928    let mut acc = String::new();
929
930    match (vars, it) {
931        (ForVars::Single(x), Iterable::Rows(items)) => {
932            let len = items.len();
933            acc.reserve(len.saturating_mul(16));
934            for (i, item) in items.into_iter().enumerate() {
935                inject_loop(stack, i, len);
936                stack.set_local(x, item);
937                acc.push_str(&render_children(env, state, body, stack)?);
938            }
939        }
940        (ForVars::Multi(names), Iterable::Rows(rows)) if names.len() >= 2 => {
941            let len = rows.len();
942            acc.reserve(len.saturating_mul(16));
943            for (i, row) in rows.into_iter().enumerate() {
944                inject_loop(stack, i, len);
945                if let Value::Array(cols) = row {
946                    for (u, name) in names.iter().enumerate() {
947                        let cell = cols.get(u).cloned().unwrap_or(Value::Null);
948                        stack.set_local(name, cell);
949                    }
950                } else {
951                    for name in names {
952                        stack.set_local(name, Value::Null);
953                    }
954                }
955                acc.push_str(&render_children(env, state, body, stack)?);
956            }
957        }
958        (ForVars::Multi(names), Iterable::Pairs(pairs)) if names.len() == 2 => {
959            let len = pairs.len();
960            acc.reserve(len.saturating_mul(16));
961            for (i, (k, v)) in pairs.into_iter().enumerate() {
962                inject_loop(stack, i, len);
963                stack.set_local(&names[0], Value::String(k));
964                stack.set_local(&names[1], v);
965                acc.push_str(&render_children(env, state, body, stack)?);
966            }
967        }
968        (ForVars::Single(_), _) | (ForVars::Multi(_), _) => {
969            stack.pop_frame();
970            return if let Some(eb) = else_body {
971                render_children(env, state, eb, stack)
972            } else {
973                Ok(String::new())
974            };
975        }
976    }
977
978    stack.pop_frame();
979    Ok(acc)
980}
981
982fn render_children(
983    env: &Environment,
984    state: &mut RenderState<'_>,
985    nodes: &[Node],
986    stack: &mut CtxStack,
987) -> Result<String> {
988    let mut out = String::new();
989    out.reserve(nodes.len().saturating_mul(32));
990    for child in nodes {
991        out.push_str(&render_node(env, state, child, stack)?);
992    }
993    Ok(out)
994}
995
996fn render_output(
997    env: &Environment,
998    state: &mut RenderState<'_>,
999    exprs: &[Expr],
1000    stack: &mut CtxStack,
1001) -> Result<String> {
1002    let mut out = String::new();
1003    out.reserve(exprs.len().saturating_mul(24));
1004    for e in exprs {
1005        out.push_str(&eval_for_output(env, state, e, stack)?);
1006    }
1007    Ok(out)
1008}
1009
1010#[cfg(feature = "async")]
1011fn filter_chain_has_async_override(env: &Environment, e: &Expr) -> bool {
1012    let mut cur = e;
1013    loop {
1014        match cur {
1015            Expr::Filter { name, input, .. } => {
1016                if env.async_custom_filters.contains_key(name) {
1017                    return true;
1018                }
1019                cur = input.as_ref();
1020            }
1021            _ => return false,
1022        }
1023    }
1024}
1025
1026fn try_apply_peeled_builtin_filter_chain_value(
1027    env: &Environment,
1028    stack: &mut CtxStack,
1029    e: &Expr,
1030) -> Option<Result<Value>> {
1031    #[cfg(feature = "async")]
1032    if filter_chain_has_async_override(env, e) {
1033        return None;
1034    }
1035    let (rev_names, leaf) = peel_builtin_upper_lower_length_chain(e, &env.custom_filters)?;
1036    match leaf {
1037        Expr::Variable(var_name) => {
1038            let v = match env.resolve_variable_ref(stack, var_name) {
1039                Ok(v) => v,
1040                Err(e) => return Some(Err(e)),
1041            };
1042            Some(apply_builtin_filter_chain_on_cow_value(v, &rev_names))
1043        }
1044        Expr::Literal(Value::String(s)) => {
1045            let mut current = s.clone();
1046            for n in &rev_names {
1047                match *n {
1048                    "upper" => current = current.to_uppercase(),
1049                    "lower" => current = current.to_lowercase(),
1050                    "trim" => {
1051                        current = current
1052                            .trim_matches(|c: char| c.is_whitespace())
1053                            .to_string();
1054                    }
1055                    "capitalize" => {
1056                        current = crate::filters::capitalize_string_slice(&current);
1057                    }
1058                    "length" => return Some(Ok(json!(current.chars().count()))),
1059                    _ => unreachable!(),
1060                }
1061            }
1062            Some(Ok(Value::String(current)))
1063        }
1064        Expr::Literal(Value::Array(a)) if rev_names == ["length"] => Some(Ok(json!(a.len()))),
1065        _ => None,
1066    }
1067}
1068
1069fn eval_for_output(
1070    env: &Environment,
1071    state: &mut RenderState<'_>,
1072    e: &Expr,
1073    stack: &mut CtxStack,
1074) -> Result<String> {
1075    match e {
1076        Expr::Literal(v) => Ok(crate::value::value_to_string(v)),
1077        Expr::Variable(name) => {
1078            let v = env.resolve_variable_ref(stack, name)?;
1079            let s = crate::value::value_to_string(v.as_ref());
1080            if env.autoescape && !crate::value::is_marked_safe(v.as_ref()) {
1081                Ok(crate::filters::escape_html(&s))
1082            } else {
1083                Ok(s)
1084            }
1085        }
1086        Expr::Filter { name, input, args } => {
1087            #[cfg(feature = "async")]
1088            if env.async_custom_filters.contains_key(name) {
1089                return Err(RunjucksError::new(format!(
1090                    "`{name}` is an async filter and can only be used with `renderStringAsync()` or `renderTemplateAsync()`"
1091                )));
1092            }
1093            if args.is_empty() {
1094                if let Some((rev_names, leaf)) = peel_builtin_upper_lower_length_chain(e, &env.custom_filters) {
1095                    match leaf {
1096                        Expr::Variable(var_name) => {
1097                            let v = env.resolve_variable_ref(stack, var_name)?;
1098                            let input_safe = crate::value::is_marked_safe(v.as_ref());
1099                            match apply_builtin_filter_chain_on_cow_value(v, &rev_names) {
1100                                Ok(val) => {
1101                                    let s = crate::value::value_to_string(&val);
1102                                    let escape = env.autoescape
1103                                        && match &val {
1104                                            Value::String(_) => !input_safe,
1105                                            _ => true,
1106                                        };
1107                                    if escape {
1108                                        return Ok(crate::filters::escape_html(&s));
1109                                    }
1110                                    return Ok(s);
1111                                }
1112                                Err(e) => return Err(e),
1113                            }
1114                        }
1115                        Expr::Literal(Value::String(s)) => {
1116                            let mut current = s.clone();
1117                            for n in &rev_names {
1118                                match *n {
1119                                    "upper" => current = current.to_uppercase(),
1120                                    "lower" => current = current.to_lowercase(),
1121                                    "trim" => {
1122                                        current = current
1123                                            .trim_matches(|c: char| c.is_whitespace())
1124                                            .to_string();
1125                                    }
1126                                    "capitalize" => {
1127                                        current = crate::filters::capitalize_string_slice(&current);
1128                                    }
1129                                    "length" => {
1130                                        let s = current.chars().count().to_string();
1131                                        let escape = env.autoescape;
1132                                        return Ok(if escape {
1133                                            crate::filters::escape_html(&s)
1134                                        } else {
1135                                            s
1136                                        });
1137                                    }
1138                                    _ => unreachable!(),
1139                                }
1140                            }
1141                            let escape = env.autoescape;
1142                            return Ok(if escape {
1143                                crate::filters::escape_html(&current)
1144                            } else {
1145                                current
1146                            });
1147                        }
1148                        Expr::Literal(Value::Array(a)) if rev_names == ["length"] => {
1149                            let s = a.len().to_string();
1150                            let escape = env.autoescape;
1151                            return Ok(if escape {
1152                                crate::filters::escape_html(&s)
1153                            } else {
1154                                s
1155                            });
1156                        }
1157                        _ => {}
1158                    }
1159                }
1160            }
1161            if args.is_empty() && !env.custom_filters.contains_key(name) {
1162                if let Expr::Variable(var_name) = input.as_ref() {
1163                    let input_v = env.resolve_variable_ref(stack, var_name)?;
1164                    match name.as_str() {
1165                        "upper" => {
1166                            let out =
1167                                crate::value::value_to_string(input_v.as_ref()).to_uppercase();
1168                            return if env.autoescape
1169                                && !crate::value::is_marked_safe(input_v.as_ref())
1170                            {
1171                                Ok(crate::filters::escape_html(&out))
1172                            } else {
1173                                Ok(out)
1174                            };
1175                        }
1176                        "lower" => {
1177                            let out =
1178                                crate::value::value_to_string(input_v.as_ref()).to_lowercase();
1179                            return if env.autoescape
1180                                && !crate::value::is_marked_safe(input_v.as_ref())
1181                            {
1182                                Ok(crate::filters::escape_html(&out))
1183                            } else {
1184                                Ok(out)
1185                            };
1186                        }
1187                        "length" => {
1188                            let n = match input_v.as_ref() {
1189                                Value::String(s) => s.chars().count(),
1190                                Value::Array(a) => a.len(),
1191                                Value::Object(o) => o.len(),
1192                                v if is_undefined_value(v) => 0,
1193                                _ => 0,
1194                            };
1195                            return Ok(n.to_string());
1196                        }
1197                        "capitalize" => {
1198                            let out =
1199                                crate::filters::chain_capitalize_like_builtin(input_v.as_ref());
1200                            let s = crate::value::value_to_string(&out);
1201                            return if env.autoescape
1202                                && !crate::value::is_marked_safe(input_v.as_ref())
1203                            {
1204                                Ok(crate::filters::escape_html(&s))
1205                            } else {
1206                                Ok(s)
1207                            };
1208                        }
1209                        _ => {}
1210                    }
1211                }
1212                if let Expr::Literal(Value::String(s)) = input.as_ref() {
1213                    match name.as_str() {
1214                        "upper" => {
1215                            let out = s.to_uppercase();
1216                            return if env.autoescape {
1217                                Ok(crate::filters::escape_html(&out))
1218                            } else {
1219                                Ok(out)
1220                            };
1221                        }
1222                        "lower" => {
1223                            let out = s.to_lowercase();
1224                            return if env.autoescape {
1225                                Ok(crate::filters::escape_html(&out))
1226                            } else {
1227                                Ok(out)
1228                            };
1229                        }
1230                        "length" => {
1231                            return Ok(s.chars().count().to_string());
1232                        }
1233                        "capitalize" => {
1234                            let out = crate::filters::capitalize_string_slice(s);
1235                            return if env.autoescape {
1236                                Ok(crate::filters::escape_html(&out))
1237                            } else {
1238                                Ok(out)
1239                            };
1240                        }
1241                        _ => {}
1242                    }
1243                }
1244                if let Expr::Literal(Value::Array(a)) = input.as_ref() {
1245                    if name == "length" {
1246                        return Ok(a.len().to_string());
1247                    }
1248                }
1249            }
1250            let v = eval_to_value(env, state, e, stack)?;
1251            let s = crate::value::value_to_string(&v);
1252            if env.autoescape && !crate::value::is_marked_safe(&v) {
1253                Ok(crate::filters::escape_html(&s))
1254            } else {
1255                Ok(s)
1256            }
1257        }
1258        _ => {
1259            let v = eval_to_value(env, state, e, stack)?;
1260            let s = crate::value::value_to_string(&v);
1261            if env.autoescape && !crate::value::is_marked_safe(&v) {
1262                Ok(crate::filters::escape_html(&s))
1263            } else {
1264                Ok(s)
1265            }
1266        }
1267    }
1268}
1269
1270fn eval_slice_bound(
1271    env: &Environment,
1272    state: &mut RenderState<'_>,
1273    e: Option<&Expr>,
1274    stack: &mut CtxStack,
1275) -> Result<Option<i64>> {
1276    let Some(e) = e else {
1277        return Ok(None);
1278    };
1279    let v = eval_to_value(env, state, e, stack)?;
1280    if v.is_null() || crate::value::is_undefined_value(&v) {
1281        return Ok(None);
1282    }
1283    let n = v
1284        .as_i64()
1285        .or_else(|| v.as_f64().map(|x| x as i64))
1286        .or_else(|| crate::value::value_to_string(&v).parse().ok());
1287    match n {
1288        Some(x) => Ok(Some(x)),
1289        None => Err(RunjucksError::new("slice bound must be a number")),
1290    }
1291}
1292
1293fn try_dispatch_builtin(
1294    state: &mut RenderState<'_>,
1295    stack: &CtxStack,
1296    name: &str,
1297    arg_vals: &[Value],
1298) -> Option<Result<Value>> {
1299    crate::render_common::try_dispatch_builtin(
1300        &mut state.cyclers,
1301        &mut state.joiners,
1302        stack.defined(name),
1303        stack.get_ref(name),
1304        name,
1305        arg_vals,
1306    )
1307}
1308
1309fn render_macro_body(
1310    env: &Environment,
1311    state: &mut RenderState<'_>,
1312    m: &MacroDef,
1313    positional: &[Value],
1314    kwargs: &[(String, Value)],
1315    outer: &mut CtxStack,
1316    module_closure: Option<&HashMap<String, Value>>,
1317) -> Result<String> {
1318    let mut inner = outer.flatten();
1319    if let Some(mc) = module_closure {
1320        for (k, v) in mc {
1321            inner.insert(k.clone(), v.clone());
1322        }
1323    }
1324    for p in &m.params {
1325        let val = if let Some(ref d) = p.default {
1326            eval_to_value(env, state, d, outer)?
1327        } else {
1328            Value::Null
1329        };
1330        inner.insert(p.name.clone(), val);
1331    }
1332    for (i, p) in m.params.iter().enumerate() {
1333        if let Some(v) = positional.get(i) {
1334            inner.insert(p.name.clone(), v.clone());
1335        }
1336    }
1337    for (k, v) in kwargs {
1338        if m.params.iter().any(|p| p.name == *k) {
1339            inner.insert(k.clone(), v.clone());
1340        }
1341    }
1342    let mut stack = CtxStack::from_root(inner);
1343    render_children(env, state, &m.body, &mut stack)
1344}
1345
1346/// Renders the `{% call %}` body for `caller()` / `caller(args…)` (Nunjucks `Caller` node).
1347fn render_caller_invocation(
1348    env: &Environment,
1349    state: &mut RenderState<'_>,
1350    frame: &CallerFrame,
1351    positional: &[Value],
1352    kwargs: &[(String, Value)],
1353    stack: &mut CtxStack,
1354) -> Result<String> {
1355    if frame.params.is_empty() {
1356        if !positional.is_empty() || !kwargs.is_empty() {
1357            return Err(RunjucksError::new("`caller()` takes no arguments"));
1358        }
1359        return render_children(env, state, &frame.body, stack);
1360    }
1361    stack.push_frame();
1362    for p in &frame.params {
1363        let val = if let Some(ref d) = p.default {
1364            eval_to_value(env, state, d, stack)?
1365        } else {
1366            Value::Null
1367        };
1368        stack.set_local(&p.name, val);
1369    }
1370    for (i, p) in frame.params.iter().enumerate() {
1371        if let Some(v) = positional.get(i) {
1372            stack.set_local(&p.name, v.clone());
1373        }
1374    }
1375    for (k, v) in kwargs {
1376        if frame.params.iter().any(|p| p.name == *k) {
1377            stack.set_local(k, v.clone());
1378        }
1379    }
1380    let out = render_children(env, state, &frame.body, stack)?;
1381    stack.pop_frame();
1382    Ok(out)
1383}
1384
1385fn eval_to_value(
1386    env: &Environment,
1387    state: &mut RenderState<'_>,
1388    e: &Expr,
1389    stack: &mut CtxStack,
1390) -> Result<Value> {
1391    match e {
1392        Expr::Literal(v) => Ok(v.clone()),
1393        Expr::Variable(name) => env.resolve_variable(stack, name),
1394        Expr::Unary { op, expr } => match op {
1395            UnaryOp::Not => {
1396                if let Expr::Variable(name) = expr.as_ref() {
1397                    let v = env.resolve_variable_ref(stack, name)?;
1398                    return Ok(Value::Bool(!is_truthy(v.as_ref())));
1399                }
1400                let v = eval_to_value(env, state, expr, stack)?;
1401                Ok(Value::Bool(!is_truthy(&v)))
1402            }
1403            UnaryOp::Neg => {
1404                if let Expr::Variable(name) = expr.as_ref() {
1405                    let v = env.resolve_variable_ref(stack, name)?;
1406                    let n = as_number(v.as_ref())
1407                        .ok_or_else(|| RunjucksError::new("unary '-' expects a numeric value"))?;
1408                    return Ok(json_num(-n));
1409                }
1410                let v = eval_to_value(env, state, expr, stack)?;
1411                let n = as_number(&v)
1412                    .ok_or_else(|| RunjucksError::new("unary '-' expects a numeric value"))?;
1413                Ok(json_num(-n))
1414            }
1415            UnaryOp::Pos => {
1416                if let Expr::Variable(name) = expr.as_ref() {
1417                    let v = env.resolve_variable_ref(stack, name)?;
1418                    if let Some(n) = as_number(v.as_ref()) {
1419                        return Ok(json_num(n));
1420                    }
1421                    return Ok(v.into_owned());
1422                }
1423                let v = eval_to_value(env, state, expr, stack)?;
1424                Ok(v)
1425            }
1426        },
1427        Expr::Binary { op, left, right } => match op {
1428            BinOp::Add => Ok(add_like_js(
1429                &eval_to_value(env, state, left, stack)?,
1430                &eval_to_value(env, state, right, stack)?,
1431            )),
1432            BinOp::Concat => Ok(Value::String(format!(
1433                "{}{}",
1434                eval_for_output(env, state, left, stack)?,
1435                eval_for_output(env, state, right, stack)?
1436            ))),
1437            BinOp::Sub => {
1438                let a = eval_to_value(env, state, left, stack)?;
1439                let b = eval_to_value(env, state, right, stack)?;
1440                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`-` expects numbers"))?;
1441                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`-` expects numbers"))?;
1442                Ok(json_num(x - y))
1443            }
1444            BinOp::Mul => {
1445                let a = eval_to_value(env, state, left, stack)?;
1446                let b = eval_to_value(env, state, right, stack)?;
1447                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`*` expects numbers"))?;
1448                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`*` expects numbers"))?;
1449                Ok(json_num(x * y))
1450            }
1451            BinOp::Div => {
1452                let a = eval_to_value(env, state, left, stack)?;
1453                let b = eval_to_value(env, state, right, stack)?;
1454                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`/` expects numbers"))?;
1455                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`/` expects numbers"))?;
1456                Ok(json!(x / y))
1457            }
1458            BinOp::FloorDiv => {
1459                let a = eval_to_value(env, state, left, stack)?;
1460                let b = eval_to_value(env, state, right, stack)?;
1461                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`//` expects numbers"))?;
1462                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`//` expects numbers"))?;
1463                if y == 0.0 {
1464                    return Err(RunjucksError::new("division by zero"));
1465                }
1466                Ok(json_num((x / y).floor()))
1467            }
1468            BinOp::Mod => {
1469                let a = eval_to_value(env, state, left, stack)?;
1470                let b = eval_to_value(env, state, right, stack)?;
1471                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`%` expects numbers"))?;
1472                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`%` expects numbers"))?;
1473                Ok(json_num(x % y))
1474            }
1475            BinOp::Pow => {
1476                let a = eval_to_value(env, state, left, stack)?;
1477                let b = eval_to_value(env, state, right, stack)?;
1478                let x = as_number(&a).ok_or_else(|| RunjucksError::new("`**` expects numbers"))?;
1479                let y = as_number(&b).ok_or_else(|| RunjucksError::new("`**` expects numbers"))?;
1480                Ok(json!(x.powf(y)))
1481            }
1482            BinOp::And => {
1483                let l = eval_to_value(env, state, left, stack)?;
1484                if !is_truthy(&l) {
1485                    return Ok(l);
1486                }
1487                eval_to_value(env, state, right, stack)
1488            }
1489            BinOp::Or => {
1490                let l = eval_to_value(env, state, left, stack)?;
1491                if is_truthy(&l) {
1492                    return Ok(l);
1493                }
1494                eval_to_value(env, state, right, stack)
1495            }
1496            BinOp::In => {
1497                let key = eval_to_value(env, state, left, stack)?;
1498                let container = eval_to_value(env, state, right, stack)?;
1499                Ok(Value::Bool(eval_in(&key, &container)?))
1500            }
1501            BinOp::Is => {
1502                let (test_name, arg_exprs) = is_test_parts(right).ok_or_else(|| {
1503                    RunjucksError::new("`is` test must be an identifier, call, string, or null")
1504                })?;
1505                if test_name == "defined" {
1506                    if let Expr::Variable(n) = &**left {
1507                        return Ok(Value::Bool(stack.defined(n)));
1508                    }
1509                }
1510                if test_name == "callable" {
1511                    if let Expr::Variable(n) = &**left {
1512                        if state.lookup_macro(n).is_some() {
1513                            return Ok(Value::Bool(true));
1514                        }
1515                    }
1516                }
1517                if arg_exprs.is_empty() {
1518                    let v = match &**left {
1519                        Expr::Variable(n) => env.resolve_variable_ref(stack, n)?,
1520                        _ => Cow::Owned(eval_to_value(env, state, left, stack)?),
1521                    };
1522                    return Ok(Value::Bool(env.apply_is_test(
1523                        test_name,
1524                        v.as_ref(),
1525                        &[],
1526                    )?));
1527                }
1528                let arg_vals: Vec<Value> = arg_exprs
1529                    .iter()
1530                    .map(|e| eval_to_value(env, state, e, stack))
1531                    .collect::<Result<_>>()?;
1532                let v = match &**left {
1533                    Expr::Variable(n) => env.resolve_variable_ref(stack, n)?,
1534                    _ => Cow::Owned(eval_to_value(env, state, left, stack)?),
1535                };
1536                if matches!(test_name, "equalto" | "eq" | "sameas") && arg_exprs.len() == 1 {
1537                    if let Expr::Variable(lhs) = &**left {
1538                        if let Expr::Variable(rhs) = &arg_exprs[0] {
1539                            if lhs == rhs {
1540                                return Ok(Value::Bool(true));
1541                            }
1542                        }
1543                    }
1544                }
1545                Ok(Value::Bool(env.apply_is_test(
1546                    test_name,
1547                    v.as_ref(),
1548                    &arg_vals,
1549                )?))
1550            }
1551        },
1552        Expr::Compare { head, rest } => {
1553            if rest.len() == 1 {
1554                let (op, rhs_e) = &rest[0];
1555                match head.as_ref() {
1556                    Expr::Variable(n) => {
1557                        // RHS first: `resolve_variable_ref` borrows `stack` immutably while
1558                        // `eval_to_value` needs `&mut` — evaluate RHS before LHS (same result for
1559                        // pure compare expressions).
1560                        let r = eval_to_value(env, state, rhs_e, stack)?;
1561                        let left = env.resolve_variable_ref(stack, n)?;
1562                        return Ok(Value::Bool(compare_values(left.as_ref(), *op, &r)));
1563                    }
1564                    _ => {
1565                        let left = eval_to_value(env, state, head, stack)?;
1566                        let r = eval_to_value(env, state, rhs_e, stack)?;
1567                        return Ok(Value::Bool(compare_values(&left, *op, &r)));
1568                    }
1569                }
1570            }
1571            let mut acc = eval_to_value(env, state, head, stack)?;
1572            for (op, rhs_e) in rest.iter() {
1573                let r = eval_to_value(env, state, rhs_e, stack)?;
1574                let ok = compare_values(&acc, *op, &r);
1575                acc = Value::Bool(ok);
1576            }
1577            Ok(acc)
1578        }
1579        Expr::InlineIf {
1580            cond,
1581            then_expr,
1582            else_expr,
1583        } => {
1584            let c = eval_to_value(env, state, cond, stack)?;
1585            if is_truthy(&c) {
1586                eval_to_value(env, state, then_expr, stack)
1587            } else if let Some(els) = else_expr {
1588                eval_to_value(env, state, els, stack)
1589            } else {
1590                Ok(Value::Null)
1591            }
1592        }
1593        Expr::GetAttr { base, attr } => {
1594            if let Expr::Variable(ns) = base.as_ref() {
1595                if state.macro_namespaces.contains_key(ns)
1596                    || state.macro_namespace_values.contains_key(ns)
1597                {
1598                    if let Some(v) = state.lookup_namespaced_value(ns, attr) {
1599                        return Ok(v.clone());
1600                    }
1601                    if state.lookup_namespaced_macro(ns, attr).is_some() {
1602                        let mut m = Map::new();
1603                        m.insert(RJ_CALLABLE.to_string(), Value::Bool(true));
1604                        return Ok(Value::Object(m));
1605                    }
1606                    return Ok(undefined_value());
1607                }
1608            }
1609            if let Some((root_name, attrs)) = collect_attr_chain_from_getattr(e) {
1610                if !state.macro_namespaces.contains_key(root_name)
1611                    && !state.macro_namespace_values.contains_key(root_name)
1612                {
1613                    let mut cur = env.resolve_variable_ref(stack, root_name)?;
1614                    for a in &attrs {
1615                        if is_undefined_value(cur.as_ref()) || cur.as_ref().is_null() {
1616                            return Ok(undefined_value());
1617                        }
1618                        match cur.as_ref() {
1619                            Value::Object(o) => {
1620                                cur =
1621                                    Cow::Owned(o.get(*a).cloned().unwrap_or_else(undefined_value));
1622                            }
1623                            _ => return Ok(undefined_value()),
1624                        }
1625                    }
1626                    return Ok(cur.into_owned());
1627                }
1628            }
1629            let b = eval_to_value(env, state, base, stack)?;
1630            if is_undefined_value(&b) || b.is_null() {
1631                return Ok(undefined_value());
1632            }
1633            match b {
1634                Value::Object(o) => Ok(o.get(attr).cloned().unwrap_or_else(undefined_value)),
1635                _ => Ok(undefined_value()),
1636            }
1637        }
1638        Expr::GetItem { base, index } => match index.as_ref() {
1639            Expr::Slice {
1640                start: s,
1641                stop: st,
1642                step: step_e,
1643            } => {
1644                let start_v = eval_slice_bound(env, state, s.as_deref(), stack)?;
1645                let stop_v = eval_slice_bound(env, state, st.as_deref(), stack)?;
1646                let step_v = eval_slice_bound(env, state, step_e.as_deref(), stack)?;
1647                if let Expr::Variable(name) = base.as_ref() {
1648                    let base_val = env.resolve_variable_ref(stack, name)?;
1649                    if is_undefined_value(base_val.as_ref()) || base_val.as_ref().is_null() {
1650                        return Ok(undefined_value());
1651                    }
1652                    if let Value::Array(a) = base_val.as_ref() {
1653                        return Ok(Value::Array(jinja_slice_array(a, start_v, stop_v, step_v)));
1654                    }
1655                    return Ok(Value::Null);
1656                }
1657                let b = eval_to_value(env, state, base, stack)?;
1658                if is_undefined_value(&b) || b.is_null() {
1659                    return Ok(undefined_value());
1660                }
1661                let Value::Array(a) = &b else {
1662                    return Ok(Value::Null);
1663                };
1664                Ok(Value::Array(jinja_slice_array(a, start_v, stop_v, step_v)))
1665            }
1666            idx_e => {
1667                if let Expr::Variable(name) = base.as_ref() {
1668                    let base_val = env.resolve_variable_ref(stack, name)?;
1669                    if is_undefined_value(base_val.as_ref()) || base_val.as_ref().is_null() {
1670                        return Ok(undefined_value());
1671                    }
1672                    match idx_e {
1673                        Expr::Literal(Value::Number(n)) => {
1674                            let idx = n
1675                                .as_u64()
1676                                .or_else(|| n.as_f64().map(|x| x as u64))
1677                                .unwrap_or(0) as usize;
1678                            match base_val.as_ref() {
1679                                Value::Array(a) => {
1680                                    return Ok(a.get(idx).cloned().unwrap_or_else(undefined_value));
1681                                }
1682                                _ => return Ok(undefined_value()),
1683                            }
1684                        }
1685                        Expr::Literal(Value::String(k)) => match base_val.as_ref() {
1686                            Value::Object(o) => {
1687                                return Ok(o.get(k).cloned().unwrap_or_else(undefined_value));
1688                            }
1689                            _ => return Ok(undefined_value()),
1690                        },
1691                        _ => {}
1692                    }
1693                }
1694                let b = eval_to_value(env, state, base, stack)?;
1695                if is_undefined_value(&b) || b.is_null() {
1696                    return Ok(undefined_value());
1697                }
1698                let i = eval_to_value(env, state, idx_e, stack)?;
1699                match (&b, &i) {
1700                    (Value::Array(a), Value::Number(n)) => {
1701                        let idx = n
1702                            .as_u64()
1703                            .or_else(|| n.as_f64().map(|x| x as u64))
1704                            .unwrap_or(0) as usize;
1705                        Ok(a.get(idx).cloned().unwrap_or_else(undefined_value))
1706                    }
1707                    (Value::Object(o), Value::String(k)) => {
1708                        Ok(o.get(k).cloned().unwrap_or_else(undefined_value))
1709                    }
1710                    _ => Ok(undefined_value()),
1711                }
1712            }
1713        },
1714        Expr::Slice { .. } => Err(RunjucksError::new(
1715            "slice expression is only valid inside `[ ]`",
1716        )),
1717        Expr::Call {
1718            callee,
1719            args,
1720            kwargs,
1721        } => {
1722            let arg_vals: Vec<Value> = args
1723                .iter()
1724                .map(|a| eval_to_value(env, state, a, stack))
1725                .collect::<Result<_>>()?;
1726            let kw_vals: Vec<(String, Value)> = kwargs
1727                .iter()
1728                .map(|(k, e)| Ok((k.clone(), eval_to_value(env, state, e, stack)?)))
1729                .collect::<Result<_>>()?;
1730            if let Expr::GetAttr { base, attr } = callee.as_ref() {
1731                if attr == "test" {
1732                    let base_v = eval_to_value(env, state, base, stack)?;
1733                    if crate::value::is_regexp_value(&base_v) {
1734                        if !kw_vals.is_empty() {
1735                            return Err(RunjucksError::new(
1736                                "regex `.test` does not accept keyword arguments",
1737                            ));
1738                        }
1739                        if arg_vals.len() != 1 {
1740                            return Err(RunjucksError::new(
1741                                "regex `.test` expects exactly one argument",
1742                            ));
1743                        }
1744                        let Some((pat, fl)) = crate::value::regexp_pattern_flags(&base_v) else {
1745                            return Err(RunjucksError::new("invalid regex value"));
1746                        };
1747                        let s = crate::value::value_to_string(&arg_vals[0]);
1748                        return Ok(Value::Bool(crate::js_regex::regexp_test(&pat, &fl, &s)?));
1749                    }
1750                }
1751            }
1752            if let Expr::GetAttr { base, attr } = callee.as_ref() {
1753                if attr == "next" && arg_vals.is_empty() && kw_vals.is_empty() {
1754                    let b = eval_to_value(env, state, base, stack)?;
1755                    if let Some(id) = parse_cycler_id(&b) {
1756                        if let Some(c) = state.cyclers.get_mut(id) {
1757                            return Ok(c.next());
1758                        }
1759                        return Ok(Value::Null);
1760                    }
1761                }
1762            }
1763            if let Expr::Variable(name) = callee.as_ref() {
1764                if name == "super" {
1765                    if !args.is_empty() || !kw_vals.is_empty() {
1766                        return Err(RunjucksError::new("`super()` takes no arguments"));
1767                    }
1768                    let (block_name, layer) = state.super_context.clone().ok_or_else(|| {
1769                        RunjucksError::new("`super()` is only valid inside a `{% block %}`")
1770                    })?;
1771                    let (body_to_render, next) = {
1772                        let chains = state.block_chains.as_ref().ok_or_else(|| {
1773                            RunjucksError::new(
1774                                "`super()` requires template inheritance (`{% extends %}`)",
1775                            )
1776                        })?;
1777                        let chain = chains.get(&block_name).ok_or_else(|| {
1778                            RunjucksError::new(format!(
1779                                "no super block available for `{block_name}`"
1780                            ))
1781                        })?;
1782                        let next = layer + 1;
1783                        if next >= chain.len() {
1784                            return Err(RunjucksError::new(
1785                                "no parent block available for `super()`",
1786                            ));
1787                        }
1788                        (chain[next].clone(), next)
1789                    };
1790                    let prev = state.super_context.replace((block_name.clone(), next));
1791                    let s = render_children(env, state, &body_to_render, stack)?;
1792                    state.super_context = prev;
1793                    return Ok(mark_safe(s));
1794                }
1795                if name == "caller" {
1796                    let frame = state.caller_stack.last().cloned().ok_or_else(|| {
1797                        RunjucksError::new(
1798                            "`caller()` is only valid inside a macro invoked from `{% call %}`",
1799                        )
1800                    })?;
1801                    let s =
1802                        render_caller_invocation(env, state, &frame, &arg_vals, &kw_vals, stack)?;
1803                    return Ok(mark_safe(s));
1804                }
1805                if let Some(mdef) = state.lookup_macro(name).cloned() {
1806                    let s = render_macro_body(env, state, &mdef, &arg_vals, &kw_vals, stack, None)?;
1807                    return Ok(mark_safe(s));
1808                }
1809                if arg_vals.is_empty() {
1810                    let v = env.resolve_variable(stack, name)?;
1811                    if let Some(id) = parse_joiner_id(&v) {
1812                        if let Some(j) = state.joiners.get_mut(id) {
1813                            return Ok(Value::String(j.invoke()));
1814                        }
1815                    }
1816                }
1817                if let Some(r) = try_dispatch_builtin(state, stack, name, &arg_vals) {
1818                    return r;
1819                }
1820                if let Some(f) = env.custom_globals.get(name) {
1821                    return f(&arg_vals, &kw_vals);
1822                }
1823                #[cfg(feature = "async")]
1824                if env.async_custom_globals.contains_key(name) {
1825                    return Err(RunjucksError::new(format!(
1826                        "`{name}` is an async global and can only be used with `renderStringAsync()` or `renderTemplateAsync()`"
1827                    )));
1828                }
1829            }
1830            if let Expr::GetAttr { base, attr } = callee.as_ref() {
1831                if let Expr::Variable(ns) = base.as_ref() {
1832                    if let Some(mdef) = state.lookup_namespaced_macro(ns, attr).cloned() {
1833                        let mc = state.macro_namespace_values.get(ns).cloned();
1834                        let s = render_macro_body(
1835                            env,
1836                            state,
1837                            &mdef,
1838                            &arg_vals,
1839                            &kw_vals,
1840                            stack,
1841                            mc.as_ref(),
1842                        )?;
1843                        return Ok(mark_safe(s));
1844                    }
1845                }
1846            }
1847            Err(RunjucksError::new(
1848                "only template macros, built-in globals (`range`, `cycler`, `joiner`), registered global callables, or `super`/`caller` are supported for `()` expressions",
1849            ))
1850        }
1851        Expr::Filter { name, input, args } => {
1852            if args.is_empty() {
1853                if let Some(r) = try_apply_peeled_builtin_filter_chain_value(env, stack, e) {
1854                    return r;
1855                }
1856            }
1857            #[cfg(feature = "async")]
1858            let async_override = env.async_custom_filters.contains_key(name);
1859            #[cfg(not(feature = "async"))]
1860            let async_override = false;
1861            if args.is_empty() && !env.custom_filters.contains_key(name) && !async_override {
1862                if let Expr::Variable(var_name) = input.as_ref() {
1863                    let input_v = env.resolve_variable_ref(stack, var_name)?;
1864                    match name.as_str() {
1865                        "upper" => {
1866                            return Ok(Value::String(
1867                                crate::value::value_to_string(input_v.as_ref()).to_uppercase(),
1868                            ));
1869                        }
1870                        "lower" => {
1871                            return Ok(Value::String(
1872                                crate::value::value_to_string(input_v.as_ref()).to_lowercase(),
1873                            ));
1874                        }
1875                        "length" => {
1876                            return Ok(match input_v.as_ref() {
1877                                Value::String(s) => json!(s.chars().count()),
1878                                Value::Array(a) => json!(a.len()),
1879                                Value::Object(o) => json!(o.len()),
1880                                v if is_undefined_value(v) => json!(0),
1881                                _ => json!(0),
1882                            });
1883                        }
1884                        "capitalize" => {
1885                            return Ok(crate::filters::chain_capitalize_like_builtin(
1886                                input_v.as_ref(),
1887                            ));
1888                        }
1889                        _ => {}
1890                    }
1891                }
1892                if let Expr::Literal(Value::String(s)) = input.as_ref() {
1893                    match name.as_str() {
1894                        "upper" => return Ok(Value::String(s.to_uppercase())),
1895                        "lower" => return Ok(Value::String(s.to_lowercase())),
1896                        "length" => return Ok(json!(s.chars().count())),
1897                        "capitalize" => {
1898                            return Ok(Value::String(crate::filters::capitalize_string_slice(s)));
1899                        }
1900                        _ => {}
1901                    }
1902                }
1903                if let Expr::Literal(Value::Array(a)) = input.as_ref() {
1904                    if name == "length" {
1905                        return Ok(json!(a.len()));
1906                    }
1907                }
1908            }
1909            #[cfg(feature = "async")]
1910            if env.async_custom_filters.contains_key(name) {
1911                return Err(RunjucksError::new(format!(
1912                    "`{name}` is an async filter and can only be used with `renderStringAsync()` or `renderTemplateAsync()`"
1913                )));
1914            }
1915            let input_v = eval_to_value(env, state, input, stack)?;
1916            let arg_vals: Vec<Value> = args
1917                .iter()
1918                .map(|a| eval_to_value(env, state, a, stack))
1919                .collect::<Result<_>>()?;
1920            crate::filters::apply_builtin(env, &mut state.rng, name, &input_v, &arg_vals)
1921        }
1922        Expr::List(items) => {
1923            let mut out = Vec::with_capacity(items.len());
1924            for it in items {
1925                out.push(eval_to_value(env, state, it, stack)?);
1926            }
1927            Ok(Value::Array(out))
1928        }
1929        Expr::Dict(pairs) => {
1930            use serde_json::Map;
1931            let mut m = Map::new();
1932            for (k, v) in pairs {
1933                let key_v = eval_to_value(env, state, k, stack)?;
1934                let key = match key_v {
1935                    Value::String(s) => s,
1936                    _ => crate::value::value_to_string(&key_v),
1937                };
1938                m.insert(key, eval_to_value(env, state, v, stack)?);
1939            }
1940            Ok(Value::Object(m))
1941        }
1942        Expr::RegexLiteral { pattern, flags } => {
1943            let mut m = Map::new();
1944            m.insert(crate::value::RJ_REGEXP.to_string(), Value::Bool(true));
1945            m.insert("pattern".to_string(), Value::String(pattern.clone()));
1946            m.insert("flags".to_string(), Value::String(flags.clone()));
1947            Ok(Value::Object(m))
1948        }
1949    }
1950}