use std::collections::{BTreeMap, BTreeSet, HashMap};
use crate::abstract_value::AbstractValue;
use crate::bound_value_analysis::{GetBinding, GetBindingPlan};
use crate::fragment_assignment::AssignmentKind;
use crate::helper_meta::HelperOutputMeta;
use crate::scalar_value::{ScalarValueDispatch, TruthCondition};
use helm_schema_core::{Guard, Predicate};
mod branch_join;
use branch_join::{
joined_branch_outcomes, joined_scalar_dispatch_arms, joined_truthy_reduction_arms,
};
#[derive(Clone, Debug, Default)]
pub(crate) struct SymbolicLocalState {
pub(crate) range_domains: HashMap<String, Vec<String>>,
pub(crate) get_bindings: HashMap<String, GetBinding>,
pub(crate) fragment_values: HashMap<String, AbstractValue>,
pub(crate) default_paths: HashMap<String, BTreeSet<helm_schema_core::ValuesPath>>,
pub(crate) output_meta:
HashMap<String, BTreeMap<helm_schema_core::ValuesPath, HelperOutputMeta>>,
pub(crate) scalar_dispatches: HashMap<String, ScalarValueDispatch>,
pub(crate) truthy_reductions: HashMap<String, Predicate>,
pub(crate) truthiness_abstentions: BTreeSet<String>,
pub(crate) truthiness_clears: BTreeSet<String>,
pub(crate) chart_value_defaults: BTreeSet<helm_schema_core::ValuesPath>,
pub(crate) typeof_sources:
HashMap<String, BTreeMap<helm_schema_core::ValuesPath, HelperOutputMeta>>,
pub(crate) int_cast_sources: HashMap<String, IntCastSource>,
pub(crate) range_member_values: HashMap<String, AbstractValue>,
pub(crate) definite_range_member_values: HashMap<String, AbstractValue>,
pub(crate) traversal_advances: BTreeSet<String>,
local_scopes: Vec<LocalScopeFrame>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct IntCastSource {
pub(crate) path: helm_schema_core::ValuesPath,
pub(crate) default_int: Option<i64>,
}
#[derive(Clone, Debug, Default)]
struct LocalScopeFrame {
previous_values: HashMap<String, VariableLocalState>,
}
#[derive(Clone, Debug, Default)]
struct VariableLocalState {
range_domain: Option<Vec<String>>,
get_binding: Option<GetBinding>,
fragment_value: Option<AbstractValue>,
traversal_advanced: bool,
default_paths: Option<BTreeSet<helm_schema_core::ValuesPath>>,
output_meta: Option<BTreeMap<helm_schema_core::ValuesPath, HelperOutputMeta>>,
scalar_dispatch: Option<ScalarValueDispatch>,
truthy_reduction: Option<Predicate>,
truthiness_abstained: bool,
truthiness_cleared: bool,
typeof_source: Option<BTreeMap<helm_schema_core::ValuesPath, HelperOutputMeta>>,
int_cast_source: Option<IntCastSource>,
range_member_value: Option<AbstractValue>,
definite_range_member_value: Option<AbstractValue>,
}
impl SymbolicLocalState {
pub(crate) fn join_branch_outcomes(&mut self, entry: &Self, outcomes: &[Self]) {
*self = joined_branch_outcomes(entry, outcomes);
}
pub(crate) fn join_scalar_dispatch_arms(
&mut self,
entry: &Self,
arms: &[(TruthCondition, Self)],
has_unconditional_else: bool,
) {
if let Some(joined) = joined_scalar_dispatch_arms(entry, arms, has_unconditional_else) {
self.scalar_dispatches = joined;
}
}
pub(crate) fn join_truthy_reduction_arms(
&mut self,
entry: &Self,
arms: &[(TruthCondition, Self)],
has_unconditional_else: bool,
) {
if let Some(joined) = joined_truthy_reduction_arms(entry, arms, has_unconditional_else) {
self.truthy_reductions.extend(joined);
}
}
pub(crate) fn conjoin_changed_truthy_reductions(
&mut self,
entry: &Self,
condition: &Predicate,
) {
const MAX_STAMPED_GUARDS: usize = 6;
if matches!(condition, Predicate::True) || condition.contains_approximation() {
return;
}
if matches!(
single_term(condition),
Some(Predicate::Guard(Guard::Range { .. }))
) {
let clears = &self.truthiness_clears;
self.truthy_reductions
.retain(|variable, _| !clears.contains(variable));
}
let mut dropped = Vec::new();
self.truthy_reductions.retain(|variable, reduction| {
let entry_reduction = entry.truthy_reductions.get(variable);
if entry_reduction == Some(reduction)
|| matches!(reduction, Predicate::False)
|| reduction.contains_approximation()
{
return true;
}
if predicate_guard_count(condition) + predicate_guard_count(reduction)
<= MAX_STAMPED_GUARDS
{
*reduction = quantify_range_member_reduction(condition, reduction)
.map(Predicate::from)
.unwrap_or_else(|| Predicate::all(vec![condition.clone(), reduction.clone()]));
return true;
}
let changed = entry_reduction
.and_then(|entry| changed_truthy_reduction(entry, reduction))
.unwrap_or_else(|| reduction.clone());
if predicate_implies(&changed, condition) {
return true;
}
let stamped =
if let Some(quantified) = quantify_range_member_reduction(condition, &changed) {
Predicate::from(quantified)
} else {
if predicate_guard_count(condition) + predicate_guard_count(&changed)
> MAX_STAMPED_GUARDS
{
dropped.push(variable.clone());
return false;
}
Predicate::all(vec![condition.clone(), changed])
};
if predicate_guard_count(&stamped) > MAX_STAMPED_GUARDS {
dropped.push(variable.clone());
return false;
}
*reduction = entry_reduction.map_or(stamped.clone(), |entry| {
union_truthy_reductions(entry, &stamped)
});
true
});
for variable in dropped {
self.truthiness_abstentions.insert(variable);
}
}
pub(crate) fn enter_local_scope(&mut self) {
self.local_scopes.push(LocalScopeFrame::default());
}
pub(crate) fn exit_local_scope(&mut self) {
let Some(scope) = self.local_scopes.pop() else {
return;
};
for (variable, previous) in scope.previous_values {
self.restore_variable_state(&variable, previous);
}
}
pub(crate) fn apply_get_binding(&mut self, plan: GetBindingPlan) {
self.record_binding_shadow(plan.kind, &plan.variable);
self.set_get_binding(plan.variable, plan.binding);
}
pub(crate) fn bind_fragment_value(
&mut self,
kind: AssignmentKind,
variable: String,
binding: Option<AbstractValue>,
) {
self.record_binding_shadow(kind, &variable);
self.set_fragment_value(variable, binding);
}
pub(crate) fn mark_traversal_advance(&mut self, variable: &str) {
self.traversal_advances.insert(variable.to_string());
}
pub(crate) fn insert_range_domain(&mut self, variable: String, literals: Vec<String>) {
self.record_scope_shadow(&variable);
self.clear_variable(&variable);
self.range_domains.insert(variable, literals);
}
pub(crate) fn set_chart_value_defaults(
&mut self,
defaults: BTreeSet<helm_schema_core::ValuesPath>,
) {
self.chart_value_defaults = defaults;
}
pub(crate) fn append_chart_value_defaults(
&mut self,
defaults: &mut BTreeSet<helm_schema_core::ValuesPath>,
) {
self.chart_value_defaults.append(defaults);
}
fn record_binding_shadow(&mut self, kind: AssignmentKind, variable: &str) {
if matches!(kind, AssignmentKind::Assignment) && self.variable_has_current_value(variable) {
return;
}
self.record_scope_shadow(variable);
}
fn record_scope_shadow(&mut self, variable: &str) {
let previous = self.variable_state(variable);
let Some(scope) = self.local_scopes.last_mut() else {
return;
};
scope
.previous_values
.entry(variable.to_string())
.or_insert(previous);
}
fn variable_state(&self, variable: &str) -> VariableLocalState {
VariableLocalState {
range_domain: self.range_domains.get(variable).cloned(),
get_binding: self.get_bindings.get(variable).cloned(),
fragment_value: self.fragment_values.get(variable).cloned(),
traversal_advanced: self.traversal_advances.contains(variable),
default_paths: self.default_paths.get(variable).cloned(),
output_meta: self.output_meta.get(variable).cloned(),
scalar_dispatch: self.scalar_dispatches.get(variable).cloned(),
truthy_reduction: self.truthy_reductions.get(variable).cloned(),
truthiness_abstained: self.truthiness_abstentions.contains(variable),
truthiness_cleared: self.truthiness_clears.contains(variable),
typeof_source: self.typeof_sources.get(variable).cloned(),
int_cast_source: self.int_cast_sources.get(variable).cloned(),
range_member_value: self.range_member_values.get(variable).cloned(),
definite_range_member_value: self.definite_range_member_values.get(variable).cloned(),
}
}
fn variable_has_current_value(&self, variable: &str) -> bool {
self.range_domains.contains_key(variable)
|| self.get_bindings.contains_key(variable)
|| self.fragment_values.contains_key(variable)
|| self.default_paths.contains_key(variable)
|| self.output_meta.contains_key(variable)
|| self.scalar_dispatches.contains_key(variable)
|| self.truthy_reductions.contains_key(variable)
|| self.truthiness_abstentions.contains(variable)
|| self.typeof_sources.contains_key(variable)
|| self.int_cast_sources.contains_key(variable)
|| self.range_member_values.contains_key(variable)
}
fn set_get_binding(&mut self, variable: String, binding: GetBinding) {
self.clear_variable(&variable);
self.get_bindings.insert(variable, binding);
}
fn restore_variable_state(&mut self, variable: &str, previous: VariableLocalState) {
restore_map_entry(&mut self.range_domains, variable, previous.range_domain);
restore_map_entry(&mut self.get_bindings, variable, previous.get_binding);
restore_map_entry(&mut self.fragment_values, variable, previous.fragment_value);
if previous.traversal_advanced {
self.traversal_advances.insert(variable.to_string());
} else {
self.traversal_advances.remove(variable);
}
restore_map_entry(&mut self.default_paths, variable, previous.default_paths);
restore_map_entry(&mut self.output_meta, variable, previous.output_meta);
restore_map_entry(
&mut self.scalar_dispatches,
variable,
previous.scalar_dispatch,
);
restore_map_entry(
&mut self.truthy_reductions,
variable,
previous.truthy_reduction,
);
if previous.truthiness_abstained {
self.truthiness_abstentions.insert(variable.to_string());
} else {
self.truthiness_abstentions.remove(variable);
}
if previous.truthiness_cleared {
self.truthiness_clears.insert(variable.to_string());
} else {
self.truthiness_clears.remove(variable);
}
restore_map_entry(&mut self.typeof_sources, variable, previous.typeof_source);
restore_map_entry(
&mut self.int_cast_sources,
variable,
previous.int_cast_source,
);
restore_map_entry(
&mut self.range_member_values,
variable,
previous.range_member_value,
);
restore_map_entry(
&mut self.definite_range_member_values,
variable,
previous.definite_range_member_value,
);
}
fn set_fragment_value(&mut self, variable: String, binding: Option<AbstractValue>) {
self.clear_variable(&variable);
if let Some(binding) = binding {
self.fragment_values.insert(variable, binding);
}
}
fn clear_variable(&mut self, variable: &str) {
self.range_domains.remove(variable);
self.get_bindings.remove(variable);
self.fragment_values.remove(variable);
self.traversal_advances.remove(variable);
self.default_paths.remove(variable);
self.output_meta.remove(variable);
self.scalar_dispatches.remove(variable);
self.truthy_reductions.remove(variable);
self.truthiness_abstentions.remove(variable);
self.truthiness_clears.remove(variable);
self.typeof_sources.remove(variable);
self.int_cast_sources.remove(variable);
self.range_member_values.remove(variable);
self.definite_range_member_values.remove(variable);
}
}
fn restore_map_entry<T>(map: &mut HashMap<String, T>, variable: &str, value: Option<T>) {
if let Some(value) = value {
map.insert(variable.to_string(), value);
} else {
map.remove(variable);
}
}
fn single_term(predicate: &Predicate) -> Option<&Predicate> {
fn collect<'a>(predicate: &'a Predicate, term: &mut Option<&'a Predicate>) -> Option<()> {
match predicate {
Predicate::True => Some(()),
Predicate::And(items) => {
for item in items {
collect(item, term)?;
}
Some(())
}
Predicate::False => None,
predicate => {
if term.replace(predicate).is_some() {
return None;
}
Some(())
}
}
}
let mut term = None;
collect(predicate, &mut term)?;
term
}
fn quantify_range_member_reduction(condition: &Predicate, reduction: &Predicate) -> Option<Guard> {
let Predicate::Guard(Guard::Range { path: range_path }) = single_term(condition)? else {
return None;
};
let member_predicate = single_term(reduction)?;
let Predicate::Guard(
Guard::Eq {
path: member_path, ..
}
| Guard::Truthy { path: member_path },
) = member_predicate
else {
return None;
};
let range_segments: Vec<&helm_schema_core::Segment> = range_path.segments().collect();
let member_segments: Vec<&helm_schema_core::Segment> = member_path.segments().collect();
let [wildcard, member] = member_segments.get(range_segments.len()..)? else {
return None;
};
if !wildcard.is_each_member() || member_segments.get(..range_segments.len())? != range_segments
{
return None;
}
match member_predicate {
Predicate::Guard(Guard::Eq { value, .. }) => Some(Guard::ContainsMemberEquals {
path: range_path.clone(),
member: member.literal()?.to_owned(),
value: value.clone(),
}),
Predicate::Guard(Guard::Truthy { .. }) => Some(Guard::ContainsTruthyMember {
path: range_path.clone(),
member: member.literal()?.to_owned(),
}),
_ => None,
}
}
fn predicate_guard_count(predicate: &Predicate) -> usize {
match predicate {
Predicate::True | Predicate::False | Predicate::Approximate { .. } => 0,
Predicate::Guard(_) => 1,
Predicate::Not(inner) => predicate_guard_count(inner),
Predicate::And(items) | Predicate::Or(items) => {
items.iter().map(predicate_guard_count).sum()
}
}
}
fn changed_truthy_reduction(entry: &Predicate, reduction: &Predicate) -> Option<Predicate> {
if matches!(entry, Predicate::False) {
return Some(reduction.clone());
}
let Predicate::Or(reduction_items) = reduction else {
return None;
};
let entry_items = match entry {
Predicate::Or(items) => items.as_slice(),
predicate => std::slice::from_ref(predicate),
};
if !entry_items
.iter()
.all(|item| reduction_items.contains(item))
{
return None;
}
let changed = reduction_items
.iter()
.filter(|item| !entry_items.contains(item))
.cloned()
.collect::<Vec<_>>();
Some(match changed.as_slice() {
[] => Predicate::False,
[predicate] => predicate.clone(),
_ => Predicate::Or(changed),
})
}
fn union_truthy_reductions(left: &Predicate, right: &Predicate) -> Predicate {
if matches!(left, Predicate::False) {
return right.clone();
}
if matches!(right, Predicate::False) || left == right {
return left.clone();
}
match left {
Predicate::Or(predicates) => {
let mut predicates = predicates.clone();
if !predicates.contains(right) {
predicates.push(right.clone());
}
Predicate::Or(predicates)
}
_ => Predicate::Or(vec![left.clone(), right.clone()]),
}
}
fn predicate_implies(antecedent: &Predicate, consequent: &Predicate) -> bool {
if antecedent.exactly_implies(consequent) {
return true;
}
if let (Predicate::Or(antecedents), Predicate::Or(consequents)) = (antecedent, consequent) {
return antecedents.iter().all(|antecedent| {
consequents
.iter()
.any(|consequent| predicate_implies(antecedent, consequent))
});
}
match consequent {
Predicate::And(predicates) => predicates
.iter()
.all(|predicate| predicate_implies(antecedent, predicate)),
Predicate::Or(predicates) => predicates
.iter()
.any(|predicate| predicate_implies(antecedent, predicate)),
_ => match antecedent {
Predicate::Or(predicates) => predicates
.iter()
.all(|predicate| predicate_implies(predicate, consequent)),
Predicate::And(predicates) => predicates
.iter()
.any(|predicate| predicate_implies(predicate, consequent)),
_ => leaf_predicate_implies(antecedent, consequent),
},
}
}
fn leaf_predicate_implies(antecedent: &Predicate, consequent: &Predicate) -> bool {
let Some(present_path) = predicate_present_path(antecedent) else {
return false;
};
match consequent {
Predicate::Not(inner) => matches!(
inner.as_ref(),
Predicate::Guard(Guard::Absent { path })
if path == present_path || path_is_strict_ancestor(path, present_path)
),
Predicate::Guard(Guard::HasKey { path, key }) => {
let mut key_path = path.clone();
key_path.push(key);
present_path == &key_path || present_path.is_descendant_of(&key_path)
}
Predicate::Guard(Guard::Truthy { path }) if path == present_path => match antecedent {
Predicate::Guard(Guard::Eq { value, .. }) => guard_value_is_truthy(value),
Predicate::Guard(Guard::MatchesPattern { pattern, .. }) => {
regex::Regex::new(pattern).is_ok_and(|pattern| !pattern.is_match(""))
}
_ => false,
},
Predicate::Guard(Guard::Range { path } | Guard::Truthy { path }) => {
path_is_strict_ancestor(path, present_path)
}
_ => false,
}
}
fn predicate_present_path(predicate: &Predicate) -> Option<&helm_schema_core::ValuesPath> {
match predicate {
Predicate::Not(inner) => match inner.as_ref() {
Predicate::Guard(Guard::Absent { path }) => Some(path),
_ => None,
},
Predicate::Guard(
Guard::Truthy { path }
| Guard::Eq { path, .. }
| Guard::MatchesPattern { path, .. }
| Guard::NotMatchesPattern { path, .. }
| Guard::TypeIs { path, .. },
) => Some(path),
_ => None,
}
}
fn path_is_strict_ancestor(
parent: &helm_schema_core::ValuesPath,
child: &helm_schema_core::ValuesPath,
) -> bool {
child.is_descendant_of(parent)
}
fn guard_value_is_truthy(value: &helm_schema_core::GuardValue) -> bool {
match value {
helm_schema_core::GuardValue::String(text) => !text.is_empty(),
helm_schema_core::GuardValue::Bool(value) => *value,
helm_schema_core::GuardValue::Int(value) => *value != 0,
helm_schema_core::GuardValue::Float(text) => {
text.parse::<f64>().is_ok_and(|value| value != 0.0)
}
helm_schema_core::GuardValue::Null => false,
}
}