1
use std::{
2
    collections::HashMap,
3
    sync::{Arc, RwLock},
4
};
5

            
6
use crate::types::matrix::try_lower_const_unsafe_index_matrix_components;
7
use conjure_cp::{
8
    ast::{
9
        Atom, DecisionVariable, DeclarationKind, DeclarationPtr, Expression, GroundDomain, Literal,
10
        Metadata, Model, Name, Range, Reference, SymbolTable,
11
        ac_operators::ACOperatorKind,
12
        comprehension::{Comprehension, ComprehensionQualifier},
13
        eval_constant, run_partial_evaluator,
14
        serde::{HasId as _, ObjId},
15
    },
16
    bug,
17
    context::Context,
18
    into_matrix_expr,
19
    representation::{ReprStore, util::try_up},
20
    rule_engine::{
21
        RuleSet,
22
        rewrite_model_with_configured_rewriter as rewrite_model_with_configured_rewriter_core,
23
    },
24
    settings::{Rewriter, SolverFamily, with_compact_heuristic, with_solver_family},
25
    solver::SolverError,
26
};
27
use uniplate::{Biplate as _, Uniplate as _};
28

            
29
use super::expand_via_solver;
30

            
31
/// Configures a temporary model for solver-based comprehension expansion.
32
3036
pub(super) fn with_temporary_model(model: Model, search_order: Option<Vec<Name>>) -> Model {
33
3036
    let mut model = model;
34
3036
    model.context = Arc::new(RwLock::new(Context::default()));
35
3036
    model.search_order = search_order;
36
3036
    model
37
3036
}
38

            
39
/// Gives `model` a symbol table of its own, so the declarations a temporary model's rewrite
40
/// introduces stay inside that model.
41
///
42
/// A generator model shares the comprehension's own symbol table, and both building and rewriting
43
/// it add declarations -- auxiliaries from flattening, dummy variables standing in for non-local
44
/// fragments. Left in the comprehension's scope they reappear in the *next* model built from that
45
/// scope, as finds with nothing constraining them (and sometimes with domains that only made sense
46
/// in the model they came from).
47
1518
pub(super) fn detach_symbols(model: &mut Model) {
48
1518
    let detached = model.symbols_ptr_unchecked_mut().detach();
49
1518
    *model.symbols_ptr_unchecked_mut() = detached;
50
1518
}
51

            
52
/// Rewrites a temporary model using the currently configured rewriter and Minion-oriented rule
53
/// sets.
54
///
55
/// Representations for a throwaway model are chosen compactly rather than by the configured
56
/// heuristic, and for Minion rather than the outer model's solver. The rewritten model is passed
57
/// directly to Minion, so inheriting (for example) Z3 array/LIA applicability would produce a model
58
/// Minion cannot consume.
59
1518
pub(super) fn rewrite_model_with_configured_rewriter<'a>(
60
1518
    model: Model,
61
1518
    rule_sets: &Vec<&'a RuleSet<'a>>,
62
1518
    configured_rewriter: Rewriter,
