1
use conjure_cp::ast::records::Field;
2
use conjure_cp::ast::{
3
    AbstractLiteral, BinaryAttr, Domain, Expression, FuncAttr, GroundDomain, JectivityAttr,
4
    Literal, MSetAttr, Metadata, Moo, Name, PartialityAttr, PartitionAttr, PermutationAttr,
5
    Reference, RelAttr, SequenceAttr, SetAttr, SymbolTable,
6
};
7
use conjure_cp::representation::{ReprAssignment, ReprDomainLevel, ReprRule};
8
use conjure_cp::{domain_int, into_matrix, range};
9
use conjure_cp_rules::representation::{
10
    FunctionAsRelation, FunctionExplicit, FunctionPacked, MSetCounts, MSetOccurrence, MSetPacked,
11
    MSetRepetition, MatrixComponents, MatrixPacked, PartitionAsSet, PartitionOccurrence,
12
    PartitionPacked, PermutationAsFunction, RecordComponents, RecordPacked, RelationAsSet,
13
    RelationOccurrence, RelationPacked, SequenceExplicit, SequencePacked, SetExplicit,
14
    SetOccurrence, SetPacked, TupleComponents, TuplePacked, VariantComponents, VariantPacked,
15
};
16
use uniplate::Uniplate;
17

            
18
#[test]
19
1
fn representation_short_names_describe_the_generated_layout() {
20
1
    assert_eq!(MatrixComponents::SHORT_NAME, "components");
21
1
    assert_eq!(MatrixPacked::SHORT_NAME, "packed");
22
1
    assert_eq!(TupleComponents::SHORT_NAME, "components");
23
1
    assert_eq!(SetPacked::SHORT_NAME, "packed");
24
1
    assert_eq!(TuplePacked::SHORT_NAME, "packed");
25
1
    assert_eq!(SetExplicit::SHORT_NAME, "explicit");
26
1
    assert_eq!(SetOccurrence::SHORT_NAME, "occurrence");
27
1
    assert_eq!(RecordComponents::SHORT_NAME, "components");
28
1
    assert_eq!(RecordPacked::SHORT_NAME, "packed");
29
1
    assert_eq!(VariantComponents::SHORT_NAME, "components");
30
1
    assert_eq!(VariantPacked::SHORT_NAME, "packed");
31
1
    assert_eq!(SequenceExplicit::SHORT_NAME, "explicit");
32
1
    assert_eq!(SequencePacked::SHORT_NAME, "packed");
33
1
    assert_eq!(RelationAsSet::SHORT_NAME, "as_set");
34
1
    assert_eq!(FunctionAsRelation::SHORT_NAME, "as_relation");
35
1
    assert_eq!(FunctionExplicit::SHORT_NAME, "explicit");
36
1
    assert_eq!(MSetRepetition::SHORT_NAME, "repetition");
37
1
    assert_eq!(MSetCounts::SHORT_NAME, "counts");
38
1
    assert_eq!(MSetOccurrence::SHORT_NAME, "occurrence");
39
1
    assert_eq!(MSetPacked::SHORT_NAME, "packed");
40
1
}
41

            
42
#[test]
43
1
fn counts_equality_compares_canonical_slots_instead_of_enumerating_values() {
44
2
    let domain = |max_size| Domain::mset(MSetAttr::new_max_size(max_size), domain_int!(1..999));
45
1
    let mut symbols = SymbolTable::new();
46
1
    let mut lhs = symbols.gen_find(&domain(6));
47
1
    let mut rhs = symbols.gen_find(&domain(8));
48
1
    MSetCounts::init_for(&mut lhs).unwrap();
49
1
    MSetCounts::init_for(&mut rhs).unwrap();
50

            
51
1
    let equality = lhs
52
1
        .get_repr::<MSetCounts>()
53
1
        .unwrap()
54
1
        .equality_expr(&rhs.get_repr::<MSetCounts>().unwrap());
55
1
    let universe = equality.universe();
56

            
57
    // Six shared slots compare count and value; the two unmatched slots must have count zero.
58
1
    assert_eq!(
59
1
        universe
60
1
            .iter()
61
96
            .filter(|expr| matches!(expr, Expression::Eq(..)))
62
            .count(),
63
1
        14
64
    );
65
1
    assert!(
66
1
        universe
67
1
            .iter()
68
96
            .all(|expr| !matches!(expr, Expression::Sum(..) | Expression::Product(..)))
69
    );
70
1
}
71

            
72
#[test]
73
1
fn mset_union_domain_preserves_the_selected_reference_representation() {
74
1
    let domain = Domain::mset(MSetAttr::new_max_size(6), domain_int!(1..999));
75
1
    let mut symbols = SymbolTable::new();
76
1
    let mut declaration = symbols.gen_find(&domain);
77
1
    MSetCounts::init_for(&mut declaration).unwrap();
78

            
79
1
    let union = Expression::Union(
80
1
        Metadata::new(),
81
1
        Moo::new(Expression::from(Reference {
82
1
            ptr: declaration,
83
1
            repr: Some(MSetCounts::STORED),
84
1
        })),
85
1
        Moo::new(Expression::AbstractLiteral(
86
1
            Metadata::new(),
87
1
            AbstractLiteral::MSet(vec![1.into(), 2.into()]),
88
1
        )),
89
1
    );
90

            
91
1
    let result = union.domain_of().unwrap();
92
1
    let (attrs, _) = result.as_mset_ground().unwrap();
93
1
    assert_eq!(attrs.size, range!(2..8));
94
1
    assert_eq!(attrs.representation.as_deref(), Some("counts"));
95
1
}
96

            
97
#[test]
98
1
fn variant_components_and_packed_round_trip() {
99
1
    let domain = Domain::variant(vec![
100
1
        Field {
101
1
            name: Name::user("flag"),
102
1
            value: Domain::bool(),
103
1
        },
104
1
        Field {
105
1
            name: Name::user("value"),
106
1
            value: domain_int!(2..4),
107
1
        },
108
1
    ]);
109
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Variant(Moo::new(Field {
110
1
        name: Name::user("value"),
111
1
        value: Literal::Int(3),
112
1
    })));
113

            
114
1
    let components = <VariantComponents as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
115
1
    let component_assignment = components.down(value.clone()).unwrap();
116
1
    assert_eq!(component_assignment.tag, Literal::Int(2));
117
1
    assert_eq!(component_assignment.fields[0], Literal::Bool(false));
118
1
    assert_eq!(component_assignment.up(), value);
119

            
120
1
    let packed = <VariantPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
121
1
    let packed_assignment = packed.down(value.clone()).unwrap();
122
1
    assert_eq!(packed_assignment.packed, Literal::Int(3));
123
1
    assert_eq!(packed_assignment.up(), value);
124
1
    assert_eq!(VariantPacked::compactness_score(domain).unwrap(), 5);
125
1
}
126

            
127
#[test]
128
1
fn packed_matrix_round_trips_primitive_values_and_weird_indices() {
129
1
    let inner_indices = GroundDomain::Int(
130
1
        vec![
131
1
            conjure_cp::ast::Range::Single(1),
132
1
            conjure_cp::ast::Range::Single(3),
133
1
        ],
134
1
        None,
135
1
    );
136
1
    let domain = Domain::matrix(
137
1
        domain_int!(2..4),
138
1
        vec![Domain::bool(), Domain::from(inner_indices.clone()).into()],
139
1
    );
140
2
    let row = |values| {
141
2
        Literal::AbstractLiteral(AbstractLiteral::Matrix(
142
2
            values,
143
2
            Moo::new(inner_indices.clone()),
144
2
        ))
145
2
    };
146
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Matrix(
147
1
        vec![
148
1
            row(vec![Literal::Int(2), Literal::Int(4)]),
149
1
            row(vec![Literal::Int(3), Literal::Int(2)]),
150
1
        ],
151
1
        Moo::new(GroundDomain::Bool),
152
1
    ));
153

            
154
1
    let state = <MatrixPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
155
1
    let assignment = state.down(value.clone()).unwrap();
156
1
    assert_eq!(assignment.packed, Literal::Int(21));
157
1
    assert_eq!(assignment.up(), value);
158
1
    assert_eq!(MatrixPacked::compactness_score(domain.clone()).unwrap(), 81);
