1
//! Inverse lookups retain actual index labels and first-match semantics.
2
use conjure_cp::ast::{
3
    Domain, Expression as Expr, Literal, Metadata, Moo, Range, Reference, ReturnType, SymbolTable,
4
    Typeable, eval_constant,
5
};
6
use conjure_cp::rule_engine::{
7
    ApplicationError::RuleNotApplicable, ApplicationResult, RuleEffect, register_rule,
8
};
9
use std::collections::BTreeMap;
10

            
11
#[register_rule("SAT", 18500, [IndexOf])]
12
2737
fn index_of_constant_scalar_matrix(expr: &Expr, _: &SymbolTable) -> ApplicationResult {
13
2737
    let Expr::IndexOf(_, matrix, value) = expr else {
14
21
        return Err(RuleNotApplicable);
15
    };
16
2716
    let value_domain = value.domain_of().ok_or(RuleNotApplicable)?;
17
2716
    if !value_domain.is_int() && !value_domain.is_bool() {
18
        return Err(RuleNotApplicable);
19
2716
    }
20
2716
    let (entries, labels) = super::element::matrix_data(matrix).ok_or(RuleNotApplicable)?;
21
2716
    let mut inverse = BTreeMap::new();
22
2716
    for (entry, label) in entries.iter().zip(labels) {
23
9576
        let entry = match eval_constant(entry) {
24
9576
            Some(Literal::Int(value)) => value,
25
            Some(Literal::Bool(value)) => i32::from(value),
26
            _ => return Err(RuleNotApplicable),
27
        };
28
9576
        let label = i32::try_from(label).map_err(|_| RuleNotApplicable)?;
29
        // Constant folding of the existing inverse lookup chooses the first matching label.
30
9576
        inverse.entry(entry).or_insert(label);
31
    }
32
2716
    let domain = Domain::int(inverse.keys().copied().map(Range::Single).collect());
33
2716
    let entries = inverse.into_values().map(Expr::from).collect();
34
2716
    let matrix = conjure_cp::into_matrix_expr![entries; domain];
35
2716
    let value = if value_domain.is_bool() {
36
        Moo::new(Expr::ToInt(Metadata::new(), value.clone()))
37
    } else {
38
2716
        value.clone()
39
    };
40
2716
    Ok(RuleEffect::pure(Expr::ElementId(
41
2716
        Metadata::new(),
42
2716
        Moo::new(matrix),
43
2716
        value,
44
2716
    )))
45
2737
}
46

            
47
/// Compound and variable entries compare whole values before representation lowering.
48
#[register_rule("SAT", 18400, [IndexOf])]
49
21
fn index_of_general_matrix(expr: &Expr, symbols: &SymbolTable) -> ApplicationResult {
50
21
    let Expr::IndexOf(_, matrix, value) = expr else {
51
21
        return Err(RuleNotApplicable);
52
    };
53
    let (entries, labels) = super::element::matrix_data(matrix).ok_or(RuleNotApplicable)?;
54
    if entries.is_empty() {
55
        return Err(RuleNotApplicable);
56
    }
57
    let value_type = value.return_type();
58
    let labels = labels
59
        .into_iter()
60
        .map(i32::try_from)
61
        .collect::<Result<Vec<_>, _>>()
62
        .map_err(|_| RuleNotApplicable)?;
63
    let domain = Domain::int(labels.iter().copied().map(Range::Single).collect());
64
    let domain = if value_type == ReturnType::Int {
65
        domain
66
            .union(&value.domain_of().ok_or(RuleNotApplicable)?)
67
            .map_err(|_| RuleNotApplicable)?
68
    } else if value_type == ReturnType::Bool {
69
        domain
70
            .union(&Domain::int(vec![Range::Bounded(0, 1)]))
71
            .map_err(|_| RuleNotApplicable)?
72
    } else {
73
        domain
74
    };
75
    let mut symbols = symbols.clone();
76
    let output: Expr = Reference::new(symbols.gen_find_auxiliary(&domain)).into();
77
    let mut definitions = Vec::new();
78
    let mut remaining: Expr = true.into();
79
    for (entry, label) in entries.into_iter().zip(labels) {
80
        let matches = super::boolean::create_bool_aux(&mut symbols);
81
        definitions.push(Expr::Iff(
82
            Metadata::new(),
83
            Moo::new(matches.clone()),
84
            Moo::new(Expr::Eq(Metadata::new(), value.clone(), Moo::new(entry))),
85
        ));
86
        let first = vec![remaining.clone(), matches.clone()];
87
        definitions.push(Expr::Imply(
88
            Metadata::new(),
89
            Moo::new(Expr::And(
90
                Metadata::new(),
91
                Moo::new(conjure_cp::into_matrix_expr!(first)),
92
            )),
93
            Moo::new(Expr::Eq(
94
                Metadata::new(),
95
                Moo::new(output.clone()),
96
                Moo::new(label.into()),
97
            )),
98
        ));
99
        let next = super::boolean::create_bool_aux(&mut symbols);
100
        definitions.push(Expr::Iff(
101
            Metadata::new(),
102
            Moo::new(next.clone()),
103
            Moo::new(Expr::And(
104
                Metadata::new(),
105
                Moo::new(conjure_cp::into_matrix_expr!(vec![
106
                    remaining,
107
                    Expr::Not(Metadata::new(), Moo::new(matches)),
108
                ])),
109
            )),
110
        ));
111
        remaining = next;
112
    }
113
    // Only scalar values have an identity fallback. Compound absence is guarded by callers.
114
    if matches!(value_type, ReturnType::Int | ReturnType::Bool) {
115
        let fallback = if value_type == ReturnType::Bool {
116
            Expr::ToInt(Metadata::new(), value.clone())
117
        } else {
118
            (**value).clone()
119
        };
120
        definitions.push(Expr::Imply(
121
            Metadata::new(),
122
            Moo::new(remaining),
123
            Moo::new(Expr::Eq(
124
                Metadata::new(),
125
                Moo::new(output.clone()),
126
                Moo::new(fallback),
127
            )),
128
        ));
129
    }
130
    Ok(RuleEffect::new(output, definitions, symbols))
131
21
}