1
use std::sync::{Arc, Mutex};
2

            
3
use conjure_cp::{
4
    ast::{
5
        Expression,
6
        comprehension::{Comprehension, ComprehensionQualifier},
7
    },
8
    rule_engine::resolve_rule_sets,
9
    settings::{SolverFamily, current_rewriter},
10
    solver::{Solver, SolverError, adaptors::Minion},
11
};
12

            
13
use super::via_solver_common::{
14
    detach_symbols, instantiate_return_expressions_from_values, retain_quantified_solution_values,
15
    rewrite_model_with_configured_rewriter, split_symbolic_guards,
16
    temporarily_materialise_quantified_vars_as_finds, with_temporary_model,
17
};
18

            
19
/// Expands the comprehension by solving quantified variables with Minion.
20
///
21
/// This returns one expression per assignment to quantified variables that satisfies the static
22
/// guards of the comprehension. Guards that reference non-quantified decision variables cannot be
23
/// solved here; they are held back and re-applied to each expanded element under the enclosing AC
24
/// operator's skip semantics.
25
994
pub fn expand_via_solver(comprehension: Comprehension) -> Result<Vec<Expression>, SolverError> {
26
    // An empty generator domain -- `int(1..exposedBlock-1)` where an enclosing comprehension has
27
    // just bound `exposedBlock` to 1 -- yields no elements at all. Building a generator model for
28
    // it would materialise a find over an empty domain, which Minion cannot hold.
29
994
    if comprehension_has_empty_generator(&comprehension) {
30
        return Ok(vec![]);
31
994
    }
32

            
33
994
    let minion = Solver::new(Minion::new());
34
994
    let skip_operator = comprehension.skip_operator;
35
994
    let (comprehension, symbolic_guards) = split_symbolic_guards(&comprehension);
36
994
    let quantified_vars = comprehension.quantified_vars();
37

            
38
    // The generator model shares the comprehension's symbol table, and rewriting it introduces
39
    // auxiliary declarations. Detach that table first: the auxiliaries belong to this throwaway
40
    // model, and leaving them in the comprehension's own scope would put them in the *next*
41
    // generator model built from the same scope -- as unconstrained finds Minion then branches on,
42
    // multiplying the assignments and so duplicating the expanded elements. Sibling comprehensions
43
    // produced by instantiating one return expression share a scope, so this really happens.
44
994
    let mut generator_model = comprehension.to_generator_model();
45
994
    detach_symbols(&mut generator_model);
46

            
47
    // Quantified variables are shared declarations, so materialise them as finds only for as long
48
    // as this model needs them.
49
    let _temp_finds =
50
994
        temporarily_materialise_quantified_vars_as_finds(&generator_model, &quantified_vars);
51

            
52
    // only branch on the quantified variables.
53
994
    let generator_model = with_temporary_model(generator_model, Some(quantified_vars.clone()));
54
994
    tracing::trace!(
55
        target: "rule_engine_rule_trace",
56
        "Via-solver generator model before rewriting:\n\n{}\n--\n",
57
14
        generator_model
58
    );
59

            
60
    // call rewrite here as well as in expand_via_solver_ac, just to be consistent
61
994
    let extra_rule_sets = &["Base", "Bubble"];
62

            
63
994
    let rule_sets = resolve_rule_sets(SolverFamily::Minion, extra_rule_sets).unwrap();
64
994
    let configured_rewriter = current_rewriter();
65

            
66
    let generator_model =
67
994
        rewrite_model_with_configured_rewriter(generator_model, &rule_sets, configured_rewriter);
68

            
69
    // Call the rewriter to rewrite inside the comprehension
70
    //
71
    // The original idea was to let the top level rewriter rewrite the return expression model
72
    // and the generator model. The comprehension wouldn't be expanded until the generator
73
    // model is in valid minion that can be ran, at which point the return expression model
74
    // should also be in valid minion.
75
    //
76
    // By calling the rewriter inside the rule, we no longer wait for the generator model to be
77
    // valid Minion, so we don't get the simplified return model either...
78
    //
79
    // We need to do this as we want to modify the generator model (add the dummy Z's) then
80
    // solve and return in one go.
81
    //
82
    // Comprehensions need a big rewrite soon, as theres lots of sharp edges such as this in
83
    // my original implementation, and I don't think we can fit our new optimisation into it.
84
    // If we wanted to avoid calling the rewriter, we would need to run the first half the rule
85
    // up to adding the return expr to the generator model, yield, then come back later to
86
    // actually solve it?
87

            
88
    // Keep return expressions unreduced until quantified assignments are substituted.
89
    // Rewriting before substitution can change guard structure in ways that are unsafe for
90
    // constant evaluation after instantiation.
91
    let return_expression_model =
92
994
        with_temporary_model(comprehension.to_return_expression_model(), None);
93

            
94
    let values = {
95
994
        let solver_model = generator_model.clone();
96

            
97
994
        let minion = minion.load_model(solver_model)?;
98

            
99
994
        let values = Arc::new(Mutex::new(Vec::new()));
100
994
        let values_ptr = Arc::clone(&values);
101
994
        let quantified_vars_for_solution = quantified_vars.clone();
102
994
        let symbols_for_solution = generator_model.symbols().clone();
103

            
104
        tracing::debug!(model=%generator_model,comprehension=%comprehension,"Minion solving comprehension (solver mode)");
105
2982
        minion.solve(Box::new(move |sols| {
106
            // Only keep quantified assignments; discard solver auxiliaries/locals.
107
2982
            let values = &mut *values_ptr.lock().unwrap();
108
2982
            values.push(retain_quantified_solution_values(
109
2982
                sols,
110
2982
                &quantified_vars_for_solution,
111
2982
                &symbols_for_solution,
112
2982
            ));
113
2982
            true
114
2982
        }))?;
115

            
116
994
        values.lock().unwrap().clone()
117
    };
118
994
    instantiate_return_expressions_from_values(
119
994
        values,
120
994
        &return_expression_model,
121
994
        &quantified_vars,
122
994
        &symbolic_guards,
123
994
        skip_operator,
124
994
    )
125
994
}
126

            
127
/// Whether any domain generator of `comprehension` ranges over nothing.
128
994
fn comprehension_has_empty_generator(comprehension: &Comprehension) -> bool {
129
994
    comprehension.qualifiers.iter().any(|qualifier| {
130
994
        let ComprehensionQualifier::Generator { ptr } = qualifier else {
131
            return false;
132
        };
133
994
        ptr.domain().is_some_and(|domain| {
134
994
            domain
135
994
                .resolve()
136
994
                .ok()
137
994
                .and_then(|resolved| resolved.values().ok())
138
994
                .is_some_and(|mut values| values.next().is_none())
139
994
        })
140
994
    })
141
994
}