159

            
160
1
    let mut symbols = SymbolTable::new();
161
1
    let mut declaration = symbols.gen_find(&domain);
162
1
    let (new_symbols, constraints) = MatrixPacked::init_for(&mut declaration).unwrap();
163
1
    assert_eq!(new_symbols.iter_local().count(), 1);
164
1
    assert!(constraints.is_empty());
165
1
    let decoded = declaration
166
1
        .get_repr::<MatrixPacked>()
167
1
        .unwrap()
168
1
        .decoded_matrix();
169
1
    assert_eq!(
170
1
        decoded
171
1
            .universe()
172
1
            .iter()
173
53
            .filter(|expr| matches!(expr, Expression::SafeIndex(..)))
174
            .count(),
175
1
        4
176
    );
177
1
}
178

            
179
#[test]
180
1
fn packed_matrix_orders_booleans_false_before_true() {
181
1
    let domain = Domain::matrix(Domain::bool(), vec![domain_int!(1..2)]);
182
1
    let state = <MatrixPacked as ReprRule>::DomainLevel::init(domain).unwrap();
183
1
    assert_eq!(
184
1
        state.values.as_ref(),
185
1
        &[Literal::Bool(false), Literal::Bool(true)]
186
    );
187
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Matrix(
188
1
        vec![Literal::Bool(true), Literal::Bool(false)],
189
1
        Moo::new(GroundDomain::Int(
190
1
            vec![conjure_cp::ast::Range::Bounded(1, 2)],
191
1
            None,
192
1
        )),
193
1
    ));
194
1
    let assignment = state.down(value.clone()).unwrap();
195
1
    assert_eq!(assignment.packed, Literal::Int(2));
196
1
    assert_eq!(assignment.up(), value);
197
1
}
198

            
199
#[test]
200
1
fn matrix_representation_initialises_and_maps_indices() {
201
1
    let domain = Domain::matrix(
202
1
        Domain::bool(),
203
1
        vec![Domain::bool(), domain_int!(1, 3, 5, 7)],
204
1
    );
205

            
206
1
    let domain_state = <MatrixComponents as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
207
1
    assert_eq!(domain_state.dimensions, vec![2, 4]);
208
1
    assert_eq!(domain_state.strides, vec![4, 1]);
209
1
    assert_eq!(
210
1
        domain_state
211
1
            .indices_lits_to_flat(&[Literal::Bool(true), Literal::Int(5)])
212
1
            .unwrap(),
213
1
        6
214
    );
215

            
216
1
    let mut symbols = SymbolTable::new();
217
1
    let mut declaration = symbols.gen_find(&domain);
218
1
    let (new_symbols, constraints) = MatrixComponents::init_for(&mut declaration).unwrap();
219
1
    assert!(constraints.is_empty());
220
1
    assert_eq!(new_symbols.iter_local().count(), 8);
221
1
    assert!(
222
1
        new_symbols
223
1
            .iter_local()
224
8
            .all(|(name, _)| name.to_string().contains("#components_"))
225
    );
226
1
    assert!(declaration.get_repr::<MatrixComponents>().is_some());
227

            
228
1
    let detached = declaration.detach();
229
1
    assert!(detached.get_repr::<MatrixComponents>().is_some());
230
1
}
231

            
232
#[test]
233
1
fn matrix_components_instantiate_uses_per_cell_element_domains() {
234
1
    let inner = Domain::sequence(sequence_attr(range!(0..10)), domain_int!(1..9));
235
1
    let domain = Domain::matrix(inner, vec![domain_int!(1..2)]);
236
1
    let mut symbols = SymbolTable::new();
237
1
    let mut declaration = symbols.gen_find(&domain);
238
    {
239
1
        let mut var = declaration.as_find_mut().expect("find");
240
1
        var.element_domains = Some(vec![
241
1
            Domain::sequence(sequence_attr(range!(3)), domain_int!(1..9)).into(),
242
1
            Domain::sequence(sequence_attr(range!(5)), domain_int!(1..9)).into(),
243
1
        ]);
244
    }
245

            
246
1
    MatrixComponents::init_for(&mut declaration).unwrap();
247
1
    let components = declaration
248
1
        .get_repr::<MatrixComponents>()
249
1
        .expect("MatrixComponents");
250
1
    let sizes: Vec<_> = components
251
1
        .elements
252
1
        .iter()
253
2
        .map(|elem| {
254
2
            let domain = elem.domain().unwrap();
255
2
            let GroundDomain::Sequence(attr, _) = domain.as_ground().unwrap() else {
256
                panic!("expected a sequence component domain, got {domain}");
257
            };
258
2
            attr.size.clone()
259
2
        })
260
        .collect();
261
1
    assert_eq!(sizes, vec![range!(3), range!(5)]);
262
1
}
263

            
264
#[test]
265
1
fn record_components_round_trip_values_in_canonical_field_order() {
266
1
    let domain = Domain::record(vec![
267
1
        Field {
268
1
            name: Name::user("z"),
269
1
            value: Domain::bool(),
270
1
        },
271
1
        Field {
272
1
            name: Name::user("a"),
273
1
            value: domain_int!(1..3),
274
1
        },
275
1
    ]);
276
1
    let state = <RecordComponents as ReprRule>::DomainLevel::init(domain).unwrap();
277
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Record(vec![
278
1
        Field {
279
1
            name: Name::user("z"),
280
1
            value: Literal::Bool(true),
281
1
        },
282
1
        Field {
283
1
            name: Name::user("a"),
284
1
            value: Literal::Int(2),
285
1
        },
286
1
    ]));
287

            
288
1
    let assignment = state.down(value).unwrap();
289
1
    assert_eq!(
290
1
        assignment.up(),
291
1
        Literal::AbstractLiteral(AbstractLiteral::Record(vec![
292
1
            Field {
293
1
                name: Name::user("a"),
294
1
                value: Literal::Int(2),
295
1
            },
296
1
            Field {
297
1
                name: Name::user("z"),
298
1
                value: Literal::Bool(true),
299
1
            },
300
1
        ]))
301
    );
302
1
}
303

            
304
#[test]
305
1
fn packed_record_round_trips_boolean_integer_and_nested_record_fields() {
306
1
    let domain = Domain::record(vec![
307
1
        Field {
308
1
            name: Name::user("z"),
309
1
            value: Domain::record(vec![
310
1
                Field {
311
1
                    name: Name::user("flag"),
312
1
                    value: Domain::bool(),
313
1
                },
314
1
                Field {
315
1
                    name: Name::user("value"),
316
1
                    value: domain_int!(1, 3),
317
1
                },
318
1
            ]),
319
1
        },
320
1
        Field {
321
1
            name: Name::user("a"),
322
1
            value: Domain::bool(),
323
1
        },
324
1
    ]);
325
1
    let state = <RecordPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
326
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Record(vec![
327
1
        Field {
328
1
            name: Name::user("z"),
329
1
            value: Literal::AbstractLiteral(AbstractLiteral::Record(vec![
330
1
                Field {
331
1
                    name: Name::user("value"),
332
1
                    value: Literal::Int(3),
333
1
                },
334
1
                Field {
335
1
                    name: Name::user("flag"),
336
1
                    value: Literal::Bool(false),
337
1
                },
338
1
            ])),
339
1
        },
340
1
        Field {
341
1
            name: Name::user("a"),
342
1
            value: Literal::Bool(false),
343
1
        },
344
1
    ]));
345

            
346
1
    let assignment = state.down(value).unwrap();
347
1
    assert_eq!(assignment.packed, Literal::Int(1));
348
1
    assert_eq!(
349
1
        assignment.up(),
350
1
        Literal::AbstractLiteral(AbstractLiteral::Record(vec![
351
1
            Field {
352
1
                name: Name::user("a"),
353
1
                value: Literal::Bool(false),
354
1
            },
355
1
            Field {
356
1
                name: Name::user("z"),
357
1
                value: Literal::AbstractLiteral(AbstractLiteral::Record(vec![
358
1
                    Field {
359
1
                        name: Name::user("flag"),
360
1
                        value: Literal::Bool(false),
361
1
                    },
362
1
                    Field {
363
1
                        name: Name::user("value"),
364
1
                        value: Literal::Int(3),
365
1
                    },
366
1
                ])),
367
1
            },
368
1
        ]))
369
    );
