1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::contract::FinalizedContract;
4use crate::contract_normalization::{
5 canonicalize_contract_uses, drop_default_guard_subsumed_duplicates,
6 drop_self_truthy_subsumed_duplicates, normalize_contract_uses,
7};
8use crate::observed_facts::{HintGrade, ObservedFacts};
9use crate::{ContractUse, Guard, ValueKind, YamlPath};
10
11#[derive(Debug, Clone, Default, PartialEq, Eq)]
16pub struct ContractIr {
17 uses: Vec<ContractUse>,
18 dependency_uses: Vec<ContractUse>,
19 observed_facts: ObservedFacts,
20 values_program_wrappers: BTreeSet<helm_schema_core::ValuesProgramWrapper>,
21 values_program_wrapper_exclusions: BTreeSet<String>,
26 dependency_values_root_fragments: BTreeSet<String>,
27}
28
29impl ContractIr {
30 #[must_use]
37 pub fn from_contract_uses(uses: Vec<ContractUse>) -> Self {
38 Self {
39 uses,
40 ..Self::default()
41 }
42 }
43
44 pub(crate) fn push(&mut self, contract_use: ContractUse) {
45 self.uses.push(contract_use);
46 }
47
48 pub(crate) fn push_dependency_use(&mut self, contract_use: ContractUse) {
49 self.dependency_uses.push(contract_use);
50 }
51
52 pub fn push_pathless_scalar(&mut self, source_expr: impl Into<String>) {
57 self.push(ContractUse::new(
58 source_expr.into(),
59 YamlPath(Vec::new()),
60 ValueKind::Scalar,
61 Vec::new(),
62 None,
63 ));
64 }
65
66 pub fn push_pathless_dependency_fragment(&mut self, source_expr: impl Into<String>) {
68 self.dependency_values_root_fragments
69 .insert(source_expr.into());
70 }
71
72 pub fn append(&mut self, mut other: Self) {
74 self.uses.append(&mut other.uses);
75 self.dependency_uses.append(&mut other.dependency_uses);
76 self.dependency_values_root_fragments
77 .append(&mut other.dependency_values_root_fragments);
78 self.observed_facts.absorb(&other.observed_facts);
79 self.values_program_wrappers
80 .append(&mut other.values_program_wrappers);
81 self.values_program_wrapper_exclusions
82 .append(&mut other.values_program_wrapper_exclusions);
83 }
84
85 pub fn add_terminal_fail_condition(&mut self, condition: helm_schema_core::Predicate) {
90 let capture = crate::eval_effect::FailCapture {
91 conjunction: vec![condition],
92 ranged: crate::range_modes::RangeModes::default(),
93 kind: crate::eval_effect::CaptureKind::Fail,
94 };
95 self.observed_facts.captures.insert(capture);
96 }
97
98 pub fn append_guards_to_all_uses(&mut self, guards: &[Guard]) {
104 for contract_use in self.uses.iter_mut().chain(&mut self.dependency_uses) {
105 contract_use.condition = contract_use
106 .condition
107 .conjoined_with_guards(guards.iter().cloned());
108 }
109 self.observed_facts.captures = std::mem::take(&mut self.observed_facts.captures)
113 .into_iter()
114 .map(|mut capture| {
115 capture.conjunction.splice(
116 0..0,
117 guards
118 .iter()
119 .cloned()
120 .map(helm_schema_core::Predicate::from),
121 );
122 capture
123 })
124 .collect();
125 if !guards.is_empty() {
129 self.observed_facts.values_default_sources.clear();
130 self.observed_facts.values_root_overlay_prefixes.clear();
131 }
132 }
133
134 pub fn mark_rendered_output_textual(&mut self) {
138 for contract_use in self.uses.iter_mut().chain(&mut self.dependency_uses) {
139 contract_use.kind = ValueKind::Serialized;
140 }
141 }
142
143 pub fn map_value_paths<F>(&mut self, mut map: F)
149 where
150 F: FnMut(&str) -> String,
151 {
152 for contract_use in self.uses.iter_mut().chain(&mut self.dependency_uses) {
153 contract_use.map_value_paths(&mut map);
154 }
155 self.dependency_values_root_fragments =
156 std::mem::take(&mut self.dependency_values_root_fragments)
157 .into_iter()
158 .map(|path| map(&path))
159 .collect();
160 self.observed_facts.map_value_paths(&mut map);
161 self.values_program_wrappers = std::mem::take(&mut self.values_program_wrappers)
162 .into_iter()
163 .map(|wrapper| helm_schema_core::ValuesProgramWrapper {
164 scope_path: map(&wrapper.scope_path),
165 key: wrapper.key,
166 spread: wrapper.spread,
167 })
168 .collect();
169 self.values_program_wrapper_exclusions =
170 std::mem::take(&mut self.values_program_wrapper_exclusions)
171 .into_iter()
172 .map(|path| map(&path))
173 .collect();
174 }
175
176 pub fn project_dependency_global_contracts(&mut self, prefix: &[String]) {
179 if prefix.is_empty() {
180 return;
181 }
182 let global_sources = dependency_global_sources(prefix);
183 project_global_uses(&mut self.uses, &global_sources);
184 project_global_uses(&mut self.dependency_uses, &global_sources);
185 project_global_fail_captures(&mut self.observed_facts.captures, &global_sources);
186 project_global_range_modes(&mut self.observed_facts.range_modes, &global_sources);
187 }
188
189 pub fn add_type_hint(&mut self, path: impl Into<String>, schema_type: impl Into<String>) {
192 let path = path.into();
193 let schema_type = schema_type.into();
194 if path.trim().is_empty() || schema_type.trim().is_empty() {
195 return;
196 }
197 self.observed_facts
198 .insert_type_hint(HintGrade::DECLARED, path, &schema_type);
199 }
200
201 pub(crate) fn absorb_observed_facts(&mut self, facts: &ObservedFacts) {
202 self.observed_facts.absorb(facts);
203 }
204
205 pub(crate) fn extend_values_program_wrappers(
206 &mut self,
207 wrappers: impl IntoIterator<Item = helm_schema_core::ValuesProgramWrapper>,
208 ) {
209 self.values_program_wrappers.extend(wrappers);
210 }
211
212 pub(crate) fn extend_values_program_wrapper_exclusions(
213 &mut self,
214 paths: impl IntoIterator<Item = String>,
215 ) {
216 self.values_program_wrapper_exclusions.extend(paths);
217 }
218
219 pub(crate) fn scrub_program_wrapper_sentinel_evidence(&mut self) {
226 let keys: std::collections::BTreeSet<String> = self
227 .values_program_wrappers
228 .iter()
229 .map(|wrapper| wrapper.key.clone())
230 .collect();
231 if keys.is_empty() {
232 return;
233 }
234 let touches = |path: &str| {
235 helm_schema_core::split_value_path(path)
236 .iter()
237 .any(|segment| keys.contains(segment))
238 };
239 self.uses
240 .retain(|contract_use| !touches(&contract_use.source_expr));
241 self.dependency_uses
242 .retain(|contract_use| !touches(&contract_use.source_expr));
243 self.observed_facts.captures.retain(|capture| {
244 let mut paths: Vec<String> = capture
245 .conjunction
246 .iter()
247 .flat_map(helm_schema_core::Predicate::value_paths)
248 .collect();
249 let mut kind = capture.kind.clone();
250 kind.map_value_paths(&mut |path: &str| {
251 paths.push(path.to_string());
252 path.to_string()
253 });
254 !paths.iter().any(|path| touches(path))
255 });
256 }
257
258 #[must_use]
261 #[tracing::instrument(skip_all)]
262 pub fn finalize(mut self) -> FinalizedContract {
263 self.scrub_program_wrapper_sentinel_evidence();
264 let Self {
265 mut uses,
266 mut dependency_uses,
267 observed_facts,
268 values_program_wrappers,
269 values_program_wrapper_exclusions,
270 dependency_values_root_fragments,
271 } = self;
272 for source_expr in &dependency_values_root_fragments {
273 dependency_uses.push(ContractUse::new(
274 source_expr.clone(),
275 YamlPath(Vec::new()),
276 ValueKind::Fragment,
277 Vec::new(),
278 None,
279 ));
280 }
281 normalize_contract_uses(&mut uses);
282 drop_self_truthy_subsumed_duplicates(&mut dependency_uses);
283 canonicalize_contract_uses(&mut dependency_uses);
284 uses.append(&mut dependency_uses);
285 drop_default_guard_subsumed_duplicates(&mut uses);
286 drop_self_truthy_subsumed_duplicates(&mut uses);
287 canonicalize_contract_uses(&mut uses);
288 let fail_conditions = observed_facts.captures.iter().cloned().collect::<Vec<_>>();
289 lower_string_requirement_merge_sources(&mut uses, &fail_conditions);
290 canonicalize_contract_uses(&mut uses);
291 FinalizedContract::new(
292 uses,
293 &observed_facts,
294 values_program_wrappers,
295 values_program_wrapper_exclusions,
296 &dependency_values_root_fragments,
297 )
298 }
299}
300
301fn string_requirements_by_ancestor(
302 fail_conditions: &[crate::eval_effect::FailCapture],
303) -> BTreeMap<String, BTreeSet<(String, Vec<helm_schema_core::Predicate>)>> {
304 let mut requirements: BTreeMap<String, BTreeSet<(String, Vec<helm_schema_core::Predicate>)>> =
305 BTreeMap::new();
306 for capture in fail_conditions {
307 let crate::eval_effect::CaptureKind::StringRequirement {
308 path,
309 route,
310 selection,
311 } = &capture.kind
312 else {
313 continue;
314 };
315 if *route == crate::eval_effect::StringRequirementRoute::Serialized {
316 continue;
317 }
318 let mut predicates = capture.conjunction.clone();
319 predicates.extend(selection.iter().cloned());
320 predicates.sort();
321 predicates.dedup();
322 let segments = helm_schema_core::split_value_path(path);
323 for end in 1..segments.len() {
324 requirements
325 .entry(helm_schema_core::join_value_path(
326 segments.get(..end).unwrap_or_default().iter().cloned(),
327 ))
328 .or_default()
329 .insert((path.clone(), predicates.clone()));
330 }
331 }
332 requirements
333}
334
335fn lower_string_requirement_merge_sources(
336 uses: &mut [ContractUse],
337 fail_conditions: &[crate::eval_effect::FailCapture],
338) {
339 let requirements_by_ancestor = string_requirements_by_ancestor(fail_conditions);
340
341 let mut overlap_cache = BTreeMap::new();
342
343 for contract_use in uses.iter_mut() {
344 let Some(merge) = &contract_use.merge_layers else {
345 continue;
346 };
347 let Some(requirements) = requirements_by_ancestor.get(&contract_use.source_expr) else {
348 continue;
349 };
350 let source_segments = helm_schema_core::split_value_path(&contract_use.source_expr);
351 let specific_layers = merge
352 .layers
353 .iter()
354 .map(|layer| helm_schema_core::split_value_path(layer))
355 .filter(|layer| {
356 layer.len() > source_segments.len() && layer.starts_with(source_segments.as_slice())
357 })
358 .collect::<Vec<_>>();
359 let [first, second, rest @ ..] = specific_layers.as_slice() else {
360 continue;
361 };
362 let mut common_suffix_len = first
363 .iter()
364 .rev()
365 .zip(second.iter().rev())
366 .take_while(|(left, right)| left == right)
367 .count();
368 for layer in rest {
369 common_suffix_len = common_suffix_len.min(
370 first
371 .iter()
372 .rev()
373 .zip(layer.iter().rev())
374 .take_while(|(left, right)| left == right)
375 .count(),
376 );
377 }
378 let suffix = first.get(first.len().saturating_sub(common_suffix_len)..);
379 let Some(suffix) = suffix.filter(|suffix| {
380 !suffix.is_empty() && suffix.iter().all(|segment| segment.as_str() != "*")
381 }) else {
382 continue;
383 };
384 let Some(requirements) =
385 merge_suffix_string_requirements(&contract_use.source_expr, requirements, suffix)
386 else {
387 continue;
388 };
389 let cache_key = (
390 contract_use.source_expr.clone(),
391 suffix.to_vec(),
392 contract_use.condition.clone(),
393 );
394 let overlaps_requirement = overlap_cache.get(&cache_key).copied().unwrap_or_else(|| {
395 let overlaps = contract_use.condition.disjuncts().iter().any(|row| {
396 requirements.iter().any(|requirement| {
397 !helm_schema_core::GuardDnf::from_conjunction(
398 row.iter().cloned().chain(requirement.iter().cloned()),
399 )
400 .is_never()
401 })
402 });
403 overlap_cache.insert(cache_key, overlaps);
404 overlaps
405 });
406 if !overlaps_requirement {
407 continue;
408 }
409 let source = contract_use.source_expr.clone();
410 let projected = helm_schema_core::join_value_path(
411 source_segments
412 .iter()
413 .cloned()
414 .chain(suffix.iter().cloned()),
415 );
416 contract_use.map_value_paths(&mut |path| {
417 if path == source {
418 projected.clone()
419 } else {
420 path.to_string()
421 }
422 });
423 }
424}
425
426fn merge_suffix_string_requirements(
427 source: &str,
428 requirements: &BTreeSet<(String, Vec<helm_schema_core::Predicate>)>,
429 suffix: &[String],
430) -> Option<BTreeSet<Vec<helm_schema_core::Predicate>>> {
431 let source_segments = helm_schema_core::split_value_path(source);
432 let mut paths = BTreeSet::new();
433 let mut conjunctions = BTreeSet::new();
434 for (path, predicates) in requirements {
435 let segments = helm_schema_core::split_value_path(path);
436 if segments.len() <= source_segments.len()
437 || !segments.starts_with(source_segments.as_slice())
438 || !segments.ends_with(suffix)
439 {
440 continue;
441 }
442 paths.insert(segments);
443 conjunctions.insert(predicates.clone());
444 }
445 (paths.len() >= 2).then_some(conjunctions)
446}
447
448fn dependency_global_sources(prefix: &[String]) -> Vec<String> {
449 (0..=prefix.len())
450 .map(|prefix_len| {
451 helm_schema_core::join_value_path(
452 prefix
453 .get(..prefix_len)
454 .unwrap_or_default()
455 .iter()
456 .cloned()
457 .chain(std::iter::once("global".to_string())),
458 )
459 })
460 .collect()
461}
462
463fn project_global_uses(uses: &mut Vec<ContractUse>, global_sources: &[String]) {
464 let Some(dependency_global) = global_sources.last() else {
465 return;
466 };
467 let dependency_global_segments = helm_schema_core::split_value_path(dependency_global);
468 let mut projected = Vec::with_capacity(uses.len());
469 for dependency_use in std::mem::take(uses) {
470 let source_segments = helm_schema_core::split_value_path(&dependency_use.source_expr);
471 let Some(relative) = source_segments.strip_prefix(dependency_global_segments.as_slice())
472 else {
473 projected.push(dependency_use);
474 continue;
475 };
476 let Some((selection_relative, key)) = global_selection_path(relative) else {
477 projected.push(dependency_use);
478 continue;
479 };
480
481 for (source_index, global_source) in global_sources.iter().enumerate() {
482 let selection_source = helm_schema_core::join_value_path(
483 helm_schema_core::split_value_path(global_source)
484 .into_iter()
485 .chain(selection_relative.iter().cloned()),
486 );
487 let mut selected_use = dependency_use.clone();
488 selected_use.map_value_paths(&mut |path| {
489 replace_value_path_prefix(path, dependency_global, global_source)
490 });
491 selected_use.condition =
492 selected_use
493 .condition
494 .conjoined_with_guards(global_source_selection_guards(
495 global_sources,
496 selection_relative,
497 source_index,
498 &selection_source,
499 key,
500 ));
501 projected.push(selected_use);
502 }
503 }
504 *uses = projected;
505}
506
507fn global_selection_path(relative: &[String]) -> Option<(&[String], &str)> {
508 let selection_len = relative
509 .iter()
510 .position(|segment| segment == "*")
511 .unwrap_or(relative.len());
512 let selection = relative.get(..selection_len)?;
513 Some((selection, selection.last()?.as_str()))
514}
515
516fn global_source_selection_guards(
517 global_sources: &[String],
518 relative: &[String],
519 source_index: usize,
520 source: &str,
521 key: &str,
522) -> Vec<Guard> {
523 let mut guards = Vec::new();
524 for higher_priority in global_sources.iter().take(source_index) {
525 let higher_source = helm_schema_core::join_value_path(
526 helm_schema_core::split_value_path(higher_priority)
527 .into_iter()
528 .chain(relative.iter().cloned()),
529 );
530 let mut container_segments = helm_schema_core::split_value_path(&higher_source);
531 container_segments.pop();
532 guards.push(Guard::AnyOf {
533 alternatives: vec![
534 vec![Guard::NotHasKey {
535 path: helm_schema_core::join_value_path(container_segments),
536 key: key.to_string(),
537 }],
538 vec![Guard::Eq {
539 path: higher_source,
540 value: helm_schema_core::GuardValue::Null,
541 }],
542 ],
543 });
544 }
545 if source_index + 1 < global_sources.len() {
546 let mut container_segments = helm_schema_core::split_value_path(source);
547 container_segments.pop();
548 guards.push(Guard::HasKey {
549 path: helm_schema_core::join_value_path(container_segments),
550 key: key.to_string(),
551 });
552 guards.push(Guard::NotEq {
553 path: source.to_string(),
554 value: helm_schema_core::GuardValue::Null,
555 });
556 }
557 guards
558}
559
560fn replace_value_path_prefix(path: &str, from: &str, to: &str) -> String {
561 let path_segments = helm_schema_core::split_value_path(path);
562 let from_segments = helm_schema_core::split_value_path(from);
563 let Some(relative) = path_segments.strip_prefix(from_segments.as_slice()) else {
564 return path.to_string();
565 };
566 helm_schema_core::join_value_path(
567 helm_schema_core::split_value_path(to)
568 .into_iter()
569 .chain(relative.iter().cloned()),
570 )
571}
572
573fn project_global_fail_captures(
574 captures: &mut BTreeSet<crate::eval_effect::FailCapture>,
575 global_sources: &[String],
576) {
577 let Some(dependency_global) = global_sources.last() else {
578 return;
579 };
580 let dependency_global_segments = helm_schema_core::split_value_path(dependency_global);
581 let mut projected = BTreeSet::new();
582 for dependency_capture in std::mem::take(captures) {
583 let Some(source_path) = dependency_capture.kind.sole_value_path() else {
584 projected.insert(dependency_capture);
585 continue;
586 };
587 let source_segments = helm_schema_core::split_value_path(source_path);
588 let Some(relative) = source_segments.strip_prefix(dependency_global_segments.as_slice())
589 else {
590 projected.insert(dependency_capture);
591 continue;
592 };
593 if relative.is_empty() {
594 if matches!(
595 dependency_capture.kind,
596 crate::eval_effect::CaptureKind::RangeInput { .. }
597 ) {
598 for global_source in global_sources {
599 let mut selected_capture = dependency_capture.clone();
600 selected_capture.conjunction = selected_capture
601 .conjunction
602 .into_iter()
603 .map(|predicate| {
604 predicate.map_value_paths(&mut |path| {
605 replace_value_path_prefix(path, dependency_global, global_source)
606 })
607 })
608 .collect();
609 selected_capture.kind.map_value_paths(&mut |path| {
610 replace_value_path_prefix(path, dependency_global, global_source)
611 });
612 projected.insert(selected_capture);
613 }
614 } else {
615 projected.insert(dependency_capture);
616 }
617 continue;
618 }
619 let Some((selection_relative, key)) = global_selection_path(relative) else {
620 projected.insert(dependency_capture);
621 continue;
622 };
623
624 for (source_index, global_source) in global_sources.iter().enumerate() {
625 let selection_source = helm_schema_core::join_value_path(
626 helm_schema_core::split_value_path(global_source)
627 .into_iter()
628 .chain(selection_relative.iter().cloned()),
629 );
630 let mut selected_capture = dependency_capture.clone();
631 selected_capture.conjunction = selected_capture
632 .conjunction
633 .into_iter()
634 .map(|predicate| {
635 predicate.map_value_paths(&mut |path| {
636 replace_value_path_prefix(path, dependency_global, global_source)
637 })
638 })
639 .collect();
640 selected_capture.kind.map_value_paths(&mut |path| {
641 replace_value_path_prefix(path, dependency_global, global_source)
642 });
643 selected_capture.conjunction.extend(
644 global_source_selection_guards(
645 global_sources,
646 selection_relative,
647 source_index,
648 &selection_source,
649 key,
650 )
651 .into_iter()
652 .map(helm_schema_core::Predicate::from),
653 );
654 projected.insert(selected_capture);
655 }
656 }
657 *captures = projected;
658}
659
660fn project_global_range_modes(
661 range_modes: &mut crate::range_modes::RangeModes,
662 global_sources: &[String],
663) {
664 let Some(dependency_global) = global_sources.last() else {
665 return;
666 };
667 let dependency_segments = helm_schema_core::split_value_path(dependency_global);
668 let projected_paths = range_modes
669 .iter()
670 .filter_map(|(path, mode)| {
671 let segments = helm_schema_core::split_value_path(path);
672 let relative = segments.strip_prefix(dependency_segments.as_slice())?;
673 Some((path.to_string(), relative.to_vec(), mode))
674 })
675 .collect::<Vec<_>>();
676
677 for (path, relative, mode) in projected_paths {
678 range_modes.remove(&path);
679 if relative.is_empty() {
680 for global_source in global_sources {
681 range_modes.merge_mode(global_source.clone(), mode);
682 }
683 continue;
684 }
685 if global_selection_path(&relative).is_none() {
686 continue;
687 }
688 for global_source in global_sources {
689 range_modes.merge_mode(
690 helm_schema_core::join_value_path(
691 helm_schema_core::split_value_path(global_source)
692 .into_iter()
693 .chain(relative.iter().cloned()),
694 ),
695 mode,
696 );
697 }
698 }
699}