use std::collections::{BTreeMap, BTreeSet};
use helm_schema_ast::{
Literal, TemplateExpr, literal_printf_format, render_printf_scalar_values,
token_initial_printf_string_argument,
};
use helm_schema_core::{GuardDnf, GuardValue, Predicate, escape_regex_literal};
use crate::abstract_value::AbstractValue;
use crate::eval_effect::{
Effects, EvalResult, SelectionPolarity, SelectionReachability, SelectionTruthSource,
};
use crate::eval_env::EvalEnv;
use crate::expr_eval::{HelperCallValueResolver, direct_values_path, eval_expr_with_helper_calls};
use crate::scalar_value::{ScalarValueDispatch, TruthCondition};
use crate::function_semantics::{
CollectionShape, OutputSemantics, PredicateSemantics, ProvenanceBehavior, function_semantics,
strict_collection_item_pattern,
};
mod collections;
mod comparisons;
mod root_mutation;
mod serialization;
mod strict_operands;
mod traversal;
mod value_facts;
#[cfg(test)]
pub(crate) use collections::default_primary_selection;
use collections::{
eval_append, eval_coalesce, eval_concat, eval_default, eval_dict, eval_first_result,
eval_last_result, eval_list, eval_merge, eval_nonempty_split, eval_omit, eval_pick, eval_pluck,
eval_prepend, eval_regex_split, eval_reverse_result, eval_split_list,
is_nonempty_string_literal,
};
use comparisons::{eval_comparison, eval_ternary, eval_type_is};
use root_mutation::eval_set_call;
use serialization::{
conjoin_formatter_operand_selection, eval_cat, eval_from_json, eval_from_yaml, eval_join,
eval_print, eval_printf, eval_regex_replace, eval_repeat, eval_replace, eval_to_json,
eval_to_json_result, eval_to_yaml, eval_to_yaml_result, eval_tpl, eval_trim_affix,
record_printf_argument_effects, record_total_conversion_effects,
};
use strict_operands::{
push_fail_capture, record_collection_item_kind_result, record_length_bearing_operand,
record_length_bearing_result, record_operand_presence_result,
record_raw_range_key_string_consumer_paths, record_strict_kind_operands,
record_strict_kind_result, record_strict_parser_invocation, record_string_call_consumers,
record_string_consumer_effects, record_string_transform_effects,
string_invocation_operand_facts,
};
use traversal::{eval_dig, eval_index};
use value_facts::{
concrete_collection_len, concrete_integer, derive_value_text, identity_value_paths,
mark_stringified_identities,
};
struct PipedOperand {
result: EvalResult,
is_direct_values_path: bool,
}
struct CallInvocation<'a> {
function: &'a str,
args: &'a [TemplateExpr],
piped: Option<PipedOperand>,
}
pub(crate) const INTENTIONAL_DISPATCH_EXCEPTIONS: &[&str] = &[
"and",
"cat",
"coalesce",
"default",
"dict",
"dig",
"empty",
"eq",
"fromJson",
"fromJsonArray",
"has",
"include",
"join",
"kindIs",
"list",
"mustToJson",
"mustToRawJson",
"ne",
"not",
"or",
"print",
"required",
"set",
"sortAlpha",
"template",
"toJson",
"toRawJson",
"tuple",
"typeIs",
];
pub(crate) fn has_catalog_or_dispatch_exception(function: &str) -> bool {
function_semantics(function).is_known() || INTENTIONAL_DISPATCH_EXCEPTIONS.contains(&function)
}
pub(crate) const fn sequence_operand_direct_access(
had_piped_operand: bool,
is_direct_values_path: bool,
) -> bool {
!had_piped_operand && is_direct_values_path
}
pub(crate) fn eval_call_with_helper_calls(
function: &str,
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
eval_invocation(
CallInvocation {
function,
args,
piped: None,
},
env,
resolver,
)
}
fn eval_invocation(
invocation: CallInvocation<'_>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let CallInvocation {
function,
args,
mut piped,
} = invocation;
if !has_catalog_or_dispatch_exception(function) {
return match piped {
Some(piped) => eval_unknown_call(args, piped.result.effects, env, resolver),
None => eval_unknown_call(args, Effects::default(), env, resolver),
};
}
let operand_count = args.len() + usize::from(piped.is_some());
match function {
function
if matches!(
function,
"first"
| "last"
| "initial"
| "rest"
| "compact"
| "reverse"
| "deepCopy"
| "mustDeepCopy"
) && operand_count == 1
|| matches!(function, "uniq" | "mustUniq")
&& (piped.is_some() || args.len() == 1) =>
{
return eval_sequence_invocation(function, args, piped.take(), env, resolver);
}
"eq" | "ne" if operand_count >= 2 => {
let piped = piped
.take()
.map(|piped| (piped.result, piped.is_direct_values_path));
return eval_comparison(function, args, piped, env, resolver);
}
"ternary" => {
let piped = piped
.take()
.map(|piped| (piped.result, piped.is_direct_values_path));
return eval_ternary(args, piped, env, resolver);
}
"replace" if operand_count == 3 => {
return eval_replace(args, piped.take().map(|piped| piped.result), env, resolver);
}
"trimPrefix" | "trimSuffix" if operand_count == 2 => {
return eval_trim_affix(
function,
args,
piped.take().map(|piped| piped.result),
env,
resolver,
);
}
"fromYaml" if operand_count == 1 => {
return eval_from_yaml(args, piped.take().map(|piped| piped.result), env, resolver);
}
"fromJson" | "fromJsonArray" if operand_count == 1 => {
return eval_from_json(args, piped.take().map(|piped| piped.result), env, resolver);
}
"join" if operand_count == 2 => {
return eval_join(args, piped.take().map(|piped| piped.result), env, resolver);
}
"split" if operand_count == 2 && args.first().is_some_and(is_nonempty_string_literal) => {
return eval_nonempty_split(
args,
piped.take().map(|piped| piped.result),
env,
resolver,
);
}
function
if function_semantics(function).is_string_transform()
&& (piped.is_some()
|| !matches!(
(function, args.len()),
("repeat", 2)
| (
"regexReplaceAll"
| "mustRegexReplaceAll"
| "regexReplaceAllLiteral"
| "mustRegexReplaceAllLiteral",
3
)
)) =>
{
return eval_string_transform_invocation(
function,
args,
piped.take().map(|piped| piped.result),
env,
resolver,
);
}
_ => {}
}
if let Some(piped) = piped {
return eval_piped_invocation(function, args, piped, env, resolver);
}
eval_direct_invocation(function, args, env, resolver)
}
fn eval_string_transform_invocation(
function: &str,
args: &[TemplateExpr],
piped: Option<EvalResult>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let (result, piped_for_facts) = match piped {
Some(result) => {
let piped_for_facts = result.clone();
(result, Some(piped_for_facts))
}
None => (eval_all_args(args, env, resolver), None),
};
let scalar_dispatch = if function == "toString" {
result
.scalar_dispatch
.as_ref()
.map(ScalarValueDispatch::stringified)
} else {
None
};
let (string_paths, raw_range_key_paths) =
string_invocation_operand_facts(function, args, piped_for_facts.as_ref(), env, resolver);
let mut effects = result.effects;
if piped_for_facts.is_some() {
for arg in args {
let arg_result = eval_expr_with_helper_calls(arg, env, resolver);
if function == "b64enc" {
effects.add_encoded_paths(identity_value_paths(arg_result.value.as_ref()));
}
effects.merge(arg_result.effects);
}
}
if piped_for_facts.is_none() && !function_semantics(function).is_total_stringification() {
record_string_call_consumers(function, args, env, resolver, &mut effects);
}
record_string_transform_effects(
function,
result.value.as_ref(),
&string_paths,
&raw_range_key_paths,
&mut effects,
);
let value = if matches!(function, "quote" | "squote") {
result
.value
.map(AbstractValue::clear_plain_slot_string_format)
} else if function == "toString" {
mark_stringified_identities(result.value)
} else {
result.value
};
let result = EvalResult::with_effects(derive_value_text(value), effects);
match scalar_dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
}
}
fn eval_sequence_invocation(
function: &str,
args: &[TemplateExpr],
piped: Option<PipedOperand>,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let had_piped = piped.is_some();
let (operand, is_direct_values_path) = if let Some(piped) = piped {
(piped.result, piped.is_direct_values_path)
} else {
let Some(arg) = args.first() else {
return EvalResult::none();
};
(
eval_expr_with_helper_calls(arg, env, resolver),
direct_values_path(arg).is_some(),
)
};
let mut result = match function {
"first" => eval_first_result(operand.clone()),
"last" => eval_last_result(operand.clone()),
"reverse" => eval_reverse_result(operand.clone()),
"uniq" | "mustUniq" => {
let mut effects = operand.effects.clone();
if had_piped {
merge_arg_effects(args, env, resolver, &mut effects);
}
EvalResult::with_effects(operand.value.clone(), effects)
}
_ => operand.clone(),
};
if matches!(function, "deepCopy" | "mustDeepCopy") {
record_operand_presence_result(&operand, &mut result.effects);
} else {
let direct_access = sequence_operand_direct_access(had_piped, is_direct_values_path);
let nil_aborts = function_semantics(function).nil_aborts(direct_access);
record_strict_kind_result(&operand, "array", nil_aborts, &mut result.effects);
}
result
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
fn eval_direct_invocation(
function: &str,
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
match function {
"include" | "template" => {
let mut result = eval_helper_call(args, env, resolver);
record_values_root_helper_include(function, args, &mut result.effects);
result
}
"set" if args.len() == 3 => eval_set_call(args, env, resolver),
"unset" if args.len() == 2 => {
let [target, key] = args else {
return EvalResult::none();
};
let operand = eval_expr_with_helper_calls(target, env, resolver);
let mut effects = operand.effects.clone();
effects.merge(eval_expr_with_helper_calls(key, env, resolver).effects);
record_strict_kind_result(
&operand,
"object",
function_semantics(function).nil_aborts(direct_values_path(target).is_some()),
&mut effects,
);
EvalResult::with_effects(operand.value, effects)
}
"default" if matches!(args, [_, _]) => {
let [fallback, primary] = args else {
return EvalResult::none();
};
let primary = eval_expr_with_helper_calls(primary, env, resolver);
eval_default(primary, std::slice::from_ref(fallback), env, resolver)
}
"and" => eval_short_circuit_args(args, true, env, resolver),
"or" => eval_short_circuit_args(args, false, env, resolver),
"not" | "empty" if matches!(args, [_]) => {
let Some(arg) = args.first() else {
return EvalResult::none();
};
let operand = eval_expr_with_helper_calls(arg, env, resolver);
let truth = operand.truth.negated();
let effects = operand.effects;
let value = Some(AbstractValue::DerivedBoolean(effects.output_paths.clone()));
let mut result = EvalResult::with_effects(value, effects);
result.set_truth_condition(truth, SelectionTruthSource::RawInput);
result
}
"dict" => eval_dict(args, env, resolver).with_truth(if args.is_empty() {
Predicate::False
} else {
Predicate::True
}),
"list" | "tuple" => eval_list(args, env, resolver).with_truth(if args.is_empty() {
Predicate::False
} else {
Predicate::True
}),
"slice" | "mustSlice" if (2..=3).contains(&args.len()) => {
let Some((subject, bounds)) = args.split_first() else {
return EvalResult::none();
};
let mut result = eval_expr_with_helper_calls(subject, env, resolver);
record_strict_kind_operands(
function,
std::slice::from_ref(subject),
"array",
env,
resolver,
&mut result.effects,
);
merge_arg_effects(bounds, env, resolver, &mut result.effects);
result
}
"splitList" if args.len() == 2 => {
let mut result = eval_split_list(args, env, resolver);
record_string_call_consumers("splitList", args, env, resolver, &mut result.effects);
result
}
"append" => {
let mut result = eval_append(args, env, resolver);
if let [subject, _] = args {
record_strict_kind_operands(
function,
std::slice::from_ref(subject),
"array",
env,
resolver,
&mut result.effects,
);
}
result
}
"omit" if !args.is_empty() => {
let mut result = eval_omit(args, env, resolver);
if let Some(subject) = args.first() {
record_strict_kind_operands(
function,
std::slice::from_ref(subject),
"object",
env,
resolver,
&mut result.effects,
);
}
result
}
function if function_semantics(function).collection == CollectionShape::Merge => {
let mut result = eval_merge(function, args, EvalResult::none(), env, resolver);
record_strict_kind_operands(
function,
args,
"object",
env,
resolver,
&mut result.effects,
);
result
}
"coalesce" => eval_coalesce(args, env, resolver),
"genSignedCert" | "genSelfSignedCert" if args.len() >= 4 => {
let operands = args
.iter()
.map(|arg| eval_expr_with_helper_calls(arg, env, resolver))
.collect::<Vec<_>>();
let mut effects = Effects::default();
for operand in &operands {
effects.merge(operand.effects.clone());
}
if let Some(operand) = operands.first() {
record_strict_kind_result(
operand,
"string",
function_semantics(function).nil_aborts(false),
&mut effects,
);
}
for (index, operand) in operands.iter().enumerate().take(3).skip(1) {
record_strict_kind_result(
operand,
"array",
function_semantics(function).nil_aborts(false),
&mut effects,
);
record_collection_item_kind_result(
operand,
"string",
strict_collection_item_pattern(function_semantics(function), index),
&mut effects,
);
}
if let Some(operand) = operands.get(3) {
record_strict_kind_result(
operand,
"integer",
function_semantics(function).nil_aborts(false),
&mut effects,
);
}
EvalResult::with_effects(None, effects)
}
"concat" => {
let mut result = eval_concat(args, env, resolver);
record_strict_kind_operands(
function,
args,
"array",
env,
resolver,
&mut result.effects,
);
result
}
function
if function_semantics(function).output == OutputSemantics::Checksum
&& args.len() == 1 =>
{
let mut result = eval_unknown_call(args, Effects::default(), env, resolver);
record_string_call_consumers(function, args, env, resolver, &mut result.effects);
if let Some(subject) = args.first() {
let subject = eval_expr_with_helper_calls(subject, env, resolver);
let subject_paths = identity_value_paths(subject.value.as_ref());
result.effects.add_shape_erased_paths(subject_paths.clone());
result
.effects
.clear_plain_slot_string_format_paths(&subject_paths);
}
result
}
"len" if args.len() == 1 => {
let scalar_dispatch = args
.first()
.and_then(|subject| split_length_dispatch(subject, env, resolver));
let mut result = eval_unknown_call(args, Effects::default(), env, resolver);
record_length_bearing_operand(args, env, resolver, &mut result.effects);
let Some(subject_expr) = args.first() else {
return result;
};
let subject = eval_expr_with_helper_calls(subject_expr, env, resolver);
record_total_conversion_effects(
identity_value_paths(subject.value.as_ref()),
&mut result.effects,
);
if let Some(length) = subject.value.as_ref().and_then(concrete_collection_len) {
result.value = Some(AbstractValue::StringSet(
[length.to_string()].into_iter().collect(),
));
}
match scalar_dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
}
}
function if function_semantics(function).output == OutputSemantics::CoercingArithmetic => {
let mut result = eval_all_args(args, env, resolver);
for arg in args {
let operand = eval_expr_with_helper_calls(arg, env, resolver);
record_total_conversion_effects(
identity_value_paths(operand.value.as_ref()),
&mut result.effects,
);
}
if function == "add1"
&& let [arg] = args
&& let Some(value) = eval_expr_with_helper_calls(arg, env, resolver)
.value
.as_ref()
.and_then(concrete_integer)
{
result.value = Some(AbstractValue::StringSet(
[(value + 1).to_string()].into_iter().collect(),
));
}
result
}
"has" if matches!(args, [_, _]) => {
let [_, subject_expr] = args else {
return EvalResult::none();
};
let mut result = eval_unknown_call(args, Effects::default(), env, resolver);
record_strict_kind_operands(
function,
std::slice::from_ref(subject_expr),
"array",
env,
resolver,
&mut result.effects,
);
let subject = eval_expr_with_helper_calls(subject_expr, env, resolver);
record_total_conversion_effects(
identity_value_paths(subject.value.as_ref()),
&mut result.effects,
);
result
}
"prepend" if matches!(args, [_, _]) => {
let mut result = eval_prepend(args, env, resolver);
if let Some(subject) = args.first() {
record_strict_kind_operands(
function,
std::slice::from_ref(subject),
"array",
env,
resolver,
&mut result.effects,
);
}
result
}
"hasKey" if matches!(args, [_, _]) => {
let [subject_expr, key_expr] = args else {
return EvalResult::none();
};
let subject = eval_expr_with_helper_calls(subject_expr, env, resolver);
let key = eval_expr_with_helper_calls(key_expr, env, resolver);
let mut effects = subject.effects.clone();
effects.merge(key.effects);
let mut result = EvalResult::with_effects(
AbstractValue::widened(effects.output_paths.clone()),
effects,
);
record_strict_kind_result(
&subject,
"object",
function_semantics(function).nil_aborts(direct_values_path(subject_expr).is_some()),
&mut result.effects,
);
record_total_conversion_effects(
identity_value_paths(subject.value.as_ref()),
&mut result.effects,
);
let key = key.value.as_ref().and_then(|value| {
let AbstractValue::StringSet(strings) = value else {
return None;
};
let mut strings = strings.iter();
match (strings.next(), strings.next()) {
(Some(key), None) => Some(key.as_str()),
_ => None,
}
});
if let Some(predicate) =
subject.value.as_ref().zip(key).and_then(|(subject, key)| {
crate::value_path_context::value_has_key(subject, key)
})
{
result.set_truth_condition(
TruthCondition::exact(predicate),
SelectionTruthSource::RawInput,
);
}
result
}
"pick" if !args.is_empty() => {
let mut result = eval_pick(args, env, resolver);
if let Some(subject) = args.first() {
record_strict_kind_operands(
function,
std::slice::from_ref(subject),
"object",
env,
resolver,
&mut result.effects,
);
}
result
}
"keys" | "values" if args.len() == 1 => {
let Some(arg) = args.first() else {
return EvalResult::none();
};
let operand = eval_expr_with_helper_calls(arg, env, resolver);
let mut result = eval_unknown_call(args, Effects::default(), env, resolver);
record_strict_kind_result(
&operand,
"object",
function_semantics(function).nil_aborts(direct_values_path(arg).is_some()),
&mut result.effects,
);
record_total_conversion_effects(
identity_value_paths(operand.value.as_ref()),
&mut result.effects,
);
if function == "keys"
&& let Some(AbstractValue::ValuesPath(path) | AbstractValue::JsonDecodedPath(path)) =
&operand.value
{
result.value = Some(AbstractValue::KeysList(path.clone()));
}
result
}
"sortAlpha" if args.len() == 1 => {
let Some(arg) = args.first() else {
return EvalResult::none();
};
let operand = eval_expr_with_helper_calls(arg, env, resolver);
match &operand.value {
Some(AbstractValue::KeysList(_)) => operand,
_ => eval_unknown_call(args, Effects::default(), env, resolver),
}
}
"pluck" if args.len() >= 2 => {
let mut result = eval_pluck(args, env, resolver);
if let Some((key, maps)) = args.split_first() {
record_strict_kind_operands(
function,
std::slice::from_ref(key),
"string",
env,
resolver,
&mut result.effects,
);
record_strict_kind_operands(
function,
maps,
"object",
env,
resolver,
&mut result.effects,
);
}
result
}
"print" => eval_print(args, env, resolver),
"printf" => eval_printf(args, env, resolver),
"regexReplaceAll"
| "mustRegexReplaceAll"
| "regexReplaceAllLiteral"
| "mustRegexReplaceAllLiteral"
if args.len() == 3 =>
{
eval_regex_replace(function, args, env, resolver)
}
"repeat" if args.len() == 2 => {
let mut result = eval_repeat(args, env, resolver);
let (string_paths, raw_range_key_paths) =
string_invocation_operand_facts("repeat", args, None, env, resolver);
record_string_transform_effects(
"repeat",
result.value.as_ref(),
&string_paths,
&raw_range_key_paths,
&mut result.effects,
);
result
}
"tpl" if args.len() == 2 => eval_tpl(args, env, resolver),
"lookup" if args.len() == 4 => {
let mut effects = Effects::default();
merge_arg_effects(args, env, resolver, &mut effects);
record_string_call_consumers("lookup", args, env, resolver, &mut effects);
EvalResult::with_effects(Some(AbstractValue::Unknown), effects)
}
"cat" => eval_cat(args, env, resolver),
"index" => eval_index(args, false, env, resolver),
"get" if args.len() == 2 => eval_index(args, true, env, resolver),
"dig" if args.len() >= 3 => eval_dig(args, env, resolver),
"required" if matches!(args, [_, _]) => {
let [message, subject] = args else {
return EvalResult::none();
};
let message = eval_expr_with_helper_calls(message, env, resolver);
let mut subject = eval_expr_with_helper_calls(subject, env, resolver);
if !matches!(
subject.value.as_ref(),
Some(AbstractValue::ValuesPath(_) | AbstractValue::JsonDecodedPath(_))
) && let Some(truth) = subject.truth.predicate()
{
push_fail_capture(vec![truth.clone().negated()], &mut subject.effects);
}
subject.effects.merge(message.effects);
subject
}
"typeIs" | "kindIs" if args.len() >= 2 => eval_type_is(function, args, env, resolver),
"toYaml" if args.len() == 1 => eval_to_yaml(args, env, resolver),
"toJson" | "mustToJson" | "toRawJson" | "mustToRawJson" if args.len() == 1 => {
eval_to_json(args, env, resolver)
}
"regexSplit" if args.len() == 3 => eval_regex_split(args, env, resolver),
function
if function_semantics(function).output == OutputSemantics::TotalNumericCast
&& args.len() == 1 =>
{
let result = eval_all_args(args, env, resolver);
let mut effects = result.effects;
record_total_conversion_effects(
identity_value_paths(result.value.as_ref()),
&mut effects,
);
EvalResult::with_effects(derive_value_text(result.value), effects)
}
function
if (function_semantics(function).collection == CollectionShape::StringSplit
|| function_semantics(function).predicate == PredicateSemantics::String)
&& !args.is_empty() =>
{
let scalar_truth = args.last().and_then(|subject_expr| {
let subject = eval_expr_with_helper_calls(subject_expr, env, resolver);
let dispatch = subject
.scalar_dispatch
.or_else(|| direct_stringified_dispatch(subject_expr, env, resolver));
scalar_pattern_condition(function, args, dispatch.as_ref()).or_else(|| {
direct_quoted_falsy_pattern_condition(
function,
args,
subject_expr,
env,
resolver,
)
})
});
let result = eval_all_args(args, env, resolver);
let mut effects = result.effects;
record_string_call_consumers(function, args, env, resolver, &mut effects);
record_strict_parser_invocation(function, args, None, env, resolver, &mut effects);
let widened = AbstractValue::widened(
result
.value
.as_ref()
.map(AbstractValue::paths)
.unwrap_or_default(),
);
let result = EvalResult::with_effects(widened, effects);
let mut result = result;
if let Some(truth) = scalar_truth {
result.set_truth_condition(truth, SelectionTruthSource::RawInput);
}
result
}
function if function_semantics(function).provenance == ProvenanceBehavior::Preserve => {
eval_all_args(args, env, resolver)
}
_ => eval_unknown_call(args, Effects::default(), env, resolver),
}
}
fn record_values_root_helper_include(
_function: &str,
args: &[TemplateExpr],
effects: &mut Effects,
) {
let Some(TemplateExpr::Literal(helm_schema_ast::Literal::String(name))) =
args.first().map(TemplateExpr::deparen)
else {
return;
};
let mut passes_values_root = false;
for arg in args.iter().skip(1) {
arg.walk(|inner| match inner {
TemplateExpr::Field(path) if path.as_slice() == ["Values"] => {
passes_values_root = true;
}
TemplateExpr::Selector { operand, path }
if path.as_slice() == ["Values"]
&& matches!(
operand.as_ref().deparen(),
TemplateExpr::Variable(variable) if variable.is_empty()
) =>
{
passes_values_root = true;
}
_ => {}
});
}
if passes_values_root {
effects
.observed_facts
.values_root_helper_includes
.insert(name.clone());
}
}
#[expect(
clippy::too_many_lines,
reason = "keeping this semantic operation together makes its state transitions easier to audit"
)]
fn eval_piped_invocation(
function: &str,
args: &[TemplateExpr],
piped: PipedOperand,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let PipedOperand {
result: current,
is_direct_values_path: piped_is_direct_values_path,
} = piped;
match function {
"default" => eval_default(current, args, env, resolver),
function if function_semantics(function).collection == CollectionShape::Merge => {
let piped_operand = current.clone();
let mut result = eval_merge(function, args, current, env, resolver);
record_strict_kind_result(
&piped_operand,
"object",
function_semantics(function).nil_aborts(false),
&mut result.effects,
);
record_strict_kind_operands(
function,
args,
"object",
env,
resolver,
&mut result.effects,
);
result
}
"slice" | "mustSlice" if (1..=2).contains(&args.len()) => {
let operand = current.clone();
let mut result = current;
record_strict_kind_result(
&operand,
"array",
function_semantics(function).nil_aborts(false),
&mut result.effects,
);
merge_arg_effects(args, env, resolver, &mut result.effects);
result
}
"len" if args.is_empty() => {
let operand = current.clone();
let mut result = eval_unknown_call(args, current.effects, env, resolver);
record_length_bearing_result(&operand, &mut result.effects);
record_total_conversion_effects(
identity_value_paths(operand.value.as_ref()),
&mut result.effects,
);
if let Some(length) = operand.value.as_ref().and_then(concrete_collection_len) {
result.value = Some(AbstractValue::StringSet(
[length.to_string()].into_iter().collect(),
));
}
result
}
function
if function_semantics(function).output == OutputSemantics::Checksum
&& args.is_empty() =>
{
let (string_paths, raw_range_key_paths) =
string_invocation_operand_facts(function, args, Some(¤t), env, resolver);
let subject_identities = identity_value_paths(current.value.as_ref());
let mut result = eval_unknown_call(args, current.effects, env, resolver);
result
.effects
.add_shape_erased_paths(subject_identities.clone());
record_string_consumer_effects(
current.value.as_ref(),
&string_paths,
&mut result.effects,
);
result
.effects
.clear_plain_slot_string_format_paths(&subject_identities);
record_raw_range_key_string_consumer_paths(&raw_range_key_paths, &mut result.effects);
result
}
"printf" => {
let piped_dispatch = current.scalar_dispatch.clone();
let mut effects = current.effects;
let piped_scalar = piped_dispatch
.as_ref()
.and_then(ScalarValueDispatch::constant_value);
let piped = identity_value_paths(current.value.as_ref());
let token_initial_string_argument = token_initial_printf_string_argument(args);
if token_initial_string_argument == Some(args.len()) {
conjoin_formatter_operand_selection(&piped, piped_dispatch.as_ref(), &mut effects);
}
record_printf_argument_effects(false, current.value.as_ref(), &piped, &mut effects);
let mut scalar_values = Vec::with_capacity(args.len());
for (index, arg) in args.iter().enumerate() {
let mut result = eval_expr_with_helper_calls(arg, env, resolver);
let identity_paths = identity_value_paths(result.value.as_ref());
if token_initial_string_argument == Some(index) {
conjoin_formatter_operand_selection(
&identity_paths,
result.scalar_dispatch.as_ref(),
&mut result.effects,
);
}
if index > 0 {
scalar_values.push(
result
.scalar_dispatch
.as_ref()
.and_then(ScalarValueDispatch::constant_value),
);
}
effects.merge(result.effects);
record_printf_argument_effects(
index == 0,
result.value.as_ref(),
&identity_paths,
&mut effects,
);
}
scalar_values.push(piped_scalar);
let dispatch = literal_printf_format(args)
.and_then(|format| {
scalar_values
.into_iter()
.collect::<Option<Vec<_>>>()
.and_then(|values| render_printf_scalar_values(format, &values))
})
.map(|value| ScalarValueDispatch::constant(GuardValue::string(value)))
.or_else(|| {
if args.len() != 1 {
return None;
}
let format = literal_printf_format(args)?;
piped_dispatch.as_ref()?.printf_string(format)
});
let result = EvalResult::with_effects(current.value, effects);
match dispatch {
Some(dispatch) => result.with_scalar_dispatch(dispatch),
None => result,
}
}
function if function_semantics(function).output == OutputSemantics::TotalNumericCast => {
let mut effects = current.effects;
record_total_conversion_effects(
identity_value_paths(current.value.as_ref()),
&mut effects,
);
merge_arg_effects(args, env, resolver, &mut effects);
EvalResult::with_effects(current.value, effects)
}
function if function_semantics(function).output == OutputSemantics::CoercingArithmetic => {
let mut effects = current.effects;
record_total_conversion_effects(
identity_value_paths(current.value.as_ref()),
&mut effects,
);
for arg in args {
let operand = eval_expr_with_helper_calls(arg, env, resolver);
record_total_conversion_effects(
identity_value_paths(operand.value.as_ref()),
&mut effects,
);
effects.merge(operand.effects);
}
let value = AbstractValue::widened(
current
.value
.as_ref()
.map(AbstractValue::paths)
.unwrap_or_default(),
);
EvalResult::with_effects(value, effects)
}
function
if function_semantics(function).collection == CollectionShape::StringSplit
|| function_semantics(function).predicate == PredicateSemantics::String =>
{
let scalar_truth =
scalar_pattern_condition(function, args, current.scalar_dispatch.as_ref());
let piped = current.clone();
let (string_paths, raw_range_key_paths) =
string_invocation_operand_facts(function, args, Some(¤t), env, resolver);
let mut effects = current.effects;
merge_arg_effects(args, env, resolver, &mut effects);
record_string_consumer_effects(current.value.as_ref(), &string_paths, &mut effects);
record_raw_range_key_string_consumer_paths(&raw_range_key_paths, &mut effects);
record_strict_parser_invocation(
function,
args,
Some((&piped, piped_is_direct_values_path)),
env,
resolver,
&mut effects,
);
let widened = AbstractValue::widened(
current
.value
.as_ref()
.map(AbstractValue::paths)
.unwrap_or_default(),
);
let mut result = EvalResult::with_effects(widened, effects);
if let Some(truth) = scalar_truth {
result.set_truth_condition(truth, SelectionTruthSource::RawInput);
}
result
}
"toYaml" => {
let mut result = eval_to_yaml_result(current);
merge_arg_effects(args, env, resolver, &mut result.effects);
result
}
"toJson" | "mustToJson" | "toRawJson" | "mustToRawJson" => {
let mut result = eval_to_json_result(current);
merge_arg_effects(args, env, resolver, &mut result.effects);
result
}
"concat" => {
let piped_operand = current.clone();
let mut result = eval_unknown_call(args, current.effects, env, resolver);
record_strict_kind_result(
&piped_operand,
"array",
function_semantics(function).nil_aborts(false),
&mut result.effects,
);
record_strict_kind_operands(
function,
args,
"array",
env,
resolver,
&mut result.effects,
);
result
}
"has" if args.len() == 1 => {
let piped_operand = current.clone();
let mut result = eval_unknown_call(args, current.effects, env, resolver);
record_strict_kind_result(
&piped_operand,
"array",
function_semantics(function).nil_aborts(false),
&mut result.effects,
);
record_total_conversion_effects(
identity_value_paths(piped_operand.value.as_ref()),
&mut result.effects,
);
result
}
"keys" | "values" if args.is_empty() => {
let operand = current.clone();
let mut result = eval_unknown_call(args, current.effects, env, resolver);
record_strict_kind_result(
&operand,
"object",
function_semantics(function).nil_aborts(false),
&mut result.effects,
);
record_total_conversion_effects(
identity_value_paths(operand.value.as_ref()),
&mut result.effects,
);
if function == "keys"
&& let Some(AbstractValue::ValuesPath(path) | AbstractValue::JsonDecodedPath(path)) =
&operand.value
{
result.value = Some(AbstractValue::KeysList(path.clone()));
}
result
}
"sortAlpha" if args.is_empty() => match ¤t.value {
Some(AbstractValue::KeysList(_)) => current,
_ => eval_unknown_call(args, current.effects, env, resolver),
},
function if function_semantics(function).provenance == ProvenanceBehavior::Preserve => {
let mut effects = current.effects;
merge_arg_effects(args, env, resolver, &mut effects);
EvalResult::with_effects(current.value, effects)
}
_ => eval_unknown_call(args, current.effects, env, resolver),
}
}
pub(crate) fn eval_pipeline_with_helper_calls(
stages: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let Some((first_stage, remaining_stages)) = stages.split_first() else {
return EvalResult::none();
};
let mut current = eval_expr_with_helper_calls(first_stage, env, resolver);
let mut current_is_direct_values_path = direct_values_path(first_stage).is_some();
for stage in remaining_stages {
let TemplateExpr::Call { function, args } = stage else {
current
.effects
.merge(eval_expr_with_helper_calls(stage, env, resolver).effects);
current_is_direct_values_path = false;
continue;
};
current = eval_invocation(
CallInvocation {
function,
args,
piped: Some(PipedOperand {
result: current,
is_direct_values_path: current_is_direct_values_path,
}),
},
env,
resolver,
);
current_is_direct_values_path = false;
}
current
}
fn eval_short_circuit_args(
args: &[TemplateExpr],
previous_truthy: bool,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let mut effects = Effects::default();
let mut values = Vec::new();
let mut execution_predicates = BTreeSet::new();
let mut operand_conditions = Vec::with_capacity(args.len());
let mut constrained_env = env.clone();
for (index, arg) in args.iter().enumerate() {
let mut result = eval_expr_with_helper_calls(arg, &constrained_env, resolver);
scope_execution_effects(&mut result.effects, &execution_predicates);
let operand_truth = result.truth.clone();
let operand_reachability = result.output_reachability(SelectionPolarity::Truthy);
operand_conditions.push(operand_truth.clone());
let selection = if index + 1 < args.len() {
if previous_truthy {
operand_reachability.complement()
} else {
operand_reachability.clone()
}
} else {
SelectionReachability::always(operand_reachability.truth_source())
}
.conjoin_predicates(execution_predicates.iter().cloned());
conjoin_result_reachability(
&mut result,
&selection,
if previous_truthy {
"and operand selection"
} else {
"or operand selection"
},
operand_truth
.when_true()
.value_paths()
.union(&operand_truth.when_false().value_paths())
.cloned()
.collect(),
);
if let Some(value) = result.value {
values.push(value);
}
effects.merge(result.effects);
if index + 1 == args.len() {
break;
}
let execution_reachability = if previous_truthy {
operand_reachability
} else {
operand_reachability.complement()
};
let next_condition = execution_reachability.execution_predicate(
if previous_truthy {
"and operand execution"
} else {
"or operand execution"
},
operand_truth
.when_true()
.value_paths()
.union(&operand_truth.when_false().value_paths())
.cloned()
.collect(),
);
if next_condition != Predicate::True {
execution_predicates.insert(next_condition);
}
constrained_env.bound_values = constrained_env
.bound_values
.with_predicate_constraints(arg, previous_truthy);
}
let mut result = EvalResult::with_effects(AbstractValue::choice(values), effects);
result.set_truth_condition(
combined_short_circuit_truth(&operand_conditions, previous_truthy),
SelectionTruthSource::RawInput,
);
result
}
fn split_length_dispatch(
expr: &TemplateExpr,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> Option<ScalarValueDispatch> {
let TemplateExpr::Call { function, args } = expr.deparen() else {
return None;
};
let [separator, subject] = args.as_slice() else {
return None;
};
if function != "split" {
return None;
}
let TemplateExpr::Literal(Literal::String(separator) | Literal::RawString(separator)) =
separator.deparen()
else {
return None;
};
if separator.is_empty() {
return None;
}
let subject = eval_expr_with_helper_calls(subject, env, resolver);
Some(subject.scalar_dispatch?.split_length(separator))
}
fn scalar_pattern_condition(
function: &str,
args: &[TemplateExpr],
dispatch: Option<&ScalarValueDispatch>,
) -> Option<TruthCondition> {
let dispatch = dispatch?;
match function {
"regexMatch" | "mustRegexMatch" => {
let pattern = literal_string(args.first()?)?;
Some(dispatch.condition_matches_pattern(pattern))
}
"contains" => {
let needle = literal_string(args.first()?)?;
Some(dispatch.condition_matches_pattern(&escape_regex_literal(needle)))
}
"hasPrefix" => {
let prefix = literal_string(args.first()?)?;
Some(dispatch.condition_matches_pattern(&format!("^{}", escape_regex_literal(prefix))))
}
"hasSuffix" => {
let suffix = literal_string(args.first()?)?;
Some(dispatch.condition_matches_pattern(&format!("{}$", escape_regex_literal(suffix))))
}
"semverCompare" => {
let constraint = literal_string(args.first()?)?;
Some(dispatch.condition_matches_semver(constraint))
}
_ => None,
}
}
fn direct_stringified_dispatch(
expr: &TemplateExpr,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> Option<ScalarValueDispatch> {
let TemplateExpr::Call { function, args } = expr.deparen() else {
return None;
};
if function != "toString" {
return None;
}
let [subject] = args.as_slice() else {
return None;
};
eval_expr_with_helper_calls(subject, env, resolver)
.scalar_dispatch
.map(|dispatch| dispatch.stringified())
}
fn direct_quoted_falsy_pattern_condition(
function: &str,
args: &[TemplateExpr],
subject_expr: &TemplateExpr,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> Option<TruthCondition> {
let TemplateExpr::Call {
function: conversion,
args: conversion_args,
} = subject_expr.deparen()
else {
return None;
};
let [subject] = conversion_args.as_slice() else {
return None;
};
if conversion != "quote" {
return None;
}
let needle = literal_string(args.first()?)?;
let falsy_spellings = [
"",
r#""""#,
r#""0""#,
r#""-0""#,
r#""false""#,
r#""<nil>""#,
r#""[]""#,
r#""map[]""#,
];
let matches_falsy = match function {
"contains" => falsy_spellings.iter().any(|value| value.contains(needle)),
"hasPrefix" => falsy_spellings
.iter()
.any(|value| value.starts_with(needle)),
"hasSuffix" => falsy_spellings.iter().any(|value| value.ends_with(needle)),
_ => return None,
};
if matches_falsy {
return None;
}
let subject = eval_expr_with_helper_calls(subject, env, resolver);
let AbstractValue::ValuesPath(path) = subject.value? else {
return None;
};
Some(TruthCondition::from_subsets(
Predicate::False,
Predicate::truthy_path(path).negated(),
false,
))
}
fn literal_string(expr: &TemplateExpr) -> Option<&str> {
let TemplateExpr::Literal(Literal::String(value) | Literal::RawString(value)) = expr.deparen()
else {
return None;
};
Some(value)
}
fn combined_short_circuit_truth(operands: &[TruthCondition], conjunction: bool) -> TruthCondition {
if operands.is_empty() {
return TruthCondition::Unknown;
}
if conjunction {
TruthCondition::all(operands.iter().cloned())
} else {
TruthCondition::any(operands.iter().cloned())
}
}
pub(super) fn conjoin_result_selection(result: &mut EvalResult, predicates: &BTreeSet<Predicate>) {
if predicates.is_empty() {
return;
}
let (embedded_paths, embedded_meta) = result.value.take().map_or_else(
|| (BTreeSet::new(), BTreeMap::new()),
|value| {
let mut value = value.with_output_meta(&result.effects.local_output_meta);
let embedded_paths = value.conjoin_output_path_branches(predicates);
let embedded_meta = value.output_meta();
result.value = Some(value);
(embedded_paths, embedded_meta)
},
);
for (path, meta) in embedded_meta {
result.effects.local_output_meta.insert(path, meta);
}
for path in identity_value_paths(result.value.as_ref()) {
if !embedded_paths.contains(&path) {
let mut meta = crate::helper_meta::HelperOutputMeta::default();
meta.conjoin_branches(predicates);
result.effects.local_output_meta.insert(path, meta);
}
}
for row in &mut result.effects.helper_rendered {
row.meta.conjoin_branches(predicates);
}
}
pub(super) fn conjoin_result_reachability(
result: &mut EvalResult,
reachability: &SelectionReachability,
marker: &'static str,
involved_paths: BTreeSet<String>,
) {
result.selection_reachability = Some(reachability.clone());
let predicates = reachability.output_selection_conjunction(marker, involved_paths);
conjoin_result_selection(result, &predicates);
}
fn scope_execution_effects(effects: &mut Effects, predicates: &BTreeSet<Predicate>) {
if predicates.is_empty() {
return;
}
for meta in effects.local_output_meta.values_mut() {
meta.conjoin_branches(predicates);
}
for row in effects
.helper_rendered
.iter_mut()
.chain(&mut effects.helper_dependency_rendered)
{
row.meta.conjoin_branches(predicates);
}
for read in &mut effects.helper_reads {
read.condition = read
.condition
.conjoined(&GuardDnf::from_conjunction(predicates.iter().cloned()));
}
effects.observed_facts.captures = std::mem::take(&mut effects.observed_facts.captures)
.into_iter()
.map(|mut capture| {
for predicate in predicates {
if !capture.conjunction.contains(predicate) {
capture.conjunction.push(predicate.clone());
}
}
capture
})
.collect();
effects.member_host_conversions = std::mem::take(&mut effects.member_host_conversions)
.into_iter()
.map(|mut conversion| {
for predicate in predicates {
if !conversion.outer_predicates.contains(predicate) {
conversion.outer_predicates.push(predicate.clone());
}
}
conversion
})
.collect();
effects.local_set_mutations.clear();
effects.root_set_mutations.clear();
effects.root_set_predicates.clear();
effects.root_set_value_dispatches.clear();
}
fn eval_helper_call(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
if let Some(TemplateExpr::Literal(Literal::String(name) | Literal::RawString(name))) =
args.first().map(TemplateExpr::deparen)
&& let Some(result) = resolver.resolve_helper_call(name, args.get(1))
{
return result;
}
if let Some(template_name) = args.first().and_then(template_base_path_suffix)
&& let Some(result) = resolver.resolve_implicit_template_call(&template_name, args.get(1))
{
return result;
}
if let Some(callee_expr) = args.first()
&& !matches!(callee_expr.deparen(), TemplateExpr::Literal(_))
{
let callee = eval_expr_with_helper_calls(callee_expr, env, resolver);
if let Some(AbstractValue::StringSet(names)) = &callee.value
&& names.len() == 1
&& let Some(name) = names.first()
&& let Some(mut result) = resolver.resolve_helper_call(name, args.get(1))
{
result.effects.merge(callee.effects.execution_only());
return result;
}
}
if env.skip_helper_call_args {
return EvalResult::none();
}
let mut effects = Effects::default();
merge_arg_effects(args, env, resolver, &mut effects);
EvalResult::with_effects(None, effects)
}
fn template_base_path_suffix(expr: &TemplateExpr) -> Option<String> {
let TemplateExpr::Call { function, args } = expr.deparen() else {
return None;
};
let (base, suffix_args) = args.split_first()?;
if function != "print" || suffix_args.is_empty() || !is_template_base_path(base) {
return None;
}
let mut suffix = String::new();
for arg in suffix_args {
let TemplateExpr::Literal(Literal::String(part) | Literal::RawString(part)) = arg.deparen()
else {
return None;
};
suffix.push_str(part);
}
(!suffix.is_empty()).then_some(suffix)
}
fn is_template_base_path(expr: &TemplateExpr) -> bool {
match expr.deparen() {
TemplateExpr::Field(path) => path.as_slice() == ["Template", "BasePath"],
TemplateExpr::Selector { operand, path } => {
path.as_slice() == ["Template", "BasePath"]
&& matches!(operand.deparen(), TemplateExpr::Variable(name) if name.is_empty())
}
_ => false,
}
}
fn eval_all_args(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
let mut values = Vec::new();
let mut effects = Effects::default();
merge_arg_values(args, env, resolver, &mut values, &mut effects);
EvalResult::with_effects(AbstractValue::choice(values), effects)
}
fn merge_arg_values(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
values: &mut Vec<AbstractValue>,
effects: &mut Effects,
) {
for arg in args {
let result = eval_expr_with_helper_calls(arg, env, resolver);
effects.merge(result.effects);
if let Some(value) = result.value {
values.push(value);
}
}
}
fn merge_arg_effects(
args: &[TemplateExpr],
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
effects: &mut Effects,
) {
for arg in args {
effects.merge(eval_expr_with_helper_calls(arg, env, resolver).effects);
}
}
fn eval_unknown_call(
args: &[TemplateExpr],
mut effects: Effects,
env: &EvalEnv,
resolver: &mut impl HelperCallValueResolver,
) -> EvalResult {
merge_arg_effects(args, env, resolver, &mut effects);
let value = AbstractValue::widened(effects.output_paths.clone());
EvalResult::with_effects(value, effects)
}