370
1
    assert_eq!(RecordPacked::compactness_score(domain).unwrap(), 8);
371
1
}
372

            
373
#[test]
374
1
fn packed_tuple_round_trips_integer_values() {
375
1
    let domain = Domain::tuple(vec![domain_int!(2..4), domain_int!(10..12)]);
376
1
    let state = <TuplePacked as ReprRule>::DomainLevel::init(domain).unwrap();
377
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![
378
1
        Literal::Int(3),
379
1
        Literal::Int(11),
380
1
    ]));
381

            
382
1
    let assignment = state.down(value.clone()).unwrap();
383
1
    assert_eq!(assignment.packed, Literal::Int(4));
384
1
    assert_eq!(assignment.up(), value);
385
1
}
386

            
387
#[test]
388
1
fn packed_tuple_round_trips_boolean_and_nested_values() {
389
1
    let domain = Domain::tuple(vec![
390
1
        Domain::bool(),
391
1
        Domain::tuple(vec![Domain::bool(), domain_int!(1..2)]),
392
1
    ]);
393
1
    let state = <TuplePacked as ReprRule>::DomainLevel::init(domain).unwrap();
394
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![
395
1
        Literal::Bool(false),
396
1
        Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![
397
1
            Literal::Bool(true),
398
1
            Literal::Int(2),
399
1
        ])),
400
1
    ]));
401

            
402
1
    let assignment = state.down(value.clone()).unwrap();
403
1
    assert_eq!(assignment.packed, Literal::Int(3));
404
1
    assert_eq!(assignment.up(), value);
405
1
    assert_eq!(
406
1
        state.values[0],
407
1
        vec![Literal::Bool(false), Literal::Bool(true)]
408
    );
409
1
}
410

            
411
#[test]
412
1
fn packed_tuple_uses_dense_holey_integer_digits() {
413
1
    let domain = Domain::tuple(vec![domain_int!(1, 3), domain_int!(5..6)]);
414
1
    let mut symbols = SymbolTable::new();
415
1
    let mut declaration = symbols.gen_find(&domain);
416
1
    let (_, constraints) = TuplePacked::init_for(&mut declaration).unwrap();
417

            
418
1
    assert!(constraints.is_empty());
419
1
    assert_eq!(TuplePacked::compactness_score(domain).unwrap(), 4);
420
1
}
421

            
422
#[test]
423
1
fn packed_tuple_supports_the_nullary_tuple() {
424
1
    let domain = Domain::tuple(vec![]);
425
1
    let state = <TuplePacked as ReprRule>::DomainLevel::init(domain).unwrap();
426
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![]));
427

            
428
1
    let assignment = state.down(value.clone()).unwrap();
429
1
    assert_eq!(assignment.packed, Literal::Int(0));
430
1
    assert_eq!(assignment.up(), value);
431
1
}
432

            
433
#[test]
434
1
fn occurrence_set_round_trips_and_enforces_cardinality() {
435
1
    let domain = Domain::set(SetAttr::new_min_max_size(1, 2), domain_int!(1..3));
436
1
    let state = <SetOccurrence as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
437
    let value =
438
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(3), Literal::Int(1)]));
439

            
440
1
    let assignment = state.down(value).unwrap();
441
1
    assert_eq!(
442
1
        assignment.up(),
443
1
        Literal::AbstractLiteral(AbstractLiteral::Set(
444
1
            vec![Literal::Int(1), Literal::Int(3),]
445
1
        ))
446
    );
447

            
448
1
    let mut symbols = SymbolTable::new();
449
1
    let mut declaration = symbols.gen_find(&domain);
450
1
    let (new_symbols, constraints) = SetOccurrence::init_for(&mut declaration).unwrap();
451
1
    assert_eq!(new_symbols.iter_local().count(), 3);
452
1
    assert_eq!(constraints.len(), 1);
453
1
}
454

            
455
#[test]
456
1
fn occurrence_set_supports_non_integer_elements() {
457
1
    let element_domain = Domain::tuple(vec![Domain::bool(), domain_int!(1..2)]);
458
1
    let domain = Domain::set(SetAttr::new_size(1), element_domain);
459
1
    let state = <SetOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
460
1
    let element = Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![
461
1
        Literal::Bool(true),
462
1
        Literal::Int(2),
463
1
    ]));
464
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Set(vec![element]));
465

            
466
1
    assert_eq!(state.down(value.clone()).unwrap().up(), value);
467
1
    assert!(
468
1
        state
469
1
            .occurs
470
1
            .iter()
471
4
            .all(|(key, _)| matches!(key, Literal::AbstractLiteral(AbstractLiteral::Tuple(_))))
472
    );
473
1
}
474

            
475
#[test]
476
1
fn explicit_set_round_trips_with_padding() {
477
1
    let domain = Domain::set(SetAttr::new_min_max_size(1, 3), domain_int!(1..4));
478
1
    let state = <SetExplicit as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
479
    let value =
480
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(4), Literal::Int(2)]));
481

            
482
1
    let assignment = state.down(value).unwrap();
483
1
    assert_eq!(assignment.set_size, Some(Literal::Int(2)));
484
1
    assert_eq!(
485
1
        assignment.elems_matrix,
486
1
        Literal::from(conjure_cp::into_matrix!(vec![
487
1
            Literal::Int(2),
488
1
            Literal::Int(4),
489
1
            Literal::Int(1),
490
1
        ]))
491
    );
492
1
    assert_eq!(
493
1
        assignment.up(),
494
1
        Literal::AbstractLiteral(AbstractLiteral::Set(
495
1
            vec![Literal::Int(2), Literal::Int(4),]
496
1
        ))
497
    );
498

            
499
1
    let mut symbols = SymbolTable::new();
500
1
    let mut declaration = symbols.gen_find(&domain);
501
1
    let (new_symbols, constraints) = SetExplicit::init_for(&mut declaration).unwrap();
502
1
    assert_eq!(new_symbols.iter_local().count(), 2);
503
1
    assert_eq!(constraints.len(), 5);
504
1
    assert_eq!(
505
1
        constraints
506
1
            .iter()
507
1
            .flat_map(Uniplate::universe)
508
58
            .filter(|expr| matches!(expr, Expression::SafeIndex(..)))
509
            .count(),
510
1
        7
511
    );
512
1
}
513

            
514
#[test]
515
1
fn fixed_explicit_set_omits_the_cardinality_marker() {
516
1
    let domain = Domain::set(SetAttr::new_size(2), domain_int!(1..3));
517
1
    let state = <SetExplicit as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
518
    let value =
519
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(3), Literal::Int(1)]));
520

            
521
1
    let assignment = state.down(value.clone()).unwrap();
522
1
    assert_eq!(assignment.set_size, None);
523
1
    assert_eq!(
524
1
        assignment.up(),
525
1
        Literal::AbstractLiteral(AbstractLiteral::Set(
526
1
            vec![Literal::Int(1), Literal::Int(3),]
527
1
        ))
528
    );
529

            
530
1
    let mut symbols = SymbolTable::new();
531
1
    let mut declaration = symbols.gen_find(&domain);
532
1
    let (new_symbols, _) = SetExplicit::init_for(&mut declaration).unwrap();
533
1
    assert_eq!(new_symbols.iter_local().count(), 1);
534
1
    assert!(
535
1
        declaration
536
1
            .get_repr::<SetExplicit>()
537
1
            .unwrap()
538
1
            .set_size
539
1
            .is_none()
540
    );
541
1
}
542

            
543
#[test]
544
1
fn packed_set_round_trips_and_enforces_cardinality() {
545
1
    let domain = Domain::set(SetAttr::new_min_max_size(1, 2), domain_int!(1..3));
546
1
    let state = <SetPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
547
    let value =
548
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(3), Literal::Int(1)]));
549

            
550
1
    let assignment = state.down(value).unwrap();
551
1
    assert_eq!(assignment.packed, Literal::Int(5));
552
1
    assert_eq!(
553
1
        assignment.up(),
554
1
        Literal::AbstractLiteral(AbstractLiteral::Set(
555
1
            vec![Literal::Int(1), Literal::Int(3),]
556
1
        ))
557
    );
558

            
559
1
    let mut symbols = SymbolTable::new();
