1
//! Comprehension expansion rules
2

            
3
mod expand_native;
4
mod expand_via_solver;
5
mod expand_via_solver_ac;
6
mod via_solver_common;
7

            
8
pub use expand_native::expand_native;
9
pub use expand_via_solver::expand_via_solver;
10
pub use expand_via_solver_ac::expand_via_solver_ac;
11

            
12
use conjure_cp::{
13
    ast::{
14
        DeclarationPtr, Domain, DomainPtr, Expression as Expr, IntVal, Metadata, Moo, Name, Range,
15
        Reference, SymbolTable, UnresolvedDomain,
16
        ac_operators::ACOperatorKind,
17
        comprehension::{Comprehension, ComprehensionQualifier},
18
        eval_constant,
19
        serde::{HasId, ObjId},
20
    },
21
    bug, into_matrix_expr,
22
    rule_engine::{
23
        ApplicationError::RuleNotApplicable, ApplicationResult, RuleEffect, register_rule,
24
    },
25
    settings::{QuantifiedExpander, comprehension_expander},
26
};
27
use std::collections::{BTreeSet, HashMap};
28
use tracing::debug;
29
use uniplate::{Biplate, Uniplate};
30

            
31
use via_solver_common::simplify_expression;
32

            
33
/// Candidate assignments below this limit are cheaper to enumerate natively. The Pythagorean
34
/// benchmark brackets the crossover between 125 assignments (native wins) and 1,000 assignments
35
/// (solver-backed expansion wins decisively), so use a deliberately coarse power-of-two boundary.
36
const AUTO_SOLVER_THRESHOLD: u64 = 512;
37

            
38
/// Resolve `auto` to one of the concrete expansion implementations for this comprehension.
39
223485
fn selected_comprehension_expander(comprehension: &Comprehension) -> QuantifiedExpander {
40
223485
    match comprehension_expander() {
41
223458
        QuantifiedExpander::Auto => auto_comprehension_expander(comprehension),
42
27
        explicit => explicit,
43
    }
44
223485
}
45

            
46
/// Choose native expansion unless the candidate space is large and the temporary solver model
47
/// has a constraint that can potentially discard quantified assignments.
48
223465
fn auto_comprehension_expander(comprehension: &Comprehension) -> QuantifiedExpander {
49
223465
    let Some(candidate_assignments) = estimated_candidate_assignments(comprehension) else {
50
4257
        if has_dependent_generators_and_nested_return(comprehension) {
51
498
            return QuantifiedExpander::ViaSolverAc;
52
3759
        }
53
3759
        return QuantifiedExpander::Native;
54
    };
55

            
56
219208
    if candidate_assignments < AUTO_SOLVER_THRESHOLD {
57
219071
        return QuantifiedExpander::Native;
58
137
    }
59

            
60
137
    let useful_guard = has_useful_quantified_guard(comprehension);
61
137
    let useful_ac_pruning = match comprehension.skip_operator {
62
        Some(
63
            ACOperatorKind::And
64
            | ACOperatorKind::Or
65
            | ACOperatorKind::Sum
66
            | ACOperatorKind::Product,
67
137
        ) => has_cheap_ac_return_pruning(comprehension),
68
        _ => false,
69
    };
70

            
71
137
    if useful_guard || useful_ac_pruning {
72
23
        QuantifiedExpander::ViaSolverAc
73
    } else {
74
114
        QuantifiedExpander::Native
75
    }
76
223465
}
77

            
78
/// Estimate the maximum number of generator assignments before guards are applied.
79
///
80
/// Dependent or otherwise unresolved domains deliberately return `None`: native expansion already
81
/// handles them one binding at a time, whereas guessing a solver-backed path risks changing which
82
/// models are supported. Multiplication saturates because only comparison with the threshold is
83
/// relevant.
84
223465
fn estimated_candidate_assignments(comprehension: &Comprehension) -> Option<u64> {
85
223465
    let mut assignments = 1u64;
86

            
87
223465
    for qualifier in &comprehension.qualifiers {
88
454209
        match qualifier {
89
            ComprehensionQualifier::Generator { ptr } => {
90
355169
                let domain = ptr.domain()?;
91
355169
                let length = domain.resolve().ok()?.length().ok()?;
92
350912
                assignments = assignments.saturating_mul(length);
93
            }
94
            ComprehensionQualifier::ExpressionGenerator { .. } => return None,
95
99040
            ComprehensionQualifier::Condition(_) => {}
96
        }
97
    }
98

            
99
219208
    Some(assignments)
100
223465
}
101

            
102
/// Detect the supported dependent-generator shape where native expansion repeatedly duplicates a
103
/// nested comprehension. The solver expander binds the outer assignment first and then expands the
104
/// nested return, which is substantially cheaper for models such as car sequencing.
105
///
106
/// This remains a narrow syntactic check: expression generators and missing domains stay native.
107
4257
fn has_dependent_generators_and_nested_return(comprehension: &Comprehension) -> bool {
108
4257
    if !matches!(
109
4257
        comprehension.skip_operator,
110
4257
        Some(
111
4257
            ACOperatorKind::And
112
4257
                | ACOperatorKind::Or
113
4257
                | ACOperatorKind::Sum
114
4257
                | ACOperatorKind::Product
115
4257
        )
116
4257
    ) {
117
        return false;
118
4257
    }
119

            
120
4257
    let mut generator_count = 0;
121
4257
    for qualifier in &comprehension.qualifiers {
122
8514
        match qualifier {
123
8514
            ComprehensionQualifier::Generator { ptr } if ptr.domain().is_some() => {
124
8514
                generator_count += 1;
125
            }
126
            ComprehensionQualifier::Generator { .. }
127
            | ComprehensionQualifier::ExpressionGenerator { .. } => return false,
128
            ComprehensionQualifier::Condition(_) => {}
129
        }
130
    }
131

            
132
4257
    generator_count >= 2
133
4257
        && comprehension
134
4257
            .return_expression
135
20490
            .any_expression(|expression| matches!(expression, Expr::Comprehension(_, _)))
136
4257
}
137

            
138
/// A useful guard is a nonconstant condition whose remaining references include at least one
139
/// quantified variable and no non-quantified decision variable.
140
137
fn has_useful_quantified_guard(comprehension: &Comprehension) -> bool {
141
137
    let quantified_vars: BTreeSet<_> = comprehension.quantified_vars().into_iter().collect();
142

            
143
600
    comprehension.qualifiers.iter().any(|qualifier| {
144
600
        let ComprehensionQualifier::Condition(condition) = qualifier else {
145
389
            return false;
146
        };
147

            
148
211
        comprehension.is_quantified_guard(condition)
149
1
            && Biplate::<Name>::universe_bi(condition)
150
1
                .iter()
151
1
                .any(|name| quantified_vars.contains(name))
152
600
    })
153
137
}
154

            
155
/// Cheaply detect return-expression pruning that is likely to repay solver setup.
156
///
157
/// A fully quantified return can be passed to the temporary solver without dummy variables. For
158
/// mixed returns, require a quantified-only conjunction: this recognises selective filters such as
159
/// the Pythagorean antecedent while rejecting single weak comparisons and pairwise representation
160
/// implications. This is deliberately syntactic; `auto` must not build or simplify a temporary
161
/// model merely to decide whether to build one.
162
137
fn has_cheap_ac_return_pruning(comprehension: &Comprehension) -> bool {
163
137
    let quantified_vars: BTreeSet<_> = comprehension.quantified_vars().into_iter().collect();
164
137
    let return_expression = &comprehension.return_expression;
165

            
166
137
    if !comprehension_has_non_quantified_guards(comprehension)
167
32
        && expression_depends_only_on_quantified_decisions(
168
32
            comprehension,
169
32
            return_expression,
170
32
            &quantified_vars,
171
32
        )
172
    {
173
        return true;
174
137
    }
175

            
176
137
    has_quantified_conjunction_in_pruning_position(
177
137
        comprehension,
178
137
        return_expression,
179
137
        &quantified_vars,
180
137
    )
181
137
}
182

            
183
/// Look only where an `and` can filter AC identities: at the return root or in an implication's
184
/// antecedent. A conjunction in the consequent describes the emitted constraint; treating it as a
185
/// filter made battleship-style returns pay for solver expansion without useful pruning.
186
137
fn has_quantified_conjunction_in_pruning_position(
187
137
    comprehension: &Comprehension,
188
137
    expression: &Expr,
189
137
    quantified_vars: &BTreeSet<Name>,
190
137
) -> bool {
191
137
    match expression {
192
        Expr::And(_, terms) => {
193
            if expression_depends_only_on_quantified_decisions(
194
                comprehension,
195
                expression,
196
                quantified_vars,
197
            ) {
198
                return true;
199
            }
200

            
201
            terms.unwrap_list_cow().is_some_and(|terms| {
202
                terms.iter().any(|term| {
203
                    has_quantified_conjunction_in_pruning_position(
204
                        comprehension,
205
                        term,
206
                        quantified_vars,
207
                    )
208
                })
209
            })
210
        }
211
847
        Expr::Imply(_, antecedent, _) => antecedent.any_expression(|subexpression| {
212
847
            matches!(subexpression, Expr::And(_, _))
213
22
                && expression_depends_only_on_quantified_decisions(
214
22
                    comprehension,
215
22
                    subexpression,
216
22
                    quantified_vars,
217
22
                )
218
847
        }),
219
8
        _ => false,
220
    }
221
137
}
222

            
223
54
fn expression_depends_only_on_quantified_decisions(
224
54
    comprehension: &Comprehension,
225
54
    expression: &Expr,
226
54
    quantified_vars: &BTreeSet<Name>,
227
54
) -> bool {
228
54
    let referenced_names = Biplate::<Name>::universe_bi(expression);
229
54
    referenced_names
230
54
        .iter()
231
60
        .any(|name| quantified_vars.contains(name))
232
52
        && comprehension.is_quantified_guard(expression)
233
54
}
234

            
235
/// Simplifies an expanded comprehension under its AC operator before the rewrite is committed.
236
///
237
/// This folds identities and dominates constant bounds (via [`simplify_expression`] / partial
238
/// evaluation) so huge `and([x > k_i, ...])` expansions collapse before rule-trace snapshots and
239
/// later rewriting see them.
240
55614
fn simplify_expanded_ac_results(results: Vec<Expr>, ac_operator: ACOperatorKind) -> Vec<Expr> {
241
55614
    let simplified = simplify_expression(ac_operator.as_expression(into_matrix_expr!(results)));
242
55614
    match (&ac_operator, &simplified) {
243
        (ACOperatorKind::And, Expr::And(_, matrix))
244
        | (ACOperatorKind::Or, Expr::Or(_, matrix))
245
        | (ACOperatorKind::Sum, Expr::Sum(_, matrix))
246
        | (ACOperatorKind::Product, Expr::Product(_, matrix))
247
        | (ACOperatorKind::Min, Expr::Min(_, matrix))
248
44895
        | (ACOperatorKind::Max, Expr::Max(_, matrix)) => matrix
249
44895
            .unwrap_list_cow()
250
44895
            .map(std::borrow::Cow::into_owned)
251
            .unwrap_or_else(|| vec![simplified]),
252
10719
        _ => vec![simplified],
253
    }
254
55614
}
255

            
256
/// Rewrite top-level `exists` comprehensions into constraints over fresh auxiliary `find`s.
257
///
258
/// `exists` is represented as `or([comprehension])`. The quantified variables become
259
/// [`DeclarationKind::FindAuxiliary`] entries so Minion does not branch on them.
260
#[register_rule("Base", 2003, [Root])]
261
442897
fn exists_quantified_to_finds(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
262
442897
    let Expr::Root(metadata, constraints) = expr else {
263
889
        return Err(RuleNotApplicable);
264
    };
265

            
266
442008
    let mut new_symbols = symbols.clone();
267
442008
    let mut new_constraints = Vec::with_capacity(constraints.len());
268
442008
    let mut changed = false;
269

            
270
442008
    for constraint in constraints {
271
3407445
        let Some(comprehension) = as_exists_comprehension(constraint) else {
272
3396966
            new_constraints.push(constraint.clone());
273
3396966
            continue;
274
        };
275

            
276
        let Some(new_constraints_for_exists) =
277
10479
            rewrite_exists_comprehension_to_constraints(&comprehension, &mut new_symbols)
278
        else {
279
            new_constraints.push(constraint.clone());
280
            continue;
281
        };
282

            
283
10479
        new_constraints.extend(new_constraints_for_exists);
284
10479
        changed = true;
285
    }
286

            
287
442008
    if changed {
288
10479
        Ok(RuleEffect::with_symbols(
289
10479
            Expr::Root(metadata.clone(), new_constraints),
290
10479
            new_symbols,
291
10479
        ))
292
    } else {
293
431529
        Err(RuleNotApplicable)
294
    }
295
442897
}
296

            
297
/// Expand comprehensions inside AC operators using `--comprehension-expander native`.
298
///
299
/// Guarded comprehensions with symbolic guards are expanded here (not by
300
/// [`expand_comprehension_native`]) so matrix representation can run first.
301
/// Skip semantics come from [`Comprehension::skip_operator`].
302
#[register_rule("Base", 1999, [And, Or, Sum, Product])]
303
190692
fn expand_ac_comprehension_native(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
304
190692
    let ac_operator_kind = expr.to_ac_operator_kind().ok_or(RuleNotApplicable)?;
305
189867
    let children = expr.children();
306
189867
    debug_assert_eq!(
307
189867
        children.len(),
308
189867
        1,
309
        "AC expressions should have exactly one child."
310
    );
311

            
312
189867
    let comprehension = children
313
189867
        .front()
314
189867
        .and_then(as_single_comprehension)
315
189867
        .ok_or(RuleNotApplicable)?;
316

            
317
    if selected_comprehension_expander(&comprehension) != QuantifiedExpander::Native {
318
        return Err(RuleNotApplicable);
319
    }
320

            
321
    if comprehension.skip_operator.is_none() {
322
        return Err(RuleNotApplicable);
323
    }
324

            
325
    if !comprehension_has_symbolic_guards(&comprehension) {
326
        return Err(RuleNotApplicable);
327
    }
328

            
329
    for qual in &comprehension.qualifiers {
330
        if let ComprehensionQualifier::ExpressionGenerator { .. } = qual {
331
            return Err(RuleNotApplicable);
332
        }
333
    }
334

            
335
    let mut symbols = symbols.clone();
336
    let results = expand_native(comprehension, &mut symbols)
337
        .unwrap_or_else(|e| bug!("native AC comprehension expansion failed: {e}"));
338
    let results = simplify_expanded_ac_results(results, ac_operator_kind);
339

            
340
    Ok(RuleEffect::with_symbols(
341
        ac_operator_kind.as_expression(into_matrix_expr!(results)),
342
        symbols,
343
    ))
344
190692
}
345

            
346
/// Expand comprehensions using `--comprehension-expander native`.
347
///
348
/// Does not unroll expression generators (`i <- expr`): quantification over decision variables
349
/// is handled by representation-specific vertical rules, not by enumerating values here.
350
/// Constant quantifier collections are constant-folded via [`eval_constant`] instead.
351
#[register_rule("Base", 2000, [Comprehension])]
352
59077
fn expand_comprehension_native(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
353
59077
    let Expr::Comprehension(_, comprehension) = expr else {
354
929
        return Err(RuleNotApplicable);
355
    };
356

            
357
58148
    let comprehension = comprehension.as_ref().clone();
358

            
359
58148
    if selected_comprehension_expander(&comprehension) != QuantifiedExpander::Native {
360
        return Err(RuleNotApplicable);
361
58148
    }
362

            
363
58148
    for qual in &comprehension.qualifiers {
364
122688
        if let ComprehensionQualifier::ExpressionGenerator { .. } = qual {
365
            return Err(RuleNotApplicable);
366
122688
        }
367
    }
368

            
369
58148
    let mut symbols = symbols.clone();
370
58148
    let comprehension_domain = comprehension.domain_of();
371
58148
    let skip_operator = comprehension.skip_operator;
372
58148
    let results = expand_native(comprehension, &mut symbols).or(Err(RuleNotApplicable))?;
373
58148
    let results = match skip_operator {
374
55614
        Some(op) => simplify_expanded_ac_results(results, op),
375
2534
        None => results,
376
    };
377
58148
    let expanded = into_matrix_expr!(results);
378
58148
    let expanded = match comprehension_domain {
379
58120
        Some(domain) if expanded.list_len() == Some(0) => {
380
            Expr::DomainAnnotation(Metadata::new(), Moo::new(expanded), domain)
381
        }
382
58148
        _ => expanded,
383
    };
384
58148
    Ok(RuleEffect::with_symbols(expanded, symbols))
385
59077
}
386

            
387
/// Expand comprehensions using `--comprehension-expander via-solver`.
388
///
389
/// Algorithm sketch:
390
/// 1. Match one comprehension node.
391
/// 2. Build a temporary generator submodel from its qualifiers/guards.
392
/// 3. Materialise quantified declarations as temporary `find` declarations.
393
/// 4. Wrap that submodel as a standalone temporary model, with search order restricted to the
394
///    quantified names.
395
/// 5. Rewrite the temporary model using the configured rewriter and Minion-oriented rules.
396
/// 6. Solve the rewritten temporary model with Minion and keep only quantified assignments from
397
///    each solution.
398
/// 7. Instantiate the original return expression under each quantified assignment.
399
/// 8. Replace the comprehension by a matrix literal containing all instantiated return values.
400
#[register_rule("Base", 2000, [Comprehension])]
401
59077
fn expand_comprehension_via_solver(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
402
59077
    let Expr::Comprehension(_, comprehension) = expr else {
403
929
        return Err(RuleNotApplicable);
404
    };
405

            
406
58148
    let comprehension = comprehension.as_ref().clone();
407

            
408
58148
    if !matches!(
409
58148
        selected_comprehension_expander(&comprehension),
410
58148
        QuantifiedExpander::ViaSolver | QuantifiedExpander::ViaSolverAc
411
58148
    ) {
412
58148
        return Err(RuleNotApplicable);
413
    }
414

            
415
    for qual in &comprehension.qualifiers {
416
        if let ComprehensionQualifier::ExpressionGenerator { .. } = qual {
417
            return Err(RuleNotApplicable);
418
        }
419
    }
420

            
421
    // A guard over non-quantified decision variables can only be re-attached to the expanded
422
    // elements through an enclosing AC operator's skip semantics. Without one there is nothing to
423
    // attach it to, so leave the comprehension alone (the native expander bails here too).
424
    if comprehension.skip_operator.is_none()
425
        && comprehension_has_non_quantified_guards(&comprehension)
426
    {
427
        return Err(RuleNotApplicable);
428
    }
429

            
430
    // A generator model the solver cannot load means this expander cannot do this comprehension
431
    // -- a guard over a type Minion has no encoding for, say -- not that something has gone
432
    // wrong. Expand it natively instead: leaving it alone would strand the comprehension, since
433
    // the native rule only fires for comprehensions `auto` chose it for.
434
    let results = match expand_via_solver(comprehension.clone()) {
435
        Ok(results) => results,
436
        Err(e) => {
437
            debug!(why=%e, "via-solver comprehension expansion failed, falling back to native expansion");
438
            let mut symbols = symbols.clone();
439
            let results = expand_native(comprehension, &mut symbols).or(Err(RuleNotApplicable))?;
440
            return Ok(RuleEffect::with_symbols(
441
                into_matrix_expr!(results),
442
                symbols,
443
            ));
444
        }
445
    };
446
    Ok(RuleEffect::with_symbols(
447
        into_matrix_expr!(results),
448
        symbols.clone(),
449
    ))
450
59077
}
451

            
452
/// Expand comprehensions inside AC operators using `--comprehension-expander via-solver-ac`.
453
///
454
/// Algorithm sketch:
455
/// 1. Match an AC operator whose single child is a comprehension.
456
/// 2. Build a temporary generator submodel from the comprehension qualifiers/guards.
457
/// 3. Add a derived constraint from the return expression to this generator model:
458
///    localise non-local references, and replace non-quantified fragments with dummy variables so
459
///    the constraint depends only on locally solvable symbols.
460
/// 4. Materialise quantified declarations as temporary `find` declarations in the temporary model.
461
/// 5. Rewrite and solve the temporary model with Minion; keep only quantified assignments.
462
/// 6. Instantiate the original return expression under those assignments.
463
/// 7. Rebuild the same AC operator around the instantiated matrix literal.
464
#[register_rule("Base", 2002, [And, Or, Sum, Product])]
465
308695
fn expand_comprehension_via_solver_ac(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
466
    // Is this an ac expression?
467
308695
    let ac_operator_kind = expr.to_ac_operator_kind().ok_or(RuleNotApplicable)?;
468

            
469
307864
    let children = expr.children();
470
307864
    debug_assert_eq!(
471
307864
        children.len(),
472
307864
        1,
473
        "AC expressions should have exactly one child."
474
    );
475

            
476
307864
    let comprehension = children
477
307864
        .front()
478
307864
        .and_then(as_single_comprehension)
479
307864
        .ok_or(RuleNotApplicable)?;
480

            
481
107189
    if selected_comprehension_expander(&comprehension) != QuantifiedExpander::ViaSolverAc {
482
106665
        return Err(RuleNotApplicable);
483
524
    }
484

            
485
524
    for qual in &comprehension.qualifiers {
486
1138
        if let ComprehensionQualifier::ExpressionGenerator { .. } = qual {
487
            return Err(RuleNotApplicable);
488
1138
        }
489
    }
490

            
491
524
    let results = match expand_via_solver_ac(comprehension.clone(), ac_operator_kind) {
492
524
        Ok(results) => results,
493
        // As in `expand_comprehension_via_solver`: fall back to native expansion rather than
494
        // stranding a comprehension this expander cannot build a solvable generator model for.
495
        Err(e) => {
496
            debug!(why=%e, "via-solver-ac comprehension expansion failed, falling back to native expansion");
497
            let mut symbols = symbols.clone();
498
            let results = expand_native(comprehension, &mut symbols).or(Err(RuleNotApplicable))?;
499
            let results = simplify_expanded_ac_results(results, ac_operator_kind);
500
            let new_expr = ac_operator_kind.as_expression(into_matrix_expr!(results));
501
            return Ok(RuleEffect::with_symbols(new_expr, symbols));
502
        }
503
    };
504

            
505
524
    let new_expr = ac_operator_kind.as_expression(into_matrix_expr!(results));
506
524
    Ok(RuleEffect::with_symbols(new_expr, symbols.clone()))
507
308695
}
508

            
509
531322
fn as_single_comprehension(expr: &Expr) -> Option<Comprehension> {
510
531322
    if let Expr::Comprehension(_, comprehension) = expr {
511
117668
        return Some(comprehension.as_ref().clone());
512
413654
    }
513

            
514
    // Borrow the elements: this runs on every `Or` the rewriter visits, and copying the list
515
    // first made the test cost the whole matrix on a wide disjunction.
516
413654
    let [Expr::Comprehension(_, comprehension)] = expr.unwrap_list_ref()? else {
517
406614
        return None;
518
    };
519

            
520
    Some(comprehension.as_ref().clone())
521
531322
}
522

            
523
3407445
fn as_exists_comprehension(expr: &Expr) -> Option<Comprehension> {
524
3407445
    let Expr::Or(_, or_child) = expr else {
525
3373854
        return None;
526
    };
527

            
528
33591
    as_single_comprehension(or_child.as_ref())
529
3407445
}
530

            
531
/// True iff some guard references decision variables the comprehension does not quantify over.
532
///
533
/// Such a guard cannot be evaluated during expansion, however the quantified variables are bound.
534
137
fn comprehension_has_non_quantified_guards(comprehension: &Comprehension) -> bool {
535
369
    comprehension.qualifiers.iter().any(|qualifier| {
536
369
        let ComprehensionQualifier::Condition(condition) = qualifier else {
537
263
            return false;
538
        };
539
106
        !comprehension.is_quantified_guard(condition)
540
369
    })
541
137
}
542

            
543
fn comprehension_has_symbolic_guards(comprehension: &Comprehension) -> bool {
544
    comprehension.qualifiers.iter().any(|qualifier| {
545
        let ComprehensionQualifier::Condition(condition) = qualifier else {
546
            return false;
547
        };
548
        eval_constant(&simplify_expression(condition.clone())).is_none()
549
    })
550
}
551

            
552
10479
fn rewrite_exists_comprehension_to_constraints(
553
10479
    comprehension: &Comprehension,
554
10479
    symbols: &mut SymbolTable,
555
10479
) -> Option<Vec<Expr>> {
556
10479
    let quantified_declarations = quantified_declarations(comprehension)?;
557

            
558
10479
    let mut replacements_by_id: HashMap<ObjId, DeclarationPtr> = HashMap::new();
559
10479
    let mut replacements_by_name: HashMap<Name, DeclarationPtr> = HashMap::new();
560

            
561
10479
    for decl in quantified_declarations {
562
11501
        let domain = decl.domain()?;
563
        let rewritten_domain =
564
11501
            replace_declaration_ptrs_in_domain(domain, &replacements_by_id, &replacements_by_name);
565
11501
        let fresh_decl = symbols.gen_find_auxiliary(&rewritten_domain);
566
11501
        replacements_by_id.insert(decl.id(), fresh_decl.clone());
567
11501
        replacements_by_name.insert(decl.name().clone(), fresh_decl);
568
    }
569

            
570
10479
    let mut conjuncts = Vec::new();
571
10479
    for qualifier in &comprehension.qualifiers {
572
11501
        if let ComprehensionQualifier::Condition(condition) = qualifier {
573
            conjuncts.push(replace_declaration_ptrs_in_expr(
574
                condition.clone(),
575
                &replacements_by_id,
576
                &replacements_by_name,
577
            ));
578
11501
        }
579
    }
580
10479
    conjuncts.push(replace_declaration_ptrs_in_expr(
581
10479
        comprehension.return_expression.clone(),
582
10479
        &replacements_by_id,
583
10479
        &replacements_by_name,
584
10479
    ));
585

            
586
10479
    Some(conjuncts)
587
10479
}
588

            
589
10479
fn quantified_declarations(comprehension: &Comprehension) -> Option<Vec<DeclarationPtr>> {
590
10479
    let quantified_names = comprehension.quantified_vars();
591
10479
    let symbols = comprehension.symbols();
592
10479
    quantified_names
593
10479
        .into_iter()
594
11501
        .map(|name| symbols.lookup_local(&name))
595
        .collect()
596
10479
}
597

            
598
11503
fn replace_declaration_ptrs_in_expr(
599
11503
    expr: Expr,
600
11503
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
601
11503
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
602
11503
) -> Expr {
603
11503
    let rewritten = expr
604
32992
        .transform_bi(&|decl: DeclarationPtr| {
605
32992
            if let Some(replacement) = replacements_by_id.get(&decl.id()) {
606
13567
                return replacement.clone();
607
19425
            }
608

            
609
19425
            let name = decl.name().clone();
610
19425
            replacements_by_name.get(&name).cloned().unwrap_or(decl)
611
32992
        })
612
25089
        .transform_bi(&|reference: Reference| {
613
25089
            replace_reference(reference, replacements_by_id, replacements_by_name)
614
25089
        });
615

            
616
11503
    rewritten.transform_bi(&|mut comprehension: Comprehension| {
617
1
        let shadowed_names = comprehension.quantified_vars();
618
1
        let visible_replacements_by_name = replacements_by_name
619
1
            .iter()
620
1
            .filter(|(name, _)| !shadowed_names.contains(name))
621
1
            .map(|(name, declaration)| (name.clone(), declaration.clone()))
622
            .collect();
623

            
624
1
        comprehension.return_expression = replace_declaration_ptrs_in_expr(
625
1
            comprehension.return_expression,
626
1
            replacements_by_id,
627
1
            &visible_replacements_by_name,
628
1
        );
629
1
        for qualifier in &mut comprehension.qualifiers {
630
            if let ComprehensionQualifier::Condition(condition) = qualifier {
631
                *condition = replace_declaration_ptrs_in_expr(
632
                    condition.clone(),
633
                    replacements_by_id,
634
                    &visible_replacements_by_name,
635
                );
636
            }
637
        }
638

            
639
1
        comprehension
640
1
    })
641
11503
}
642

            
643
11501
fn replace_declaration_ptrs_in_domain(
644
11501
    domain: DomainPtr,
645
11501
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
646
11501
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
647
11501
) -> DomainPtr {
648
11501
    let mut rewritten = domain
649
        .transform_bi(&|expr: Expr| {
650
            replace_declaration_ptrs_in_expr(expr, replacements_by_id, replacements_by_name)
651
        })
652
        .transform_bi(&|reference: Reference| {
653
            replace_reference(reference, replacements_by_id, replacements_by_name)
654
        });
655

            
656
    // `Range<T>` does not participate in the generic biplate traversal, so recurse through
657
    // unresolved domain structure once to rewrite symbolic integer bounds.
658
11501
    rewrite_int_ranges_in_domain_ptr(&mut rewritten, replacements_by_id, replacements_by_name);
659

            
660
11501
    rewritten
661
11501
}
662

            
663
26132
fn replace_reference(
664
26132
    reference: Reference,
665
26132
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
666
26132
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
667
26132
) -> Reference {
668
26132
    let replacement = replacements_by_id
669
26132
        .get(&reference.ptr().id())
670
26132
        .cloned()
671
26132
        .or_else(|| {
672
26132
            let name = reference.name().clone();
673
26132
            replacements_by_name.get(&name).cloned()
674
26132
        });
675

            
676
26132
    replacement.map(Reference::new).unwrap_or(reference)
677
26132
}
678

            
679
11501
fn rewrite_int_ranges_in_domain_ptr(
680
11501
    domain: &mut DomainPtr,
681
11501
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
682
11501
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
683
11501
) {
684
11501
    let mut rewritten = domain.as_ref().clone();
685
11501
    rewrite_int_ranges_in_domain(&mut rewritten, replacements_by_id, replacements_by_name);
686
11501
    *domain = Moo::new(rewritten);
687
11501
}
688

            
689
11501
fn rewrite_int_ranges_in_domain(
690
11501
    domain: &mut Domain,
691
11501
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
692
11501
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
693
11501
) {
694
11501
    let Domain::Unresolved(unresolved) = domain else {
695
9947
        return;
696
    };
697

            
698
1554
    rewrite_int_ranges_in_unresolved_domain(
699
1554
        Moo::make_mut(unresolved),
700
1554
        replacements_by_id,
701
1554
        replacements_by_name,
702
1554
    );
703
11501
}
704

            
705
1554
fn rewrite_int_ranges_in_unresolved_domain(
706
1554
    unresolved: &mut UnresolvedDomain,
707
1554
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
708
1554
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
709
1554
) {
710
1554
    match unresolved {
711
        UnresolvedDomain::Int(ranges, _) => {
712
1554
            for range in ranges {
713
1554
                rewrite_int_range(range, replacements_by_id, replacements_by_name);
714
            }
715
        }
716
        // The collection is an ordinary expression, so quantified references inside it are
717
        // rewritten by the same walk that handles the rest of the comprehension body.
718
        UnresolvedDomain::IntFromValues(_) => {}
719
        UnresolvedDomain::Tuple(inner_domains, _) => {
720
            for inner_domain in inner_domains {
721
                rewrite_int_ranges_in_domain_ptr(
722
                    inner_domain,
723
                    replacements_by_id,
724
                    replacements_by_name,
725
                );
726
            }
727
        }
728
        UnresolvedDomain::Record(entries, _) => {
729
            for entry in entries {
730
                rewrite_int_ranges_in_domain_ptr(
731
                    &mut entry.value,
732
                    replacements_by_id,
733
                    replacements_by_name,
734
                );
735
            }
736
        }
737
        UnresolvedDomain::Variant(entries, _) => {
738
            for entry in entries {
739
                rewrite_int_ranges_in_domain_ptr(
740
                    &mut entry.value,
741
                    replacements_by_id,
742
                    replacements_by_name,
743
                );
744
            }
745
        }
746
        UnresolvedDomain::Matrix(inner, index_domains, _) => {
747
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
748
            for index_domain in index_domains {
749
                rewrite_int_ranges_in_domain_ptr(
750
                    index_domain,
751
                    replacements_by_id,
752
                    replacements_by_name,
753
                );
754
            }
755
        }
756
        UnresolvedDomain::Sequence(attr, inner) => {
757
            rewrite_int_range(&mut attr.size, replacements_by_id, replacements_by_name);
758
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
759
        }
760
        UnresolvedDomain::Set(attr, inner) => {
761
            rewrite_int_range(&mut attr.size, replacements_by_id, replacements_by_name);
762
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
763
        }
764
        UnresolvedDomain::MSet(attr, inner) => {
765
            rewrite_int_range(&mut attr.size, replacements_by_id, replacements_by_name);
766
            rewrite_int_range(
767
                &mut attr.occurrence,
768
                replacements_by_id,
769
                replacements_by_name,
770
            );
771
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
772
        }
773
        UnresolvedDomain::Function(attr, domain, codomain) => {
774
            rewrite_int_range(&mut attr.size, replacements_by_id, replacements_by_name);
775
            rewrite_int_ranges_in_domain_ptr(domain, replacements_by_id, replacements_by_name);
776
            rewrite_int_ranges_in_domain_ptr(codomain, replacements_by_id, replacements_by_name);
777
        }
778
        UnresolvedDomain::Relation(attr, domains) => {
779
            rewrite_int_range(&mut attr.size, replacements_by_id, replacements_by_name);
780
            for domain in domains {
781
                rewrite_int_ranges_in_domain_ptr(domain, replacements_by_id, replacements_by_name);
782
            }
783
        }
784
        UnresolvedDomain::Partition(attr, inner) => {
785
            rewrite_int_range(
786
                &mut attr.num_parts,
787
                replacements_by_id,
788
                replacements_by_name,
789
            );
790
            rewrite_int_range(&mut attr.part_len, replacements_by_id, replacements_by_name);
791
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
792
        }
793
        UnresolvedDomain::Permutation(attr, inner) => {
794
            rewrite_int_range(
795
                &mut attr.num_moved,
796
                replacements_by_id,
797
                replacements_by_name,
798
            );
799
            rewrite_int_ranges_in_domain_ptr(inner, replacements_by_id, replacements_by_name);
800
        }
801
        UnresolvedDomain::EnumeratedType(_, _) | UnresolvedDomain::UnnamedType(_) => {}
802
        UnresolvedDomain::Reference(_) => {}
803
    }
804
1554
}
805

            
806
1554
fn rewrite_int_range(
807
1554
    range: &mut Range<IntVal>,
808
1554
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
809
1554
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
810
1554
) {
811
1554
    match range {
812
        Range::Single(value) | Range::UnboundedL(value) | Range::UnboundedR(value) => {
813
            rewrite_int_value(value, replacements_by_id, replacements_by_name);
814
        }
815
        Range::Bounded(lower, upper) => {
816
1554
            rewrite_int_value(lower, replacements_by_id, replacements_by_name);
817
1554
            rewrite_int_value(upper, replacements_by_id, replacements_by_name);
818
        }
819
        Range::Unbounded => {}
820
    }
821
1554
}
822

            
823
3108
fn rewrite_int_value(
824
3108
    int_val: &mut IntVal,
825
3108
    replacements_by_id: &HashMap<ObjId, DeclarationPtr>,
826
3108
    replacements_by_name: &HashMap<Name, DeclarationPtr>,
827
3108
) {
828
3108
    match int_val {
829
        IntVal::Reference(reference) => {
830
1043
            *reference =
831
1043
                replace_reference(reference.clone(), replacements_by_id, replacements_by_name);
832
        }
833
        IntVal::Expr(expr) => {
834
1022
            let rewritten = replace_declaration_ptrs_in_expr(
835
1022
                (**expr).clone(),
836
1022
                replacements_by_id,
837
1022
                replacements_by_name,
838
1022
            );
839
1022
            *expr = Moo::new(rewritten);
840
        }
841
1043
        IntVal::Const(_) => {}
842
    }
843
3108
}
844

            
845
#[cfg(test)]
846
mod tests {
847
    use conjure_cp::ast::{
848
        Atom, Domain, Literal, SymbolTablePtr, comprehension::ComprehensionBuilder,
849
    };
850

            
851
    use super::*;
852

            
853
16
    fn literal(literal: Literal) -> Expr {
854
16
        Expr::Atomic(Metadata::new(), Atom::Literal(literal))
855
16
    }
856

            
857
13
    fn reference(declaration: DeclarationPtr) -> Expr {
858
13
        Expr::Atomic(Metadata::new(), Atom::new_ref(declaration))
859
13
    }
860

            
861
8
    fn eq(lhs: Expr, rhs: Expr) -> Expr {
862
8
        Expr::Eq(Metadata::new(), Moo::new(lhs), Moo::new(rhs))
863
8
    }
864

            
865
7
    fn comprehension_with_int_generator(
866
7
        upper: i32,
867
7
        parent: SymbolTablePtr,
868
7
    ) -> (Comprehension, DeclarationPtr) {
869
7
        let declaration = DeclarationPtr::new_quantified(
870
7
            Name::user("i"),
871
7
            Domain::int(vec![Range::Bounded(1, upper)]),
872
7
        );
873
7
        let comprehension = ComprehensionBuilder::new(parent)
874
7
            .generator(declaration)
875
7
            .with_return_value(literal(Literal::Bool(true)));
876
7
        let ComprehensionQualifier::Generator { ptr } = &comprehension.qualifiers[0] else {
877
            panic!("expected generator qualifier");
878
        };
879

            
880
7
        (comprehension.clone(), ptr.clone())
881
7
    }
882

            
883
    #[test]
884
1
    fn auto_keeps_small_guarded_comprehensions_native() {
885
1
        let (mut comprehension, quantified) = comprehension_with_int_generator(
886
1
            AUTO_SOLVER_THRESHOLD as i32 - 1,
887
1
            SymbolTablePtr::new(),
888
1
        );
889
1
        comprehension
890
1
            .qualifiers
891
1
            .push(ComprehensionQualifier::Condition(eq(
892
1
                reference(quantified),
893
1
                literal(Literal::Int(1)),
894
1
            )));
895

            
896
1
        assert_eq!(
897
1
            auto_comprehension_expander(&comprehension),
898
1
            QuantifiedExpander::Native
899
        );
900
1
    }
901

            
902
    #[test]
903
1
    fn auto_uses_solver_for_large_quantified_guards() {
904
1
        let parent = SymbolTablePtr::new();
905
1
        let outer = DeclarationPtr::new_find(Name::user("outer"), Domain::bool());
906
1
        parent.write().insert(outer.clone());
907
        let (mut comprehension, quantified) =
908
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
909
1
        comprehension
910
1
            .qualifiers
911
1
            .push(ComprehensionQualifier::Condition(eq(
912
1
                reference(quantified),
913
1
                literal(Literal::Int(1)),
914
1
            )));
915
1
        comprehension.return_expression = reference(outer);
916
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
917

            
918
1
        assert_eq!(
919
1
            auto_comprehension_expander(&comprehension),
920
1
            QuantifiedExpander::ViaSolverAc
921
        );
922
1
    }
923

            
924
    #[test]
925
1
    fn auto_keeps_single_ac_comparison_with_outer_decision_native() {
926
1
        let parent = SymbolTablePtr::new();
927
1
        let outer = DeclarationPtr::new_find(Name::user("outer"), Domain::bool());
928
1
        parent.write().insert(outer.clone());
929
        let (mut comprehension, quantified) =
930
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
931
1
        let quantified_filter = eq(reference(quantified), literal(Literal::Int(1)));
932
1
        comprehension.return_expression = Expr::Imply(
933
1
            Metadata::new(),
934
1
            Moo::new(quantified_filter),
935
1
            Moo::new(reference(outer)),
936
1
        );
937
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
938

            
939
1
        assert_eq!(
940
1
            auto_comprehension_expander(&comprehension),
941
1
            QuantifiedExpander::Native
942
        );
943
1
    }
944

            
945
    #[test]
946
1
    fn auto_uses_solver_for_quantified_conjunction_in_ac_return() {
947
1
        let parent = SymbolTablePtr::new();
948
1
        let outer = DeclarationPtr::new_find(Name::user("outer"), Domain::bool());
949
1
        parent.write().insert(outer.clone());
950
        let (mut comprehension, quantified) =
951
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
952
1
        let filters = vec![
953
1
            eq(reference(quantified.clone()), literal(Literal::Int(1))),
954
1
            eq(reference(quantified), literal(Literal::Int(2))),
955
        ];
956
1
        let quantified_filter = Expr::And(Metadata::new(), Moo::new(into_matrix_expr!(filters)));
957
1
        comprehension.return_expression = Expr::Imply(
958
1
            Metadata::new(),
959
1
            Moo::new(quantified_filter),
960
1
            Moo::new(reference(outer)),
961
1
        );
962
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
963

            
964
1
        assert_eq!(
965
1
            auto_comprehension_expander(&comprehension),
966
1
            QuantifiedExpander::ViaSolverAc
967
        );
968
1
    }
969

            
970
    #[test]
971
1
    fn auto_ignores_quantified_conjunctions_in_implication_consequents() {
972
1
        let parent = SymbolTablePtr::new();
973
1
        let outer = DeclarationPtr::new_find(Name::user("outer"), Domain::bool());
974
1
        parent.write().insert(outer.clone());
975
        let (mut comprehension, quantified) =
976
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
977
1
        let single_filter = eq(reference(quantified.clone()), literal(Literal::Int(1)));
978
1
        let quantified_conjunction = Expr::And(
979
1
            Metadata::new(),
980
1
            Moo::new(into_matrix_expr!(vec![
981
1
                eq(reference(quantified.clone()), literal(Literal::Int(1)),),
982
1
                eq(reference(quantified), literal(Literal::Int(2))),
983
1
            ])),
984
1
        );
985
1
        let consequent = Expr::And(
986
1
            Metadata::new(),
987
1
            Moo::new(into_matrix_expr!(vec![
988
1
                quantified_conjunction,
989
1
                reference(outer),
990
1
            ])),
991
1
        );
992
1
        comprehension.return_expression = Expr::Imply(
993
1
            Metadata::new(),
994
1
            Moo::new(single_filter),
995
1
            Moo::new(consequent),
996
1
        );
997
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
998

            
999
1
        assert_eq!(
1
            auto_comprehension_expander(&comprehension),
1
            QuantifiedExpander::Native
        );
1
    }
    #[test]
1
    fn auto_uses_solver_for_dependent_generators_with_nested_returns() {
1
        let parent = SymbolTablePtr::new();
        let (mut comprehension, outer) =
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
1
        let dependent_domain = Domain::int(vec![Range::Bounded(
1
            IntVal::Const(1),
1
            IntVal::Reference(Reference::new(outer)),
1
        )]);
1
        let inner_generator = DeclarationPtr::new_quantified(Name::user("j"), dependent_domain);
1
        comprehension
1
            .qualifiers
1
            .push(ComprehensionQualifier::Generator {
1
                ptr: inner_generator,
1
            });
1
        let nested = ComprehensionBuilder::new(SymbolTablePtr::new())
1
            .with_return_value(literal(Literal::Bool(true)));
1
        comprehension.return_expression = Expr::Comprehension(Metadata::new(), Moo::new(nested));
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
1
        assert_eq!(
1
            auto_comprehension_expander(&comprehension),
1
            QuantifiedExpander::ViaSolverAc
        );
1
    }
    #[test]
1
    fn auto_keeps_large_unprunable_comprehensions_native() {
1
        let parent = SymbolTablePtr::new();
1
        let outer = DeclarationPtr::new_find(Name::user("outer"), Domain::bool());
1
        parent.write().insert(outer.clone());
        let (mut comprehension, _) =
1
            comprehension_with_int_generator(AUTO_SOLVER_THRESHOLD as i32, parent);
1
        comprehension.return_expression = reference(outer);
1
        comprehension.skip_operator = Some(ACOperatorKind::And);
1
        assert_eq!(
1
            auto_comprehension_expander(&comprehension),
1
            QuantifiedExpander::Native
        );
1
    }
    #[test]
1
    fn replaces_references_in_expressions() {
1
        let quantified = DeclarationPtr::new_quantified(Name::user("matrix_bools"), Domain::bool());
1
        let replacement = DeclarationPtr::new_find(Name::Machine(0), Domain::bool());
1
        let safe_index = Expr::SafeIndex(
1
            Metadata::new(),
1
            Moo::new(Expr::Atomic(
1
                Metadata::new(),
1
                Atom::new_ref(quantified.clone()),
1
            )),
1
            vec![Expr::Atomic(
1
                Metadata::new(),
1
                Atom::Literal(Literal::Int(1)),
1
            )],
1
        );
        let inner_comprehension =
1
            ComprehensionBuilder::new(SymbolTablePtr::new()).with_return_value(safe_index);
1
        let expr = Expr::Comprehension(Metadata::new(), Moo::new(inner_comprehension));
1
        let replacements_by_id = HashMap::from([(quantified.id(), replacement.clone())]);
        let replacements_by_name =
1
            HashMap::from([(quantified.name().clone(), replacement.clone())]);
        let rewritten =
1
            replace_declaration_ptrs_in_expr(expr, &replacements_by_id, &replacements_by_name);
1
        let Expr::Comprehension(_, comprehension) = rewritten else {
            panic!("expected a comprehension expression");
        };
1
        let Expr::SafeIndex(_, subject, _) = &comprehension.return_expression else {
            panic!("expected a safe index return expression");
        };
1
        let Expr::Atomic(_, Atom::Reference(reference)) = subject.as_ref() else {
            panic!("expected a reference subject");
        };
1
        assert_eq!(reference.id(), replacement.id());
1
    }
}