1
use std::{
2
    cell::RefCell,
3
    collections::{BTreeSet, HashMap, VecDeque},
4
    sync::{Arc, Mutex},
5
};
6

            
7
use conjure_cp::{
8
    ast::{
9
        Atom, DeclarationPtr, Expression, Metadata, Model, Moo, Name, Reference, ReturnType,
10
        SymbolTable, Typeable as _,
11
        ac_operators::ACOperatorKind,
12
        comprehension::{Comprehension, ComprehensionQualifier},
13
        serde::HasId as _,
14
    },
15
    bug_assert,
16
    rule_engine::resolve_rule_sets,
17
    settings::{SolverFamily, current_rewriter},
18
    solver::{Solver, SolverError, adaptors::Minion},
19
};
20
use tracing::debug;
21
use uniplate::{Biplate, zipper::Zipper};
22

            
23
use super::via_solver_common::{
24
    detach_symbols, instantiate_return_expressions_from_values, retain_quantified_solution_values,
25
    rewrite_model_with_configured_rewriter, simplify_expression, split_symbolic_guards,
26
    temporarily_materialise_quantified_vars_as_finds, with_temporary_model,
27
};
28

            
29
/// Expands the comprehension using Minion, returning the resulting expressions.
30
///
31
/// This method is only suitable for comprehensions inside an AC operator. The AC operator that
32
/// contains this comprehension should be passed into the `ac_operator` argument.
33
///
34
/// This method performs additional pruning of "uninteresting" values, only possible when the
35
/// comprehension is inside an AC operator.
36
524
pub fn expand_via_solver_ac(
37
524
    comprehension: Comprehension,
38
524
    ac_operator: ACOperatorKind,
39
524
) -> Result<Vec<Expression>, SolverError> {
40
    // Guards over non-quantified decision variables belong to the outer model, not the generator
41
    // submodel; hold them back and re-apply them per element with this operator's skip semantics.
42
524
    let (comprehension, symbolic_guards) = split_symbolic_guards(&comprehension);
43
524
    let quantified_vars = comprehension.quantified_vars();
44

            
45
    // Add the return-expression constraint only when it can restrict at least one quantified
46
    // variable. If dummy replacement removes every quantified dependency, the constraint cannot
47
    // prune the generator assignments and ordinary guard-only solver expansion is cheaper.
48
524
    let (generator_model, _) = prepare_ac_generator_model(&comprehension, ac_operator);
49

            
50
    // REWRITE GENERATOR MODEL AND PASS TO MINION
51
    // ==========================================
52

            
53
524
    let generator_model = with_temporary_model(generator_model, Some(quantified_vars.clone()));
54

            
55
524
    let extra_rule_sets = &["Base", "Bubble"];
56

            
57
524
    let rule_sets = resolve_rule_sets(SolverFamily::Minion, extra_rule_sets).unwrap();
58
524
    let configured_rewriter = current_rewriter();
59

            
60
    // REWRITE RETURN EXPRESSION
61
    // =========================
62

            
63
    // Keep return expressions unreduced until quantified assignments are substituted.
64
    // Rewriting before substitution can introduce index auxiliaries that remain symbolic and may
65
    // produce unsupported Minion shapes after expansion.
66
    let return_expression_model =
67
524
        with_temporary_model(comprehension.to_return_expression_model(), None);
68

            
69
    let values = {
70
524
        let solver_model = generator_model.clone();
71
        // Minion expects quantified variables in the temporary generator model as find
72
        // declarations. Keep this conversion scoped to solver-backed expansion.
73
        let _temp_finds =
74
524
            temporarily_materialise_quantified_vars_as_finds(&solver_model, &quantified_vars);
75

            
76
        // Rewrite with quantified vars materialised as finds so Minion flattening can
77
        // introduce auxiliaries for constraints involving quantified variables.
78
        let solver_model =
79
524
            rewrite_model_with_configured_rewriter(solver_model, &rule_sets, configured_rewriter);
80
524
        let symbols_for_solution = solver_model.symbols().clone();
81

            
82
524
        let minion = Solver::new(Minion::new());
83
524
        let minion = minion.load_model(solver_model);
84

            
85
524
        let minion = match minion {
86
            Err(e) => {
87
                // Callers fall back to native expansion, so this is a routine outcome rather
88
                // than a problem; the model is large, so keep it out of the default log.
89
                debug!(why=%e,model=%generator_model,"Loading generator model failed, failing solver-backed AC comprehension expansion rule");
90
                return Err(e);
91
            }
92
524
            Ok(minion) => minion,
93
        };
94

            
95
524
        let values = Arc::new(Mutex::new(Vec::new()));
96
524
        let values_ptr = Arc::clone(&values);
97
524
        let quantified_vars_for_solution = quantified_vars.clone();
98

            
99
        // SOLVE FOR THE QUANTIFIED VARIABLES, AND SUBSTITUTE INTO THE RETURN EXPRESSION
100
        // ============================================================================
101

            
102
        tracing::debug!(model=%generator_model,comprehension=%comprehension,"Minion solving comprehnesion (ac mode)");
103

            
104
7309
        minion.solve(Box::new(move |sols| {
105
            // Only keep quantified assignments; discard solver auxiliaries/locals.
106
7309
            let values = &mut *values_ptr.lock().unwrap();
107
7309
            values.push(retain_quantified_solution_values(
108
7309
                sols,
109
7309
                &quantified_vars_for_solution,
110
7309
                &symbols_for_solution,
111
7309
            ));
112
7309
            true
113
7309
        }))?;
114

            
115
524
        values.lock().unwrap().clone()
116
    };
117
524
    instantiate_return_expressions_from_values(
118
524
        values,
119
524
        &return_expression_model,
120
524
        &quantified_vars,
121
524
        &symbolic_guards,
122
524
        Some(ac_operator),
123
524
    )
124
524
}
125

            
126
/// Build the detached AC generator model and add its derived return constraint when useful.
127
524
fn prepare_ac_generator_model(
128
524
    comprehension: &Comprehension,
129
524
    ac_operator: ACOperatorKind,
130
524
) -> (Model, bool) {
131
524
    let mut generator_model = comprehension.to_generator_model();
132
524
    detach_symbols(&mut generator_model);
133

            
134
524
    let return_expression = comprehension.return_expression.clone();
135
524
    if contains_comprehension(&return_expression) {
136
        // A nested comprehension may have generator domains depending on variables this model is
137
        // still solving for. It is expanded later, once those variables have concrete bindings.
138
497
        return (generator_model, false);
139
27
    }
140

            
141
27
    let quantified_vars: BTreeSet<_> = comprehension.quantified_vars().into_iter().collect();
142
    let constraint = {
143
27
        let mut symtab = generator_model.symbols_mut();
144
27
        derive_return_expression_constraint(
145
27
            return_expression,
146
27
            &mut symtab,
147
27
            &quantified_vars,
148
27
            &ac_operator,
149
27
        )
150
    };
151
27
    let constraint = simplify_expression(constraint);
152
27
    let useful = Biplate::<Name>::universe_bi(&constraint)
153
27
        .iter()
154
27
        .any(|name| quantified_vars.contains(name));
155

            
156
27
    if useful {
157
27
        generator_model.add_constraint(constraint);
158
    }
159

            
160
27
    (generator_model, useful)
161
524
}
162

            
163
/// True iff `expr` contains a comprehension.
164
///
165
/// Comprehensions are leaves of the traversal, so this finds nested ones without descending into
166
/// their own scopes.
167
524
fn contains_comprehension(expr: &Expression) -> bool {
168
3146
    expr.any_expression(|subexpr| matches!(subexpr, Expression::Comprehension(_, _)))
169
524
}
170

            
171
/// Eliminate all references to non-quantified variables by introducing dummy variables, then
172
/// derive the constraint that discards AC-identity return values.
173
///
174
/// Dummy variables must be the same type as the AC operators identity value.
175
///
176
/// To reduce the number of dummy variables, we turn the largest expression containing only
177
/// non-quantified variables and of the correct type into a dummy variable.
178
///
179
/// If there is no such expression, (e.g. and[(a<i) | i: int(1..10)]) , we use the smallest
180
/// expression of the correct type that contains a non-quantified variable. This ensures that
181
/// we lose as few references to quantified variables as possible.
182
27
fn derive_return_expression_constraint(
183
27
    return_expression: Expression,
184
27
    symtab: &mut SymbolTable,
185
27
    quantified_vars: &BTreeSet<Name>,
186
27
    ac_operator: &ACOperatorKind,
187
27
) -> Expression {
188
27
    let return_expression = localise_non_local_references_deep(return_expression, symtab);
189

            
190
27
    let mut zipper = Zipper::new(return_expression);
191

            
192
    // for sum/product we want to put integer expressions into dummy variables,
193
    // for and/or we want to put boolean expressions into dummy variables.
194
27
    let dummy_var_type = ac_operator.identity().return_type();
195

            
196
    'outer: loop {
197
105
        let focus: &mut Expression = zipper.focus_mut();
198

            
199
        let (non_quantified_vars, quantified_references) =
200
105
            partition_variables(focus, quantified_vars);
201

            
202
        // an expression or its descendants needs to be turned into a dummy variable if it
203
        // contains non-quantified variables.
204
105
        let has_non_quantified_vars = !non_quantified_vars.is_empty();
205

            
206
        // does this expression contain quantified variables?
207
105
        let has_quantified_vars = !quantified_references.is_empty();
208

            
209
        // can this expression be turned into a dummy variable?
210
105
        let can_be_dummy_var = can_be_dummy_variable(focus, &dummy_var_type);
211

            
212
        // The expression and its descendants don't need a dummy variables, so we don't
213
        // need to descend into its children.
214
105
        if !has_non_quantified_vars {
215
            // go to next node or quit
216
27
            while zipper.go_right().is_none() {
217
                let Some(()) = zipper.go_up() else {
218
                    // visited all nodes
219
                    break 'outer;
220
                };
221
            }
222
27
            continue;
223
78
        }
224

            
225
        // The expression contains non-quantified variables:
226

            
227
        // does this expression have any children that can be turned into dummy variables?
228
1125
        let has_eligible_child = focus.any_expression_descendant(|expr| {
229
            // eligible if it can be turned into a dummy variable, and turning it into a
230
            // dummy variable removes a non-quantified variable from the model.
231
1125
            can_be_dummy_variable(expr, &dummy_var_type)
232
132
                && contains_non_quantified_variables(expr, quantified_vars)
233
1125
        });
234

            
235
        // This expression has no child that can be turned into a dummy variable, but can
236
        // be a dummy variable => turn it into a dummy variable and continue.
237
78
        if !has_eligible_child && can_be_dummy_var {
238
            // introduce dummy var and continue
239
39
            let dummy_domain = focus.domain_of().unwrap();
240
39
            let dummy_decl = symtab.gen_find_auxiliary(&dummy_domain);
241
39
            *focus = Expression::Atomic(
242
39
                Metadata::new(),
243
39
                Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
244
39
            );
245

            
246
            // go to next node
247
78
            while zipper.go_right().is_none() {
248
66
                let Some(()) = zipper.go_up() else {
249
                    // visited all nodes
250
27
                    break 'outer;
251
                };
252
            }
253
12
            continue;
254
        }
255
        // This expression has no child that can be turned into a dummy variable, and
256
        // cannot be a dummy variable => backtrack upwards to find a parent that can be a
257
        // dummy variable, and make it a dummy variable.
258
39
        else if !has_eligible_child && !can_be_dummy_var {
259
            // TODO: remove this case, make has_eligible_child check better?
260

            
261
            // go upwards until we find something that can be a dummy variable, make it
262
            // a dummy variable, then continue.
263
            while let Some(()) = zipper.go_up() {
264
                let focus = zipper.focus_mut();
265
                if can_be_dummy_variable(focus, &dummy_var_type) {
266
                    // TODO: this expression we are rewritng might already contain
267
                    // dummy vars - we might need a pass to get rid of the unused
268
                    // ones!
269
                    //
270
                    // introduce dummy var and continue
271
                    let dummy_domain = focus.domain_of().unwrap();
272
                    let dummy_decl = symtab.gen_find_auxiliary(&dummy_domain);
273
                    *focus = Expression::Atomic(
274
                        Metadata::new(),
275
                        Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
276
                    );
277

            
278
                    // go to next node
279
                    while zipper.go_right().is_none() {
280
                        let Some(()) = zipper.go_up() else {
281
                            // visited all nodes
282
                            break 'outer;
283
                        };
284
                    }
285
                    continue;
286
                }
287
            }
288
            unreachable!();
289
        }
290
        // If the expression contains quantified variables as well as non-quantified
291
        // variables, try to retain the quantified variables by finding a child that can be
292
        // made a dummy variable which has only non-quantified variables.
293
39
        else if has_eligible_child && has_quantified_vars {
294
39
            zipper
295
39
                .go_down()
296
39
                .expect("we know the focus has a child, so zipper.go_down() should succeed");
297
        }
298
        // This expression contains no quantified variables, so no point trying to turn a
299
        // child into a dummy variable.
300
        else if has_eligible_child && !has_quantified_vars {
301
            // introduce dummy var and continue
302
            let dummy_domain = focus.domain_of().unwrap();
303
            let dummy_decl = symtab.gen_find_auxiliary(&dummy_domain);
304
            *focus = Expression::Atomic(
305
                Metadata::new(),
306
                Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
307
            );
308

            
309
            // go to next node
310
            while zipper.go_right().is_none() {
311
                let Some(()) = zipper.go_up() else {
312
                    // visited all nodes
313
                    break 'outer;
314
                };
315
            }
316
        } else {
317
            unreachable!()
318
        }
319
    }