560
1
    let mut declaration = symbols.gen_find(&domain);
561
1
    let (new_symbols, constraints) = SetPacked::init_for(&mut declaration).unwrap();
562
1
    assert_eq!(new_symbols.iter_local().count(), 1);
563
1
    assert_eq!(constraints.len(), 1);
564

            
565
1
    let representation = declaration.get_repr::<SetPacked>().unwrap();
566
1
    let membership = representation.membership_expr(Expression::from(2));
567
1
    assert!(
568
1
        membership
569
1
            .universe()
570
1
            .iter()
571
2
            .any(|expr| matches!(expr, Expression::UnsafeMod(..)))
572
    );
573
1
    assert!(
574
1
        membership
575
1
            .universe()
576
1
            .iter()
577
7
            .all(|expr| !matches!(expr, Expression::MinionWInSet(..)))
578
    );
579

            
580
1
    assert_eq!(SetPacked::compactness_score(domain.clone()).unwrap(), 6);
581
1
    assert_eq!(SetOccurrence::compactness_score(domain).unwrap(), 8);
582
1
}
583

            
584
#[test]
585
1
fn packed_set_supports_non_integer_elements() {
586
1
    let domain = Domain::set(SetAttr::new_min_max_size(1, 2), Domain::bool());
587
1
    let state = <SetPacked as ReprRule>::DomainLevel::init(domain).unwrap();
588
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Bool(true)]));
589

            
590
1
    let assignment = state.down(value.clone()).unwrap();
591
1
    assert_eq!(assignment.packed, Literal::Int(2));
592
1
    assert_eq!(assignment.up(), value);
593
1
}
594

            
595
#[test]
596
1
fn repetition_mset_round_trips_and_omits_fixed_size_marker() {
597
1
    let domain = Domain::mset(MSetAttr::new(range!(3), range!(0..2)), domain_int!(1..2));
598
1
    let state = <MSetRepetition as ReprRule>::DomainLevel::init(domain).unwrap();
599
1
    let value = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![
600
1
        Literal::Int(2),
601
1
        Literal::Int(1),
602
1
        Literal::Int(2),
603
1
    ]));
604

            
605
1
    let assignment = state.down(value).unwrap();
606
1
    assert!(assignment.mset_size.is_none());
607
1
    assert_eq!(
608
1
        assignment.up(),
609
1
        Literal::AbstractLiteral(AbstractLiteral::MSet(vec![
610
1
            Literal::Int(1),
611
1
            Literal::Int(2),
612
1
            Literal::Int(2),
613
1
        ]))
614
    );
615
1
}
616

            
617
#[test]
618
1
fn counts_mset_round_trips_distinct_values_and_multiplicities() {
619
1
    let domain = Domain::mset(MSetAttr::new(range!(0..4), range!(0..3)), domain_int!(0..2));
620
1
    let state = <MSetCounts as ReprRule>::DomainLevel::init(domain).unwrap();
621
1
    let value = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![
622
1
        Literal::Int(1),
623
1
        Literal::Int(1),
624
1
        Literal::Int(2),
625
1
    ]));
626

            
627
1
    let assignment = state.down(value.clone()).unwrap();
628
1
    assert_eq!(
629
1
        assignment.values_matrix,
630
1
        Literal::from(into_matrix!(vec![
631
1
            Literal::Int(1),
632
1
            Literal::Int(2),
633
1
            Literal::Int(0)
634
1
        ]))
635
    );
636
1
    assert_eq!(
637
1
        assignment.counts_matrix,
638
1
        Literal::from(into_matrix!(vec![
639
1
            Literal::Int(2),
640
1
            Literal::Int(1),
641
1
            Literal::Int(0)
642
1
        ]))
643
    );
644
1
    assert_eq!(assignment.up(), value);
645
1
}
646

            
647
#[test]
648
1
fn mset_min_occurrence_only_constrains_values_that_are_present() {
649
1
    let domain = Domain::mset(MSetAttr::new(range!(0..6), range!(2..3)), domain_int!(1..2));
650
1
    let valid = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![
651
1
        Literal::Int(1),
652
1
        Literal::Int(1),
653
1
    ]));
654
1
    let invalid = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![Literal::Int(1)]));
655

            
656
1
    let repetition = <MSetRepetition as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
657
1
    let counts = <MSetCounts as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
658
1
    let occurrence = <MSetOccurrence as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
659
1
    let packed = <MSetPacked as ReprRule>::DomainLevel::init(domain).unwrap();
660

            
661
1
    assert_eq!(repetition.down(valid.clone()).unwrap().up(), valid);
662
1
    assert!(repetition.down(invalid.clone()).is_err());
663
1
    assert_eq!(counts.down(valid.clone()).unwrap().up(), valid);
664
1
    assert!(counts.down(invalid.clone()).is_err());
665
1
    assert_eq!(occurrence.down(valid.clone()).unwrap().up(), valid);
666
1
    assert!(occurrence.down(invalid.clone()).is_err());
667
1
    assert_eq!(packed.down(valid.clone()).unwrap().up(), valid);
668
1
    assert!(packed.down(invalid).is_err());
669
1
}
670

            
671
#[test]
672
1
fn occurrence_mset_supports_non_integer_elements() {
673
1
    let domain = Domain::mset(
674
1
        MSetAttr::new(range!(0..3), range!(0..2)),
675
1
        Domain::tuple(vec![Domain::bool(), domain_int!(1..2)]),
676
1
    );
677
1
    let state = <MSetOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
678
1
    let element = Literal::AbstractLiteral(AbstractLiteral::Tuple(vec![
679
1
        Literal::Bool(true),
680
1
        Literal::Int(2),
681
1
    ]));
682
1
    let value = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![element.clone(), element]));
683

            
684
1
    assert_eq!(state.down(value.clone()).unwrap().up(), value);
685
1
    assert!(
686
1
        state
687
1
            .occurs
688
1
            .iter()
689
4
            .all(|(key, _)| matches!(key, Literal::AbstractLiteral(AbstractLiteral::Tuple(_))))
690
    );
691
1
}
692

            
693
#[test]
694
1
fn packed_mset_round_trips_mixed_radix_counts() {
695
1
    let domain = Domain::mset(MSetAttr::new(range!(1..4), range!(0..2)), Domain::bool());
696
1
    let state = <MSetPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
697
1
    let value = Literal::AbstractLiteral(AbstractLiteral::MSet(vec![
698
1
        Literal::Bool(false),
699
1
        Literal::Bool(true),
700
1
        Literal::Bool(true),
701
1
    ]));
702

            
703
1
    let assignment = state.down(value.clone()).unwrap();
704
1
    assert_eq!(assignment.packed, Literal::Int(7));
705
1
    assert_eq!(assignment.up(), value);
706
1
    assert_eq!(MSetPacked::compactness_score(domain).unwrap(), 8);
707
1
}
708

            
709
8
fn sequence_attr(size: conjure_cp::ast::Range<i32>) -> SequenceAttr {
710
8
    SequenceAttr {
711
8
        size,
712
8
        jectivity: conjure_cp::ast::JectivityAttr::None,
713
8
        representation: None,
714
8
    }
715
8
}
716

            
717
#[test]
718
1
fn explicit_sequence_round_trips_fixed_length() {
719
1
    let domain = Domain::sequence(sequence_attr(range!(2)), domain_int!(1..3));
720
1
    let state = <SequenceExplicit as ReprRule>::DomainLevel::init(domain).unwrap();
721
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Sequence(vec![
722
1
        Literal::Int(3),
723
1
        Literal::Int(1),
724
1
    ]));
725

            
726
1
    let assignment = state.down(value.clone()).unwrap();
727
1
    assert!(assignment.length.is_none());
728
1
    assert_eq!(assignment.up(), value);
729
1
}
730

            
731
#[test]
732
1
fn explicit_sequence_pads_variable_length_with_the_first_domain_value() {
733
1
    let domain = Domain::sequence(sequence_attr(range!(0..3)), domain_int!(1..3));
734
1
    let state = <SequenceExplicit as ReprRule>::DomainLevel::init(domain).unwrap();
735
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Sequence(vec![Literal::Int(2)]));
736

            
737
1
    let assignment = state.down(value.clone()).unwrap();
