1
//! Compact's structural work estimate for Minion's collection representations.
2
//!
3
//! One primitive variable and one estimated expanded structural AST node each cost one unit.
4
//! Domain bits break ties. These are work counts, not fitted coefficients or predicted seconds.
5
//! Matrix/tuple children use their ordinary component layouts; unknown layouts, large probes
6
//! and unsupported domains decline the entire comparison, falling back to domain cardinality.
7
//! User constraints and search behaviour are deliberately not predicted by this estimate.
8

            
9
use conjure_cp::ast::comprehension::ComprehensionQualifier;
10
use conjure_cp::ast::{DeclarationPtr, DomainPtr, Expression, GroundDomain, Name};
11
use conjure_cp::representation::{ReprRulePtr, get_repr_rules};
12
use conjure_cp::settings::{Channelling, SolverFamily, channelling, try_current_solver_family};
13
use std::collections::HashMap;
14
use uniplate::Uniplate;
15

            
16
// Resource bounds, not scoring weights. Refuse lookahead rather than partially rank candidates.
17
const MAX_EXTENT: u64 = 256;
18
const MAX_DEPTH: usize = 8;
19
const MAX_VISITS: usize = 16_384;
20

            
21
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
22
struct Cost {
23
    variables: u64,
24
    structural_nodes: u64,
25
    domain_bits: u64,
26
}
27

            
28
impl Cost {
29
1861
    fn add(self, other: Self) -> Option<Self> {
30
        Some(Self {
31
1861
            variables: self.variables.checked_add(other.variables)?,
32
1861
            structural_nodes: self.structural_nodes.checked_add(other.structural_nodes)?,
33
1861
            domain_bits: self.domain_bits.checked_add(other.domain_bits)?,
34
        })
35
1861
    }
36

            
37
231
    fn times(self, count: u64) -> Option<Self> {
38
        Some(Self {
39
231
            variables: self.variables.checked_mul(count)?,
40
231
            structural_nodes: self.structural_nodes.checked_mul(count)?,
41
231
            domain_bits: self.domain_bits.checked_mul(count)?,
42
        })
43
231
    }
44

            
45
889
    fn key(self) -> (u64, u64) {
46
889
        (
47
889
            self.variables.saturating_add(self.structural_nodes),
48
889
            self.domain_bits,
49
889
        )
50
889
    }
51
}
52

            
53
25224
pub(super) fn choose(
54
25224
    declaration: &DeclarationPtr,
55
25224
    candidates: &[(ReprRulePtr, usize)],
56
25224
) -> Option<ReprRulePtr> {
57
25224
    if try_current_solver_family() != Some(SolverFamily::Minion) || channelling() != Channelling::No
58
    {
59
25091
        return None;
60
133
    }
61
133
    let domain = declaration.domain()?;
62
133
    if !(declaration.as_find().is_some() || declaration.is_find_auxiliary())
63
133
        || !is_collection(&domain)
64
    {
65
        return None;
66
133
    }
67
133
    let mut estimates = Vec::new();
68
133
    for &(rule, old_score) in candidates {
69
399
        let mut estimator = Estimator::default();
70
399
        let Some(cost) = estimator.representation(declaration, rule, 0) else {
71
            tracing::info!(declaration = %declaration.name(), representation = rule.name(),
72
                "compact structural cost unavailable; using domain cardinality for this choice");
73
            return None;
74
        };
75
399
        tracing::info!(declaration = %declaration.name(), representation = rule.name(),
76
126
            variables = cost.variables, structural_nodes = cost.structural_nodes,
77
126
            domain_bits = cost.domain_bits, units = cost.key().0, domain_score = old_score,
78
126
            "compact representation cost");
79
399
        estimates.push((rule, cost, old_score));
80
    }
81
399
    let &(selected, _, _) = estimates.iter().min_by_key(|(rule, cost, old_score)| {
82
399
        (
83
399
            cost.key(),
84
399
            rule.short_name() == "packed",
85
399
            *old_score,
86
399
            super::representation::representation_fallback_rank(*rule),
87
399
            rule.name(),
88
399
        )
89
399
    })?;
90
133
    tracing::info!(declaration = %declaration.name(), representation = selected.name(),
91
42
        "compact selected representation");
92
133
    Some(selected)
93
25224
}
94

            
95
231
fn is_collection(domain: &DomainPtr) -> bool {
96
231
    domain.resolve().is_ok_and(|domain| {
97
231
        matches!(
98
231
            domain.as_ref(),
99
            GroundDomain::Function(..) | GroundDomain::Set(..) | GroundDomain::Relation(..)
100
        )
101
231
    })
102
231
}
103

            
104
struct Estimator {
105
    remaining: usize,
106
    domains: HashMap<DomainPtr, Option<Cost>>,
107
}
108

            
109
impl Default for Estimator {
110
399
    fn default() -> Self {
111
399
        Self {
112
399
            remaining: MAX_VISITS,
113
399
            domains: HashMap::new(),
114
399
        }
115
399
    }
116
}
117

            
118
impl Estimator {
119
2771
    fn visit(&mut self, depth: usize) -> Option<()> {
120
2771
        if depth > MAX_DEPTH {
121
            return None;
122
2771
        }
123
2771
        self.remaining = self.remaining.checked_sub(1)?;
124
2771
        Some(())
125
2771
    }
126

            
127
679
    fn representation(
128
679
        &mut self,
129
679
        decl: &DeclarationPtr,
130
679
        rule: ReprRulePtr,
131
679
        depth: usize,
132
679
    ) -> Option<Cost> {
133
679
        self.visit(depth)?;
134
679
        let domain = decl.domain()?.resolve().ok()?;
135
        // Check physical extents before asking initialisers to allocate variables or constraints.
136
679
        bounded_extent(domain.as_ref())?;
137
679
        let probe = rule.probe_structure_for(decl).ok()?;
138
679
        let mut cost = Cost::default();
139
679
        for constraint in &probe.constraints {
140
2275
            cost.structural_nodes = cost
141
2275
                .structural_nodes
142
2275
                .checked_add(self.expression(constraint)?)?;
143
        }
144
679
        for child in probe.domains {
145
1763
            cost = cost.add(self.domain(&child, depth + 1)?)?;
146
        }
147
679
        Some(cost)
148
679
    }
149

            
150
2092
    fn domain(&mut self, domain: &DomainPtr, depth: usize) -> Option<Cost> {
151
2092
        self.visit(depth)?;
152
2092
        if let Some(cost) = self.domains.get(domain) {
153
1159
            return *cost;
154
933
        }
155
        // Also stops a representation that accidentally introduces its own domain again.
156
933
        self.domains.insert(domain.clone(), None);
157
933
        let ground = domain.resolve().ok()?;
158
933
        let cost = match ground.as_ref() {
159
            GroundDomain::Bool | GroundDomain::Int(..) => {
160
555
                let cardinality = ground.length().ok()?;
161
555
                Cost {
162
555
                    variables: 1,
163
555
                    structural_nodes: 0,
164
555
                    domain_bits: u64::from(
165
555
                        u64::BITS - cardinality.saturating_sub(1).leading_zeros(),
166
555
                    ),
167
555
                }
168
            }
169
            GroundDomain::Matrix(inner, indices, preference) => {
170
231
                if preference.as_deref().is_some_and(|p| p != "components") {
171
                    return None;
172
231
                }
173
280
                let cells = product_lengths(indices.iter().map(|d| d.as_ref()))?;
174
231
                if cells > MAX_EXTENT {
175
                    return None;
176
231
                }
177
231
                self.domain(&inner.clone().into(), depth + 1)?
178
231
                    .times(cells)?
179
            }
180
            GroundDomain::Tuple(fields, preference) => {
181
49
                if preference.as_deref().is_some_and(|p| p != "components") {
182
                    return None;
183
49
                }
184
49
                let mut cost = Cost::default();
185
49
                for field in fields {
186
98
                    cost = cost.add(self.domain(&field.clone().into(), depth + 1)?)?;
187
                }
188
49
                cost
189
            }
190
98
            _ if is_collection(domain) => {
191
                let decl =
192
98
                    DeclarationPtr::new_find(Name::user("__compact_cost_probe"), domain.clone());
193
98
                let preference = domain.representation_preference();
194
98
                let mut rules: Vec<_> = get_repr_rules()
195
3626
                    .filter(|rule| {
196
3626
                        !rule.is_integer_encoding()
197
2842
                            && rule.applies_to(SolverFamily::Minion)
198
2646
                            && preference.is_none_or(|pref| rule.short_name() == pref)
199
3626
                    })
200
                    .collect();
201
23520
                rules.sort_by_key(|rule| rule.name());
202
98
                let mut best: Option<Cost> = None;
203
98
                for rule in rules {
204
2646
                    if rule.probe_for(&decl).is_err() {
205
2366
                        continue;
206
280
                    }
207
                    // Do not silently treat an applicable but unestimated layout as expensive.
208
280
                    let candidate = self.representation(&decl, rule, depth + 1)?;
209
280
                    if best.is_none_or(|cost| candidate.key() < cost.key()) {
210
147
                        best = Some(candidate);
211
133
                    }
212
                }
213
98
                best?
214
            }
215
            _ => return None,
216
        };
217
933
        self.domains.insert(domain.clone(), Some(cost));
218
933
        Some(cost)
219
2092
    }
220

            
221
45279
    fn expression(&mut self, expression: &Expression) -> Option<u64> {
222
45279
        self.remaining = self.remaining.checked_sub(1)?;
223
45279
        if let Expression::Comprehension(_, comprehension) = expression {
224
140
            let mut iterations = 1u64;
225
140
            let mut body = self.expression(&comprehension.return_expression)?;
226
140
            for qualifier in &comprehension.qualifiers {
227
280
                match qualifier {
228
                    ComprehensionQualifier::Generator { ptr } => {
229
98
                        iterations = iterations.checked_mul(ptr.domain()?.length().ok()?)?;
230
                    }
231
                    ComprehensionQualifier::ExpressionGenerator { ptr } => {
232
182
                        let source = ptr.as_quantified_expr()?;
233
182
                        let ground = source.domain_of()?.resolve().ok()?;
234
182
                        iterations = iterations.checked_mul(collection_extent(ground.as_ref())?)?;
235
                    }
236
                    ComprehensionQualifier::Condition(condition) => {
237
                        body = body.checked_add(self.expression(condition)?)?;
238
                    }
239
                }
240
            }
241
            // Guards may prune iterations; this deliberately uses the full upper bound.
242
140
            return 1u64.checked_add(iterations.checked_mul(body)?);
243
45139
        }
244
45139
        expression.children().iter().try_fold(1u64, |nodes, child| {
245
42864
            nodes.checked_add(self.expression(child)?)
246
42864
        })
247
45279
    }
248
}
249

            
250
553
fn product_lengths<'a>(mut domains: impl Iterator<Item = &'a GroundDomain>) -> Option<u64> {
251
924
    domains.try_fold(1u64, |product, domain| {
252
924
        product.checked_mul(domain.length().ok()?)
253
924
    })
254
553
}
255

            
256
182
fn collection_extent(domain: &GroundDomain) -> Option<u64> {
257
182
    let extent = match domain {
258
        GroundDomain::Matrix(_, indices, _) => product_lengths(indices.iter().map(|d| d.as_ref()))?,
259
        GroundDomain::Set(attributes, inner) => attributes
260
            .size
261
            .high()
262
            .copied()
263
            .map(u64::try_from)
264
            .transpose()
265
            .ok()?
266
            .unwrap_or(inner.length().ok()?)
267
            .min(inner.length().ok()?),
268
        GroundDomain::Relation(attributes, fields) => {
269
364
            let universe = product_lengths(fields.iter().map(|d| d.as_ref()))?;
270
182
            attributes
271
182
                .size
272
182
                .high()
273
182
                .copied()
274
182
                .map(u64::try_from)
275
182
                .transpose()
276
182
                .ok()?
277
182
                .unwrap_or(universe)
278
182
                .min(universe)
279
        }
280
        _ => return None,
281
    };
282
182
    (extent <= MAX_EXTENT).then_some(extent)
283
182
}
284

            
285
679
fn bounded_extent(domain: &GroundDomain) -> Option<()> {
286
679
    let extent = match domain {
287
        GroundDomain::Function(_, keys, values) => {
288
147
            keys.length().ok()?.checked_mul(values.length().ok()?)?
289
        }
290
392
        GroundDomain::Set(_, inner) => inner.length().ok()?,
291
280
        GroundDomain::Relation(_, fields) => product_lengths(fields.iter().map(|d| d.as_ref()))?,
292
        _ => return None,
293
    };
294
679
    (extent <= MAX_EXTENT).then_some(())
295
679
}