63
1518
) -> Model {
64
1518
    with_solver_family(SolverFamily::Minion, || {
65
1518
        with_compact_heuristic(|| {
66
1518
            rewrite_model_with_configured_rewriter_core(model, rule_sets, configured_rewriter)
67
1518
                .unwrap()
68
1518
        })
69
1518
    })
70
1518
}
71

            
72
/// Splits out the guards that reference non-quantified decision variables.
73
///
74
/// These guards cannot be decided while expanding: the outer decision variables they mention do
75
/// not belong to the generator submodel, so putting them in it both asks Minion a meaningless
76
/// question and leaves undeclared names (e.g. the components of a represented outer matrix) in
77
/// the model handed to it. They are re-applied to each expanded element instead, through the
78
/// enclosing AC operator's skip semantics -- the same treatment the native expander gives them.
79
///
80
/// Returns the comprehension with those guards removed, alongside the guards themselves.
81
2512
pub(super) fn split_symbolic_guards(
82
2512
    comprehension: &Comprehension,
83
2512
) -> (Comprehension, Vec<Expression>) {
84
2512
    let mut symbolic_guards = vec![];
85
2512
    let mut qualifiers = vec![];
86

            
87
2512
    for qualifier in &comprehension.qualifiers {
88
3126
        match qualifier {
89
            ComprehensionQualifier::Condition(condition)
90
                if !comprehension.is_quantified_guard(condition) =>
91
            {
92
                symbolic_guards.push(condition.clone());
93
            }
94
3126
            qualifier => qualifiers.push(qualifier.clone()),
95
        }
96
    }
97

            
98
2512
    let mut stripped = comprehension.clone();
99
2512
    stripped.qualifiers = qualifiers;
100
2512
    (stripped, symbolic_guards)
101
2512
}
102

            
103
/// The domain bound to substitute for a guarded-out element in a min/max skip operation: the
104
/// return expression's own upper bound (for `min`) or lower bound (for `max`). Minion requires
105
/// every decision variable to have a finite, bounded domain, so this is always expected to
106
/// resolve; if it somehow doesn't, that's a genuine "can't expand this comprehension" failure
107
/// (not a bug to hide), surfaced as a model-feature error rather than a panic.
108
3
pub(super) fn min_max_skip_value(
109
3
    return_expression: &Expression,
110
3
    want_max: bool,
111
3
) -> Result<Literal, SolverError> {
112
    let not_supported = || {
113
        SolverError::ModelFeatureNotSupported(format!(
114
            "min/max comprehension with a symbolic guard: could not determine a bounded domain \
115
             for the return expression {return_expression} to build a safe skip value"
116
        ))
117
    };
118
3
    let ranges = return_expression
119
3
        .domain_of()
120
3
        .and_then(|domain| domain.as_ground().cloned())
121
3
        .and_then(|ground| match ground {
122
3
            GroundDomain::Int(ranges, _) => Some(ranges),
123
            _ => None,
124
3
        })
125
3
        .ok_or_else(not_supported)?;
126
3
    let spanning = Range::spanning(&ranges);
127
3
    let bound = if want_max {
128
2
        spanning.high()
129
    } else {
130
1
        spanning.low()
131
    };
132
3
    Ok(Literal::Int(*bound.ok_or_else(not_supported)?))
133
3
}
134

            
135
/// Instantiates rewritten return expressions with quantified assignments.
136
///
137
/// `symbolic_guards` are the guards held back by [`split_symbolic_guards`]. They are instantiated
138
/// under the same assignment as the return expression: a guard that becomes false drops the
139
/// element, a guard that becomes true is discharged, and one that stays symbolic wraps the element
140
/// in `skip_operator`'s skip operation.
141
///
142
/// This does not mutate any parent symbol table.
143
1518
pub(super) fn instantiate_return_expressions_from_values(
144
1518
    values: Vec<HashMap<Name, Literal>>,
145
1518
    return_expression_model: &Model,
146
1518
    quantified_vars: &[Name],
147
1518
    symbolic_guards: &[Expression],
148
1518
    skip_operator: Option<ACOperatorKind>,
149
1518
) -> Result<Vec<Expression>, SolverError> {
150
1518
    let mut return_expressions = vec![];
151

            
152
    // As in the native expander, the safe value to substitute for a guarded-out min/max element
153
    // must come from the return expression's *general* domain, computed once before any quantified
154
    // variable is bound to a concrete value.
155
1518
    let min_max_skip_value = match skip_operator {
156
        Some(op @ (ACOperatorKind::Min | ACOperatorKind::Max)) if !symbolic_guards.is_empty() => {
157
            let return_expression = return_expression_model.clone().into_single_expression();
158
            Some(min_max_skip_value(
159
                &return_expression,
160
                op == ACOperatorKind::Max,
161
            )?)
162
        }
163
1518
        _ => None,
164
    };
165

            
166
1518
    for value in values {
167
10291
        let return_expression_model = return_expression_model.clone();
168
10291
        let child_symtab = return_expression_model.symbols().clone();
169
10291
        let mut return_expression = return_expression_model.into_single_expression();
170

            
171
        // We only bind quantified variables.
172
10291
        let value: HashMap<_, _> = value
173
10291
            .into_iter()
174
44824
            .filter(|(name, _)| quantified_vars.contains(name))
175
            .collect();
176

            
177
        // Bind quantified references by updating declaration targets, then simplify. The held-back
178
        // guards are bound alongside the return expression: they quantify over the same variables.
179
10291
        let mut binding_targets = Vec::with_capacity(symbolic_guards.len() + 1);
180
10291
        binding_targets.push(return_expression.clone());
181
10291
        binding_targets.extend(symbolic_guards.iter().cloned());
182
        let _temp_value_bindings =
183
10291
            temporarily_bind_quantified_vars_to_values(&child_symtab, &binding_targets, &value);
184

            
185
10291
        let Some(guards) = instantiate_symbolic_guards(symbolic_guards)? else {
186
            // A guard is false under this assignment: the element is skipped entirely.
187
            continue;
188
        };
189

            
190
10291
        return_expression = concretise_resolved_reference_atoms(return_expression);
191
10291
        return_expression = expand_nested_comprehensions(return_expression)?;
192
10291
        let Some(mut return_expression) = strip_guarded_safe_index_conditions(return_expression)
193
        else {
194
            continue;
195
        };
196
10291
        return_expression = simplify_expression(return_expression);
197

            
198
10291
        for guard in guards {
199
            let Some(ac_operator) = skip_operator else {
200
                return Err(SolverError::ModelInvalid(format!(
201
                    "comprehension has symbolic guard but no AC operator context for \
202
                     solver-backed expansion: {guard}"
203
                )));
204
            };
205
            return_expression = match (ac_operator, &min_max_skip_value) {
206
                (ACOperatorKind::Min | ACOperatorKind::Max, Some(skip_value)) => ac_operator
207
                    .make_min_max_skip_operation(guard, return_expression, skip_value.clone()),
208
                (ACOperatorKind::Min | ACOperatorKind::Max, None) => {
209
                    bug!("min/max comprehension expansion is missing its precomputed skip value")
210
                }
211
                _ => ac_operator.make_skip_operation(guard, return_expression),
212
            };
213
        }
214

            
215
10291
        return_expressions.push(return_expression);
216
    }
217

            
218
1518
    Ok(return_expressions)
219
1518
}
220

            
221
/// Expands the comprehensions nested inside an instantiated return expression.
222
///
223
/// A comprehension is a leaf of `Expression`'s traversal, so a nested one keeps a scope of its own
224
/// -- including generator domains that depend on the variables quantified by the comprehension
225
/// being expanded here, as in `sum pos : int(start..start + n) . ...` inside a `forAll start`.
226
/// Such a domain only resolves while the bindings for this assignment are in force, so the nested
227
/// comprehension has to be expanded now instead of being left to a later pass, by which time
228
/// `start` is a quantified variable again with no value.
229
///
230
/// Comprehensions this expander cannot handle are left alone for the rules that can.
231
324902
fn expand_nested_comprehensions(expr: Expression) -> Result<Expression, SolverError> {
232
324902
    let children = expr
233
324902
        .children()
234
324902
        .into_iter()
235
324902
        .map(expand_nested_comprehensions)
236
324902
        .collect::<Result<_, _>>()?;
237
324902
    let expr = expr.with_children(children);
238

            
239
324902
    let Expression::Comprehension(_, comprehension) = &expr else {
240
323908
        return Ok(expr);
241
    };
242
994
    let comprehension = comprehension.as_ref().clone();
243

            
244
994
    let has_expression_generator = comprehension.qualifiers.iter().any(|qualifier| {
245
994
        matches!(
246
994
            qualifier,
247
            ComprehensionQualifier::ExpressionGenerator { .. }
248
        )
249
994
    });
250
994
    let (_, symbolic_guards) = split_symbolic_guards(&comprehension);
251
994
    if has_expression_generator
252
994
        || (comprehension.skip_operator.is_none() && !symbolic_guards.is_empty())
253
    {
254
        return Ok(expr);
255
994
    }
256

            
257
994
    Ok(into_matrix_expr!(expand_via_solver(comprehension)?))
258
324902
}
259

            
260
/// Instantiates the held-back guards under the bindings currently in force.
261
///
262
/// Returns `Ok(None)` when a guard is false (the element is skipped), otherwise the guards that
263
/// are still symbolic and so have to be attached to the element.
264
10291
fn instantiate_symbolic_guards(
265
10291
    symbolic_guards: &[Expression],
266
10291
) -> Result<Option<Vec<Expression>>, SolverError> {
267
10291
    let mut remaining = vec![];
268

            
269
10291
    for guard in symbolic_guards {
270
        let guard = simplify_expression(concretise_resolved_reference_atoms(guard.clone()));
271
        match eval_constant(&guard) {
272
            Some(Literal::Bool(true)) => {}
273
            Some(Literal::Bool(false)) => return Ok(None),
274
            Some(other) => {
275
                return Err(SolverError::ModelInvalid(format!(
276
                    "comprehension guard must evaluate to Bool, got {other}: {guard}"
277
                )));
278
            }
279
            None => remaining.push(guard),
280
        }
281
    }
282

            
283
10291
    Ok(Some(remaining))
284
10291
}
285

            
286
/// Keeps only the quantified assignments of a solver solution, discarding auxiliaries and locals.
287
///
288
/// A quantified variable with an abstract domain is branched on through its representation, so the
289
/// solver reports its representation variables rather than the variable itself. Those are lifted
290
/// back into one abstract value with [`try_up`], giving a value that can be substituted into the
291
/// return expression.
292
10291
pub(super) fn retain_quantified_solution_values(
293
10291
    values: HashMap<Name, Literal>,
294
10291
    quantified_vars: &[Name],
295
10291
    symbols: &SymbolTable,
296
10291
) -> HashMap<Name, Literal> {
297
10291
    let mut quantified_values = HashMap::new();
298

            
299
10291
    for name in quantified_vars {
300
        // `try_up` reads a directly assigned value when there is one, and otherwise goes up
301
        // through the declaration's representation.
302
44824
        let Some(decl) = symbols.lookup(name) else {
303
            continue;
304
        };
305
44824
        let Ok(value) = try_up(decl, &values) else {
306
            continue;
307
        };
308
44824
        quantified_values.insert(name.clone(), value);
309
    }
310

            
311
10291
    quantified_values
312
10291
}
313

            
314
/// Simplifies an instantiated comprehension element before it enters the rewriter worklist.
315
///
316
/// Applies constant folding and deep partial evaluation (including boolean `x = true` lowering in
317
/// the `Eq` arm), and lowers constant in-bounds `MatrixComponents` [`Expression::UnsafeIndex`] nodes
318
/// using the same oracle as `unsafe_const_index_matrix_components`, so ground tautologies and ground
319
/// indexing from expansion do not each pay a per-site rewriter worklist update.
320
2383651
pub(super) fn simplify_expression(mut expr: Expression) -> Expression {
321
    // Keep applying evaluators to a fixed point, or until no changes are made.
322
2383651
    for _ in 0..128 {
323
39962887
        let next = expr.clone().transform_bi(&|subexpr: Expression| {
324
39962887
            if let Some(lit) = eval_constant(&subexpr) {
325
22971391
                return Expression::Atomic(Metadata::new(), Atom::Literal(lit));
326
16991496
            }
327
16991496
            if let Some(lowered) = try_lower_const_unsafe_index_matrix_components(&subexpr) {
328
726877
                return lowered;
329
16264619
            }
330
16264619
            if let Ok(reduction) = run_partial_evaluator(&subexpr) {
331
1568047
                return reduction.new_expression;
332
14696572
            }
333
14696572
            subexpr
334
39962887
        });
335

            
336
4445173
        if next == expr {
337
2383651
            break;
338
2061522
        }
339
2061522
        expr = next;
340
    }
341

            
342
2383651
    expr
343
2383651
}
344

            
345
/// Strips internal `InDomain` guards that were introduced by bubbling a boolean `SafeIndex`
346
/// inside a comprehension return expression.
347
///
348
/// When a source comprehension already has a guard that filters out dummy/out-of-domain values,
349
/// earlier rewrites can turn that filter into a conjunction like
350
/// `and([SafeIndex(...), __inDomain(index, domain)])`. If we instantiate that directly, a
351
/// false `__inDomain` becomes a literal `false` element, which changes the comprehension from
352
/// "skip this element" to "include false".
353
///
354
/// We recover the original filtering behaviour only for this narrow internal pattern:
355
/// a top-level conjunction with exactly one non-guard term and one or more `InDomain` guards
356
/// that constrain indices used by that term. If any such guard is false after instantiation,
357
/// the element is skipped entirely.
358
2004827
pub(super) fn strip_guarded_safe_index_conditions(expr: Expression) -> Option<Expression> {
359
2004827
    let mut conjuncts = Vec::new();
360
2004827
    collect_top_level_and_terms(expr.clone(), &mut conjuncts);
361

            
362
2004827
    if conjuncts.len() == 1 && conjuncts[0] == expr {
363
1709251
        return Some(expr);
364
295576
    }
365

            
366
    let (guards, mut non_guards): (Vec<_>, Vec<_>) =
367
295576
        conjuncts.into_iter().partition(is_indomain_guard);
368

            
369
295576
    if guards.is_empty() || non_guards.len() != 1 {
370
295576
        return Some(expr);
371
    }
372

            
373
    let guarded_term = non_guards.pop().expect("length checked above");
374

            
375
    if !guards
376
        .iter()
377
        .all(|guard| guard_targets_safe_index_index(guard, &guarded_term))
378
    {
379
        return Some(expr);
380
    }
381

            
382
    for guard in &guards {
383
        let simplified_guard = simplify_expression(guard.clone());
384
        match eval_constant(&simplified_guard) {
385
            Some(Literal::Bool(true)) => {}
386
            Some(Literal::Bool(false)) => return None,
387
            _ => return Some(expr),
388
        }
389
    }
390

            
391
    Some(guarded_term)
392
2004827
}
393

            
394
2891467
fn collect_top_level_and_terms(expr: Expression, out: &mut Vec<Expression>) {
395
2891467
    if let Expression::And(_, ref children) = expr
396
325232
        && let Some(children) = children.as_ref().clone().into_list()
397
    {
398
324936
        for child in children {
399
886640
            collect_top_level_and_terms(child, out);
400
        }
401
    } else {
402
2566531
        out.push(expr);
403
    }
404
2891467
}
405

            
406
857280
fn is_indomain_guard(expr: &Expression) -> bool {
407
857280
    matches!(expr, Expression::InDomain(_, _, _))
408
857280
}
409

            
410
fn guard_targets_safe_index_index(guard: &Expression, expr: &Expression) -> bool {
411
    let Expression::InDomain(_, guarded_index, _) = guard else {
412
        return false;
413
    };
414

            
415
    expr.any_expression(|subexpr| {
416
        let Expression::SafeIndex(_, _, indices) = subexpr else {
417
            return false;
418
        };
419

            
420
        indices.iter().any(|index| index == guarded_index.as_ref())
421
    })
422
}
423

            
424
10291
fn concretise_resolved_reference_atoms(expr: Expression) -> Expression {
425
140540
    expr.transform_bi(&|atom: Atom| match atom {
426
113300
        Atom::Reference(reference) => reference
427
113300
            .resolve_constant()
428
113300
            .map_or_else(|| Atom::Reference(reference), Atom::Literal),
429
27240
        other => other,
430
140540
    })
431
10291
}
432

            
433
1086724
pub(super) fn lift_machine_references_into_parent_scope(
434
1086724
    expr: Expression,
435
1086724
    child_symtab: &SymbolTable,
436
1086724
    parent_symtab: &mut SymbolTable,
437
1086724
) -> Expression {
438
1086724
    let mut machine_name_translations: HashMap<ObjId, DeclarationPtr> = HashMap::new();
439

            
440
1086724
    for (name, decl) in child_symtab.iter_local() {
441
        // Do not add quantified declarations for quantified vars to the parent symbol table.
442
1310224
        if matches!(
443
1310224
            &decl.kind() as &DeclarationKind,
444
            DeclarationKind::Quantified(_)
445
        ) {
446
            continue;
447
1310224
        }
448

            
449
1310224
        if !matches!(name, Name::Machine(_)) {
450
1309756
            continue;
451
468
        }
452

            
453
468
        let id = decl.id();
454
468
        let new_decl = parent_symtab.gen_find_auxiliary(&decl.domain().unwrap());
455
468
        machine_name_translations.insert(id, new_decl);
456
    }
457

            
458
3161250
    expr.transform_bi(&|atom: Atom| {
459
3161250
        if let Atom::Reference(ref decl) = atom
460
1642339
            && let id = decl.id()
461
1642339
            && let Some(new_decl) = machine_name_translations.get(&id)
462
        {
463
            Atom::Reference(Reference::new(new_decl.clone()))
464
        } else {
465
3161250
            atom
466
        }
467
3161250
    })
468
1086724
}
469

            
470
/// Guard that temporarily converts quantified declarations to temporary value-lettings.
471
struct TempQuantifiedValueLettingGuard {
472
    originals: Vec<(DeclarationPtr, DeclarationKind)>,
473
}
474

            
475
impl Drop for TempQuantifiedValueLettingGuard {
476
10291
    fn drop(&mut self) {
477
10291
        for (mut decl, kind) in self.originals.drain(..) {
478
44824
            let _ = decl.replace_kind(kind);
479
        }
480
10291
    }
481
}
482

            
483
132540
fn maybe_bind_temp_value_letting(
484
132540
    originals: &mut Vec<(DeclarationPtr, DeclarationKind)>,
485
132540
    decl: &DeclarationPtr,
486
132540
    lit: &Literal,
487
132540
) {
488
132540
    if originals
489
132540
        .iter()
490
394521
        .any(|(existing, _)| existing.id() == decl.id())
491
    {
492
87716
        return;
493
44824
    }
494

            
495
44824
    let mut decl = decl.clone();
496
44824
    let old_kind = decl.kind().clone();
497
44824
    let temp_kind = DeclarationKind::TemporaryValueLetting(Expression::Atomic(
498
44824
        Metadata::new(),
499
44824
        Atom::Literal(lit.clone()),
500
44824
    ));
501
44824
    let _ = decl.replace_kind(temp_kind);
502
44824
    originals.push((decl, old_kind));
503
132540
}
504

            
505
10291
fn temporarily_bind_quantified_vars_to_values(
506
10291
    symbols: &SymbolTable,
507
10291
    exprs: &[Expression],
508
10291
    values: &HashMap<Name, Literal>,
509
10291
) -> TempQuantifiedValueLettingGuard {
510
10291
    let mut originals = Vec::new();
511

            
512
10291
    for (name, lit) in values {
513
44824
        let Some(decl) = symbols.lookup_local(name) else {
514
            continue;
515
        };
516

            
517
44824
        maybe_bind_temp_value_letting(&mut originals, &decl, lit);
518

            
519
44824
        let kind = decl.kind();
520
44824
        if let DeclarationKind::Quantified(inner) = &*kind
521
            && let Some(generator) = inner.generator()
522
        {
523
            maybe_bind_temp_value_letting(&mut originals, generator, lit);
524
44824
        }
525
    }
526

            
527
    // Some expressions can still reference quantified declarations from an earlier scope
528
    // (e.g. after comprehension rewrites that rebuild generator declarations). Bind those
529
    // declaration pointers directly as well.
530
10291
    for decl in exprs
531
10291
        .iter()
532
10291
        .flat_map(uniplate::Biplate::<DeclarationPtr>::universe_bi)
533
    {
534
113300
        let name = decl.name().clone();
535
113300
        let Some(lit) = values.get(&name) else {
536
25584
            continue;
537
        };
538

            
539
87716
        maybe_bind_temp_value_letting(&mut originals, &decl, lit);
540

            
541
87716
        let kind = decl.kind();
542
87716
        if let DeclarationKind::Quantified(inner) = &*kind
543
            && let Some(generator) = inner.generator()
544
        {
545
            maybe_bind_temp_value_letting(&mut originals, generator, lit);
546
87716
        }
547
    }
548

            
549
10291
    TempQuantifiedValueLettingGuard { originals }
550
10291
}
551

            
552
/// Guard that temporarily converts quantified declarations to find declarations.
553
pub(super) struct TempQuantifiedFindGuard {
554
    originals: Vec<(DeclarationPtr, DeclarationKind, ReprStore)>,
555
}
556

            
557
impl Drop for TempQuantifiedFindGuard {
558
1518
    fn drop(&mut self) {
559
1518
        for (mut decl, kind, reprs) in self.originals.drain(..) {
560
2132
            let _ = decl.replace_kind(kind);
561
2132
            *decl.reprs_mut() = reprs;
562
        }
563
1518
    }
564
}
565

            
566
/// Converts quantified declarations in `model` to temporary find declarations.
567
///
568
/// Declarations are shared, so the guard also restores the representations they carried. Rewriting
569
/// the temporary model picks a representation for each of these finds and records it on the
570
/// declaration; left there, the next temporary model built from the same comprehension scope would
571
/// start from a variable already claiming to be represented, while its representation variables
572
/// belong to the previous model and constrain nothing here -- so the solver is free to assign them
573
/// anything, and every such assignment comes back as another copy of the same expanded element.
574
1518
pub(super) fn temporarily_materialise_quantified_vars_as_finds(
575
1518
    model: &Model,
576
1518
    quantified_vars: &[Name],
577
1518
) -> TempQuantifiedFindGuard {
578
1518
    let symbols = model.symbols().clone();
579
1518
    let mut originals = Vec::new();
580

            
581
1518
    for name in quantified_vars {
582
2132
        let Some(mut decl) = symbols.lookup_local(name) else {
583
            continue;
584
        };
585

            
586
2132
        let old_kind = decl.kind().clone();
587
2132
        let Some(domain) = decl.domain() else {
588
            continue;
589
        };
590
2132
        let old_reprs = decl.reprs().clone();
591

            
592
2132
        let new_kind = DeclarationKind::Find(DecisionVariable::new(domain));
593
2132
        let _ = decl.replace_kind(new_kind);
594
2132
        originals.push((decl, old_kind, old_reprs));
595
    }
596

            
597
1518
    TempQuantifiedFindGuard { originals }
598
1518
}