1use crate::budget::{BudgetKind, BudgetTracker};
2use crate::consts::{
3 CONDITION_REF_PREFIX, FN_AND, FN_CONDITION, FN_EQUALS, FN_NOT, FN_OR, MAX_PARAM_COMBINATIONS, MAX_SAT_ITERATIONS,
4 MAX_TOTAL_SAT_ITERATIONS, PARAM_UNKNOWN_SENTINEL, PSEUDO_PREFIX, PSEUDO_REGION,
5};
6use crate::ir::*;
7use crate::model::PseudoParameterOverrides;
8use crate::resolver::{MappingData, ParameterInfo};
9use log::{debug, info};
10use std::collections::{HashMap, HashSet};
11use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::{Arc, OnceLock};
13
14const MAX_REPORTED_BUDGET_EXHAUSTED_QUERIES: usize = 5;
19
20const WHOLE_CONDITION_SET_DESCRIPTION: &str =
24 "this template's condition set as a whole (remaining conditions were assumed compatible)";
25const INLINE_CONDITION_DESCRIPTION: &str = "<inline condition>";
26const SYNTHETIC_CONDITION_PREFIX: &str = "__";
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Satisfiability {
32 Satisfiable,
33 Unsatisfiable,
34 Unknown,
35}
36
37#[derive(Debug, Clone)]
38pub enum ConditionExpr {
39 Equals(ValueExpr, ValueExpr),
40 And(Vec<ConditionExpr>),
41 Or(Vec<ConditionExpr>),
42 Not(Box<ConditionExpr>),
43 ConditionRef(String),
44 Invalid,
50}
51
52#[derive(Debug, Clone)]
53pub enum ValueExpr {
54 ParamRef(String),
55 Literal(String),
56 PseudoParam(String),
57 MappingLookup { map_name: String, key1: Box<ValueExpr>, key2: Box<ValueExpr> },
58 Other,
59}
60
61#[derive(Debug, Clone)]
62pub struct MutexGroup {
63 pub conditions: Vec<String>,
64 pub parameter: String,
65 pub values: Vec<String>,
66}
67
68#[derive(Debug, Clone)]
69pub struct Implication {
70 pub antecedent: String,
71 pub consequent: String,
72}
73
74#[derive(Debug)]
75pub struct ConditionModel {
76 pub conditions: HashMap<String, ConditionExpr>,
77 pub parameters: HashMap<String, ParameterInfo>,
78 pub mutex_groups: Vec<MutexGroup>,
79 pub implications: Vec<Implication>,
80 rule_implications: Vec<Implication>,
89 pseudo_overrides: PseudoParameterOverrides,
90 mappings: MappingData,
91 sat_iterations_used: AtomicU64,
96 referenced_param_values: OnceLock<HashMap<String, Vec<String>>>,
103 budget_exhausted_queries: std::sync::Mutex<Vec<String>>,
104 budget_tracker: Arc<BudgetTracker>,
107}
108
109pub fn format_condition_expr(expr: &ConditionExpr) -> String {
110 match expr {
111 ConditionExpr::Equals(a, b) => {
112 format!("Equals({}, {})", format_value_expr(a), format_value_expr(b))
113 }
114 ConditionExpr::And(exprs) => {
115 let items: Vec<String> = exprs.iter().map(format_condition_expr).collect();
116 format!("And({})", items.join(", "))
117 }
118 ConditionExpr::Or(exprs) => {
119 let items: Vec<String> = exprs.iter().map(format_condition_expr).collect();
120 format!("Or({})", items.join(", "))
121 }
122 ConditionExpr::Not(e) => format!("Not({})", format_condition_expr(e)),
123 ConditionExpr::ConditionRef(name) => format!("Condition({})", name),
124 ConditionExpr::Invalid => "Invalid".to_string(),
125 }
126}
127
128fn format_value_expr(expr: &ValueExpr) -> String {
129 match expr {
130 ValueExpr::ParamRef(name) => format!("Param({})", name),
131 ValueExpr::Literal(s) => format!("\"{}\"", s),
132 ValueExpr::PseudoParam(name) => name.clone(),
133 ValueExpr::MappingLookup { map_name, key1, key2 } => {
134 format!("FindInMap({}, {}, {})", map_name, format_value_expr(key1), format_value_expr(key2))
135 }
136 ValueExpr::Other => "?".into(),
137 }
138}
139
140fn format_satisfiability_assumptions(assumptions: &[(String, bool)]) -> String {
141 let mut descriptions = assumptions
142 .iter()
143 .map(|(name, value)| {
144 let condition_description =
145 if name.starts_with(SYNTHETIC_CONDITION_PREFIX) { INLINE_CONDITION_DESCRIPTION } else { name };
146 format!("{condition_description}={value}")
147 })
148 .collect::<Vec<_>>();
149 descriptions.sort_unstable();
150 descriptions.join(", ")
151}
152
153impl ConditionModel {
154 pub fn from_ir(
155 ir: &TemplateIR,
156 parameters: &HashMap<String, ParameterInfo>,
157 pseudo_overrides: &PseudoParameterOverrides,
158 mappings: &MappingData,
159 ) -> Self {
160 Self::from_ir_with_tracker(ir, parameters, pseudo_overrides, mappings, Arc::new(BudgetTracker::new()))
161 }
162
163 pub(crate) fn from_ir_with_tracker(
167 ir: &TemplateIR,
168 parameters: &HashMap<String, ParameterInfo>,
169 pseudo_overrides: &PseudoParameterOverrides,
170 mappings: &MappingData,
171 tracker: Arc<BudgetTracker>,
172 ) -> Self {
173 let mut conditions = HashMap::new();
174
175 if ir.conditions != NULL_REF
176 && let Some(entries) = ir.arena.as_map(ir.conditions)
177 {
178 for (name, node_ref) in entries {
179 let expr = parse_condition_expr(&ir.arena, *node_ref, parameters);
180 conditions.insert(name.clone(), expr);
181 }
182 }
183
184 let mutex_groups = extract_mutex_groups(&conditions);
185 let implications = extract_implications(&conditions);
186
187 info!(
188 "Condition model: {} conditions, {} mutex groups (params: {:?}), {} implications",
189 conditions.len(),
190 mutex_groups.len(),
191 mutex_groups.iter().map(|g| g.parameter.as_str()).collect::<Vec<_>>(),
192 implications.len()
193 );
194 ConditionModel {
195 conditions,
196 parameters: parameters.clone(),
197 mutex_groups,
198 implications,
199 rule_implications: Vec::new(),
200 pseudo_overrides: pseudo_overrides.clone(),
201 mappings: mappings.clone(),
202 sat_iterations_used: AtomicU64::new(0),
203 referenced_param_values: OnceLock::new(),
204 budget_exhausted_queries: std::sync::Mutex::new(Vec::new()),
205 budget_tracker: tracker,
206 }
207 }
208
209 #[must_use]
250 pub fn is_satisfiable(&self, assumptions: &[(String, bool)]) -> bool {
251 self.satisfiability(assumptions) != Satisfiability::Unsatisfiable
252 }
253
254 #[must_use]
259 pub fn satisfiability(&self, assumptions: &[(String, bool)]) -> Satisfiability {
260 self.satisfiability_with_param_overrides(assumptions, &HashMap::new())
261 }
262
263 #[must_use]
274 pub fn is_satisfiable_in_region(&self, assumptions: &[(String, bool)], region: &str) -> bool {
275 let overrides = HashMap::from([(PSEUDO_REGION.to_string(), vec![region.to_string()])]);
276 self.satisfiability_with_param_overrides(assumptions, &overrides) != Satisfiability::Unsatisfiable
277 }
278
279 #[must_use]
284 fn satisfiability_with_param_overrides<'model>(
285 &'model self,
286 assumptions: &[(String, bool)],
287 param_overrides: &'model HashMap<String, Vec<String>>,
288 ) -> Satisfiability {
289 if self.satisfiability_budget_exhausted() {
292 return Satisfiability::Unknown;
293 }
294
295 let mut assumed: HashMap<&'model str, bool> = HashMap::with_capacity(assumptions.len());
296 for (name, expected) in assumptions {
297 let Some((condition_name, _)) = self.conditions.get_key_value(name.as_str()) else {
300 continue;
301 };
302 if let Some(&already_assumed) = assumed.get(condition_name.as_str())
303 && already_assumed != *expected
304 {
305 return Satisfiability::Unsatisfiable;
306 }
307 assumed.insert(condition_name.as_str(), *expected);
308 }
309 if assumed.is_empty() {
310 return Satisfiability::Satisfiable;
311 }
312 debug!("Checking satisfiability of {:?} against {} conditions", assumptions, self.conditions.len());
313
314 let mut query = SatisfiabilityQuery::prepare(self, &assumed, param_overrides);
315
316 if query.parameter_point_count() > MAX_PARAM_COMBINATIONS {
321 let steps = query.steps;
322 self.charge_satisfiability_work(steps);
323 let query_description = format_satisfiability_assumptions(assumptions);
324 self.record_curtailed_analysis(query_description);
325 self.budget_tracker.record(BudgetKind::ConditionParameterCombinations);
326 return Satisfiability::Unknown;
327 }
328
329 let satisfiable = query.some_point_satisfies(&assumed);
330 let steps = query.steps;
331 self.charge_satisfiability_work(steps);
332 if steps > MAX_SAT_ITERATIONS {
333 let query_description = format_satisfiability_assumptions(assumptions);
337 self.record_curtailed_analysis(query_description);
338 self.budget_tracker.record(BudgetKind::ConditionSatIterationsPerQuery);
339 return Satisfiability::Unknown;
340 }
341 if satisfiable { Satisfiability::Satisfiable } else { Satisfiability::Unsatisfiable }
342 }
343
344 fn charge_satisfiability_work(&self, steps: u64) {
348 let before = self.sat_iterations_used.fetch_add(steps, Ordering::Relaxed);
349 if before < MAX_TOTAL_SAT_ITERATIONS && before.saturating_add(steps) >= MAX_TOTAL_SAT_ITERATIONS {
350 self.record_curtailed_analysis(WHOLE_CONDITION_SET_DESCRIPTION.to_string());
351 self.budget_tracker.record(BudgetKind::ConditionSatIterationsTotal);
352 }
353 }
354
355 fn record_curtailed_analysis(&self, description: String) {
358 if let Ok(mut curtailed) = self.budget_exhausted_queries.lock()
359 && curtailed.len() < MAX_REPORTED_BUDGET_EXHAUSTED_QUERIES
360 {
361 curtailed.push(description);
362 }
363 }
364
365 #[must_use]
370 pub fn satisfiability_budget_exhausted(&self) -> bool {
371 self.sat_iterations_used.load(Ordering::Relaxed) >= MAX_TOTAL_SAT_ITERATIONS
372 }
373
374 #[must_use]
376 pub fn sat_iterations_used(&self) -> u64 {
377 self.sat_iterations_used.load(Ordering::Relaxed)
378 }
379
380 pub fn budget_exhausted_queries(&self) -> Vec<String> {
381 self.budget_exhausted_queries.lock().unwrap_or_else(|p| p.into_inner()).clone()
382 }
383
384 #[cfg(test)]
390 fn add_sat_iterations_for_test(&self, count: u64) {
391 self.charge_satisfiability_work(count);
392 }
393
394 fn collect_referenced_param_values(&self) -> HashMap<String, Vec<String>> {
419 let mut compared_literals: HashMap<String, Vec<String>> = HashMap::new();
420 for expr in self.conditions.values() {
421 collect_equals_pairs(expr, &mut compared_literals);
422 }
423 let mut param_values: HashMap<String, Vec<String>> = HashMap::new();
424 for (param_name, literals) in &compared_literals {
425 if let Some(fixed) = self.pseudo_overrides.fixed_value(param_name) {
426 param_values.insert(param_name.clone(), vec![fixed]);
427 continue;
428 }
429 if let Some(allowed_values) = self.parameters.get(param_name).and_then(|p| p.allowed_values.clone()) {
430 param_values.insert(param_name.clone(), allowed_values);
431 continue;
432 }
433 let mut values = literals.clone();
434 values.push(PARAM_UNKNOWN_SENTINEL.to_string());
435 values.sort();
436 values.dedup();
437 param_values.insert(param_name.clone(), values);
438 }
439 param_values
440 }
441
442 fn referenced_param_values(&self) -> &HashMap<String, Vec<String>> {
445 self.referenced_param_values.get_or_init(|| self.collect_referenced_param_values())
446 }
447
448 #[must_use]
449 pub fn conditions_compatible(&self, cond_a: &str, cond_b: &str) -> bool {
450 self.is_satisfiable(&[(cond_a.into(), true), (cond_b.into(), true)])
451 }
452
453 #[must_use]
454 pub fn condition_implies(&self, cond_a: &str, cond_b: &str) -> bool {
455 if cond_a == cond_b {
458 return true;
459 }
460 !self.is_satisfiable(&[(cond_a.into(), true), (cond_b.into(), false)])
461 }
462
463 pub fn resources_compatible(&self, cond_a: Option<&str>, cond_b: Option<&str>) -> bool {
464 match (cond_a, cond_b) {
465 (None, _) | (_, None) => true,
466 (Some(a), Some(b)) => self.conditions_compatible(a, b),
467 }
468 }
469
470 pub fn get(&self, name: &str) -> Option<&ConditionExpr> {
471 self.conditions.get(name)
472 }
473
474 pub fn names(&self) -> impl Iterator<Item = &str> {
475 self.conditions.keys().map(|s| s.as_str())
476 }
477
478 pub fn tautological_equals(&self) -> Vec<(String, bool)> {
479 let mut result = Vec::new();
480 for (name, expr) in &self.conditions {
481 Self::find_tautological(expr, name, &mut result);
482 }
483 result
484 }
485
486 fn find_tautological(expr: &ConditionExpr, cond_name: &str, out: &mut Vec<(String, bool)>) {
487 match expr {
488 ConditionExpr::Equals(a, b) => {
489 let always_equal = match (a, b) {
490 (ValueExpr::Literal(la), ValueExpr::Literal(lb)) => Some(la == lb),
491 (ValueExpr::ParamRef(pa), ValueExpr::ParamRef(pb)) => Some(pa == pb),
492 (ValueExpr::PseudoParam(pa), ValueExpr::PseudoParam(pb)) => Some(pa == pb),
493 _ => None,
494 };
495 if let Some(equal) = always_equal {
496 out.push((cond_name.to_string(), equal));
497 }
498 }
499 ConditionExpr::And(children) | ConditionExpr::Or(children) => {
500 for c in children {
501 Self::find_tautological(c, cond_name, out);
502 }
503 }
504 ConditionExpr::Not(child) => Self::find_tautological(child, cond_name, out),
505 ConditionExpr::ConditionRef(_) | ConditionExpr::Invalid => {}
506 }
507 }
508
509 pub fn referenced_params(&self) -> Vec<String> {
510 let mut params = Vec::new();
511 for expr in self.conditions.values() {
512 collect_param_refs_from_expr(expr, &mut params);
513 }
514 params.sort();
515 params.dedup();
516 params
517 }
518
519 pub fn register_inline(&mut self, name: String, expr: ConditionExpr) {
520 self.register_inline_batch(std::iter::once((name, expr)));
521 }
522
523 pub fn register_inline_batch(&mut self, items: impl IntoIterator<Item = (String, ConditionExpr)>) {
527 for (name, expr) in items {
528 self.conditions.insert(name, expr);
529 }
530 self.mutex_groups = extract_mutex_groups(&self.conditions);
531 self.implications = extract_implications(&self.conditions);
532 self.referenced_param_values = OnceLock::new();
539 }
540
541 pub fn register_rule_implications(&mut self, arena: &Arena, rules: &[(String, NodeRef, Vec<NodeRef>)]) {
542 let mut rule_implications: Vec<Implication> = Vec::new();
548 for (rule_name, condition_node, assertion_nodes) in rules {
549 let antecedent = if *condition_node != NULL_REF {
550 let expr = parse_condition_expr(arena, *condition_node, &self.parameters);
551 let synth_name = format!("__rule_cond_{}", rule_name);
552 self.conditions.insert(synth_name.clone(), expr);
553 Some(synth_name)
554 } else {
555 None
556 };
557
558 for (idx, assert_node) in assertion_nodes.iter().enumerate() {
559 if *assert_node == NULL_REF {
560 continue;
561 }
562 let assert_expr = parse_condition_expr(arena, *assert_node, &self.parameters);
563 let assert_name = format!("__rule_assert_{}_{}", rule_name, idx);
564 self.conditions.insert(assert_name.clone(), assert_expr);
565
566 if let Some(ref ante) = antecedent {
567 rule_implications.push(Implication { antecedent: ante.clone(), consequent: assert_name });
568 }
569 }
570 }
571
572 self.mutex_groups = extract_mutex_groups(&self.conditions);
573 self.implications = extract_implications(&self.conditions);
574 self.rule_implications = rule_implications;
575 self.referenced_param_values = OnceLock::new();
576 }
577
578 pub fn undefined_condition_refs(&self) -> Vec<(String, String)> {
579 let mut result = Vec::new();
580 for (owner, expr) in &self.conditions {
581 let mut refs = Vec::new();
582 collect_condition_refs(expr, &mut refs);
583 for r in refs {
584 if !self.conditions.contains_key(&r) {
585 result.push((owner.clone(), r));
586 }
587 }
588 }
589 result.sort();
592 result
593 }
594
595 pub fn invalid_condition_bodies(&self) -> Vec<String> {
600 let mut names: Vec<String> = self
601 .conditions
602 .iter()
603 .filter(|(name, expr)| matches!(expr, ConditionExpr::Invalid) && !name.starts_with("__"))
604 .map(|(name, _)| name.clone())
605 .collect();
606 names.sort();
607 names
608 }
609
610 pub fn detect_cycles(&self) -> Vec<Vec<String>> {
611 let mut adj: HashMap<String, Vec<String>> = HashMap::new();
612 for (name, expr) in &self.conditions {
613 let mut refs = Vec::new();
614 collect_condition_refs(expr, &mut refs);
615 let mut targets: Vec<String> = refs.into_iter().filter(|r| self.conditions.contains_key(r)).collect();
616 targets.sort();
617 adj.insert(name.clone(), targets);
618 }
619
620 let mut sorted_names: Vec<&String> = self.conditions.keys().collect();
621 sorted_names.sort();
622
623 let mut cycles = Vec::new();
624 let mut visited: HashSet<String> = HashSet::new();
625 let mut on_stack: HashSet<String> = HashSet::new();
626 let mut path: Vec<String> = Vec::new();
627
628 for name in &sorted_names {
629 if !visited.contains(name.as_str()) {
630 Self::dfs_cycles(name, &adj, &mut visited, &mut on_stack, &mut path, &mut cycles);
631 }
632 }
633 cycles
634 }
635
636 fn dfs_cycles(
637 node: &str,
638 adj: &HashMap<String, Vec<String>>,
639 visited: &mut HashSet<String>,
640 on_stack: &mut HashSet<String>,
641 path: &mut Vec<String>,
642 cycles: &mut Vec<Vec<String>>,
643 ) {
644 visited.insert(node.to_string());
645 on_stack.insert(node.to_string());
646 path.push(node.to_string());
647
648 if let Some(neighbors) = adj.get(node) {
649 for neighbor in neighbors {
650 if !visited.contains(neighbor.as_str()) {
651 Self::dfs_cycles(neighbor, adj, visited, on_stack, path, cycles);
652 } else if on_stack.contains(neighbor.as_str()) {
653 let cycle_start = path.iter().position(|n| n == neighbor).unwrap();
654 let cycle: Vec<String> = path[cycle_start..].to_vec();
655 cycles.push(cycle);
656 }
657 }
658 }
659
660 path.pop();
661 on_stack.remove(node);
662 }
663
664 pub fn detect_equivalent_conditions(&self) -> Vec<(String, String)> {
665 let mut canonical_groups: HashMap<String, Vec<&str>> = HashMap::new();
666 for (name, expr) in &self.conditions {
667 if name.starts_with("__") {
668 continue;
669 }
670 if expr_has_opaque_value(expr) {
671 continue;
672 }
673 let canonical = canonical_form(expr);
674 canonical_groups.entry(canonical).or_default().push(name.as_str());
675 }
676
677 let mut pairs = Vec::new();
678 for (_, mut group) in canonical_groups {
679 if group.len() < 2 {
680 continue;
681 }
682 group.sort();
683 for other in &group[1..] {
684 pairs.push((group[0].to_string(), other.to_string()));
685 }
686 }
687 pairs
688 }
689}
690
691fn expr_has_opaque_value(expr: &ConditionExpr) -> bool {
692 match expr {
693 ConditionExpr::Equals(a, b) => value_is_opaque(a) || value_is_opaque(b),
694 ConditionExpr::And(items) | ConditionExpr::Or(items) => items.iter().any(expr_has_opaque_value),
695 ConditionExpr::Not(inner) => expr_has_opaque_value(inner),
696 ConditionExpr::Invalid => true,
700 ConditionExpr::ConditionRef(_) => false,
701 }
702}
703
704fn value_is_opaque(val: &ValueExpr) -> bool {
705 match val {
706 ValueExpr::Other => true,
707 ValueExpr::MappingLookup { key1, key2, .. } => value_is_opaque(key1) || value_is_opaque(key2),
712 _ => false,
713 }
714}
715
716fn canonical_form(expr: &ConditionExpr) -> String {
717 match expr {
718 ConditionExpr::Equals(a, b) => {
719 let ca = canonical_value(a);
720 let cb = canonical_value(b);
721 let (left, right) = if ca <= cb { (ca, cb) } else { (cb, ca) };
722 format!("EQ({},{})", left, right)
723 }
724 ConditionExpr::And(items) => {
725 let mut parts: Vec<String> = items.iter().map(canonical_form).collect();
726 parts.sort();
727 format!("AND({})", parts.join(","))
728 }
729 ConditionExpr::Or(items) => {
730 let mut parts: Vec<String> = items.iter().map(canonical_form).collect();
731 parts.sort();
732 format!("OR({})", parts.join(","))
733 }
734 ConditionExpr::Not(inner) => {
735 format!("NOT({})", canonical_form(inner))
736 }
737 ConditionExpr::ConditionRef(name) => {
738 format!("CREF({})", name)
739 }
740 ConditionExpr::Invalid => "INVALID".to_string(),
741 }
742}
743
744fn canonical_value(val: &ValueExpr) -> String {
745 match val {
746 ValueExpr::ParamRef(name) => format!("P({})", name),
747 ValueExpr::Literal(s) => format!("L({})", s),
748 ValueExpr::PseudoParam(name) => format!("PP({})", name),
749 ValueExpr::MappingLookup { map_name, key1, key2 } => {
750 format!("MAP({},{},{})", map_name, canonical_value(key1), canonical_value(key2))
751 }
752 ValueExpr::Other => "?".to_string(),
753 }
754}
755
756fn collect_param_refs_from_expr(expr: &ConditionExpr, out: &mut Vec<String>) {
757 match expr {
758 ConditionExpr::Equals(a, b) => {
759 collect_param_refs_from_value(a, out);
760 collect_param_refs_from_value(b, out);
761 }
762 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
763 for e in exprs {
764 collect_param_refs_from_expr(e, out);
765 }
766 }
767 ConditionExpr::Not(e) => collect_param_refs_from_expr(e, out),
768 ConditionExpr::ConditionRef(_) | ConditionExpr::Invalid => {}
769 }
770}
771
772fn collect_param_refs_from_value(expr: &ValueExpr, out: &mut Vec<String>) {
773 match expr {
774 ValueExpr::ParamRef(n) => out.push(n.clone()),
775 ValueExpr::MappingLookup { key1, key2, .. } => {
776 collect_param_refs_from_value(key1, out);
777 collect_param_refs_from_value(key2, out);
778 }
779 _ => {}
780 }
781}
782
783pub fn parse_condition_expr(
784 arena: &Arena,
785 node_ref: NodeRef,
786 parameters: &HashMap<String, ParameterInfo>,
787) -> ConditionExpr {
788 match arena.node(node_ref) {
789 Node::Intrinsic(IntrinsicFn::Equals(a, b)) => {
790 let va = parse_value_expr(arena, *a, parameters);
791 let vb = parse_value_expr(arena, *b, parameters);
792 ConditionExpr::Equals(va, vb)
793 }
794 Node::Intrinsic(IntrinsicFn::And(children)) => {
795 let exprs = children.iter().map(|c| parse_condition_expr(arena, *c, parameters)).collect();
796 ConditionExpr::And(exprs)
797 }
798 Node::Intrinsic(IntrinsicFn::Or(children)) => {
799 let exprs = children.iter().map(|c| parse_condition_expr(arena, *c, parameters)).collect();
800 ConditionExpr::Or(exprs)
801 }
802 Node::Intrinsic(IntrinsicFn::Not(child)) => {
803 let expr = parse_condition_expr(arena, *child, parameters);
804 ConditionExpr::Not(Box::new(expr))
805 }
806 Node::Intrinsic(IntrinsicFn::Ref(target)) => {
807 if let Some(name) = target.strip_prefix(CONDITION_REF_PREFIX) {
808 ConditionExpr::ConditionRef(name.to_string())
809 } else {
810 ConditionExpr::Invalid
814 }
815 }
816 _ => {
817 if let Some(entries) = arena.as_map(node_ref)
819 && entries.len() == 1
820 {
821 let (key, val) = &entries[0];
822 match key.as_str() {
823 FN_EQUALS => {
824 if let Some(arr) = arena.as_list(*val)
825 && arr.len() == 2
826 {
827 let va = parse_value_expr(arena, arr[0], parameters);
828 let vb = parse_value_expr(arena, arr[1], parameters);
829 return ConditionExpr::Equals(va, vb);
830 }
831 }
832 FN_AND => {
833 if let Some(arr) = arena.as_list(*val) {
834 let exprs = arr.iter().map(|c| parse_condition_expr(arena, *c, parameters)).collect();
835 return ConditionExpr::And(exprs);
836 }
837 }
838 FN_OR => {
839 if let Some(arr) = arena.as_list(*val) {
840 let exprs = arr.iter().map(|c| parse_condition_expr(arena, *c, parameters)).collect();
841 return ConditionExpr::Or(exprs);
842 }
843 }
844 FN_NOT => {
845 if let Some(arr) = arena.as_list(*val)
846 && !arr.is_empty()
847 {
848 let expr = parse_condition_expr(arena, arr[0], parameters);
849 return ConditionExpr::Not(Box::new(expr));
850 }
851 }
852 FN_CONDITION => {
853 if let Some(name) = arena.as_str(*val) {
854 return ConditionExpr::ConditionRef(name.to_string());
855 }
856 }
857 _ => {}
858 }
859 }
860 ConditionExpr::Invalid
861 }
862 }
863}
864
865fn parse_value_expr(arena: &Arena, node_ref: NodeRef, parameters: &HashMap<String, ParameterInfo>) -> ValueExpr {
866 match arena.node(node_ref) {
867 Node::String(s) => ValueExpr::Literal(s.clone()),
868 Node::Int(i) => ValueExpr::Literal(i.to_string()),
869 Node::Bool(b) => ValueExpr::Literal(b.to_string()),
870 Node::Intrinsic(IntrinsicFn::Ref(target)) => {
871 if target.starts_with(PSEUDO_PREFIX) {
872 ValueExpr::PseudoParam(target.clone())
873 } else if parameters.contains_key(target) {
874 ValueExpr::ParamRef(target.clone())
875 } else {
876 ValueExpr::Other
877 }
878 }
879 Node::Intrinsic(IntrinsicFn::FindInMap(m, k1, k2, _)) => {
887 let map_name = arena.as_str(*m).unwrap_or("?").to_string();
888 let key1 = parse_value_expr(arena, *k1, parameters);
889 let key2 = parse_value_expr(arena, *k2, parameters);
890 ValueExpr::MappingLookup { map_name, key1: Box::new(key1), key2: Box::new(key2) }
891 }
892 _ => ValueExpr::Other,
893 }
894}
895
896fn collect_equals_pairs(expr: &ConditionExpr, out: &mut HashMap<String, Vec<String>>) {
902 match expr {
903 ConditionExpr::Equals(a, b) => {
904 if let Some((symbol, literal)) = match_symbol_literal_pair(a, b) {
905 out.entry(symbol).or_default().push(literal);
906 } else {
907 collect_param_refs_from_value_into_pairs(a, out);
908 collect_param_refs_from_value_into_pairs(b, out);
909 }
910 }
911 ConditionExpr::And(exprs) | ConditionExpr::Or(exprs) => {
912 for e in exprs {
913 collect_equals_pairs(e, out);
914 }
915 }
916 ConditionExpr::Not(e) => collect_equals_pairs(e, out),
917 ConditionExpr::ConditionRef(_) | ConditionExpr::Invalid => {}
918 }
919}
920
921fn match_symbol_literal_pair(a: &ValueExpr, b: &ValueExpr) -> Option<(String, String)> {
925 match (a, b) {
926 (ValueExpr::ParamRef(p), ValueExpr::Literal(v)) | (ValueExpr::Literal(v), ValueExpr::ParamRef(p)) => {
927 Some((p.clone(), v.clone()))
928 }
929 (ValueExpr::PseudoParam(p), ValueExpr::Literal(v)) | (ValueExpr::Literal(v), ValueExpr::PseudoParam(p)) => {
930 Some((p.clone(), v.clone()))
931 }
932 _ => None,
933 }
934}
935
936fn collect_param_refs_from_value_into_pairs(expr: &ValueExpr, out: &mut HashMap<String, Vec<String>>) {
937 match expr {
938 ValueExpr::ParamRef(p) | ValueExpr::PseudoParam(p) => {
939 out.entry(p.clone()).or_default();
940 }
941 ValueExpr::MappingLookup { key1, key2, .. } => {
942 collect_param_refs_from_value_into_pairs(key1, out);
943 collect_param_refs_from_value_into_pairs(key2, out);
944 }
945 _ => {}
946 }
947}
948
949fn collect_condition_refs(expr: &ConditionExpr, out: &mut Vec<String>) {
950 let mut borrowed = Vec::new();
951 condition_ref_names(expr, &mut borrowed);
952 out.extend(borrowed.into_iter().map(str::to_string));
953}
954
955pub fn collect_condition_deps(expr: &ConditionExpr, out: &mut Vec<String>) {
956 collect_condition_refs(expr, out);
957}
958
959fn extract_mutex_groups(conditions: &HashMap<String, ConditionExpr>) -> Vec<MutexGroup> {
960 let mut param_tests: HashMap<String, Vec<(String, String)>> = HashMap::new(); for (name, expr) in conditions {
965 if let Some((param, _lit, is_positive)) = extract_equals_test(expr) {
966 if is_positive {
970 param_tests.entry(param).or_default().push((name.clone(), _lit));
971 }
972 }
973 }
974
975 param_tests
976 .into_iter()
977 .filter(|(_, tests)| tests.len() > 1)
978 .map(|(param, tests)| {
979 let conditions = tests.iter().map(|(n, _)| n.clone()).collect();
980 let values = tests.iter().map(|(_, v)| v.clone()).collect();
981 MutexGroup { conditions, parameter: param, values }
982 })
983 .collect()
984}
985
986fn extract_equals_test(expr: &ConditionExpr) -> Option<(String, String, bool)> {
987 match expr {
988 ConditionExpr::Equals(a, b) => {
989 if let (ValueExpr::ParamRef(p), ValueExpr::Literal(v)) | (ValueExpr::Literal(v), ValueExpr::ParamRef(p)) =
990 (a, b)
991 {
992 return Some((p.clone(), v.clone(), true));
993 }
994 None
995 }
996 ConditionExpr::Not(inner) => {
997 if let Some((param, lit, positive)) = extract_equals_test(inner) {
998 Some((param, lit, !positive))
999 } else {
1000 None
1001 }
1002 }
1003 _ => None,
1004 }
1005}
1006
1007struct SatisfiabilityQuery<'model> {
1032 model: &'model ConditionModel,
1033 dependencies: Vec<&'model str>,
1038 position_of_dependency: HashMap<&'model str, usize>,
1039 parameters: Vec<(&'model str, &'model [String])>,
1044 position_of_parameter: HashMap<&'model str, usize>,
1045 selected_value: Vec<usize>,
1047 bound_depth: usize,
1051 rule_constraints: Vec<(usize, usize)>,
1054 value_at_point: Vec<PointValue>,
1055 forced_value: Vec<Option<bool>>,
1062 forcings_made: u64,
1066 being_evaluated: Vec<bool>,
1067 provisional_reads: u64,
1072 steps: u64,
1075}
1076
1077#[derive(Clone, Copy, PartialEq, Eq)]
1079enum PointValue {
1080 NotEvaluated,
1081 Determined(bool),
1082 Undetermined,
1086}
1087
1088struct PointEnumeration<'query> {
1091 expectations: &'query [(usize, bool)],
1093 dependency_masks: &'query [u64],
1095 unbound_masks: &'query [u64],
1098}
1099
1100impl<'model> SatisfiabilityQuery<'model> {
1101 fn prepare(
1102 model: &'model ConditionModel,
1103 assumed: &HashMap<&'model str, bool>,
1104 param_overrides: &'model HashMap<String, Vec<String>>,
1105 ) -> Self {
1106 let mut assumed_names: Vec<&'model str> = assumed.keys().copied().collect();
1107 assumed_names.sort_unstable();
1108
1109 let mut query = SatisfiabilityQuery {
1110 model,
1111 dependencies: Vec::new(),
1112 position_of_dependency: HashMap::new(),
1113 parameters: Vec::new(),
1114 position_of_parameter: HashMap::new(),
1115 selected_value: Vec::new(),
1116 bound_depth: 0,
1117 rule_constraints: Vec::new(),
1118 value_at_point: Vec::new(),
1119 forced_value: Vec::new(),
1120 forcings_made: 0,
1121 being_evaluated: Vec::new(),
1122 provisional_reads: 0,
1123 steps: 0,
1124 };
1125 query.add_dependencies_reachable_from(&assumed_names);
1126 query.add_parameters_of_dependencies(param_overrides);
1127 query.add_applicable_rule_constraints(param_overrides);
1128 query.order_parameters_by_reader_count();
1129 query.selected_value = vec![0; query.parameters.len()];
1130 query.value_at_point = vec![PointValue::NotEvaluated; query.dependencies.len()];
1131 query.forced_value = vec![None; query.dependencies.len()];
1132 query.being_evaluated = vec![false; query.dependencies.len()];
1133 query
1134 }
1135
1136 fn order_parameters_by_reader_count(&mut self) {
1142 if self.parameters.len() < 2 {
1143 return;
1144 }
1145 let model = self.model;
1146 let mut reader_count: HashMap<&'model str, usize> = HashMap::new();
1147 let mut names: Vec<&'model str> = Vec::new();
1148 for dependency in &self.dependencies {
1149 let Some(expr) = model.conditions.get(*dependency) else {
1150 continue;
1151 };
1152 names.clear();
1153 parameter_names(expr, &mut names);
1154 names.sort_unstable();
1155 names.dedup();
1156 for name in &names {
1157 *reader_count.entry(name).or_default() += 1;
1158 }
1159 }
1160 self.steps += self.dependencies.len() as u64;
1161 self.parameters.sort_by(|(left, left_candidates), (right, right_candidates)| {
1165 (left_candidates.len() > 1)
1166 .cmp(&(right_candidates.len() > 1))
1167 .then_with(|| {
1168 let left_readers = reader_count.get(left).copied().unwrap_or_default();
1169 let right_readers = reader_count.get(right).copied().unwrap_or_default();
1170 right_readers.cmp(&left_readers)
1171 })
1172 .then_with(|| left.cmp(right))
1173 });
1174 self.position_of_parameter = self.parameters.iter().enumerate().map(|(at, (name, _))| (*name, at)).collect();
1175 }
1176
1177 fn add_dependencies_reachable_from(&mut self, seeds: &[&'model str]) {
1182 let conditions = &self.model.conditions;
1183 let mut pending: Vec<&'model str> = seeds.to_vec();
1184 while let Some(name) = pending.pop() {
1185 let Some((dependency, expr)) = conditions.get_key_value(name) else {
1186 continue;
1187 };
1188 let dependency = dependency.as_str();
1189 if self.position_of_dependency.contains_key(dependency) {
1190 continue;
1191 }
1192 self.position_of_dependency.insert(dependency, self.dependencies.len());
1193 self.dependencies.push(dependency);
1194 self.steps += 1;
1195 condition_ref_names(expr, &mut pending);
1196 }
1197 }
1198
1199 fn add_parameters_of_dependencies(&mut self, param_overrides: &'model HashMap<String, Vec<String>>) {
1202 let conditions = &self.model.conditions;
1203 let mut names: Vec<&'model str> = Vec::new();
1204 for dependency in &self.dependencies {
1205 if let Some(expr) = conditions.get(*dependency) {
1206 parameter_names(expr, &mut names);
1207 }
1208 }
1209 names.sort_unstable();
1210 names.dedup();
1211 for name in names {
1212 self.add_parameter(name, param_overrides);
1213 }
1214 }
1215
1216 fn add_parameter(&mut self, name: &'model str, param_overrides: &'model HashMap<String, Vec<String>>) {
1222 if self.position_of_parameter.contains_key(name) {
1223 return;
1224 }
1225 let candidates = param_overrides
1226 .get(name)
1227 .or_else(|| self.model.referenced_param_values().get(name))
1228 .map_or(&[][..], Vec::as_slice);
1229 if candidates.is_empty() {
1230 return;
1231 }
1232 self.position_of_parameter.insert(name, self.parameters.len());
1233 self.parameters.push((name, candidates));
1234 self.steps += 1;
1235 }
1236
1237 fn add_applicable_rule_constraints(&mut self, param_overrides: &'model HashMap<String, Vec<String>>) {
1248 let model = self.model;
1249 if model.rule_implications.is_empty() {
1250 return;
1251 }
1252 let mut applied = vec![false; model.rule_implications.len()];
1253 loop {
1254 let mut newly_applied = false;
1255 for (position, implication) in model.rule_implications.iter().enumerate() {
1256 if applied[position] {
1257 continue;
1258 }
1259 let (Some((antecedent, _)), Some((consequent, _))) = (
1260 model.conditions.get_key_value(implication.antecedent.as_str()),
1261 model.conditions.get_key_value(implication.consequent.as_str()),
1262 ) else {
1263 applied[position] = true;
1264 continue;
1265 };
1266 let endpoints = [antecedent.as_str(), consequent.as_str()];
1267 let mut steps = 0;
1268 let constrained_parameters = reachable_parameter_names(&model.conditions, &endpoints, &mut steps);
1269 self.steps += steps;
1270 if !constrained_parameters.iter().any(|name| self.position_of_parameter.contains_key(name)) {
1271 continue;
1272 }
1273 applied[position] = true;
1274 newly_applied = true;
1275 self.add_dependencies_reachable_from(&endpoints);
1276 self.add_parameters_of_dependencies(param_overrides);
1277 self.rule_constraints
1278 .push((self.position_of_dependency[endpoints[0]], self.position_of_dependency[endpoints[1]]));
1279 }
1280 if !newly_applied {
1281 return;
1282 }
1283 }
1284 }
1285
1286 fn parameter_point_count(&self) -> u64 {
1288 self.parameters.iter().fold(1u64, |points, (_, candidates)| points.saturating_mul(candidates.len() as u64))
1289 }
1290
1291 fn some_point_satisfies(&mut self, assumed: &HashMap<&'model str, bool>) -> bool {
1301 let mut expectations: Vec<(usize, bool)> = assumed
1302 .iter()
1303 .filter_map(|(name, expected)| self.position_of_dependency.get(name).map(|&at| (at, *expected)))
1304 .collect();
1305 expectations.sort_unstable();
1306 let parameter_masks = self.varying_parameter_masks();
1307 let dependency_masks = self.dependency_parameter_masks(¶meter_masks);
1308 let mut unbound_masks = vec![0u64; self.parameters.len() + 1];
1309 for at in (0..self.parameters.len()).rev() {
1310 unbound_masks[at] = unbound_masks[at + 1] | parameter_masks[at];
1311 }
1312 self.bound_depth = self.parameters.iter().take_while(|(_, candidates)| candidates.len() < 2).count();
1316 let enumeration = PointEnumeration {
1317 expectations: &expectations,
1318 dependency_masks: &dependency_masks,
1319 unbound_masks: &unbound_masks,
1320 };
1321 self.extend_point(&enumeration)
1322 }
1323
1324 fn extend_point(&mut self, enumeration: &PointEnumeration<'_>) -> bool {
1327 self.steps += 1;
1328 if self.steps > MAX_SAT_ITERATIONS {
1329 return true;
1335 }
1336 if !self.assumptions_can_still_hold(enumeration.expectations) {
1337 return false;
1338 }
1339 let at = self.bound_depth;
1340 if at == self.parameters.len() {
1341 return true;
1342 }
1343 for candidate in 0..self.parameters[at].1.len() {
1344 self.selected_value[at] = candidate;
1345 self.discard_values_affected_by(enumeration.unbound_masks[at], enumeration.dependency_masks);
1348 self.bound_depth = at + 1;
1349 let satisfied = self.extend_point(enumeration);
1350 self.bound_depth = at;
1351 if satisfied {
1352 return true;
1353 }
1354 }
1355 false
1356 }
1357
1358 fn assumptions_can_still_hold(&mut self, expectations: &[(usize, bool)]) -> bool {
1373 self.forced_value.fill(None);
1374 loop {
1375 let forcings_before = self.forcings_made;
1376 for &(dependency, expected) in expectations {
1377 if !self.require_dependency(dependency, expected) {
1378 return false;
1379 }
1380 }
1381 if !self.rule_constraints_hold() {
1382 return false;
1383 }
1384 if self.forcings_made == forcings_before {
1385 return true;
1386 }
1387 }
1388 }
1389
1390 fn require_dependency(&mut self, dependency: usize, expected: bool) -> bool {
1394 self.steps += 1;
1395 if let Some(determined) = self.dependency_value(dependency) {
1396 return determined == expected;
1397 }
1398 match self.forced_value[dependency] {
1399 Some(already_forced) => return already_forced == expected,
1400 None => {
1401 self.forced_value[dependency] = Some(expected);
1402 self.forcings_made += 1;
1403 }
1404 }
1405 let Some(expr) = self.model.conditions.get(self.dependencies[dependency]) else {
1406 return true;
1407 };
1408 self.require(expr, expected)
1409 }
1410
1411 fn require(&mut self, expr: &'model ConditionExpr, expected: bool) -> bool {
1417 self.steps += 1;
1418 match expr {
1419 ConditionExpr::ConditionRef(name) => match self.position_of_dependency.get(name.as_str()).copied() {
1420 Some(dependency) => self.require_dependency(dependency, expected),
1421 None => true,
1423 },
1424 ConditionExpr::Not(operand) => self.require(operand, !expected),
1425 ConditionExpr::And(operands) if expected => operands.iter().all(|operand| self.require(operand, true)),
1426 ConditionExpr::Or(operands) if !expected => operands.iter().all(|operand| self.require(operand, false)),
1427 ConditionExpr::And(operands) => self.require_any(operands, false),
1428 ConditionExpr::Or(operands) => self.require_any(operands, true),
1429 ConditionExpr::Equals(_, _) | ConditionExpr::Invalid => true,
1432 }
1433 }
1434
1435 fn require_any(&mut self, operands: &'model [ConditionExpr], satisfying: bool) -> bool {
1440 let mut undecided: Option<&'model ConditionExpr> = None;
1441 let mut undecided_operands = 0usize;
1442 for operand in operands {
1443 match self.evaluate(operand) {
1444 Some(value) if value == satisfying => return true,
1445 Some(_) => {}
1446 None => {
1447 undecided_operands += 1;
1448 undecided = Some(operand);
1449 }
1450 }
1451 }
1452 match (undecided_operands, undecided) {
1453 (0, _) => false,
1454 (1, Some(last_candidate)) => self.require(last_candidate, satisfying),
1455 _ => true,
1456 }
1457 }
1458
1459 fn rule_constraints_hold(&mut self) -> bool {
1463 for at in 0..self.rule_constraints.len() {
1464 let (antecedent, consequent) = self.rule_constraints[at];
1465 if self.dependency_belief(antecedent) == Some(true) && !self.require_dependency(consequent, true) {
1466 return false;
1467 }
1468 if self.dependency_belief(consequent) == Some(false) && !self.require_dependency(antecedent, false) {
1469 return false;
1470 }
1471 }
1472 true
1473 }
1474
1475 fn varying_parameter_masks(&self) -> Vec<u64> {
1480 let mut masks = Vec::with_capacity(self.parameters.len());
1481 let mut next_bit = 0u32;
1482 for (_, candidates) in &self.parameters {
1483 if candidates.len() < 2 {
1484 masks.push(0);
1485 continue;
1486 }
1487 masks.push(1u64 << next_bit);
1488 next_bit = (next_bit + 1).min(u64::BITS - 1);
1489 }
1490 masks
1491 }
1492
1493 fn dependency_parameter_masks(&mut self, parameter_masks: &[u64]) -> Vec<u64> {
1499 let model = self.model;
1500 let mut masks = vec![0u64; self.dependencies.len()];
1501 let mut references: Vec<Vec<usize>> = vec![Vec::new(); self.dependencies.len()];
1502 let mut names: Vec<&'model str> = Vec::new();
1503 let mut referenced: Vec<&'model str> = Vec::new();
1504 for (at, dependency) in self.dependencies.iter().enumerate() {
1505 let Some(expr) = model.conditions.get(*dependency) else {
1506 continue;
1507 };
1508 names.clear();
1509 parameter_names(expr, &mut names);
1510 for name in &names {
1511 if let Some(&position) = self.position_of_parameter.get(*name) {
1512 masks[at] |= parameter_masks[position];
1513 }
1514 }
1515 referenced.clear();
1516 condition_ref_names(expr, &mut referenced);
1517 references[at] =
1518 referenced.iter().filter_map(|name| self.position_of_dependency.get(*name).copied()).collect();
1519 }
1520 self.steps += self.dependencies.len() as u64;
1521
1522 loop {
1525 let mut grew = false;
1526 for at in 0..masks.len() {
1527 let propagated = references[at].iter().fold(masks[at], |mask, &reference| mask | masks[reference]);
1528 if propagated != masks[at] {
1529 masks[at] = propagated;
1530 grew = true;
1531 }
1532 }
1533 self.steps += masks.len() as u64;
1534 if !grew {
1535 return masks;
1536 }
1537 }
1538 }
1539
1540 fn discard_values_affected_by(&mut self, changed_parameters: u64, dependency_masks: &[u64]) {
1543 for (at, value) in self.value_at_point.iter_mut().enumerate() {
1544 if dependency_masks[at] & changed_parameters != 0 {
1545 *value = PointValue::NotEvaluated;
1546 }
1547 }
1548 }
1549
1550 fn dependency_value(&mut self, dependency: usize) -> Option<bool> {
1555 match self.value_at_point[dependency] {
1556 PointValue::Determined(value) => return Some(value),
1557 PointValue::Undetermined => return None,
1558 PointValue::NotEvaluated => {}
1559 }
1560 if self.being_evaluated[dependency] {
1561 self.provisional_reads += 1;
1566 return None;
1567 }
1568 let Some(expr) = self.model.conditions.get(self.dependencies[dependency]) else {
1569 self.value_at_point[dependency] = PointValue::Undetermined;
1570 return None;
1571 };
1572 self.being_evaluated[dependency] = true;
1573 let provisional_reads_before = self.provisional_reads;
1574 let value = self.evaluate(expr);
1575 self.being_evaluated[dependency] = false;
1576 if self.provisional_reads == provisional_reads_before {
1577 self.value_at_point[dependency] = value.map_or(PointValue::Undetermined, PointValue::Determined);
1578 }
1579 value
1580 }
1581
1582 fn dependency_belief(&mut self, dependency: usize) -> Option<bool> {
1585 if let Some(determined) = self.dependency_value(dependency) {
1586 return Some(determined);
1587 }
1588 let forced = self.forced_value[dependency];
1589 if forced.is_some() {
1590 self.provisional_reads += 1;
1591 }
1592 forced
1593 }
1594
1595 fn evaluate(&mut self, expr: &'model ConditionExpr) -> Option<bool> {
1601 self.steps += 1;
1607 match expr {
1608 ConditionExpr::Equals(left, right) => {
1609 let left = self.value_at_current_point(left)?;
1610 let right = self.value_at_current_point(right)?;
1611 Some(left == right)
1612 }
1613 ConditionExpr::And(operands) => {
1614 let mut all_determined = true;
1615 for operand in operands {
1616 match self.evaluate(operand) {
1617 Some(false) => return Some(false),
1618 Some(true) => {}
1619 None => all_determined = false,
1620 }
1621 }
1622 all_determined.then_some(true)
1623 }
1624 ConditionExpr::Or(operands) => {
1625 let mut all_determined = true;
1626 for operand in operands {
1627 match self.evaluate(operand) {
1628 Some(true) => return Some(true),
1629 Some(false) => {}
1630 None => all_determined = false,
1631 }
1632 }
1633 all_determined.then_some(false)
1634 }
1635 ConditionExpr::Not(operand) => self.evaluate(operand).map(|value| !value),
1636 ConditionExpr::ConditionRef(name) => {
1637 let dependency = self.position_of_dependency.get(name.as_str()).copied()?;
1640 self.dependency_belief(dependency)
1641 }
1642 ConditionExpr::Invalid => None,
1645 }
1646 }
1647
1648 fn value_at_current_point(&mut self, expr: &'model ValueExpr) -> Option<String> {
1651 match expr {
1652 ValueExpr::Literal(literal) => Some(literal.clone()),
1653 ValueExpr::ParamRef(name) => self.selected_parameter_value(name).map(str::to_string),
1654 ValueExpr::PseudoParam(name) => self
1655 .selected_parameter_value(name)
1656 .map(str::to_string)
1657 .or_else(|| self.model.pseudo_overrides.fixed_value(name)),
1658 ValueExpr::MappingLookup { map_name, key1, key2 } => {
1659 let top_level_key = self.value_at_current_point(key1)?;
1660 let second_level_key = self.value_at_current_point(key2)?;
1661 let mapped = self.model.mappings.get(map_name)?.get(&top_level_key)?.get(&second_level_key)?;
1662 match mapped {
1663 serde_json::Value::String(text) => Some(text.clone()),
1664 other => Some(other.to_string()),
1665 }
1666 }
1667 ValueExpr::Other => None,
1668 }
1669 }
1670
1671 fn selected_parameter_value(&self, name: &str) -> Option<&'model str> {
1674 let position = *self.position_of_parameter.get(name)?;
1675 if position >= self.bound_depth {
1676 return None;
1677 }
1678 self.parameters[position].1.get(self.selected_value[position]).map(String::as_str)
1679 }
1680}
1681
1682fn condition_ref_names<'expr>(expr: &'expr ConditionExpr, out: &mut Vec<&'expr str>) {
1685 match expr {
1686 ConditionExpr::ConditionRef(name) => out.push(name.as_str()),
1687 ConditionExpr::And(operands) | ConditionExpr::Or(operands) => {
1688 for operand in operands {
1689 condition_ref_names(operand, out);
1690 }
1691 }
1692 ConditionExpr::Not(operand) => condition_ref_names(operand, out),
1693 ConditionExpr::Equals(_, _) | ConditionExpr::Invalid => {}
1694 }
1695}
1696
1697fn parameter_names<'expr>(expr: &'expr ConditionExpr, out: &mut Vec<&'expr str>) {
1700 match expr {
1701 ConditionExpr::Equals(left, right) => {
1702 parameter_names_of_value(left, out);
1703 parameter_names_of_value(right, out);
1704 }
1705 ConditionExpr::And(operands) | ConditionExpr::Or(operands) => {
1706 for operand in operands {
1707 parameter_names(operand, out);
1708 }
1709 }
1710 ConditionExpr::Not(operand) => parameter_names(operand, out),
1711 ConditionExpr::ConditionRef(_) | ConditionExpr::Invalid => {}
1712 }
1713}
1714
1715fn parameter_names_of_value<'expr>(expr: &'expr ValueExpr, out: &mut Vec<&'expr str>) {
1716 match expr {
1717 ValueExpr::ParamRef(name) | ValueExpr::PseudoParam(name) => out.push(name.as_str()),
1718 ValueExpr::MappingLookup { key1, key2, .. } => {
1719 parameter_names_of_value(key1, out);
1720 parameter_names_of_value(key2, out);
1721 }
1722 ValueExpr::Literal(_) | ValueExpr::Other => {}
1723 }
1724}
1725
1726fn reachable_parameter_names<'conditions>(
1729 conditions: &'conditions HashMap<String, ConditionExpr>,
1730 seeds: &[&'conditions str],
1731 steps: &mut u64,
1732) -> Vec<&'conditions str> {
1733 let mut visited: HashSet<&'conditions str> = HashSet::new();
1734 let mut pending: Vec<&'conditions str> = seeds.to_vec();
1735 let mut names = Vec::new();
1736 while let Some(name) = pending.pop() {
1737 if !visited.insert(name) {
1738 continue;
1739 }
1740 *steps += 1;
1741 if let Some(expr) = conditions.get(name) {
1742 parameter_names(expr, &mut names);
1743 condition_ref_names(expr, &mut pending);
1744 }
1745 }
1746 names.sort_unstable();
1747 names.dedup();
1748 names
1749}
1750
1751fn extract_implications(conditions: &HashMap<String, ConditionExpr>) -> Vec<Implication> {
1762 let mut implications = Vec::new();
1763
1764 for (name, expr) in conditions {
1765 match expr {
1766 ConditionExpr::And(children) => {
1767 let mut refs = Vec::new();
1768 collect_same_operator_refs(children, true, &mut refs);
1769 for ref_name in refs {
1770 implications.push(Implication { antecedent: name.clone(), consequent: ref_name });
1771 }
1772 }
1773 ConditionExpr::Or(children) => {
1774 let mut refs = Vec::new();
1775 collect_same_operator_refs(children, false, &mut refs);
1776 for ref_name in refs {
1777 implications.push(Implication { antecedent: ref_name, consequent: name.clone() });
1778 }
1779 }
1780 _ => {}
1781 }
1782 }
1783
1784 implications
1785}
1786
1787fn collect_same_operator_refs(exprs: &[ConditionExpr], in_and: bool, out: &mut Vec<String>) {
1791 for child in exprs {
1792 match child {
1793 ConditionExpr::ConditionRef(name) => out.push(name.clone()),
1794 ConditionExpr::And(children) if in_and => collect_same_operator_refs(children, in_and, out),
1795 ConditionExpr::Or(children) if !in_and => collect_same_operator_refs(children, in_and, out),
1796 _ => {}
1797 }
1798 }
1799}
1800
1801#[cfg(test)]
1802mod tests {
1803 use super::*;
1804 use crate::model::PseudoParameterOverrides;
1805 use crate::parser;
1806 use crate::resolver::{extract_mappings, extract_parameters};
1807 use std::fmt::Write;
1808
1809 fn build_condition_model(input: &str) -> ConditionModel {
1810 let ir = parser::parse(input.as_bytes()).unwrap();
1811 let (params, _) = extract_parameters(&ir);
1812 let (mappings, _) = extract_mappings(&ir);
1813 let pseudo = PseudoParameterOverrides::default();
1814 ConditionModel::from_ir(&ir, ¶ms, &pseudo, &mappings)
1815 }
1816
1817 #[test]
1818 fn parse_simple_conditions() {
1819 let input = r#"
1820Parameters:
1821 Env:
1822 Type: String
1823 AllowedValues:
1824 - Prod
1825 - Dev
1826Conditions:
1827 isProduction:
1828 Fn::Equals:
1829 - !Ref Env
1830 - Prod
1831 isDevelopment:
1832 Fn::Equals:
1833 - !Ref Env
1834 - Dev
1835Resources:
1836 R:
1837 Type: T
1838"#;
1839 let model = build_condition_model(input);
1840 assert_eq!(model.conditions.len(), 2);
1841 assert!(model.conditions.contains_key("isProduction"));
1842 assert!(model.conditions.contains_key("isDevelopment"));
1843 }
1844
1845 #[test]
1846 fn mutex_group_extraction() {
1847 let input = r#"
1848Parameters:
1849 Env:
1850 Type: String
1851 AllowedValues: [Prod, Dev]
1852Conditions:
1853 isProduction:
1854 Fn::Equals: [!Ref Env, Prod]
1855 isDevelopment:
1856 Fn::Equals: [!Ref Env, Dev]
1857Resources:
1858 R:
1859 Type: T
1860"#;
1861 let model = build_condition_model(input);
1862 assert!(!model.mutex_groups.is_empty());
1863 assert_eq!(model.mutex_groups[0].conditions.len(), 2);
1864 }
1865
1866 #[test]
1867 fn satisfiable_single_true() {
1868 let input = r#"
1869Parameters:
1870 Env:
1871 Type: String
1872 AllowedValues: [Prod, Dev]
1873Conditions:
1874 isProduction:
1875 Fn::Equals: [!Ref Env, Prod]
1876 isDevelopment:
1877 Fn::Equals: [!Ref Env, Dev]
1878Resources:
1879 R:
1880 Type: T
1881"#;
1882 let model = build_condition_model(input);
1883 assert!(model.is_satisfiable(&[("isProduction".into(), true)]));
1884 }
1885
1886 #[test]
1887 fn satisfiable_mutex_violation() {
1888 let input = r#"
1889Parameters:
1890 Env:
1891 Type: String
1892 AllowedValues: [Prod, Dev]
1893Conditions:
1894 isProduction:
1895 Fn::Equals: [!Ref Env, Prod]
1896 isDevelopment:
1897 Fn::Equals: [!Ref Env, Dev]
1898Resources:
1899 R:
1900 Type: T
1901"#;
1902 let model = build_condition_model(input);
1903 assert!(!model.conditions_compatible("isProduction", "isDevelopment"));
1904 }
1905
1906 #[test]
1907 fn resources_compatible_both_unconditional() {
1908 let input = r#"
1909Resources:
1910 R:
1911 Type: T
1912"#;
1913 let model = build_condition_model(input);
1914 assert!(model.resources_compatible(None, None));
1915 }
1916
1917 #[test]
1918 fn no_conditions_template() {
1919 let input = r#"{"Resources":{"R":{"Type":"T"}}}"#;
1920 let model = build_condition_model(input);
1921 assert!(model.conditions.is_empty());
1922 assert!(model.is_satisfiable(&[]));
1923 }
1924
1925 #[test]
1926 fn condition_implies_same_condition() {
1927 let input = r#"
1928Parameters:
1929 Env:
1930 Type: String
1931 AllowedValues: [Prod, Dev]
1932Conditions:
1933 isProduction:
1934 Fn::Equals: [!Ref Env, Prod]
1935Resources:
1936 R:
1937 Type: T
1938"#;
1939 let model = build_condition_model(input);
1940 assert!(model.condition_implies("isProduction", "isProduction"));
1942 }
1943
1944 #[test]
1945 fn is_satisfiable_contradictory_assumptions() {
1946 let input = r#"
1947Parameters:
1948 Env:
1949 Type: String
1950 AllowedValues: [Prod, Dev]
1951Conditions:
1952 isProduction:
1953 Fn::Equals: [!Ref Env, Prod]
1954Resources:
1955 R:
1956 Type: T
1957"#;
1958 let model = build_condition_model(input);
1959 assert!(!model.is_satisfiable(&[("isProduction".into(), true), ("isProduction".into(), false),]));
1961 }
1962
1963 #[test]
1964 fn condition_implies_mutex_conditions() {
1965 let input = r#"
1966Parameters:
1967 Env:
1968 Type: String
1969 AllowedValues: [Prod, Dev]
1970Conditions:
1971 isProduction:
1972 Fn::Equals: [!Ref Env, Prod]
1973 isDevelopment:
1974 Fn::Equals: [!Ref Env, Dev]
1975Resources:
1976 R:
1977 Type: T
1978"#;
1979 let model = build_condition_model(input);
1980 assert!(!model.condition_implies("isProduction", "isDevelopment"));
1982 assert!(!model.condition_implies("isDevelopment", "isProduction"));
1983 }
1984
1985 #[test]
1986 fn compatible_or_condition_without_allowed_values() {
1987 let input = r#"
1988Parameters:
1989 Env:
1990 Type: String
1991Conditions:
1992 IsProd:
1993 Fn::Equals: [!Ref Env, Prod]
1994 IsProdOrStage:
1995 Fn::Or:
1996 - Condition: IsProd
1997 - Fn::Equals: [!Ref Env, Stage]
1998Resources:
1999 R:
2000 Type: T
2001"#;
2002 let model = build_condition_model(input);
2003 assert!(model.conditions_compatible("IsProd", "IsProdOrStage"));
2004 assert!(model.condition_implies("IsProd", "IsProdOrStage"));
2005 assert!(!model.condition_implies("IsProdOrStage", "IsProd"));
2006 }
2007
2008 #[test]
2009 fn tautological_equals_literal_match() {
2010 let input = r#"
2011Conditions:
2012 AlwaysTrue:
2013 Fn::Equals: ["same", "same"]
2014 AlwaysFalse:
2015 Fn::Equals: ["a", "b"]
2016Resources:
2017 R:
2018 Type: T
2019"#;
2020 let model = build_condition_model(input);
2021 let taut = model.tautological_equals();
2022 assert!(taut.iter().any(|(n, v)| n == "AlwaysTrue" && *v));
2023 assert!(taut.iter().any(|(n, v)| n == "AlwaysFalse" && !*v));
2024 }
2025
2026 #[test]
2027 fn tautological_equals_nested_in_and() {
2028 let input = r#"
2029Parameters:
2030 Env:
2031 Type: String
2032Conditions:
2033 Nested:
2034 Fn::And:
2035 - Fn::Equals: ["x", "x"]
2036 - Fn::Equals: [!Ref Env, Prod]
2037Resources:
2038 R:
2039 Type: T
2040"#;
2041 let model = build_condition_model(input);
2042 let taut = model.tautological_equals();
2043 assert!(
2044 taut.iter().any(|(n, v)| n == "Nested" && *v),
2045 "should detect tautological Equals inside And: {:?}",
2046 taut
2047 );
2048 }
2049
2050 #[test]
2051 fn referenced_params_collected() {
2052 let input = r#"
2053Parameters:
2054 Env:
2055 Type: String
2056 Region:
2057 Type: String
2058Conditions:
2059 IsProd:
2060 Fn::Equals: [!Ref Env, Prod]
2061 IsUsEast:
2062 Fn::Equals: [!Ref Region, us-east-1]
2063Resources:
2064 R:
2065 Type: T
2066"#;
2067 let model = build_condition_model(input);
2068 let refs = model.referenced_params();
2069 assert!(refs.contains(&"Env".to_string()));
2070 assert!(refs.contains(&"Region".to_string()));
2071 assert_eq!(refs.len(), 2);
2072 }
2073
2074 #[test]
2075 fn and_implication_extracted() {
2076 let input = r#"
2077Parameters:
2078 Env:
2079 Type: String
2080 DB:
2081 Type: String
2082 AllowedValues: [yes, no]
2083Conditions:
2084 IsProd:
2085 Fn::Equals: [!Ref Env, Prod]
2086 CreateDB:
2087 Fn::Equals: [!Ref DB, yes]
2088 ProdAndDB:
2089 Fn::And:
2090 - Condition: IsProd
2091 - Condition: CreateDB
2092Resources:
2093 R:
2094 Type: T
2095"#;
2096 let model = build_condition_model(input);
2097 assert!(model.implications.iter().any(|i| i.antecedent == "ProdAndDB" && i.consequent == "IsProd"));
2099 assert!(model.implications.iter().any(|i| i.antecedent == "ProdAndDB" && i.consequent == "CreateDB"));
2100 }
2101
2102 #[test]
2103 fn or_implication_extracted() {
2104 let input = r#"
2105Parameters:
2106 Env:
2107 Type: String
2108 AllowedValues: [Prod, Dev, Stage]
2109Conditions:
2110 IsProd:
2111 Fn::Equals: [!Ref Env, Prod]
2112 IsDev:
2113 Fn::Equals: [!Ref Env, Dev]
2114 ProdOrDev:
2115 Fn::Or:
2116 - Condition: IsProd
2117 - Condition: IsDev
2118Resources:
2119 R:
2120 Type: T
2121"#;
2122 let model = build_condition_model(input);
2123 assert!(model.implications.iter().any(|i| i.antecedent == "IsProd" && i.consequent == "ProdOrDev"));
2125 assert!(model.implications.iter().any(|i| i.antecedent == "IsDev" && i.consequent == "ProdOrDev"));
2126 }
2127
2128 #[test]
2129 fn three_way_mutex_group() {
2130 let input = r#"
2131Parameters:
2132 Env:
2133 Type: String
2134 AllowedValues: [Prod, Dev, Stage]
2135Conditions:
2136 IsProd:
2137 Fn::Equals: [!Ref Env, Prod]
2138 IsDev:
2139 Fn::Equals: [!Ref Env, Dev]
2140 IsStage:
2141 Fn::Equals: [!Ref Env, Stage]
2142Resources:
2143 R:
2144 Type: T
2145"#;
2146 let model = build_condition_model(input);
2147 assert_eq!(model.mutex_groups.len(), 1);
2148 assert_eq!(model.mutex_groups[0].conditions.len(), 3);
2149 assert!(!model.conditions_compatible("IsProd", "IsDev"));
2150 assert!(!model.conditions_compatible("IsProd", "IsStage"));
2151 assert!(!model.conditions_compatible("IsDev", "IsStage"));
2152 }
2153
2154 #[test]
2155 fn resources_compatible_one_unconditional() {
2156 let input = r#"
2157Parameters:
2158 Env:
2159 Type: String
2160 AllowedValues: [Prod, Dev]
2161Conditions:
2162 IsProd:
2163 Fn::Equals: [!Ref Env, Prod]
2164Resources:
2165 R:
2166 Type: T
2167"#;
2168 let model = build_condition_model(input);
2169 assert!(model.resources_compatible(Some("IsProd"), None));
2170 assert!(model.resources_compatible(None, Some("IsProd")));
2171 }
2172
2173 #[test]
2174 fn condition_names_iterator() {
2175 let input = r#"
2176Conditions:
2177 A:
2178 Fn::Equals: ["x", "x"]
2179 B:
2180 Fn::Equals: ["y", "y"]
2181Resources:
2182 R:
2183 Type: T
2184"#;
2185 let model = build_condition_model(input);
2186 let mut names: Vec<&str> = model.names().collect();
2187 names.sort();
2188 assert_eq!(names, vec!["A", "B"]);
2189 }
2190
2191 #[test]
2192 fn get_returns_none_for_unknown_condition() {
2193 let input = r#"{"Resources":{"R":{"Type":"T"}}}"#;
2194 let model = build_condition_model(input);
2195 assert!(model.get("NonExistent").is_none(), "unknown condition should return None");
2196 }
2197
2198 #[test]
2199 fn not_condition_parsed() {
2200 let input = r#"
2201Parameters:
2202 Env:
2203 Type: String
2204 AllowedValues: [Prod, Dev]
2205Conditions:
2206 IsProd:
2207 Fn::Equals: [!Ref Env, Prod]
2208 NotProd:
2209 Fn::Not:
2210 - Condition: IsProd
2211Resources:
2212 R:
2213 Type: T
2214"#;
2215 let model = build_condition_model(input);
2216 assert!(!model.conditions_compatible("IsProd", "NotProd"));
2217 assert!(!model.condition_implies("IsProd", "NotProd"));
2218 }
2219
2220 #[test]
2221 fn pseudo_param_without_override_is_a_free_variable_in_sat_solver() {
2222 let input = r#"
2223Parameters:
2224 Env:
2225 Type: String
2226 AllowedValues: [Prod, Dev]
2227Conditions:
2228 IsUsEast1:
2229 Fn::Equals: [!Ref "AWS::Region", us-east-1]
2230 IsProd:
2231 Fn::Equals: [!Ref Env, Prod]
2232Resources:
2233 R:
2234 Type: T
2235"#;
2236 let model = build_condition_model(input);
2237 assert!(
2238 model.is_satisfiable(&[("IsUsEast1".into(), true)]),
2239 "IsUsEast1=true must be reachable: AWS::Region can equal 'us-east-1'"
2240 );
2241 assert!(
2242 model.is_satisfiable(&[("IsUsEast1".into(), false)]),
2243 "IsUsEast1=false must be reachable: AWS::Region can equal anything other than 'us-east-1' \
2244 (the auto-derived default region must not pin the pseudo-parameter to a constant - that \
2245 produced false-positive W1028 unreachable-branch diagnostics on partition/region branches)"
2246 );
2247 assert!(
2248 model.is_satisfiable(&[("IsProd".into(), true)]),
2249 "IsProd=true must be reachable: Env can equal 'Prod'"
2250 );
2251 assert!(
2252 model.is_satisfiable(&[("IsProd".into(), false)]),
2253 "IsProd=false must be reachable: Env can equal 'Dev'"
2254 );
2255 }
2256
2257 #[test]
2258 fn is_satisfiable_in_region_pins_aws_region() {
2259 let input = r#"
2260Conditions:
2261 IsUsEast1:
2262 Fn::Equals: [!Ref "AWS::Region", us-east-1]
2263Resources:
2264 R:
2265 Type: T
2266"#;
2267 let model = build_condition_model(input);
2268 assert!(
2273 model.is_satisfiable_in_region(&[("IsUsEast1".into(), true)], "us-east-1"),
2274 "IsUsEast1=true must hold when the target region IS us-east-1"
2275 );
2276 assert!(
2277 !model.is_satisfiable_in_region(&[("IsUsEast1".into(), true)], "us-west-2"),
2278 "IsUsEast1=true must be unsatisfiable when the target region is us-west-2"
2279 );
2280 assert!(
2282 model.is_satisfiable_in_region(&[("IsUsEast1".into(), false)], "us-west-2"),
2283 "IsUsEast1=false must hold at us-west-2"
2284 );
2285 }
2286
2287 #[test]
2288 fn pseudo_param_explicit_override_pins_value_in_sat_solver() {
2289 let ir = parser::parse(
2290 r#"
2291Conditions:
2292 IsAwsPartition:
2293 Fn::Equals: [!Ref "AWS::Partition", aws]
2294Resources:
2295 R:
2296 Type: T
2297"#
2298 .as_bytes(),
2299 )
2300 .unwrap();
2301 let (params, _) = extract_parameters(&ir);
2302 let (mappings, _) = extract_mappings(&ir);
2303 let pseudo = PseudoParameterOverrides { partition: Some("aws-cn".to_string()), ..Default::default() };
2304
2305 let model = ConditionModel::from_ir(&ir, ¶ms, &pseudo, &mappings);
2306
2307 assert!(
2308 !model.is_satisfiable(&[("IsAwsPartition".into(), true)]),
2309 "with partition pinned to aws-cn, IsAwsPartition=true must be unsatisfiable"
2310 );
2311 assert!(
2312 model.is_satisfiable(&[("IsAwsPartition".into(), false)]),
2313 "with partition pinned to aws-cn, IsAwsPartition=false must hold"
2314 );
2315 }
2316
2317 #[test]
2318 fn pseudo_param_custom_region_changes_satisfiability() {
2319 let ir = parser::parse(
2320 r#"
2321Conditions:
2322 IsUsEast1:
2323 Fn::Equals: [!Ref "AWS::Region", us-east-1]
2324Resources:
2325 R:
2326 Type: T
2327"#
2328 .as_bytes(),
2329 )
2330 .unwrap();
2331 let (params, _) = extract_parameters(&ir);
2332 let (mappings, _) = extract_mappings(&ir);
2333 let pseudo = PseudoParameterOverrides { region: Some("eu-west-1".to_string()), ..Default::default() };
2334 let model = ConditionModel::from_ir(&ir, ¶ms, &pseudo, &mappings);
2335 assert!(!model.is_satisfiable(&[("IsUsEast1".into(), true)]));
2337 assert!(model.is_satisfiable(&[("IsUsEast1".into(), false)]));
2338 }
2339
2340 #[test]
2341 fn pseudo_param_partition_false_branch_is_reachable_without_override() {
2342 let input = r#"
2343Conditions:
2344 HasEcrPublic:
2345 Fn::Equals: [!Ref "AWS::Partition", aws]
2346Resources:
2347 R:
2348 Type: T
2349"#;
2350 let model = build_condition_model(input);
2351
2352 assert!(
2353 model.is_satisfiable(&[("HasEcrPublic".into(), true)]),
2354 "HasEcrPublic=true must be reachable: AWS::Partition can equal 'aws'"
2355 );
2356 assert!(
2357 model.is_satisfiable(&[("HasEcrPublic".into(), false)]),
2358 "HasEcrPublic=false must be reachable: AWS::Partition can equal 'aws-cn' or 'aws-us-gov'"
2359 );
2360 }
2361
2362 #[test]
2363 fn mapping_lookup_resolves_in_condition() {
2364 let input = r#"
2365Parameters:
2366 Env:
2367 Type: String
2368 AllowedValues: [Prod, Dev]
2369Mappings:
2370 EnvMap:
2371 Prod:
2372 Label: production
2373 Dev:
2374 Label: development
2375Conditions:
2376 IsProdLabel:
2377 Fn::Equals:
2378 - !FindInMap [EnvMap, !Ref Env, Label]
2379 - production
2380Resources:
2381 R:
2382 Type: T
2383"#;
2384 let model = build_condition_model(input);
2385 assert!(model.is_satisfiable(&[("IsProdLabel".into(), true)]));
2388 assert!(model.is_satisfiable(&[("IsProdLabel".into(), false)]));
2389 }
2390
2391 #[test]
2392 fn mapping_lookup_format_value_expr_readable() {
2393 let expr = ValueExpr::MappingLookup {
2394 map_name: "MyMap".to_string(),
2395 key1: Box::new(ValueExpr::ParamRef("Env".to_string())),
2396 key2: Box::new(ValueExpr::Literal("Label".to_string())),
2397 };
2398 let formatted = format_value_expr(&expr);
2399 assert!(formatted.contains("FindInMap"));
2400 assert!(formatted.contains("MyMap"));
2401 }
2402
2403 #[test]
2404 fn mapping_lookup_param_refs_collected() {
2405 let input = r#"
2406Parameters:
2407 Env:
2408 Type: String
2409 AllowedValues: [Prod, Dev]
2410Mappings:
2411 EnvMap:
2412 Prod:
2413 Label: production
2414 Dev:
2415 Label: development
2416Conditions:
2417 IsProdLabel:
2418 Fn::Equals:
2419 - !FindInMap [EnvMap, !Ref Env, Label]
2420 - production
2421Resources:
2422 R:
2423 Type: T
2424"#;
2425 let model = build_condition_model(input);
2426 let refs = model.referenced_params();
2427 assert!(refs.contains(&"Env".to_string()));
2428 }
2429
2430 #[test]
2431 fn extract_equals_test_unwraps_not() {
2432 let expr = ConditionExpr::Not(Box::new(ConditionExpr::Equals(
2433 ValueExpr::ParamRef("Env".into()),
2434 ValueExpr::Literal("Prod".into()),
2435 )));
2436 let result = extract_equals_test(&expr);
2437 let (param, lit, positive) = result.expect("extract_equals_test should return Some for Not(Equals)");
2438 assert_eq!(param, "Env");
2439 assert_eq!(lit, "Prod");
2440 assert!(!positive);
2441 }
2442
2443 #[test]
2444 fn nested_and_implication_extracts_deep_condition_refs() {
2445 let input = r#"
2446Parameters:
2447 A:
2448 Type: String
2449 AllowedValues: [yes, no]
2450 B:
2451 Type: String
2452 AllowedValues: [yes, no]
2453 C:
2454 Type: String
2455 AllowedValues: [yes, no]
2456Conditions:
2457 CondA:
2458 Fn::Equals: [!Ref A, yes]
2459 CondB:
2460 Fn::Equals: [!Ref B, yes]
2461 CondC:
2462 Fn::Equals: [!Ref C, yes]
2463 NestedAnd:
2464 Fn::And:
2465 - Fn::And:
2466 - Condition: CondA
2467 - Condition: CondB
2468 - Condition: CondC
2469Resources:
2470 R:
2471 Type: T
2472"#;
2473 let model = build_condition_model(input);
2474 assert!(model.implications.iter().any(|i| i.antecedent == "NestedAnd" && i.consequent == "CondA"));
2476 assert!(model.implications.iter().any(|i| i.antecedent == "NestedAnd" && i.consequent == "CondB"));
2477 assert!(model.implications.iter().any(|i| i.antecedent == "NestedAnd" && i.consequent == "CondC"));
2478 }
2479
2480 #[test]
2481 fn nested_or_implication_extracts_deep_condition_refs() {
2482 let input = r#"
2483Parameters:
2484 A:
2485 Type: String
2486 AllowedValues: [yes, no]
2487 B:
2488 Type: String
2489 AllowedValues: [yes, no]
2490Conditions:
2491 CondA:
2492 Fn::Equals: [!Ref A, yes]
2493 CondB:
2494 Fn::Equals: [!Ref B, yes]
2495 NestedOr:
2496 Fn::Or:
2497 - Fn::Or:
2498 - Condition: CondA
2499 - Condition: CondB
2500Resources:
2501 R:
2502 Type: T
2503"#;
2504 let model = build_condition_model(input);
2505 assert!(model.implications.iter().any(|i| i.antecedent == "CondA" && i.consequent == "NestedOr"));
2507 assert!(model.implications.iter().any(|i| i.antecedent == "CondB" && i.consequent == "NestedOr"));
2508 }
2509
2510 #[test]
2511 fn register_inline_adds_condition_and_rebuilds_mutex() {
2512 let input = r#"
2513Parameters:
2514 Env:
2515 Type: String
2516 AllowedValues: [Prod, Dev]
2517Conditions:
2518 IsProd:
2519 Fn::Equals: [!Ref Env, Prod]
2520Resources:
2521 R:
2522 Type: T
2523"#;
2524 let mut model = build_condition_model(input);
2525 assert_eq!(model.conditions.len(), 1);
2526 model.register_inline(
2528 "__inline_1".to_string(),
2529 ConditionExpr::Equals(ValueExpr::ParamRef("Env".to_string()), ValueExpr::Literal("Dev".to_string())),
2530 );
2531 assert_eq!(model.conditions.len(), 2);
2532 assert!(!model.mutex_groups.is_empty());
2534 assert!(!model.conditions_compatible("IsProd", "__inline_1"));
2535 }
2536
2537 #[test]
2538 fn ifexpr_literal_equals_eagerly_evaluated() {
2539 let input = r#"{
2541 "Conditions": {"C": {"Fn::Equals": ["x", "x"]}},
2542 "Resources": {
2543 "R": {
2544 "Type": "T",
2545 "Properties": {
2546 "V": {"Fn::If": [{"Fn::Equals": ["same", "same"]}, "yes", "no"]}
2547 }
2548 }
2549 }
2550}"#;
2551 let model = crate::model::SemanticModel::from_bytes(input.as_bytes()).unwrap();
2552 match model.resolve("R", "Properties.V") {
2554 Some(crate::resolver::ResolvedValue::Concrete { value: v }) => {
2555 assert_eq!(v.as_str().unwrap(), "yes");
2556 }
2557 other => panic!("Expected Concrete(\"yes\") from eager eval, got {:?}", other),
2558 }
2559 }
2560
2561 #[test]
2562 fn ifexpr_literal_not_equals_eagerly_evaluated_to_false_branch() {
2563 let input = r#"{
2564 "Conditions": {"C": {"Fn::Equals": ["x", "x"]}},
2565 "Resources": {
2566 "R": {
2567 "Type": "T",
2568 "Properties": {
2569 "V": {"Fn::If": [{"Fn::Equals": ["a", "b"]}, "yes", "no"]}
2570 }
2571 }
2572 }
2573}"#;
2574 let model = crate::model::SemanticModel::from_bytes(input.as_bytes()).unwrap();
2575 match model.resolve("R", "Properties.V") {
2576 Some(crate::resolver::ResolvedValue::Concrete { value: v }) => {
2577 assert_eq!(v.as_str().unwrap(), "no");
2578 }
2579 other => panic!("Expected Concrete(\"no\") from eager eval, got {:?}", other),
2580 }
2581 }
2582
2583 #[test]
2584 fn ifexpr_non_literal_registers_inline_condition() {
2585 let input = r#"
2587Parameters:
2588 Env:
2589 Type: String
2590 AllowedValues: [Prod, Dev]
2591Resources:
2592 R:
2593 Type: T
2594 Properties:
2595 V:
2596 Fn::If:
2597 - Fn::Equals: [!Ref Env, Prod]
2598 - big
2599 - small
2600"#;
2601 let model = crate::model::SemanticModel::from_bytes(input.as_bytes()).unwrap();
2602 let inline_count = model.conditions.conditions.keys().filter(|k| k.starts_with("__inline_cond_")).count();
2604 assert!(inline_count > 0, "Expected at least one inline condition registered");
2605 }
2606
2607 fn chain_with_contradiction(chain_len: usize) -> String {
2619 let mut s = String::from(
2620 "Parameters:\n P0:\n Type: String\n AllowedValues: [yes, no]\n \
2621 P1:\n Type: String\n AllowedValues: [yes, no]\n\
2622 Conditions:\n C000:\n Fn::Equals: [!Ref P0, yes]\n \
2623 C001:\n Fn::Equals: [!Ref P1, yes]\n",
2624 );
2625 for i in 2..chain_len {
2626 let outer_inner = if i % 2 == 0 { "Fn::Or" } else { "Fn::And" };
2628 let _ = write!(
2629 s,
2630 " C{:03}:\n Fn::Not:\n - {}:\n - Fn::Not:\n - Condition: C{:03}\n - Fn::Not:\n - Condition: C{:03}\n",
2631 i,
2632 outer_inner,
2633 i - 1,
2634 i - 2
2635 );
2636 }
2637 let top = format!("C{:03}", chain_len - 1);
2638 let _ = write!(
2639 s,
2640 " Contra:\n Fn::And:\n - Condition: {top}\n \
2641 - Fn::Not:\n - Condition: {top}\n\
2642 Resources:\n R:\n Type: T\n",
2643 );
2644 s
2645 }
2646
2647 #[test]
2648 fn contradiction_over_a_long_condition_chain_is_decided_exactly_and_cheaply() {
2649 const SHORT_CHAIN: usize = 8;
2660 const LONG_CHAIN: usize = 32;
2661 const DOUBLED_CHAIN: usize = 64;
2662
2663 for chain_length in [SHORT_CHAIN, LONG_CHAIN, DOUBLED_CHAIN] {
2664 let model = build_condition_model(&chain_with_contradiction(chain_length));
2665 assert!(
2666 !model.is_satisfiable(&[("Contra".to_string(), true)]),
2667 "a contradiction over a {chain_length}-condition chain must be proven \
2668 unsatisfiable, not conceded as satisfiable because the search was too expensive"
2669 );
2670 }
2671
2672 const MAX_GROWTH_FROM_DOUBLING: u64 = 4;
2677 let long_model = build_condition_model(&chain_with_contradiction(LONG_CHAIN));
2678 let _ = long_model.is_satisfiable(&[("Contra".to_string(), true)]);
2679 let doubled_model = build_condition_model(&chain_with_contradiction(DOUBLED_CHAIN));
2680 let _ = doubled_model.is_satisfiable(&[("Contra".to_string(), true)]);
2681 let long_chain_work = long_model.sat_iterations_used();
2682 let doubled_chain_work = doubled_model.sat_iterations_used();
2683 assert!(
2684 doubled_chain_work <= long_chain_work * MAX_GROWTH_FROM_DOUBLING,
2685 "doubling the chain length must not multiply the work by more than \
2686 {MAX_GROWTH_FROM_DOUBLING}x; {LONG_CHAIN} conditions cost {long_chain_work} steps and \
2687 {DOUBLED_CHAIN} cost {doubled_chain_work}"
2688 );
2689 assert!(
2690 doubled_chain_work < MAX_SAT_ITERATIONS,
2691 "deciding a chain of {DOUBLED_CHAIN} conditions over two binary parameters must stay \
2692 far inside the per-query budget; cost {doubled_chain_work} of {MAX_SAT_ITERATIONS}"
2693 );
2694 }
2695
2696 fn conditions_layered_over_shared_pseudo_parameters(layered: usize) -> String {
2701 const PARTITIONS: [&str; 4] = ["aws", "aws-us-gov", "aws-cn", "aws-iso"];
2702 const REGIONS: [&str; 4] = ["us-east-1", "us-west-2", "us-gov-west-1", "cn-north-1"];
2703 let mut template = String::from("Parameters:\n Stage:\n Type: String\nConditions:\n");
2704 let mut base_names = Vec::new();
2705 for (index, partition) in PARTITIONS.iter().enumerate() {
2706 let _ = write!(template, " IsPartition{index}:\n Fn::Equals: [!Ref 'AWS::Partition', {partition}]\n");
2707 base_names.push(format!("IsPartition{index}"));
2708 }
2709 for (index, region) in REGIONS.iter().enumerate() {
2710 let _ = write!(template, " IsRegion{index}:\n Fn::Equals: [!Ref 'AWS::Region', {region}]\n");
2711 base_names.push(format!("IsRegion{index}"));
2712 }
2713 let _ = write!(template, " IsProd:\n Fn::Equals: [!Ref Stage, prod]\n");
2714 base_names.push("IsProd".to_string());
2715
2716 let mut names = base_names.clone();
2717 for index in 0..layered {
2718 let operator = if index % 2 == 0 { "Fn::And" } else { "Fn::Or" };
2719 let operands: Vec<&String> = (0..3).map(|offset| &names[(index * 7 + offset * 3) % names.len()]).collect();
2720 let _ = write!(template, " Layered{index:03}:\n {operator}:\n");
2721 for operand in operands {
2722 let _ = writeln!(template, " - Condition: {operand}");
2723 }
2724 names.push(format!("Layered{index:03}"));
2725 }
2726 let _ = write!(
2727 template,
2728 "Resources:\n R:\n Type: AWS::SNS::Topic\n Condition: {}\n",
2729 names[names.len() - 1]
2730 );
2731 template
2732 }
2733
2734 #[test]
2735 fn conditions_layered_over_shared_inputs_are_analyzed_in_full() {
2736 const LAYERED_CONDITIONS: usize = 91;
2745 let model = build_condition_model(&conditions_layered_over_shared_pseudo_parameters(LAYERED_CONDITIONS));
2746
2747 let mut names: Vec<&str> = model.names().collect();
2748 names.sort_unstable();
2749 assert_eq!(names.len(), LAYERED_CONDITIONS + 9, "the fixture must reach the scale under test");
2750
2751 let mut decided_pairs = 0u64;
2752 for (offset, first) in names.iter().enumerate() {
2753 for second in &names[offset + 1..] {
2754 let _ = model.conditions_compatible(first, second);
2755 decided_pairs += 1;
2756 }
2757 }
2758
2759 assert!(
2760 !model.satisfiability_budget_exhausted(),
2761 "the full pairwise pass over {} conditions must fit in the cumulative budget; it spent \
2762 {} of {MAX_TOTAL_SAT_ITERATIONS} deciding {decided_pairs} pairs",
2763 names.len(),
2764 model.sat_iterations_used()
2765 );
2766 assert!(
2767 model.budget_exhausted_queries().is_empty(),
2768 "no query over conditions layered on shared inputs may exceed its budget: {:?}",
2769 model.budget_exhausted_queries()
2770 );
2771 assert!(
2775 !model.conditions_compatible("IsPartition0", "IsPartition1"),
2776 "conditions comparing the same pseudo-parameter with different values must be proven \
2777 incompatible"
2778 );
2779 }
2780
2781 #[test]
2782 fn cumulative_budget_exhaustion_is_reported_rather_than_silently_narrowing_analysis() {
2783 let model = build_condition_model(&chain_with_contradiction(8));
2788 assert!(model.budget_exhausted_queries().is_empty(), "nothing is curtailed before any work is charged");
2789
2790 model.add_sat_iterations_for_test(MAX_TOTAL_SAT_ITERATIONS);
2791
2792 assert!(model.satisfiability_budget_exhausted(), "the budget must register as spent");
2793 assert!(
2794 !model.budget_exhausted_queries().is_empty(),
2795 "spending the cumulative budget must be reported so a curtailed analysis is never silent"
2796 );
2797 }
2798
2799 #[test]
2800 fn self_referential_conditions_are_undetermined_rather_than_looping() {
2801 let model = build_condition_model(
2806 "Parameters:\n Env:\n Type: String\n AllowedValues: [prod, dev]\n\
2807 Conditions:\n Looping:\n Fn::And:\n - Condition: AlsoLooping\n \
2808 - Fn::Equals: [!Ref Env, prod]\n \
2809 AlsoLooping:\n Fn::Or:\n - Condition: Looping\n \
2810 - Fn::Equals: [!Ref Env, dev]\n\
2811 Resources:\n R:\n Type: T\n",
2812 );
2813 assert!(
2814 model.is_satisfiable(&[("Looping".to_string(), true)]),
2815 "a condition on a reference cycle cannot be decided, so it must be assumed satisfiable"
2816 );
2817 assert!(model.is_satisfiable(&[("Looping".to_string(), false)]), "the same holds for the negated assumption");
2818 assert!(
2819 model.sat_iterations_used() < MAX_SAT_ITERATIONS,
2820 "a cycle must be cut immediately, not explored until the budget runs out; cost {}",
2821 model.sat_iterations_used()
2822 );
2823 }
2824
2825 #[test]
2826 fn condition_over_an_unresolvable_value_does_not_constrain_a_query() {
2827 let model = build_condition_model(
2832 "Parameters:\n Subnets:\n Type: CommaDelimitedList\n \
2833 Env:\n Type: String\n AllowedValues: [prod, dev]\n\
2834 Conditions:\n \
2835 FirstSubnetIsReserved:\n Fn::Equals: [!Select [0, !Ref Subnets], reserved]\n \
2836 IsProd:\n Fn::Equals: [!Ref Env, prod]\n \
2837 IsDev:\n Fn::Equals: [!Ref Env, dev]\n \
2838 ReservedInProd:\n Fn::And:\n - Condition: FirstSubnetIsReserved\n \
2839 - Condition: IsProd\n\
2840 Resources:\n R:\n Type: T\n",
2841 );
2842 assert!(
2843 model.is_satisfiable(&[("FirstSubnetIsReserved".to_string(), true)]),
2844 "an unresolvable comparison must be assumed able to hold"
2845 );
2846 assert!(
2847 model.conditions_compatible("ReservedInProd", "IsProd"),
2848 "an unresolvable operand must not make a combined condition look impossible"
2849 );
2850 assert!(
2851 !model.conditions_compatible("ReservedInProd", "IsDev"),
2852 "the resolvable operand must still constrain the combination: the And requires prod, \
2853 which excludes dev"
2854 );
2855 }
2856
2857 #[test]
2858 fn conditions_sharing_a_parameter_through_derived_conditions_stay_exact() {
2859 let model = build_condition_model(
2864 "Parameters:\n Env:\n Type: String\n AllowedValues: [prod, gamma, dev]\n \
2865 Feature:\n Type: String\n AllowedValues: ['on', 'off']\n\
2866 Conditions:\n \
2867 IsProd:\n Fn::Equals: [!Ref Env, prod]\n \
2868 IsDev:\n Fn::Equals: [!Ref Env, dev]\n \
2869 FeatureOn:\n Fn::Equals: [!Ref Feature, 'on']\n \
2870 ProdWithFeature:\n Fn::And:\n - Condition: IsProd\n - Condition: FeatureOn\n \
2871 DevWithFeature:\n Fn::And:\n - Condition: IsDev\n - Condition: FeatureOn\n \
2872 EitherWithFeature:\n Fn::Or:\n - Condition: ProdWithFeature\n \
2873 - Condition: DevWithFeature\n\
2874 Resources:\n R:\n Type: T\n",
2875 );
2876 assert!(
2877 !model.conditions_compatible("ProdWithFeature", "DevWithFeature"),
2878 "conditions requiring different values of the same parameter cannot both hold, however \
2879 many references separate them from that parameter"
2880 );
2881 assert!(
2882 model.conditions_compatible("ProdWithFeature", "EitherWithFeature"),
2883 "a condition and a disjunction it satisfies must remain compatible"
2884 );
2885 assert!(
2886 model.condition_implies("ProdWithFeature", "FeatureOn"),
2887 "a conjunction must be proven to imply its operand"
2888 );
2889 assert!(
2890 !model.condition_implies("EitherWithFeature", "IsProd"),
2891 "a disjunction must not be proven to imply only one of its branches"
2892 );
2893 }
2894
2895 fn wide_parameter_closure(param_count: usize) -> String {
2900 let mut s = String::from("Parameters:\n");
2901 for i in 0..param_count {
2902 let _ = write!(s, " P{i:02}:\n Type: String\n AllowedValues: [yes, no]\n");
2903 }
2904 s.push_str("Conditions:\n");
2905 for i in 0..param_count {
2906 let _ = write!(s, " Base{i:02}:\n Fn::Equals: [!Ref P{i:02}, yes]\n");
2907 }
2908 s.push_str(" Wide:\n Fn::And:\n");
2909 for i in 0..param_count {
2910 let _ = writeln!(s, " - Condition: Base{i:02}");
2911 }
2912 s.push_str("Resources:\n R:\n Type: T\n");
2913 s
2914 }
2915
2916 #[test]
2917 fn satisfiability_with_wide_parameter_closure_is_capped_conservatively() {
2918 const WIDE_PARAMS: usize = 24;
2922 let mut model = build_condition_model(&wide_parameter_closure(WIDE_PARAMS));
2923 let wide_expression = model.get("Wide").cloned().expect("the generated model must contain Wide");
2924 model.register_inline("__inline_cond_test".to_string(), wide_expression);
2925
2926 assert!(
2931 model.is_satisfiable(&[("Base00".to_string(), true), ("__inline_cond_test".to_string(), true)]),
2932 "a condition whose closure references more parameters than the cap must be \
2933 assumed satisfiable, not enumerated"
2934 );
2935 assert_eq!(
2936 model.budget_exhausted_queries(),
2937 vec!["<inline condition>=true, Base00=true".to_string()],
2938 "budget advisories must canonicalize assumption order and describe synthetic conditions generically"
2939 );
2940
2941 assert!(
2945 model.sat_iterations_used() < MAX_PARAM_COMBINATIONS,
2946 "the parameter cap must short-circuit before enumerating the cartesian product; \
2947 charged {} iterations, expected well under {MAX_PARAM_COMBINATIONS}",
2948 model.sat_iterations_used()
2949 );
2950 }
2951
2952 #[test]
2953 fn satisfiability_with_narrow_parameter_closure_stays_exact() {
2954 let model = build_condition_model(
2958 "Parameters:\n Env:\n Type: String\n AllowedValues: [prod, dev]\n\
2959 Conditions:\n IsProd:\n Fn::Equals: [!Ref Env, prod]\n \
2960 IsDev:\n Fn::Equals: [!Ref Env, dev]\n\
2961 Resources:\n R:\n Type: T\n",
2962 );
2963 assert!(
2964 !model.conditions_compatible("IsProd", "IsDev"),
2965 "mutually exclusive conditions over a small parameter space must remain provably \
2966 incompatible"
2967 );
2968 }
2969
2970 #[test]
2971 fn cumulative_satisfiability_budget_accumulates_across_queries_then_halts_search() {
2972 const CHAIN: usize = 8;
2977 let model = build_condition_model(&chain_with_contradiction(CHAIN));
2978
2979 assert_eq!(model.sat_iterations_used(), 0, "a freshly built model has spent none of its cumulative budget");
2980 assert!(!model.satisfiability_budget_exhausted(), "a freshly built model's cumulative budget is not exhausted");
2981
2982 let mut previous = 0u64;
2986 for _ in 0..3 {
2987 let _ = model.is_satisfiable(&[("Contra".to_string(), true)]);
2988 let used = model.sat_iterations_used();
2989 assert!(
2990 used > previous,
2991 "each query while under budget must add to the shared cumulative counter; a \
2992 per-query reset would be a silent denial-of-service regression. was {previous}, \
2993 now {used}"
2994 );
2995 previous = used;
2996 }
2997 assert!(
2998 !model.satisfiability_budget_exhausted(),
2999 "a handful of queries must not exhaust the (large) cumulative budget"
3000 );
3001
3002 let to_threshold = MAX_TOTAL_SAT_ITERATIONS - model.sat_iterations_used() - 1;
3008 model.add_sat_iterations_for_test(to_threshold);
3009 assert!(
3010 !model.satisfiability_budget_exhausted(),
3011 "one iteration short of the cap must not be exhausted; counter is {}",
3012 model.sat_iterations_used()
3013 );
3014 model.add_sat_iterations_for_test(1);
3015 assert!(
3016 model.satisfiability_budget_exhausted(),
3017 "reaching MAX_TOTAL_SAT_ITERATIONS must trip the exhausted flag; counter is {}",
3018 model.sat_iterations_used()
3019 );
3020
3021 let before_short_circuit = model.sat_iterations_used();
3024 let conservative = model.is_satisfiable(&[("Contra".to_string(), true)]);
3025 assert!(
3026 conservative,
3027 "a query issued after the cumulative budget is exhausted must return the conservative \
3028 satisfiable answer"
3029 );
3030 assert_eq!(
3031 model.sat_iterations_used(),
3032 before_short_circuit,
3033 "an exhausted-budget query must short-circuit without performing or charging further \
3034 search work"
3035 );
3036 }
3037
3038 #[test]
3039 fn rules_section_implication_eliminates_impossible_scenario() {
3040 let yaml = b"
3044Parameters:
3045 Env:
3046 Type: String
3047Conditions:
3048 IsProd: !Equals [!Ref Env, prod]
3049Rules:
3050 RejectProd:
3051 RuleCondition: !Equals [!Ref Env, prod]
3052 Assertions:
3053 - Assert: !Equals [!Ref Env, dev]
3054Resources:
3055 B:
3056 Type: AWS::S3::Bucket
3057 Condition: IsProd
3058";
3059 let model = crate::SemanticModel::from_bytes(yaml).expect("model builds");
3060 assert!(
3061 !model.conditions.is_satisfiable(&[("IsProd".to_string(), true)]),
3062 "IsProd=true contradicts the Rules assertion and must be unsatisfiable"
3063 );
3064 assert!(model.conditions.is_satisfiable(&[("IsProd".to_string(), false)]), "IsProd=false remains satisfiable");
3065 }
3066
3067 #[test]
3068 fn rules_without_rule_condition_do_not_constrain_unrelated_queries() {
3069 let yaml = b"
3072Parameters:
3073 Env:
3074 Type: String
3075 Other:
3076 Type: String
3077Conditions:
3078 UsesOther: !Equals [!Ref Other, x]
3079Rules:
3080 AlwaysDev:
3081 Assertions:
3082 - Assert: !Equals [!Ref Env, dev]
3083Resources:
3084 B:
3085 Type: AWS::S3::Bucket
3086 Condition: UsesOther
3087";
3088 let model = crate::SemanticModel::from_bytes(yaml).expect("model builds");
3089 assert!(model.conditions.is_satisfiable(&[("UsesOther".to_string(), true)]));
3090 assert!(model.conditions.is_satisfiable(&[("UsesOther".to_string(), false)]));
3091 }
3092
3093 #[test]
3094 fn mixed_nesting_produces_no_unsound_implications() {
3095 let yaml = b"
3101Parameters:
3102 Env:
3103 Type: String
3104 AllowedValues: [dev, prod]
3105 Flag:
3106 Type: String
3107 AllowedValues: ['true', 'false']
3108Conditions:
3109 A: !Equals [!Ref Env, prod]
3110 B: !Equals [!Ref Env, dev]
3111 C: !Equals [!Ref Flag, 'true']
3112 Complex: !And [!Or [!Condition A, !Condition B], !Not [!Condition C]]
3113 Inline: !And [!Condition B, !Condition C]
3114 NotA: !Not [!Condition A]
3115Resources:
3116 R:
3117 Type: AWS::S3::Bucket
3118 Condition: NotA
3119";
3120 let model = crate::SemanticModel::from_bytes(yaml).expect("model builds");
3121 assert!(
3122 !model.conditions.implications.iter().any(|i| i.antecedent == "Complex" || i.consequent == "Complex"),
3123 "no implication may involve the mixed-nesting condition: {:?}",
3124 model.conditions.implications
3125 );
3126 assert!(
3128 model.conditions.is_satisfiable(&[("NotA".to_string(), true), ("Inline".to_string(), false)]),
3129 "sound implications must not prune this satisfiable scenario"
3130 );
3131 }
3132}