738
1
    assert_eq!(assignment.length, Some(Literal::Int(1)));
739
1
    let Literal::AbstractLiteral(AbstractLiteral::Matrix(elems, _)) = &assignment.values_matrix
740
    else {
741
        panic!("expected a matrix");
742
    };
743
    // Order is preserved; inactive positions are padded with the domain's first value (not
744
    // sorted, unlike a set's canonical symmetry-breaking padding).
745
1
    assert_eq!(
746
1
        elems,
747
1
        &vec![Literal::Int(2), Literal::Int(1), Literal::Int(1)]
748
    );
749
1
    assert_eq!(assignment.up(), value);
750
1
}
751

            
752
#[test]
753
1
fn packed_sequence_round_trips_and_preserves_order() {
754
1
    let domain = Domain::sequence(sequence_attr(range!(0..2)), domain_int!(1..3));
755
1
    let state = <SequencePacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
756
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Sequence(vec![
757
1
        Literal::Int(3),
758
1
        Literal::Int(1),
759
1
    ]));
760

            
761
1
    let assignment = state.down(value.clone()).unwrap();
762
1
    let packed = assignment.packed.clone();
763
1
    assert_eq!(assignment.up(), value);
764

            
765
    // A different order must encode to a different packed value.
766
1
    let reversed = Literal::AbstractLiteral(AbstractLiteral::Sequence(vec![
767
1
        Literal::Int(1),
768
1
        Literal::Int(3),
769
1
    ]));
770
1
    let reversed_assignment = state.down(reversed.clone()).unwrap();
771
1
    assert_ne!(packed, reversed_assignment.packed);
772
1
    assert_eq!(reversed_assignment.up(), reversed);
773

            
774
    // length digit (0..=2, radix 3) * two value digits (int(1..3), radix 3 each)
775
1
    assert_eq!(
776
1
        SequencePacked::compactness_score(domain).unwrap(),
777
1
        3 * 3 * 3
778
    );
779
1
}
780

            
781
#[test]
782
1
fn packed_sequence_supports_a_fixed_length_with_no_length_digit() {
783
1
    let domain = Domain::sequence(sequence_attr(range!(2)), Domain::bool());
784
1
    let state = <SequencePacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
785
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Sequence(vec![
786
1
        Literal::Bool(false),
787
1
        Literal::Bool(true),
788
1
    ]));
789

            
790
1
    let assignment = state.down(value.clone()).unwrap();
791
1
    assert_eq!(assignment.up(), value);
792
1
    assert_eq!(SequencePacked::compactness_score(domain).unwrap(), 4);
793
1
}
794

            
795
#[test]
796
1
fn explicit_sequence_rejects_unbounded_size() {
797
1
    let domain = Domain::sequence(
798
1
        sequence_attr(conjure_cp::ast::Range::Unbounded),
799
1
        domain_int!(1..3),
800
1
    );
801
1
    assert!(<SequenceExplicit as ReprRule>::DomainLevel::init(domain).is_err());
802
1
}
803

            
804
#[test]
805
1
fn relation_as_set_round_trips_binary_pairs() {
806
1
    let domain = Domain::relation(
807
1
        RelAttr {
808
1
            representation: None,
809
1
            size: range!(2),
810
1
            binary: vec![],
811
1
        },
812
1
        vec![domain_int!(1..3), domain_int!(4..6)],
813
1
    );
814
1
    let state = <RelationAsSet as ReprRule>::DomainLevel::init(domain).unwrap();
815
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![
816
1
        vec![Literal::Int(1), Literal::Int(4)],
817
1
        vec![Literal::Int(2), Literal::Int(5)],
818
1
    ]));
819

            
820
1
    let assignment = state.down(value.clone()).unwrap();
821
1
    assert_eq!(assignment.up(), value);
822
1
}
823

            
824
#[test]
825
1
fn relation_as_set_round_trips_ternary_relations() {
826
    // Arity is no longer restricted to binary; only binary *attributes* (reflexive/symmetric/
827
    // etc) require exactly two columns, checked separately below.
828
1
    let domain = Domain::relation(
829
1
        RelAttr {
830
1
            representation: None,
831
1
            size: range!(1),
832
1
            binary: vec![],
833
1
        },
834
1
        vec![domain_int!(1..3), domain_int!(1..3), domain_int!(1..3)],
835
1
    );
836
1
    let state = <RelationAsSet as ReprRule>::DomainLevel::init(domain).unwrap();
837
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![vec![
838
1
        Literal::Int(1),
839
1
        Literal::Int(2),
840
1
        Literal::Int(3),
841
1
    ]]));
842

            
843
1
    let assignment = state.down(value.clone()).unwrap();
844
1
    assert_eq!(assignment.up(), value);
845
1
}
846

            
847
#[test]
848
1
fn relation_as_set_rejects_binary_attributes_on_a_ternary_relation() {
849
1
    let domain = Domain::relation(
850
1
        RelAttr {
851
1
            representation: None,
852
1
            size: range!(1),
853
1
            binary: vec![BinaryAttr::Reflexive],
854
1
        },
855
1
        vec![domain_int!(1..3), domain_int!(1..3), domain_int!(1..3)],
856
1
    );
857
1
    assert!(<RelationAsSet as ReprRule>::DomainLevel::init(domain).is_err());
858
1
}
859

            
860
#[test]
861
1
fn relation_as_set_rejects_binary_attributes_when_columns_differ() {
862
1
    let domain = Domain::relation(
863
1
        RelAttr {
864
1
            representation: None,
865
1
            size: range!(1),
866
1
            binary: vec![BinaryAttr::Reflexive],
867
1
        },
868
1
        vec![domain_int!(1..3), domain_int!(4..6)],
869
1
    );
870
1
    assert!(<RelationAsSet as ReprRule>::DomainLevel::init(domain).is_err());
871
1
}
872

            
873
#[test]
874
1
fn relation_as_set_reflexive_builds_one_structural_constraint() {
875
1
    let domain = Domain::relation(
876
1
        RelAttr {
877
1
            representation: None,
878
1
            size: range!(3),
879
1
            binary: vec![BinaryAttr::Reflexive],
880
1
        },
881
1
        vec![domain_int!(1..3), domain_int!(1..3)],
882
1
    );
883
1
    let mut symbols = SymbolTable::new();
884
1
    let mut declaration = symbols.gen_find(&domain);
885
1
    let (_, constraints) = RelationAsSet::init_for(&mut declaration).unwrap();
886
1
    assert_eq!(constraints.len(), 1);
887
1
}
888

            
889
6
fn binary_attr_constraint_count(binary: Vec<BinaryAttr>) -> usize {
890
6
    let domain = Domain::relation(
891
6
        RelAttr {
892
6
            representation: None,
893
6
            size: range!(3),
894
6
            binary,
895
6
        },
896
6
        vec![domain_int!(1..3), domain_int!(1..3)],
897
6
    );
898
6
    let mut symbols = SymbolTable::new();
899
6
    let mut declaration = symbols.gen_find(&domain);
900
6
    let (_, constraints) = RelationAsSet::init_for(&mut declaration).unwrap();
901
6
    constraints.len()
902
6
}
903

            
904
#[test]
905
1
fn relation_left_total_and_right_total_each_build_one_structural_constraint() {
906
1
    assert_eq!(binary_attr_constraint_count(vec![BinaryAttr::LeftTotal]), 1);
907
1
    assert_eq!(
908
1
        binary_attr_constraint_count(vec![BinaryAttr::RightTotal]),
909
1
        1
910
    );
911
1
}
912

            
913
#[test]
914
1
fn relation_derived_binary_attrs_expand_to_their_base_formula_count() {
915
    // LinearOrder = Total + AntiSymmetric + Transitive
916
1
    assert_eq!(
917
1
        binary_attr_constraint_count(vec![BinaryAttr::LinearOrder]),
918
1
        3
919
    );
920
    // WeakOrder = Total + Transitive
921
1
    assert_eq!(binary_attr_constraint_count(vec![BinaryAttr::WeakOrder]), 2);
922
    // PreOrder = Reflexive + Transitive
923
1
    assert_eq!(binary_attr_constraint_count(vec![BinaryAttr::PreOrder]), 2);
924
    // StrictPartialOrder = Irreflexive + ASymmetric + Transitive
925
1
    assert_eq!(
926
1
        binary_attr_constraint_count(vec![BinaryAttr::StrictPartialOrder]),
927
1
        3
928
    );
929
1
}
930

            
931
#[test]
932
1
fn relation_occurrence_round_trips_binary_pairs() {
933
1
    let domain = Domain::relation(
934
1
        RelAttr {
935
1
            representation: None,
936
1
            size: range!(2),
937
1
            binary: vec![],
938
1
        },
939
1
        vec![domain_int!(1..3), domain_int!(1..2)],
940
1
    );
941
1
    let state = <RelationOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
942
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![
943
1
        vec![Literal::Int(1), Literal::Int(1)],
944
1
        vec![Literal::Int(2), Literal::Int(2)],
945
1
    ]));
