Krikri::VariableSubstitutor::Rerender
Constants
The pure, single-level bare indirection (x: "{{ y }}" - no
filters, no dotted/bracket access, no surrounding text) whose
referenced variable's type is still recoverable without a
render-then-reparse round trip. The capture group holds the
referenced name.
The message ansible-core's Templar raises for the same shape (TemplateRecursionError, "recursive loop detected in template string"). Shared with the re-raise helpers below so a krikri-jinja TemplateError wrapping this error (the engine wraps every resolver exception) is recognized by its message and unwrapped back to the real class at the engine boundary.
Class methods
Resolves a whole-value bare indirection straight to the referenced
variable's own value, preserving its TYPE the way ansible-core
2.19's native typing does: an indirected string stays a string
(buluma.bind's bind_python_version: "{{ bind_default_python_ version }}" against bind_default_python_version: "3" must
compare equal to the STRING '3'), and an indirected number stays
that number (robertdebock.java's java_version == 8). The
render-then-reparse detour both callers used cannot tell the two
apart - the rendered text "3" always re-typed as Int64, so the
strict == this branch introduced turned the bind case False.
Returns nil whenever the fast path does not apply - not a pure
indirection, referenced variable missing, a non-scalar target, or
a target that is itself a template this helper cannot resolve
structurally - and every caller then falls back to its existing
render path, so behavior elsewhere is unchanged.
Manual enter/exit pair for call sites whose body can't be a block (VarSubstitutor#substitute_impl wraps a long multi-return body in a method-level ensure instead).
The increment happens BEFORE the cap check, not after: the old
raise-then-never-increment order let every caller's matching
exit_retemplating (with_depth_guard's ensure, substitute_impl's
own ensure) decrement a level that was never entered - so at the
cap the counter leaked DOWNWARD one per failed entry and the
mutual-cycle descent oscillated just under it forever, each level
re-running the whole cycle instead of ever surfacing the error
(the round-5460170 hang). Increment-then-check keeps enter/exit
strictly paired, so the depth really accumulates through the
nested re-passes and the cap raises exactly once, at the top.
source_expr: the expression the value was resolved FROM (its leading identifier decides the unsafe gate) - nil keeps the old always-render behavior for call sites with no expression in hand (legacy/edge callers), but every evaluation-path caller threads it.
The fallback-rescue convention across both evaluators and the
lenient render path (evaluate_expr_dotted's rescue -> plain
lookup, whole_span_structured's rescue -> nil, JinjaRenderer#
render's rescue -> original text) exists for UNKNOWN constructs,
and a TemplateRecursionError is not one: swallowing it made every
fallback re-attempt the same cycle (the round-5460170 hang - the
guard tripped at the cap over a thousand times in five seconds
without the error ever surfacing), and the lenient render's
give-back-the-original-text behavior fed the still-templated text
back to the outer re-pass loop, which re-rendered it forever.
Call this first in any rescue that sits on a re-render path: it
re-raises the error (unwrapping the engine's "line N: " prefix
when krikri-jinja wrapped it) so the cycle fails the task the way
ansible-core does.
Renders raw (known to contain {{) back to its real value:
one whole-string span -> ExpressionEvaluator (preserves result
types); anything else -> full substitution. source_expr gates
unsafe roots the same way #if_templated does, and selects the
other host's scope for a hostvars-rooted expression (see
#if_templated).
The scope-agnostic half of #render_raw: render_vars is already the scope to render with (the reading host's, or the other host's merged scope when #if_templated/#render_raw detected a hostvars origin - detection must not re-run here, the merged scope would rebuild itself needlessly).
ansible-core 2.19 native typing for a WHOLE-single-span
{{ expr }} value (the template's whole AST is one output node
wrapping one expression): the expression's own result TYPE is the
value's type, so {{ 42 }} is the int 42, {{ '42' }} the str
"42", {{ none }} None - not something re-inferred from the
rendered TEXT. The string detour every re-render path used
(render to text, then re-type by shape) cannot tell a string
expression's output from a number's ("42" either way), so vars
like b: "{{ 42 }}" stayed strings here while Ansible keeps
them ints (live-verified vs ansible-playbook 2.19.11, with the
same matrix for list/dict/bool/None). Evaluating the span
STRUCTURALLY once (the engine's real typed evaluation) recovers
the type at the one point it is still known.
Returns nil whenever this path does NOT apply or does not succeed - not a whole single span (multi-span/mixed text, or block tags, which Ansible renders to plain text), the expression genuinely resolves to nothing (Undefined), or the engine cannot evaluate it. A None result comes back as a JSON null JSON::Any, not Crystal nil. Every caller falls back to its pre-existing render-then-retype path on nil, which keeps those shapes' behavior byte-identical: the engine evaluation only ran for a value that then resolves to nothing (no side effects to double), and a strict-undefined failure still fires from the fallback's own strict substitute.
Runs block under the re-templating depth guard. Every entry
point that re-renders a variable's own unrendered {{ }} value
must go through this - a cycle can re-enter through any of them
(Rerender.if_templated, VariableLookup#rerender_if_templated,
VarSubstitutor#substitute), so the counter has to be shared.