320

            
321
27
    let new_return_expression = Expression::Neq(
322
27
        Metadata::new(),
323
27
        Moo::new(Expression::Atomic(
324
27
            Metadata::new(),
325
27
            ac_operator.identity().into(),
326
27
        )),
327
27
        Moo::new(zipper.rebuild_root()),
328
27
    );
329

            
330
    // double check that the above transformation didn't miss any stray non-quantified vars
331
    bug_assert!(
332
27
        Biplate::<DeclarationPtr>::universe_bi(&new_return_expression)
333
27
            .iter()
334
165
            .all(|x| symtab.lookup_local(&x.name()).is_some()),
335
        "generator model should only contain references to variables in its symbol table."
336
    );
337

            
338
27
    new_return_expression
339
27
}
340

            
341
/// Replaces references to declarations outside `symtab` with local dummy declarations.
342
///
343
/// This preserves locality by construction for temporary generator models passed to Minion.
344
/// All rewrites then operate purely on local references and cannot reintroduce parent-scope vars.
345
27
fn localise_non_local_references_deep(expr: Expression, symtab: &mut SymbolTable) -> Expression {
346
27
    let symtab_cell = RefCell::new(symtab);
347

            
348
27
    let expr = expr.transform_bi(&|mut comprehension: Comprehension| {
349
        {
350
            let mut symtab_borrow = symtab_cell.borrow_mut();
351
            let symtab_ref: &mut SymbolTable = &mut symtab_borrow;
352

            
353
            comprehension.return_expression = localise_non_local_references_deep(
354
                comprehension.return_expression.clone(),
355
                symtab_ref,
356
            );
357

            
358
            for qualifier in &mut comprehension.qualifiers {
359
                if let ComprehensionQualifier::Condition(condition) = qualifier {
360
                    *condition = localise_non_local_references_deep(condition.clone(), symtab_ref);
361
                }
362
            }
363
        }
364

            
365
        comprehension
366
    });
367

            
368
27
    let mut symtab_borrow = symtab_cell.borrow_mut();
369
27
    let symtab_ref: &mut SymbolTable = &mut symtab_borrow;
370
27
    localise_non_local_references_shallow(expr, symtab_ref)
371
27
}
372

            
373
27
fn localise_non_local_references_shallow(expr: Expression, symtab: &mut SymbolTable) -> Expression {
374
27
    let dummy_vars_by_decl_id: RefCell<HashMap<_, DeclarationPtr>> = RefCell::new(HashMap::new());
375
27
    let symtab = RefCell::new(symtab);
376

            
377
510
    expr.transform_bi(&|atom: Atom| {
378
510
        let Atom::Reference(reference) = atom else {
379
102
            return atom;
380
        };
381

            
382
408
        let reference_name = reference.name().clone();
383

            
384
        // Already local to this temporary generator model.
385
408
        if symtab.borrow().lookup_local(&reference_name).is_some() {
386
330
            return Atom::Reference(reference);
387
78
        }
388

            
389
78
        let decl = reference.ptr().clone();
390
78
        let decl_id = decl.id();
391

            
392
78
        let existing_dummy = dummy_vars_by_decl_id.borrow().get(&decl_id).cloned();
393
78
        let dummy_decl = if let Some(existing_dummy) = existing_dummy {
394
51
            existing_dummy
395
        } else {
396
            let new_dummy = {
397
27
                let domain = decl.domain().unwrap_or_else(|| {
398
                    panic!("non-local reference '{}' has no domain", decl.name())
399
                });
400
27
                symtab.borrow_mut().gen_find_auxiliary(&domain)
401
            };
402
27
            dummy_vars_by_decl_id
403
27
                .borrow_mut()
404
27
                .insert(decl_id, new_dummy.clone());
405
27
            new_dummy
406
        };
407

            
408
78
        Atom::Reference(Reference::new(dummy_decl))
409
510
    })
410
27
}
411

            
412
/// Returns the non-quantified and quantified references inside the expression.
413
///
414
/// Membership is tested against the comprehension's original quantified names. In particular,
415
/// dummy declarations introduced while localising outer references are local to the temporary
416
/// model but remain non-quantified for pruning analysis.
417
#[inline]
418
105
fn partition_variables(
419
105
    expr: &Expression,
420
105
    quantified_vars: &BTreeSet<Name>,
421
105
) -> (VecDeque<Name>, VecDeque<Name>) {
422
    let (non_quantified_vars, quantified_vars): (VecDeque<Name>, VecDeque<Name>) =
423
105
        Biplate::<Name>::universe_bi(expr)
424
105
            .into_iter()
425
960
            .partition(|name| !quantified_vars.contains(name));
426

            
427
105
    (non_quantified_vars, quantified_vars)
428
105
}
429

            
430
/// Returns `true` if `expr` can be turned into a dummy variable.
431
#[inline]
432
1230
fn can_be_dummy_variable(expr: &Expression, dummy_variable_type: &ReturnType) -> bool {
433
    // do not put root expression in a dummy variable or things go wrong.
434
1230
    if matches!(expr, Expression::Root(_, _)) {
435
        return false;
436
1230
    };
437

            
438
    // is the expression the same type as the dummy variable?
439
1230
    expr.return_type() == *dummy_variable_type
440
1230
}
441

            
442
/// Returns `true` if `expr` or its descendants contains non-quantified variables.
443
#[inline]
444
132
fn contains_non_quantified_variables(expr: &Expression, quantified_vars: &BTreeSet<Name>) -> bool {
445
132
    let names_referenced: VecDeque<Name> = expr.universe_bi();
446
132
    names_referenced
447
132
        .iter()
448
321
        .any(|name| !quantified_vars.contains(name))
449
132
}