946

            
947
1
    let assignment = state.down(value.clone()).unwrap();
948
1
    assert_eq!(assignment.up(), value);
949
1
}
950

            
951
#[test]
952
1
fn relation_occurrence_round_trips_ternary_relations() {
953
1
    let domain = Domain::relation(
954
1
        RelAttr {
955
1
            representation: None,
956
1
            size: range!(1),
957
1
            binary: vec![],
958
1
        },
959
1
        vec![domain_int!(1..2), domain_int!(1..2), Domain::bool()],
960
1
    );
961
1
    let state = <RelationOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
962
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![vec![
963
1
        Literal::Int(2),
964
1
        Literal::Int(1),
965
1
        Literal::Bool(true),
966
1
    ]]));
967

            
968
1
    let assignment = state.down(value.clone()).unwrap();
969
1
    assert_eq!(assignment.up(), value);
970
1
}
971

            
972
#[test]
973
1
fn relation_occurrence_rejects_a_compound_column() {
974
    // A set-typed column can't index a matrix; only `RelationAsSet` supports it.
975
1
    let domain = Domain::relation(
976
1
        RelAttr {
977
1
            representation: None,
978
1
            size: range!(1),
979
1
            binary: vec![],
980
1
        },
981
1
        vec![
982
1
            domain_int!(1..2),
983
1
            Domain::set(SetAttr::new(range!(2)), domain_int!(1..3)),
984
1
        ],
985
1
    );
986
1
    assert!(<RelationOccurrence as ReprRule>::DomainLevel::init(domain).is_err());
987
1
}
988

            
989
#[test]
990
1
fn relation_occurrence_reflexive_and_symmetric_build_two_structural_constraints_plus_cardinality() {
991
1
    let domain = Domain::relation(
992
1
        RelAttr {
993
1
            representation: None,
994
1
            size: conjure_cp::ast::Range::<i32>::Unbounded,
995
1
            binary: vec![BinaryAttr::Reflexive, BinaryAttr::Symmetric],
996
1
        },
997
1
        vec![domain_int!(1..3), domain_int!(1..3)],
998
1
    );
999
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = RelationOccurrence::init_for(&mut declaration).unwrap();
    // One cardinality range constraint (size is unbounded, so min != max) plus one formula per
    // binary attribute.
1
    assert_eq!(constraints.len(), 3);
1
}
#[test]
1
fn relation_occurrence_fixed_size_builds_an_equality_cardinality_constraint() {
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(2),
1
            binary: vec![],
1
        },
1
        vec![domain_int!(1..3), domain_int!(1..2)],
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = RelationOccurrence::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 1);
1
}
#[test]
1
fn relation_packed_round_trips_binary_pairs() {
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(2),
1
            binary: vec![],
1
        },
1
        vec![domain_int!(1..3), domain_int!(1..2)],
1
    );
1
    let state = <RelationPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![
1
        vec![Literal::Int(1), Literal::Int(1)],
1
        vec![Literal::Int(2), Literal::Int(2)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn relation_packed_round_trips_a_set_typed_column() {
    // More general than RelationOccurrence: any enumerable column works, not just
    // matrix-indexable ones, as long as the combined cartesian product stays small.
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(2),
1
            binary: vec![],
1
        },
1
        vec![
1
            domain_int!(1..2),
1
            Domain::set(SetAttr::new(range!(2)), domain_int!(1..3)),
1
        ],
1
    );
1
    let state = <RelationPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Relation(vec![
1
        vec![
1
            Literal::Int(1),
1
            Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(1), Literal::Int(2)])),
1
        ],
1
        vec![
1
            Literal::Int(1),
1
            Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(1), Literal::Int(3)])),
1
        ],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn relation_packed_rejects_a_too_large_cartesian_product() {
    // 6 columns of int(1..3) is 3^6 = 729 possible tuples, far over the 30-bit cap.
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(2),
1
            binary: vec![],
1
        },
1
        vec![domain_int!(1..3); 6],
1
    );
1
    assert!(<RelationPacked as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn relation_packed_reflexive_builds_a_cardinality_and_a_structural_constraint() {
    // Unlike RelationAsSet (which delegates cardinality entirely to set_decl's own eventual
    // representation), RelationPacked always emits its own cardinality constraint directly, so a
    // fixed size plus one binary attribute gives two constraints, not one.
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(3),
1
            binary: vec![BinaryAttr::Reflexive],
1
        },
1
        vec![domain_int!(1..3), domain_int!(1..3)],
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = RelationPacked::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 2);
1
}
#[test]
1
fn relation_packed_is_more_compact_than_occurrence_for_a_bounded_size() {
    // Both represent every possible relation value, so for an *unbounded* size their compactness
    // is mathematically identical (sum_k C(n,k) == 2^n); packed only wins once the size is
    // bounded narrowly enough for the binomial sum to undercut 2^n.
1
    let domain = Domain::relation(
1
        RelAttr {
1
            representation: None,
1
            size: range!(2),
1
            binary: vec![],
1
        },
1
        vec![domain_int!(1..3), domain_int!(1..3)],
1
    );
1
    assert!(
1
        RelationPacked::compactness_score(domain.clone()).unwrap()
1
            < RelationOccurrence::compactness_score(domain).unwrap()
    );
1
}
14
fn func_attr(
14
    size: conjure_cp::ast::Range<i32>,
14
    partiality: PartialityAttr,
14
    jectivity: JectivityAttr,
14
) -> FuncAttr {
14
    FuncAttr {
14
        representation: None,
14
        size,
14
        partiality,
14
        jectivity,
14
    }
14
}
#[test]
1
fn function_as_relation_round_trips_pairs() {
1
    let domain = Domain::function(
1
        func_attr(range!(0..2), PartialityAttr::Partial, JectivityAttr::None),
1
        domain_int!(1..5),
1
        Domain::bool(),
1
    );
1
    let state = <FunctionAsRelation as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![
1
        (Literal::Int(2), Literal::Bool(true)),
1
        (Literal::Int(4), Literal::Bool(false)),
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn function_as_relation_total_function_builds_only_the_well_formedness_constraint() {
1
    let domain = Domain::function(
1
        func_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            PartialityAttr::Total,
1
            JectivityAttr::None,
1
        ),
1
        domain_int!(1..3),
1
        Domain::bool(),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionAsRelation::init_for(&mut declaration).unwrap();
    // Well-formedness, and nothing else: totality follows from it together with the relation's
    // fixed size, and `image` reads the relation directly rather than a forward lookup table.
1
    assert_eq!(constraints.len(), 1);
1
}
#[test]
1
fn function_as_relation_bijective_adds_injective_and_surjective_constraints() {
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::Bijective),
1
        domain_int!(1..3),
1
        domain_int!(1..3),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionAsRelation::init_for(&mut declaration).unwrap();
    // well-formedness + injective (pairwise) + one surjectivity witness pair (In + Eq) per
    // codomain value.
1
    assert_eq!(constraints.len(), 1 + 1 + 2 * 3);
1
}
#[test]
1
fn explicit_function_round_trips_total_function() {
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::None),
1
        domain_int!(1..3),
1
        Domain::bool(),
1
    );
