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 jinja_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 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.
The null-file-term marker for the strings-only lookup-args wire (the rerender below and lookup_file in lookup_file.cr are the only hands it passes through): a private-use control character like param_sentinels.cr's own sentinels, so no real role's lookup term - a path, a glob, anything a playbook can write - can ever collide with the equality check in lookup_file.
Python * repeat semantics: a String or Array operand repeated by
an int-coercible one (Bool counts as its int-subclass value; a
Float64 multiplier is a real TypeError, not a repeat). Returns nil
when the pair isn't a repeat shape at all. Checked in BOTH
orientations by the caller (str * int and int * str). A
negative count repeats zero times (Python semantics); a count that
would materialize more than MAX_REPEAT_ELEMENTS elements raises
rather than exhausting memory the way real Python's MemoryError
fails the task.
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.
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 (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.
Dotted NUMERIC parts (.0, Jinja's list-index shorthand) are
plain references too: excluding them sent l.0.0 to the Jinja
fallback, whose stringification loses the container's native
shape (structured_container saw a JSON TEXT string, not a
dict, so a mixed-text parent={{ l.0.0 }} rendered
{"name":"s1"} where Ansible renders Python repr
{'name': 's1'} - live-verified vs 2.19.11). VariableLookup's
apply_dotted_parts owns the Array-vs-Hash-key decision.
Constructors
Class methods
Public because the krikri-jinja engine's own lookup-terms re-render (krikri_jinja_lookups.cr's prepare_lookup_terms - the native-typed whole-span route set_fact values and debug msgs take) renders the same constants the same way and must warn the same way.
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 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 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 JinjaRenderer#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.
Set by #evaluate_lookup's dispatch when the result it returns kept its LIST form (ansible-core's ",".join raised TypeError and the sequence came back as-is) rather than comma-joining - the embedded-rendering decision in #evaluate_bare_lookup_call reads it straight after its own #evaluate_lookup call. Provenance from the dispatch, never from the returned text's shape: a lookup('file') whose content merely looks like a JSON array is a joined/str result and stays verbatim text (issue #695).
Set by #evaluate_lookup's dispatch when the result it returns kept its LIST form (ansible-core's ",".join raised TypeError and the sequence came back as-is) rather than comma-joining - the embedded-rendering decision in #evaluate_bare_lookup_call reads it straight after its own #evaluate_lookup call. Provenance from the dispatch, never from the returned text's shape: a lookup('file') whose content merely looks like a JSON array is a joined/str result and stays verbatim text (issue #695).
Set by the env lookup's dispatch when a missing env variable's
default= option ITSELF evaluated to the "undefined" sentinel (the
default=undef() shape): the lookup's real value is ansible's
undefined marker, not the sentinel as an ordinary degraded result.
The strict consumers raise with the plugin's own hint (see
evaluate_bare_lookup_call); a lenient consumer (| default(...))
keeps consuming the sentinel, which the filter engine's undefined?
already treats as a miss. Reset at every #evaluate_lookup entry,
like #lookup_list_form.
Set by the env lookup's dispatch when a missing env variable's
default= option ITSELF evaluated to the "undefined" sentinel (the
default=undef() shape): the lookup's real value is ansible's
undefined marker, not the sentinel as an ordinary degraded result.
The strict consumers raise with the plugin's own hint (see
evaluate_bare_lookup_call); a lenient consumer (| default(...))
keeps consuming the sentinel, which the filter engine's undefined?
already treats as a miss. Reset at every #evaluate_lookup entry,
like #lookup_list_form.