Skip to main content

conjure_cp_core/ast/
assertions.rs

1//! Debug-only structural assertions for AST/model integrity.
2//!
3//! The assertions in this module validate a few key invariants:
4//! - `Root` exists exactly once, at the top-level model root, and nowhere else.
5//! - all referenced names resolve to declarations present in reachable symbol tables.
6//! - a combined model well-formedness check that applies all assertions.
7
8use super::Model;
9
10#[cfg(debug_assertions)]
11use std::collections::{BTreeSet, HashSet};
12
13#[cfg(debug_assertions)]
14use super::{Expression, Name, Reference, SymbolTablePtr, serde::HasId};
15#[cfg(debug_assertions)]
16use uniplate::Biplate;
17
18/// Debug-assert that a model is well-formed by applying all AST assertions in this module.
19#[cfg(debug_assertions)]
20pub fn debug_assert_model_well_formed(model: &Model, origin: &str) {
21    debug_assert_root_at_top_level_only(model, origin);
22    debug_assert_all_names_resolved(model, origin);
23}
24
25/// Debug-assert that a model is well-formed by applying all AST assertions in this module.
26#[cfg(not(debug_assertions))]
27pub fn debug_assert_model_well_formed(_model: &Model, _origin: &str) {}
28
29/// Debug-assert that all names referenced by expressions/domains resolve to declared symbols.
30#[cfg(debug_assertions)]
31pub fn debug_assert_all_names_resolved(model: &Model, origin: &str) {
32    let mut declared_names: BTreeSet<Name> = BTreeSet::new();
33    let mut referenced_names: BTreeSet<Name> = BTreeSet::new();
34
35    for table_ptr in collect_reachable_symbol_tables(model) {
36        let table = table_ptr.read();
37
38        for (name, decl) in table.iter_local() {
39            declared_names.insert(name.clone());
40
41            if let Some(expr) = decl.as_value_letting() {
42                referenced_names.extend(Biplate::<Reference>::universe_bi(&*expr).into_iter().map(
43                    |reference| {
44                        let name = reference.name();
45                        canonical_resolution_name(&name).clone()
46                    },
47                ));
48            }
49
50            if let Some(domain) = decl.domain() {
51                referenced_names.extend(
52                    Biplate::<Reference>::universe_bi(domain.as_ref())
53                        .into_iter()
54                        .map(|reference| {
55                            let name = reference.name();
56                            canonical_resolution_name(&name).clone()
57                        }),
58                );
59            }
60        }
61    }
62
63    referenced_names.extend(
64        Biplate::<Reference>::universe_bi(model.root())
65            .into_iter()
66            .map(|reference| {
67                let name = reference.name();
68                canonical_resolution_name(&name).clone()
69            }),
70    );
71
72    if let Some(dominance) = &model.dominance {
73        referenced_names.extend(
74            Biplate::<Reference>::universe_bi(dominance)
75                .into_iter()
76                .map(|reference| {
77                    let name = reference.name();
78                    canonical_resolution_name(&name).clone()
79                }),
80        );
81    }
82
83    if let Some(objective) = &model.objective {
84        referenced_names.extend(
85            Biplate::<Reference>::universe_bi(&objective.expression)
86                .into_iter()
87                .map(|reference| {
88                    let name = reference.name();
89                    canonical_resolution_name(&name).clone()
90                }),
91        );
92    }
93
94    for clause in model.clauses() {
95        for literal in clause.iter() {
96            referenced_names.extend(Biplate::<Reference>::universe_bi(literal).into_iter().map(
97                |reference| {
98                    let name = reference.name();
99                    canonical_resolution_name(&name).clone()
100                },
101            ));
102        }
103    }
104
105    let unresolved: Vec<Name> = referenced_names
106        .difference(&declared_names)
107        .cloned()
108        .collect();
109
110    debug_assert!(
111        unresolved.is_empty(),
112        "Model from '{origin}' contains unresolved names: {unresolved:?}"
113    );
114}
115
116/// Debug-assert that all names referenced by expressions/domains resolve to declared symbols.
117#[cfg(not(debug_assertions))]
118pub fn debug_assert_all_names_resolved(_model: &Model, _origin: &str) {}
119
120#[cfg(debug_assertions)]
121fn canonical_resolution_name(name: &Name) -> &Name {
122    match name {
123        // Names wrapped in a selected representation still resolve through the source declaration.
124        Name::WithRepresentation(inner, _) => canonical_resolution_name(inner),
125        _ => name,
126    }
127}
128
129/// Debug-assert that there is exactly one `Root` expression, and it is the model's top-level root.
130#[cfg(debug_assertions)]
131pub fn debug_assert_root_at_top_level_only(model: &Model, origin: &str) {
132    debug_assert!(
133        matches!(model.root(), Expression::Root(_, _)),
134        "Model from '{origin}' does not have Root at top-level"
135    );
136
137    let root_count_in_main_tree = Biplate::<Expression>::universe_bi(model)
138        .iter()
139        .filter(|expr| matches!(expr, Expression::Root(_, _)))
140        .count();
141
142    let root_count_in_clauses = model
143        .clauses()
144        .iter()
145        .flat_map(|clause| clause.iter())
146        .map(|expr| {
147            Biplate::<Expression>::universe_bi(expr)
148                .iter()
149                .filter(|inner| matches!(inner, Expression::Root(_, _)))
150                .count()
151        })
152        .sum::<usize>();
153
154    let total_root_count = root_count_in_main_tree + root_count_in_clauses;
155    debug_assert_eq!(
156        total_root_count, 1,
157        "Model from '{origin}' should contain exactly one Root expression at top-level, found {total_root_count}"
158    );
159}
160
161/// Debug-assert that there is exactly one `Root` expression, and it is the model's top-level root.
162#[cfg(not(debug_assertions))]
163pub fn debug_assert_root_at_top_level_only(_model: &Model, _origin: &str) {}
164
165#[cfg(debug_assertions)]
166fn collect_reachable_symbol_tables(model: &Model) -> Vec<SymbolTablePtr> {
167    let mut pending_tables: Vec<SymbolTablePtr> = vec![model.symbols_ptr_unchecked().clone()];
168    pending_tables.extend(Biplate::<SymbolTablePtr>::universe_bi(model.root()));
169
170    if let Some(dominance) = &model.dominance {
171        pending_tables.extend(Biplate::<SymbolTablePtr>::universe_bi(dominance));
172    }
173
174    if let Some(objective) = &model.objective {
175        pending_tables.extend(Biplate::<SymbolTablePtr>::universe_bi(
176            &objective.expression,
177        ));
178    }
179
180    for clause in model.clauses() {
181        for literal in clause.iter() {
182            pending_tables.extend(Biplate::<SymbolTablePtr>::universe_bi(literal));
183        }
184    }
185
186    let mut seen_tables = HashSet::new();
187    let mut out = Vec::new();
188
189    while let Some(table_ptr) = pending_tables.pop() {
190        if !seen_tables.insert(table_ptr.id()) {
191            continue;
192        }
193
194        let parent = table_ptr.read().parent().clone();
195        if let Some(parent) = parent {
196            pending_tables.push(parent);
197        }
198
199        out.push(table_ptr);
200    }
201
202    out
203}