1
    let state = <FunctionExplicit as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![
1
        (Literal::Int(1), Literal::Bool(true)),
1
        (Literal::Int(2), Literal::Bool(false)),
1
        (Literal::Int(3), Literal::Bool(true)),
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn explicit_function_round_trips_partial_function_and_pads_undefined_positions() {
1
    let domain = Domain::function(
1
        func_attr(range!(0..2), PartialityAttr::Partial, JectivityAttr::None),
1
        domain_int!(1..3),
1
        domain_int!(10..12),
1
    );
1
    let state = <FunctionExplicit as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![(
1
        Literal::Int(2),
1
        Literal::Int(11),
1
    )]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn explicit_function_total_bijective_builds_alldiff_and_surjective_constraints() {
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::Bijective),
1
        domain_int!(1..3),
1
        domain_int!(1..3),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionExplicit::init_for(&mut declaration).unwrap();
    // one allDiff (injective, total) + one surjective "or" per codomain value.
1
    assert_eq!(constraints.len(), 1 + 3);
1
}
#[test]
1
fn explicit_function_partial_injective_builds_guarded_pairwise_constraints() {
1
    let domain = Domain::function(
1
        func_attr(
1
            range!(0..3),
1
            PartialityAttr::Partial,
1
            JectivityAttr::Injective,
1
        ),
1
        domain_int!(1..3),
1
        domain_int!(1..3),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionExplicit::init_for(&mut declaration).unwrap();
    // one cardinality bound (maxSize 3 is not Unbounded) + one canonical-padding constraint per
    // domain position + guarded pairwise over 3 domain positions: C(3,2) = 3 constraints.
1
    assert_eq!(constraints.len(), 1 + 3 + 3);
1
}
#[test]
1
fn compactness_prefers_function_as_relation_for_a_sparse_partial_function() {
    // A large domain (100 values) with at most 2 entries: the relation only needs to size
    // itself for those 2 entries, while the explicit matrix needs one atom per domain value.
1
    let domain = Domain::function(
1
        func_attr(range!(0..2), PartialityAttr::Partial, JectivityAttr::None),
1
        domain_int!(1..100),
1
        Domain::bool(),
1
    );
1
    assert!(
1
        FunctionAsRelation::compactness_score(domain.clone()).unwrap()
1
            < FunctionExplicit::compactness_score(domain).unwrap()
    );
1
}
#[test]
1
fn compactness_prefers_function_explicit_for_a_total_function() {
    // A small total function: both score similarly (entries == domain size), but explicit
    // avoids the relation's extra tuple-key overhead, so it should not score worse.
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::None),
1
        domain_int!(1..3),
1
        Domain::bool(),
1
    );
1
    assert!(
1
        FunctionExplicit::compactness_score(domain.clone()).unwrap()
1
            <= FunctionAsRelation::compactness_score(domain).unwrap()
    );
1
}
22
fn partition_attr(
22
    num_parts: conjure_cp::ast::Range<i32>,
22
    part_len: conjure_cp::ast::Range<i32>,
22
    is_regular: bool,
22
) -> PartitionAttr {
22
    PartitionAttr {
22
        representation: None,
22
        num_parts,
22
        part_len,
22
        is_regular,
22
    }
22
}
#[test]
1
fn partition_as_set_round_trips_values() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), true),
1
        domain_int!(1..6),
1
    );
1
    let state = <PartitionAsSet as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_as_set_round_trips_a_set_typed_inner_domain() {
1
    let inner = Domain::set(SetAttr::new(range!(1)), domain_int!(1..3));
1
    let domain = Domain::partition(partition_attr(range!(1), range!(2), false), inner);
1
    let state = <PartitionAsSet as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![vec![
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(1)])),
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(2)])),
1
    ]]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_as_set_rejects_a_non_partition_domain() {
1
    let domain = domain_int!(1..3);
1
    assert!(<PartitionAsSet as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn partition_as_set_non_regular_builds_exactly_once_and_non_empty_constraints() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, false),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionAsSet::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 2);
1
}
#[test]
1
fn partition_as_set_regular_infers_a_fixed_part_size_from_a_fixed_num_parts() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, true),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionAsSet::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 2);
1
}
#[test]
1
fn partition_as_set_regular_infers_a_fixed_num_parts_from_a_fixed_part_size() {
1
    let domain = Domain::partition(
1
        partition_attr(conjure_cp::ast::Range::Unbounded, range!(3), true),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionAsSet::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 2);
1
}
#[test]
1
fn partition_as_set_regular_adds_a_third_constraint_when_part_size_is_not_fixed() {
    // Neither `numParts` nor `partSize` is pinned to a single value here, so the regular ->
    // fixed-size inference (`numParts` or `partSize` singleton + `regular` forces the other)
    // cannot kick in, and the pairwise "all parts equal size" constraint is genuinely needed.
1
    let domain = Domain::partition(
1
        partition_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            conjure_cp::ast::Range::Unbounded,
1
            true,
1
        ),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionAsSet::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 3);
1
}
#[test]
1
fn partition_as_set_regular_is_redundant_when_part_size_is_already_fixed() {
    // `regular` is automatically implied once `partSize` fixes an exact size, so stating it
    // again should not add a third structural constraint (mirrors Conjure's own `fixedPartSize`
    // guard in `PartitionAsSet.hs`).
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), true),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionAsSet::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 2);
1
}
#[test]
1
fn partition_occurrence_round_trips_values() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), true),
1
        domain_int!(1..6),
1
    );
1
    let state = <PartitionOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_occurrence_canonicalises_part_order() {
    // Parts are relabelled in first-occurrence (domain scan) order, regardless of the order the
    // literal itself lists them in -- this must agree with `symmetry_breaking_expr`'s own
    // ordering, or a valid literal could fail the very constraint meant to allow it.
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), false),
1
        domain_int!(1..6),
1
    );
1
    let state = <PartitionOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let given = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
    ]));
1
    let canonical = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
    ]));
1
    let assignment = state.down(given).unwrap();
1
    assert_eq!(assignment.up(), canonical);
1
}
#[test]
1
fn partition_occurrence_round_trips_uneven_part_sizes() {
1
    let domain = Domain::partition(
1
        partition_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            conjure_cp::ast::Range::Unbounded,
1
            false,
1
        ),
1
        domain_int!(1..5),
1
    );
1
    let state = <PartitionOccurrence as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1)],
1
        vec![Literal::Int(2), Literal::Int(3), Literal::Int(4)],
1
        vec![Literal::Int(5)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_occurrence_rejects_a_non_partition_domain() {
1
    let domain = domain_int!(1..3);
1
    assert!(<PartitionOccurrence as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn partition_occurrence_rejects_a_non_int_inner_domain() {
1
    let inner = Domain::set(SetAttr::new(range!(1)), domain_int!(1..3));
1
    let domain = Domain::partition(partition_attr(range!(1), range!(2), false), inner);
1
    assert!(<PartitionOccurrence as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn partition_occurrence_non_regular_builds_five_structural_constraints() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, false),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionOccurrence::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 5);
1
}
#[test]
1
fn partition_occurrence_regular_adds_a_sixth_constraint_when_part_size_is_not_fixed() {
1
    let domain = Domain::partition(
1
        partition_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            conjure_cp::ast::Range::Unbounded,
1
            true,
1
        ),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionOccurrence::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 6);
1
}
#[test]
1
fn partition_occurrence_regular_infers_a_fixed_part_size_from_a_fixed_num_parts() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, true),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionOccurrence::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 5);
1
}
#[test]
1
fn partition_packed_round_trips_values() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), true),
1
        domain_int!(1..6),
1
    );
1
    let state = <PartitionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_packed_canonicalises_part_order() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), range!(3), false),
1
        domain_int!(1..6),
1
    );
1
    let state = <PartitionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let given = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
    ]));
1
    let canonical = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1), Literal::Int(2), Literal::Int(3)],
1
        vec![Literal::Int(4), Literal::Int(5), Literal::Int(6)],
1
    ]));
1
    let assignment = state.down(given).unwrap();
1
    assert_eq!(assignment.up(), canonical);
1
}
#[test]
1
fn partition_packed_round_trips_uneven_part_sizes() {
1
    let domain = Domain::partition(
1
        partition_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            conjure_cp::ast::Range::Unbounded,
1
            false,
1
        ),
1
        domain_int!(1..5),
1
    );
