Krikri::VariableSubstitutor::ExpressionEvaluator
ExpressionEvaluator - Orchestrates evaluation of all expression types Delegates to specialized evaluators based on expression type
Constants
Tokens whose presence on one side of a top-level or/and mean
that side is a genuine boolean condition (comparison, is-test,
negation, or another nested or/and) rather than a plain value -
in that case the whole expression must keep going through
ConditionalEvaluator's boolean coercion instead of #evaluate_
value_or_and's value-passthrough semantics.
A bare identifier whose OWN stored raw value is a pure, single-
level {{ other_var }} indirection (no filters, no dotted/
bracket access) - the exact shape KNOWN_MISSING.md's "native
typing" gap documents as the one that actually diverges in
practice (found live in robertdebock.java/buluma.java's own
java_version == 8 gate). This engine's {{ }} substitution
deliberately preserves the SOURCE type as a string through such
an indirection rather than re-inferring a scalar type from
rendered text (see crinja_renderer.cr's own rerender_string_ value comment on why - protecting buluma.bind's ( bind_python_version == '3') idiom, which needs the opposite
behavior) - correct for real Ansible's OWN pre-2.19 templating
model, but ansible-core 2.19 made native types the default, so
a same-run X == <int> against exactly this indirected shape
can take the wrong branch silently. Not chased as a general
fix (measured at ~0.16% frequency across 611 real roles - see
KNOWN_MISSING.md's own decision-rule writeup, which explicitly
defers the full native-typing rewrite); this is the narrow
one-off it names as the alternative.
All three caches are keyed on raw expression TEXT, and an
expression can legitimately vary per loop item (assert:'s
that: "{{ item.a }} == 'x'" finalizes to different text per
item) - unbounded growth over a long run. The memoized values
are PURE functions of the text, so resetting is always safe; a
reset only costs re-computation.
real Ansible's password lookup default charset (ascii_letters +
digits + ".,:-_", its own DEFAULT_PASSWORD_CHARS) and default
length (20).
Evaluate any expression and return string result. A thin guard in
front of #evaluate_expr for the inline ternary TRUTHY if COND else FALSY (real Jinja2/Ansible syntax, used directly in default vars
like konstruktoid-hardening's sysctl_conf_dir: "{{ '/usr/lib/sysctl.d' if usr_lib_sysctl_d_dir else '/etc/sysctl.d' }}") - split out from the main body (rather than added as another
branch in it) purely to keep that method's already-high cyclomatic
complexity from tipping over ameba's threshold. Checked before any
of #evaluate_expr's own checks since COND itself commonly contains
a comparison - splitting first keeps that comparison scoped to COND
instead of being (wrongly) evaluated against the whole expression.
A plain variable reference: name, dotted path, bracket index.
No filters, operators, calls or literals - matching
REGEX_BARE_VAR_REF's spirit in variable_substitutor.cr.
Constructors
Instance methods
The undefined-typed form of #evaluate: Undefined::INSTANCE when the expression genuinely does not resolve to any value, the rendered String otherwise - which may itself be the literal text "undefined" when a real stored value collides with the sentinel.
This is the seam the "undefined"-string sentinel architecture
could not cross (KNOWN_MISSING.md's dotted-index collision entry):
#evaluate's String return type cannot distinguish a genuine miss
from a real value that happens to BE the text "undefined", so any
caller that re-checks its own output (rendered == "undefined")
misreads the collision as a miss and, under strict-undefined,
fails a task real Ansible runs. The disambiguation here never
compares strings: a rendered "undefined" is demoted to Undefined
only when the undefined-typed structural resolver
(VariableLookup#resolve - nil on a miss, JSON::Any otherwise)
ALSO finds no value. For the plain dotted/bracket chain shapes
its strict-undefined and dynamic-dict-key callers feed it, that
resolver is complete (quoted/integer/bare/bracket index keys,
numeric dot-indexing into lists, dynamic keys via recursion,
recursive re-templating of templated bases). Shapes resolve can't
parse degrade to the old behavior rather than to a regression:
they only reach the ambiguous branch when the render was already
the sentinel text.
#evaluate, but formatting a CONTAINER result the way real Ansible
renders one into final text - Python's repr (['a', 'b']),
not this codebase's internal JSON-compact form (["a","b"]).
Deliberately narrow: only a plain variable reference (bare,
dotted or indexed) is re-resolved structurally here, because
those are the shapes whose value is available WITHOUT re-running
the evaluation, and {{ some_list }} is where this difference
actually shows up. A filter chain still renders through
#evaluate's JSON form - see KNOWN_MISSING.md; closing that needs
the evaluator to carry structured results out to the final
boundary, which is the round trip CrinjaRenderer#evaluate_value!
warns about.
Only VarSubstitutor's outermost {{ }} expansion may call this.
Structured (raw JSON::Any) evaluation of a full expression, for callers outside this class - Krikri.bracket_index_failure_message's strict-probe use (round 812045, pluggero.bibata_cursor), which must see a JSON-null result as a real None value, not as the "undefined"-sentinel string the String-returning #evaluate collapses it to. Deliberately does NOT rescue: the caller decides what a Crinja failure means on its own path.