1
use std::{
2
    cell::RefCell,
3
    collections::{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
    rule_engine::resolve_rule_sets,
16
    settings::{SolverFamily, current_rewriter},
17
    solver::{Solver, SolverError, adaptors::Minion},
18
};
19
use tracing::warn;
20
use uniplate::{Biplate, Uniplate as _, zipper::Zipper};
21

            
22
use super::via_solver_common::{
23
    instantiate_return_expressions_from_values, retain_quantified_solution_values,
24
    rewrite_model_with_configured_rewriter, temporarily_materialise_quantified_vars_as_finds,
25
    with_temporary_model,
26
};
27

            
28
/// Expands the comprehension using Minion, returning the resulting expressions.
29
///
30
/// This method is only suitable for comprehensions inside an AC operator. The AC operator that
31
/// contains this comprehension should be passed into the `ac_operator` argument.
32
///
33
/// This method performs additional pruning of "uninteresting" values, only possible when the
34
/// comprehension is inside an AC operator.
35
729
pub fn expand_via_solver_ac(
36
729
    comprehension: Comprehension,
37
729
    ac_operator: ACOperatorKind,
38
729
) -> Result<Vec<Expression>, SolverError> {
39
729
    let quantified_vars = comprehension.quantified_vars();
40

            
41
    // ADD RETURN EXPRESSION TO GENERATOR MODEL AS CONSTRAINT
42
    // ======================================================
43
729
    let return_expression = comprehension.return_expression.clone();
44

            
45
    // Replace all boolean expressions referencing non-quantified variables in the return
46
    // expression with dummy variables. This allows us to add it as a constraint to the
47
    // generator model.
48
729
    let generator_model = add_return_expression_to_generator_model(
49
729
        comprehension.to_generator_model(),
50
729
        return_expression,
51
729
        &ac_operator,
52
    );
53

            
54
    // REWRITE GENERATOR MODEL AND PASS TO MINION
55
    // ==========================================
56

            
57
729
    let generator_model = with_temporary_model(generator_model, Some(quantified_vars.clone()));
58

            
59
729
    let extra_rule_sets = &["Base", "Constant", "Bubble"];
60

            
61
729
    let rule_sets = resolve_rule_sets(SolverFamily::Minion, extra_rule_sets).unwrap();
62
729
    let configured_rewriter = current_rewriter();
63

            
64
    // REWRITE RETURN EXPRESSION
65
    // =========================
66

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

            
73
477
    let values = {
74
729
        let solver_model = generator_model.clone();
75
        // Minion expects quantified variables in the temporary generator model as find
76
        // declarations. Keep this conversion scoped to solver-backed expansion.
77
729
        let _temp_finds =
78
729
            temporarily_materialise_quantified_vars_as_finds(&solver_model, &quantified_vars);
79

            
80
        // Rewrite with quantified vars materialised as finds so Minion flattening can
81
        // introduce auxiliaries for constraints involving quantified variables.
82
729
        let solver_model =
83
729
            rewrite_model_with_configured_rewriter(solver_model, &rule_sets, configured_rewriter);
84

            
85
729
        let minion = Solver::new(Minion::new());
86
729
        let minion = minion.load_model(solver_model);
87

            
88
729
        let minion = match minion {
89
252
            Err(e) => {
90
252
                warn!(why=%e,model=%generator_model,"Loading generator model failed, failing solver-backed AC comprehension expansion rule");
91
252
                return Err(e);
92
            }
93
477
            Ok(minion) => minion,
94
        };
95

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

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

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

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

            
115
477
        values.lock().unwrap().clone()
116
    };
117
477
    Ok(instantiate_return_expressions_from_values(
118
477
        values,
119
477
        &return_expression_model,
120
477
        &quantified_vars,
121
477
    ))
122
729
}
123

            
124
/// Eliminate all references to non-quantified variables by introducing dummy variables to the
125
/// return expression. This modified return expression is added to the generator model, which is
126
/// returned.
127
///
128
/// Dummy variables must be the same type as the AC operators identity value.
129
///
130
/// To reduce the number of dummy variables, we turn the largest expression containing only
131
/// non-quantified variables and of the correct type into a dummy variable.
132
///
133
/// If there is no such expression, (e.g. and[(a<i) | i: int(1..10)]) , we use the smallest
134
/// expression of the correct type that contains a non-quantified variable. This ensures that
135
/// we lose as few references to quantified variables as possible.
136
729
fn add_return_expression_to_generator_model(
137
729
    mut generator_model: Model,
138
729
    return_expression: Expression,
139
729
    ac_operator: &ACOperatorKind,
140
729
) -> Model {
141
729
    let mut symtab = generator_model.symbols_mut();
142
729
    let return_expression = localise_non_local_references_deep(return_expression, &mut symtab);
143

            
144
729
    let mut zipper = Zipper::new(return_expression);
145

            
146
    // for sum/product we want to put integer expressions into dummy variables,
147
    // for and/or we want to put boolean expressions into dummy variables.
148
729
    let dummy_var_type = ac_operator.identity().return_type();
149

            
150
    'outer: loop {
151
729
        let focus: &mut Expression = zipper.focus_mut();
152

            
153
729
        let (non_quantified_vars, quantified_vars) = partition_variables(focus, &symtab);
154

            
155
        // an expression or its descendants needs to be turned into a dummy variable if it
156
        // contains non-quantified variables.
157
729
        let has_non_quantified_vars = !non_quantified_vars.is_empty();
158

            
159
        // does this expression contain quantified variables?
160
729
        let has_quantified_vars = !quantified_vars.is_empty();
161

            
162
        // can this expression be turned into a dummy variable?
163
729
        let can_be_dummy_var = can_be_dummy_variable(focus, &dummy_var_type);
164

            
165
        // The expression and its descendants don't need a dummy variables, so we don't
166
        // need to descend into its children.
167
729
        if !has_non_quantified_vars {
168
            // go to next node or quit
169
729
            while zipper.go_right().is_none() {
170
729
                let Some(()) = zipper.go_up() else {
171
                    // visited all nodes
172
729
                    break 'outer;
173
                };
174
            }
175
            continue;
176
        }
177

            
178
        // The expression contains non-quantified variables:
179

            
180
        // does this expression have any children that can be turned into dummy variables?
181
        let has_eligible_child = focus.universe().iter().skip(1).any(|expr| {
182
            // eligible if it can be turned into a dummy variable, and turning it into a
183
            // dummy variable removes a non-quantified variable from the model.
184
            can_be_dummy_variable(expr, &dummy_var_type)
185
                && contains_non_quantified_variables(expr, &symtab)
186
        });
187

            
188
        // This expression has no child that can be turned into a dummy variable, but can
189
        // be a dummy variable => turn it into a dummy variable and continue.
190
        if !has_eligible_child && can_be_dummy_var {
191
            // introduce dummy var and continue
192
            let dummy_domain = focus.domain_of().unwrap();
193
            let dummy_decl = symtab.gensym(&dummy_domain);
194
            *focus = Expression::Atomic(
195
                Metadata::new(),
196
                Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
197
            );
198

            
199
            // go to next node
200
            while zipper.go_right().is_none() {
201
                let Some(()) = zipper.go_up() else {
202
                    // visited all nodes
203
                    break 'outer;
204
                };
205
            }
206
            continue;
207
        }
208
        // This expression has no child that can be turned into a dummy variable, and
209
        // cannot be a dummy variable => backtrack upwards to find a parent that can be a
210
        // dummy variable, and make it a dummy variable.
211
        else if !has_eligible_child && !can_be_dummy_var {
212
            // TODO: remove this case, make has_eligible_child check better?
213

            
214
            // go upwards until we find something that can be a dummy variable, make it
215
            // a dummy variable, then continue.
216
            while let Some(()) = zipper.go_up() {
217
                let focus = zipper.focus_mut();
218
                if can_be_dummy_variable(focus, &dummy_var_type) {
219
                    // TODO: this expression we are rewritng might already contain
220
                    // dummy vars - we might need a pass to get rid of the unused
221
                    // ones!
222
                    //
223
                    // introduce dummy var and continue
224
                    let dummy_domain = focus.domain_of().unwrap();
225
                    let dummy_decl = symtab.gensym(&dummy_domain);
226
                    *focus = Expression::Atomic(
227
                        Metadata::new(),
228
                        Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
229
                    );
230

            
231
                    // go to next node
232
                    while zipper.go_right().is_none() {
233
                        let Some(()) = zipper.go_up() else {
234
                            // visited all nodes
235
                            break 'outer;
236
                        };
237
                    }
238
                    continue;
239
                }
240
            }
241
            unreachable!();
242
        }
243
        // If the expression contains quantified variables as well as non-quantified
244
        // variables, try to retain the quantified variables by finding a child that can be
245
        // made a dummy variable which has only non-quantified variables.
246
        else if has_eligible_child && has_quantified_vars {
247
            zipper
248
                .go_down()
249
                .expect("we know the focus has a child, so zipper.go_down() should succeed");
250
        }
251
        // This expression contains no quantified variables, so no point trying to turn a
252
        // child into a dummy variable.
253
        else if has_eligible_child && !has_quantified_vars {
254
            // introduce dummy var and continue
255
            let dummy_domain = focus.domain_of().unwrap();
256
            let dummy_decl = symtab.gensym(&dummy_domain);
257
            *focus = Expression::Atomic(
258
                Metadata::new(),
259
                Atom::Reference(conjure_cp::ast::Reference::new(dummy_decl)),
260
            );
261

            
262
            // go to next node
263
            while zipper.go_right().is_none() {
264
                let Some(()) = zipper.go_up() else {
265
                    // visited all nodes
266
                    break 'outer;
267
                };
268
            }
269
        } else {
270
            unreachable!()
271
        }
272
    }
273

            
274
729
    let new_return_expression = Expression::Neq(
275
729
        Metadata::new(),
276
729
        Moo::new(Expression::Atomic(
277
729
            Metadata::new(),
278
729
            ac_operator.identity().into(),
279
729
        )),
280
729
        Moo::new(zipper.rebuild_root()),
281
729
    );
282

            
283
    // double check that the above transformation didn't miss any stray non-quantified vars
284
729
    assert!(
285
729
        Biplate::<DeclarationPtr>::universe_bi(&new_return_expression)
286
729
            .iter()
287
2520
            .all(|x| symtab.lookup_local(&x.name()).is_some()),
288
        "generator model should only contain references to variables in its symbol table."
289
    );
290

            
291
729
    std::mem::drop(symtab);
292

            
293
729
    generator_model.add_constraint(new_return_expression);
294

            
295
729
    generator_model
296
729
}
297

            
298
/// Replaces references to declarations outside `symtab` with local dummy declarations.
299
///
300
/// This preserves locality by construction for temporary generator models passed to Minion.
301
/// All rewrites then operate purely on local references and cannot reintroduce parent-scope vars.
302
783
fn localise_non_local_references_deep(expr: Expression, symtab: &mut SymbolTable) -> Expression {
303
783
    let symtab_cell = RefCell::new(symtab);
304

            
305
783
    let expr = expr.transform_bi(&|mut comprehension: Comprehension| {
306
        {
307
54
            let mut symtab_borrow = symtab_cell.borrow_mut();
308
54
            let symtab_ref: &mut SymbolTable = &mut symtab_borrow;
309

            
310
54
            comprehension.return_expression = localise_non_local_references_deep(
311
54
                comprehension.return_expression.clone(),
312
54
                symtab_ref,
313
            );
314

            
315
54
            for qualifier in &mut comprehension.qualifiers {
316
54
                if let ComprehensionQualifier::Condition(condition) = qualifier {
317
                    *condition = localise_non_local_references_deep(condition.clone(), symtab_ref);
318
54
                }
319
            }
320
        }
321

            
322
54
        comprehension
323
54
    });
324

            
325
783
    let mut symtab_borrow = symtab_cell.borrow_mut();
326
783
    let symtab_ref: &mut SymbolTable = &mut symtab_borrow;
327
783
    localise_non_local_references_shallow(expr, symtab_ref)
328
783
}
329

            
330
783
fn localise_non_local_references_shallow(expr: Expression, symtab: &mut SymbolTable) -> Expression {
331
783
    let dummy_vars_by_decl_id: RefCell<HashMap<_, DeclarationPtr>> = RefCell::new(HashMap::new());
332
783
    let symtab = RefCell::new(symtab);
333

            
334
3294
    expr.transform_bi(&|atom: Atom| {
335
3294
        let Atom::Reference(reference) = atom else {
336
585
            return atom;
337
        };
338

            
339
2709
        let reference_name = reference.name().clone();
340

            
341
        // Already local to this temporary generator model.
342
2709
        if symtab.borrow().lookup_local(&reference_name).is_some() {
343
1458
            return Atom::Reference(reference);
344
1251
        }
345

            
346
1251
        let decl = reference.ptr().clone();
347
1251
        let decl_id = decl.id();
348

            
349
1251
        let existing_dummy = dummy_vars_by_decl_id.borrow().get(&decl_id).cloned();
350
1251
        let dummy_decl = if let Some(existing_dummy) = existing_dummy {
351
333
            existing_dummy
352
        } else {
353
918
            let new_dummy = {
354
918
                let domain = decl.domain().unwrap_or_else(|| {
355
                    panic!("non-local reference '{}' has no domain", decl.name())
356
                });
357
918
                symtab.borrow_mut().gensym(&domain)
358
            };
359
918
            dummy_vars_by_decl_id
360
918
                .borrow_mut()
361
918
                .insert(decl_id, new_dummy.clone());
362
918
            new_dummy
363
        };
364

            
365
1251
        Atom::Reference(Reference::new(dummy_decl))
366
3294
    })
367
783
}
368

            
369
/// Returns a tuple of non-quantified decision variables and quantified variables inside the expression.
370
///
371
/// As lettings, givens, etc. will eventually be subsituted for constants, this only returns
372
/// non-quantified _decision_ variables.
373
#[inline]
374
729
fn partition_variables(
375
729
    expr: &Expression,
376
729
    symtab: &SymbolTable,
377
729
) -> (VecDeque<Name>, VecDeque<Name>) {
378
    // doing this as two functions non_quantified_variables and quantified_variables might've been
379
    // easier to read.
380
    //
381
    // However, doing this in one function avoids an extra universe call...
382
729
    let (non_quantified_vars, quantified_vars): (VecDeque<Name>, VecDeque<Name>) =
383
729
        Biplate::<Name>::universe_bi(expr)
384
729
            .into_iter()
385
2520
            .partition(|x| symtab.lookup_local(x).is_none());
386

            
387
729
    (non_quantified_vars, quantified_vars)
388
729
}
389

            
390
/// Returns `true` if `expr` can be turned into a dummy variable.
391
#[inline]
392
729
fn can_be_dummy_variable(expr: &Expression, dummy_variable_type: &ReturnType) -> bool {
393
    // do not put root expression in a dummy variable or things go wrong.
394
729
    if matches!(expr, Expression::Root(_, _)) {
395
        return false;
396
729
    };
397

            
398
    // is the expression the same type as the dummy variable?
399
729
    expr.return_type() == *dummy_variable_type
400
729
}
401

            
402
/// Returns `true` if `expr` or its descendants contains non-quantified variables.
403
#[inline]
404
fn contains_non_quantified_variables(expr: &Expression, symtab: &SymbolTable) -> bool {
405
    let names_referenced: VecDeque<Name> = expr.universe_bi();
406
    // a name is a non-quantified variable if its definition is not in the local scope of the
407
    // comprehension's generators.
408
    names_referenced
409
        .iter()
410
        .any(|x| symtab.lookup_local(x).is_none())
411
}