1
    let state = <PartitionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![
1
        vec![Literal::Int(1)],
1
        vec![Literal::Int(2), Literal::Int(3), Literal::Int(4)],
1
        vec![Literal::Int(5)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_packed_round_trips_a_set_typed_inner_domain() {
    // Unlike PartitionOccurrence, packing only needs an enumerable inner domain, not a
    // matrix-indexable one. The inner domain has exactly 2 values ({1} and {2}), so a single
    // size-2 part covers it completely.
1
    let inner = Domain::set(SetAttr::new(range!(1)), domain_int!(1..2));
1
    let domain = Domain::partition(partition_attr(range!(1), range!(2), false), inner);
1
    let state = <PartitionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Partition(vec![vec![
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(1)])),
1
        Literal::AbstractLiteral(AbstractLiteral::Set(vec![Literal::Int(2)])),
1
    ]]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn partition_packed_rejects_a_non_partition_domain() {
1
    let domain = domain_int!(1..3);
1
    assert!(<PartitionPacked as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn partition_packed_non_regular_builds_five_structural_constraints() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, false),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionPacked::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 5);
1
}
#[test]
1
fn partition_packed_regular_adds_a_sixth_constraint_when_part_size_is_not_fixed() {
1
    let domain = Domain::partition(
1
        partition_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            conjure_cp::ast::Range::Unbounded,
1
            true,
1
        ),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionPacked::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 6);
1
}
#[test]
1
fn partition_packed_regular_infers_a_fixed_part_size_from_a_fixed_num_parts() {
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, true),
1
        domain_int!(1..6),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PartitionPacked::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 5);
1
}
#[test]
1
fn partition_packed_is_more_compact_than_occurrence_for_a_bounded_size() {
    // Both use the same maxNumParts, but PartitionOccurrence's WhichPart lives in its own
    // n-cell matrix domain (compactness counts each cell), while PartitionPacked folds the same
    // n digits into one int -- so packed's domain-size compactness score should never exceed
    // occurrence's for the same partition domain.
1
    let domain = Domain::partition(
1
        partition_attr(range!(2), conjure_cp::ast::Range::Unbounded, false),
1
        domain_int!(1..6),
1
    );
1
    assert!(
1
        PartitionPacked::compactness_score(domain.clone()).unwrap()
1
            <= PartitionOccurrence::compactness_score(domain).unwrap()
    );
1
}
6
fn permutation_attr(num_moved: conjure_cp::ast::Range<i32>) -> PermutationAttr {
6
    PermutationAttr {
6
        representation: None,
6
        num_moved,
6
    }
6
}
#[test]
1
fn permutation_as_function_round_trips_a_single_cycle() {
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..5),
1
    );
1
    let state = <PermutationAsFunction as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Permutation(vec![vec![
1
        Literal::Int(1),
1
        Literal::Int(3),
1
        Literal::Int(5),
1
    ]]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn permutation_as_function_round_trips_multiple_disjoint_cycles() {
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..6),
1
    );
1
    let state = <PermutationAsFunction as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Permutation(vec![
1
        vec![Literal::Int(1), Literal::Int(2)],
1
        vec![Literal::Int(3), Literal::Int(4), Literal::Int(5)],
1
    ]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn permutation_as_function_round_trips_the_identity() {
    // Every element is an (implicit) fixed point: the literal has no cycles at all.
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..4),
1
    );
1
    let state = <PermutationAsFunction as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Permutation(vec![]));
1
    let assignment = state.down(value.clone()).unwrap();
1
    assert_eq!(assignment.up(), value);
1
}
#[test]
1
fn permutation_as_function_rejects_a_cycle_element_outside_its_domain() {
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..3),
1
    );
1
    let state = <PermutationAsFunction as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Permutation(vec![vec![
1
        Literal::Int(1),
1
        Literal::Int(99),
1
    ]]));
1
    assert!(state.down(value).is_err());
1
}
#[test]
1
fn permutation_as_function_rejects_an_element_appearing_in_two_cycles() {
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..4),
1
    );
1
    let state = <PermutationAsFunction as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Permutation(vec![
1
        vec![Literal::Int(1), Literal::Int(2)],
1
        vec![Literal::Int(2), Literal::Int(3)],
1
    ]));
1
    assert!(state.down(value).is_err());
1
}
#[test]
1
fn permutation_as_function_rejects_a_non_permutation_domain() {
1
    let domain = domain_int!(1..3);
1
    assert!(<PermutationAsFunction as ReprRule>::DomainLevel::init(domain).is_err());
1
}
#[test]
1
fn permutation_as_function_builds_three_structural_constraints() {
    // numMoved's range check, plus the two round-trip constraints (backwards . forwards = id and
    // forwards . backwards = id). Nested constraints from whatever representation gets chosen for
    // the forwards/backwards Function-typed aux declarations themselves are added by a later pass
    // over those new declarations, not eagerly folded into this count.
1
    let domain = Domain::permutation(
1
        permutation_attr(conjure_cp::ast::Range::Unbounded),
1
        domain_int!(1..4),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = PermutationAsFunction::init_for(&mut declaration).unwrap();
1
    assert_eq!(constraints.len(), 3);
1
}
#[test]
1
fn packed_function_round_trips_a_total_function() {
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::None),
1
        domain_int!(1..3),
1
        domain_int!(7..8),
1
    );
1
    let state = <FunctionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![
1
        (Literal::Int(1), Literal::Int(8)),
1
        (Literal::Int(2), Literal::Int(7)),
1
        (Literal::Int(3), Literal::Int(8)),
1
    ]));
1
    let down = state.down(value.clone()).unwrap();
1
    assert!(matches!(down.packed, Literal::Int(_)));
1
    assert_eq!(down.up(), value);
1
}
/// A partial function's digits carry definedness themselves: digit 0 means "defines nothing here",
/// so the domain has exactly `(codomain + 1) ** |domain|` values and an undefined key round-trips
/// by being absent rather than by holding some padding value.
#[test]
1
fn packed_function_round_trips_a_partial_function() {
1
    let domain = Domain::function(
1
        func_attr(
1
            conjure_cp::ast::Range::Unbounded,
1
            PartialityAttr::Partial,
1
            JectivityAttr::None,
1
        ),
1
        domain_int!(1..3),
1
        domain_int!(7..8),
1
    );
1
    let state = <FunctionPacked as ReprRule>::DomainLevel::init(domain.clone()).unwrap();
1
    assert_eq!(FunctionPacked::compactness_score(domain).unwrap(), 27);
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![
1
        (Literal::Int(1), Literal::Int(8)),
1
        (Literal::Int(3), Literal::Int(7)),
1
    ]));
1
    let down = state.down(value.clone()).unwrap();
1
    assert_eq!(down.up(), value);
1
}
#[test]
1
fn packed_function_rejects_a_total_function_missing_a_key() {
1
    let domain = Domain::function(
1
        func_attr(range!(3), PartialityAttr::Total, JectivityAttr::None),
1
        domain_int!(1..3),
1
        domain_int!(7..8),
1
    );
1
    let state = <FunctionPacked as ReprRule>::DomainLevel::init(domain).unwrap();
1
    let value = Literal::AbstractLiteral(AbstractLiteral::Function(vec![(
1
        Literal::Int(1),
1
        Literal::Int(8),
1
    )]));
1
    assert!(state.down(value).is_err());
1
}
#[test]
1
fn packed_function_partial_builds_a_cardinality_constraint() {
1
    let domain = Domain::function(
1
        func_attr(range!(2), PartialityAttr::Partial, JectivityAttr::None),
1
        domain_int!(1..3),
1
        domain_int!(7..8),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionPacked::init_for(&mut declaration).unwrap();
    // Exactly the `size` constraint: nothing else is structural for a plain partial function.
1
    assert_eq!(constraints.len(), 1);
1
}
#[test]
1
fn packed_function_bijective_builds_injective_and_surjective_constraints() {
1
    let domain = Domain::function(
1
        func_attr(range!(2), PartialityAttr::Total, JectivityAttr::Bijective),
1
        domain_int!(1..2),
1
        domain_int!(7..8),
1
    );
1
    let mut symbols = SymbolTable::new();
1
    let mut declaration = symbols.gen_find(&domain);
1
    let (_, constraints) = FunctionPacked::init_for(&mut declaration).unwrap();
    // One pairwise distinctness constraint, plus one coverage constraint per codomain value.
1
    assert_eq!(constraints.len(), 1 + 2);
1
}