Skip to main content

paramodel_elements/
constraint.rs

1// Copyright (c) Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Declarative constraint algebra.
5//!
6//! Each parameter kind has its own constraint enum with kind-specific
7//! leaves plus the shared Boolean combinators (`And`, `Or`, `Not`,
8//! `Always`, `Never`; `BoolConstraint` drops `And`/`Or` since the
9//! underlying value only takes two states). The outer [`Constraint`]
10//! enum discriminates by kind and dispatches `test` by native payload.
11//!
12//! Canonicalisation (per SRD-0004 D5) collapses identity elements,
13//! flattens nested same-kind combinators, peels double negation, and
14//! deduplicates children. The final sort step that gives us
15//! fingerprint-stable canonical forms requires a total order over every
16//! leaf (including `f64` leaves); that lands alongside constraint
17//! fingerprinting in a follow-up slice. Until then [`canonicalize`]
18//! does everything except sort.
19//!
20//! [`canonicalize`]: IntConstraint::canonicalize
21
22use std::collections::BTreeSet;
23
24use indexmap::IndexSet;
25use serde::{Deserialize, Serialize};
26
27use crate::domain::RegexPattern;
28use crate::fingerprint::Fingerprint;
29use crate::value::{SelectionItem, Value};
30
31// ---------------------------------------------------------------------------
32// Shared helpers for Boolean-algebra canonicalisation.
33// ---------------------------------------------------------------------------
34
35/// Remove adjacent duplicates using `PartialEq`.
36///
37/// A replacement for `Vec::dedup` that doesn't require `Ord`. After the
38/// canonical sort runs, equal children are adjacent, so this collapses
39/// every duplicate.
40fn dedup_partial_eq<T: PartialEq>(xs: &mut Vec<T>) {
41    let mut i = 1;
42    while i < xs.len() {
43        if xs[i] == xs[i - 1] {
44            xs.remove(i);
45        } else {
46            i += 1;
47        }
48    }
49}
50
51// ---------------------------------------------------------------------------
52// Canonical byte encoding — shared primitive helpers.
53// ---------------------------------------------------------------------------
54//
55// Every constraint serialises to a stable byte form via write_canonical.
56// Per-kind variant tags live in disjoint byte ranges (Int = 0x10..=0x1A,
57// Double = 0x20..=0x27, Bool = 0x30..=0x33, String = 0x40..=0x49,
58// Selection = 0x50..=0x59), so the outer Constraint's canonical form
59// needs no additional discriminator: the first byte already identifies
60// the kind. The byte layout is the source of truth for both sort order
61// (canonicalisation) and fingerprints.
62
63const CANONICAL_NAN_BITS: u64 = 0x7ff8_0000_0000_0000;
64
65fn write_u32_le(out: &mut Vec<u8>, v: u32) {
66    out.extend_from_slice(&v.to_le_bytes());
67}
68
69fn write_i64_le(out: &mut Vec<u8>, v: i64) {
70    out.extend_from_slice(&v.to_le_bytes());
71}
72
73fn write_f64_le_canonical(out: &mut Vec<u8>, v: f64) {
74    // Fold every NaN payload to the canonical quiet NaN so equivalent
75    // floats serialise identically.
76    let v = if v.is_nan() { f64::from_bits(CANONICAL_NAN_BITS) } else { v };
77    out.extend_from_slice(&v.to_le_bytes());
78}
79
80fn write_str_len_prefixed(out: &mut Vec<u8>, s: &str) {
81    let len = u32::try_from(s.len()).expect("string length fits in u32");
82    write_u32_le(out, len);
83    out.extend_from_slice(s.as_bytes());
84}
85
86fn write_i64_set(out: &mut Vec<u8>, values: &BTreeSet<i64>) {
87    write_u32_le(out, u32::try_from(values.len()).expect("set size fits in u32"));
88    for v in values {
89        write_i64_le(out, *v);
90    }
91}
92
93fn write_string_set(out: &mut Vec<u8>, values: &BTreeSet<String>) {
94    write_u32_le(out, u32::try_from(values.len()).expect("set size fits in u32"));
95    for v in values {
96        write_str_len_prefixed(out, v);
97    }
98}
99
100fn write_selection_item_set(out: &mut Vec<u8>, values: &BTreeSet<SelectionItem>) {
101    write_u32_le(out, u32::try_from(values.len()).expect("set size fits in u32"));
102    for v in values {
103        write_str_len_prefixed(out, v.as_str());
104    }
105}
106
107// ---------------------------------------------------------------------------
108// IntConstraint.
109// ---------------------------------------------------------------------------
110
111/// Integer-value constraint.
112///
113/// Variants track SRD-0004 §Constraint algebra. `Multiple(0)` is a legal
114/// representation but always tests `false`; the constructor [`Self::multiple`]
115/// rejects zero so callers that go through the helper never produce it.
116#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
117#[serde(tag = "op", rename_all = "snake_case")]
118pub enum IntConstraint {
119    /// Always satisfied.
120    Always,
121    /// Never satisfied.
122    Never,
123    /// `value >= n`.
124    Min {
125        /// The inclusive lower bound.
126        n: i64,
127    },
128    /// `value <= n`.
129    Max {
130        /// The inclusive upper bound.
131        n: i64,
132    },
133    /// `min <= value <= max`.
134    Range {
135        /// Inclusive lower bound.
136        min: i64,
137        /// Inclusive upper bound.
138        max: i64,
139    },
140    /// `value` is one of these.
141    InSet {
142        /// The permitted values.
143        values: BTreeSet<i64>,
144    },
145    /// `value` is none of these.
146    NotInSet {
147        /// The forbidden values.
148        values: BTreeSet<i64>,
149    },
150    /// `value % n == 0`. Always `false` for `n == 0`.
151    Multiple {
152        /// The divisor.
153        n: i64,
154    },
155    /// All children must hold.
156    And {
157        /// Conjuncts.
158        children: Vec<Self>,
159    },
160    /// At least one child must hold.
161    Or {
162        /// Disjuncts.
163        children: Vec<Self>,
164    },
165    /// Child must not hold.
166    Not {
167        /// The negated child.
168        child: Box<Self>,
169    },
170}
171
172impl IntConstraint {
173    /// Construct a `Multiple` leaf. Rejects `n == 0`.
174    #[must_use]
175    pub const fn multiple(n: i64) -> Option<Self> {
176        if n == 0 {
177            None
178        } else {
179            Some(Self::Multiple { n })
180        }
181    }
182
183    /// Test a candidate value.
184    #[must_use]
185    pub fn test(&self, value: i64) -> bool {
186        match self {
187            Self::Always => true,
188            Self::Never => false,
189            Self::Min { n } => value >= *n,
190            Self::Max { n } => value <= *n,
191            Self::Range { min, max } => value >= *min && value <= *max,
192            Self::InSet { values } => values.contains(&value),
193            Self::NotInSet { values } => !values.contains(&value),
194            Self::Multiple { n } => *n != 0 && value % *n == 0,
195            Self::And { children } => children.iter().all(|c| c.test(value)),
196            Self::Or { children } => children.iter().any(|c| c.test(value)),
197            Self::Not { child } => !child.test(value),
198        }
199    }
200
201    /// Conjunction, flattening nested `And`.
202    #[must_use]
203    pub fn and(self, rhs: Self) -> Self {
204        let mut children = Vec::new();
205        absorb_and_int(&mut children, self);
206        absorb_and_int(&mut children, rhs);
207        Self::And { children }
208    }
209
210    /// Disjunction, flattening nested `Or`.
211    #[must_use]
212    pub fn or(self, rhs: Self) -> Self {
213        let mut children = Vec::new();
214        absorb_or_int(&mut children, self);
215        absorb_or_int(&mut children, rhs);
216        Self::Or { children }
217    }
218
219    /// Collapse identities, peel double negation, flatten same-kind
220    /// combinators, sort `And`/`Or` children by canonical bytes, and
221    /// dedup.
222    #[must_use]
223    pub fn canonicalize(self) -> Self {
224        match self {
225            Self::Not { child } => match child.canonicalize() {
226                Self::Not { child: inner } => *inner,
227                Self::Always => Self::Never,
228                Self::Never => Self::Always,
229                other => Self::Not {
230                    child: Box::new(other),
231                },
232            },
233            Self::And { children } => {
234                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
235                for c in children {
236                    match c.canonicalize() {
237                        Self::Always => {}
238                        Self::Never => return Self::Never,
239                        Self::And { children: sub } => flat.extend(sub),
240                        other => flat.push(other),
241                    }
242                }
243                flat.sort_by_cached_key(Self::canonical_bytes);
244                dedup_partial_eq(&mut flat);
245                match flat.len() {
246                    0 => Self::Always,
247                    1 => flat.pop().expect("len == 1"),
248                    _ => Self::And { children: flat },
249                }
250            }
251            Self::Or { children } => {
252                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
253                for c in children {
254                    match c.canonicalize() {
255                        Self::Never => {}
256                        Self::Always => return Self::Always,
257                        Self::Or { children: sub } => flat.extend(sub),
258                        other => flat.push(other),
259                    }
260                }
261                flat.sort_by_cached_key(Self::canonical_bytes);
262                dedup_partial_eq(&mut flat);
263                match flat.len() {
264                    0 => Self::Never,
265                    1 => flat.pop().expect("len == 1"),
266                    _ => Self::Or { children: flat },
267                }
268            }
269            leaf => leaf,
270        }
271    }
272
273    /// Canonical byte form of this constraint *as written*.
274    ///
275    /// Two constraints with the same canonical bytes are structurally
276    /// identical. [`Self::fingerprint`] canonicalises first and is
277    /// what you want for semantic identity.
278    #[must_use]
279    pub fn canonical_bytes(&self) -> Vec<u8> {
280        let mut out = Vec::new();
281        self.write_canonical(&mut out);
282        out
283    }
284
285    /// Canonical fingerprint — semantically identical constraints hash
286    /// to the same value. Auto-canonicalises the tree before hashing.
287    #[must_use]
288    pub fn fingerprint(&self) -> Fingerprint {
289        let canonical = self.clone().canonicalize();
290        Fingerprint::of(&canonical.canonical_bytes())
291    }
292
293    fn write_canonical(&self, out: &mut Vec<u8>) {
294        const T_ALWAYS:   u8 = 0x10;
295        const T_NEVER:    u8 = 0x11;
296        const T_MIN:      u8 = 0x12;
297        const T_MAX:      u8 = 0x13;
298        const T_RANGE:    u8 = 0x14;
299        const T_INSET:    u8 = 0x15;
300        const T_NOTINSET: u8 = 0x16;
301        const T_MULTIPLE: u8 = 0x17;
302        const T_AND:      u8 = 0x18;
303        const T_OR:       u8 = 0x19;
304        const T_NOT:      u8 = 0x1A;
305
306        match self {
307            Self::Always => out.push(T_ALWAYS),
308            Self::Never => out.push(T_NEVER),
309            Self::Min { n } => {
310                out.push(T_MIN);
311                write_i64_le(out, *n);
312            }
313            Self::Max { n } => {
314                out.push(T_MAX);
315                write_i64_le(out, *n);
316            }
317            Self::Range { min, max } => {
318                out.push(T_RANGE);
319                write_i64_le(out, *min);
320                write_i64_le(out, *max);
321            }
322            Self::InSet { values } => {
323                out.push(T_INSET);
324                write_i64_set(out, values);
325            }
326            Self::NotInSet { values } => {
327                out.push(T_NOTINSET);
328                write_i64_set(out, values);
329            }
330            Self::Multiple { n } => {
331                out.push(T_MULTIPLE);
332                write_i64_le(out, *n);
333            }
334            Self::And { children } => {
335                out.push(T_AND);
336                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
337                for c in children {
338                    c.write_canonical(out);
339                }
340            }
341            Self::Or { children } => {
342                out.push(T_OR);
343                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
344                for c in children {
345                    c.write_canonical(out);
346                }
347            }
348            Self::Not { child } => {
349                out.push(T_NOT);
350                child.write_canonical(out);
351            }
352        }
353    }
354}
355
356impl std::ops::Not for IntConstraint {
357    type Output = Self;
358    fn not(self) -> Self {
359        Self::Not {
360            child: Box::new(self),
361        }
362    }
363}
364
365fn absorb_and_int(into: &mut Vec<IntConstraint>, c: IntConstraint) {
366    match c {
367        IntConstraint::And { children } => into.extend(children),
368        other => into.push(other),
369    }
370}
371
372fn absorb_or_int(into: &mut Vec<IntConstraint>, c: IntConstraint) {
373    match c {
374        IntConstraint::Or { children } => into.extend(children),
375        other => into.push(other),
376    }
377}
378
379// ---------------------------------------------------------------------------
380// DoubleConstraint.
381// ---------------------------------------------------------------------------
382
383/// Double-precision float constraint.
384///
385/// No `InSet` or `Multiple` — those are ill-defined on floats (equality
386/// is brittle, modulo is non-sensical).
387#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
388#[serde(tag = "op", rename_all = "snake_case")]
389pub enum DoubleConstraint {
390    /// Always satisfied.
391    Always,
392    /// Never satisfied.
393    Never,
394    /// `value >= n`.
395    Min {
396        /// Inclusive lower bound.
397        n: f64,
398    },
399    /// `value <= n`.
400    Max {
401        /// Inclusive upper bound.
402        n: f64,
403    },
404    /// `min <= value <= max`.
405    Range {
406        /// Inclusive lower bound.
407        min: f64,
408        /// Inclusive upper bound.
409        max: f64,
410    },
411    /// All children must hold.
412    And {
413        /// Conjuncts.
414        children: Vec<Self>,
415    },
416    /// At least one child must hold.
417    Or {
418        /// Disjuncts.
419        children: Vec<Self>,
420    },
421    /// Child must not hold.
422    Not {
423        /// The negated child.
424        child: Box<Self>,
425    },
426}
427
428impl DoubleConstraint {
429    /// Test a candidate value. `NaN` inputs always fail.
430    #[must_use]
431    pub fn test(&self, value: f64) -> bool {
432        if value.is_nan() {
433            return false;
434        }
435        match self {
436            Self::Always => true,
437            Self::Never => false,
438            Self::Min { n } => value >= *n,
439            Self::Max { n } => value <= *n,
440            Self::Range { min, max } => value >= *min && value <= *max,
441            Self::And { children } => children.iter().all(|c| c.test(value)),
442            Self::Or { children } => children.iter().any(|c| c.test(value)),
443            Self::Not { child } => !child.test(value),
444        }
445    }
446
447    /// Conjunction, flattening nested `And`.
448    #[must_use]
449    pub fn and(self, rhs: Self) -> Self {
450        let mut children = Vec::new();
451        absorb_and_double(&mut children, self);
452        absorb_and_double(&mut children, rhs);
453        Self::And { children }
454    }
455
456    /// Disjunction, flattening nested `Or`.
457    #[must_use]
458    pub fn or(self, rhs: Self) -> Self {
459        let mut children = Vec::new();
460        absorb_or_double(&mut children, self);
461        absorb_or_double(&mut children, rhs);
462        Self::Or { children }
463    }
464
465    /// Same rule set as [`IntConstraint::canonicalize`].
466    #[must_use]
467    pub fn canonicalize(self) -> Self {
468        match self {
469            Self::Not { child } => match child.canonicalize() {
470                Self::Not { child: inner } => *inner,
471                Self::Always => Self::Never,
472                Self::Never => Self::Always,
473                other => Self::Not {
474                    child: Box::new(other),
475                },
476            },
477            Self::And { children } => {
478                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
479                for c in children {
480                    match c.canonicalize() {
481                        Self::Always => {}
482                        Self::Never => return Self::Never,
483                        Self::And { children: sub } => flat.extend(sub),
484                        other => flat.push(other),
485                    }
486                }
487                flat.sort_by_cached_key(Self::canonical_bytes);
488                dedup_partial_eq(&mut flat);
489                match flat.len() {
490                    0 => Self::Always,
491                    1 => flat.pop().expect("len == 1"),
492                    _ => Self::And { children: flat },
493                }
494            }
495            Self::Or { children } => {
496                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
497                for c in children {
498                    match c.canonicalize() {
499                        Self::Never => {}
500                        Self::Always => return Self::Always,
501                        Self::Or { children: sub } => flat.extend(sub),
502                        other => flat.push(other),
503                    }
504                }
505                flat.sort_by_cached_key(Self::canonical_bytes);
506                dedup_partial_eq(&mut flat);
507                match flat.len() {
508                    0 => Self::Never,
509                    1 => flat.pop().expect("len == 1"),
510                    _ => Self::Or { children: flat },
511                }
512            }
513            leaf => leaf,
514        }
515    }
516
517    /// Canonical byte form of this constraint *as written*.
518    #[must_use]
519    pub fn canonical_bytes(&self) -> Vec<u8> {
520        let mut out = Vec::new();
521        self.write_canonical(&mut out);
522        out
523    }
524
525    /// Canonical fingerprint — auto-canonicalises first. `NaN` leaves
526    /// are folded to the canonical quiet-NaN pattern before hashing.
527    #[must_use]
528    pub fn fingerprint(&self) -> Fingerprint {
529        let canonical = self.clone().canonicalize();
530        Fingerprint::of(&canonical.canonical_bytes())
531    }
532
533    fn write_canonical(&self, out: &mut Vec<u8>) {
534        const T_ALWAYS: u8 = 0x20;
535        const T_NEVER:  u8 = 0x21;
536        const T_MIN:    u8 = 0x22;
537        const T_MAX:    u8 = 0x23;
538        const T_RANGE:  u8 = 0x24;
539        const T_AND:    u8 = 0x25;
540        const T_OR:     u8 = 0x26;
541        const T_NOT:    u8 = 0x27;
542
543        match self {
544            Self::Always => out.push(T_ALWAYS),
545            Self::Never => out.push(T_NEVER),
546            Self::Min { n } => {
547                out.push(T_MIN);
548                write_f64_le_canonical(out, *n);
549            }
550            Self::Max { n } => {
551                out.push(T_MAX);
552                write_f64_le_canonical(out, *n);
553            }
554            Self::Range { min, max } => {
555                out.push(T_RANGE);
556                write_f64_le_canonical(out, *min);
557                write_f64_le_canonical(out, *max);
558            }
559            Self::And { children } => {
560                out.push(T_AND);
561                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
562                for c in children {
563                    c.write_canonical(out);
564                }
565            }
566            Self::Or { children } => {
567                out.push(T_OR);
568                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
569                for c in children {
570                    c.write_canonical(out);
571                }
572            }
573            Self::Not { child } => {
574                out.push(T_NOT);
575                child.write_canonical(out);
576            }
577        }
578    }
579}
580
581impl std::ops::Not for DoubleConstraint {
582    type Output = Self;
583    fn not(self) -> Self {
584        Self::Not {
585            child: Box::new(self),
586        }
587    }
588}
589
590fn absorb_and_double(into: &mut Vec<DoubleConstraint>, c: DoubleConstraint) {
591    match c {
592        DoubleConstraint::And { children } => into.extend(children),
593        other => into.push(other),
594    }
595}
596
597fn absorb_or_double(into: &mut Vec<DoubleConstraint>, c: DoubleConstraint) {
598    match c {
599        DoubleConstraint::Or { children } => into.extend(children),
600        other => into.push(other),
601    }
602}
603
604// ---------------------------------------------------------------------------
605// BoolConstraint.
606// ---------------------------------------------------------------------------
607
608/// Boolean-value constraint.
609///
610/// No `And`/`Or` — with only two possible values every conjunction /
611/// disjunction is either a tautology, a contradiction, or equal to one
612/// of its leaves. `EqTo(false)` and `EqTo(true)` span the space.
613#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
614#[serde(tag = "op", rename_all = "snake_case")]
615pub enum BoolConstraint {
616    /// Always satisfied.
617    Always,
618    /// Never satisfied.
619    Never,
620    /// `value == b`.
621    EqTo {
622        /// Required value.
623        b: bool,
624    },
625    /// Child must not hold.
626    Not {
627        /// The negated child.
628        child: Box<Self>,
629    },
630}
631
632impl BoolConstraint {
633    /// Test a candidate value.
634    #[must_use]
635    pub fn test(&self, value: bool) -> bool {
636        match self {
637            Self::Always => true,
638            Self::Never => false,
639            Self::EqTo { b } => value == *b,
640            Self::Not { child } => !child.test(value),
641        }
642    }
643
644    /// Peel double negation and fold `Not` over the identity variants.
645    #[must_use]
646    pub fn canonicalize(self) -> Self {
647        match self {
648            Self::Not { child } => match child.canonicalize() {
649                Self::Not { child: inner } => *inner,
650                Self::Always => Self::Never,
651                Self::Never => Self::Always,
652                Self::EqTo { b } => Self::EqTo { b: !b },
653            },
654            other => other,
655        }
656    }
657
658    /// Canonical byte form of this constraint *as written*.
659    #[must_use]
660    pub fn canonical_bytes(&self) -> Vec<u8> {
661        let mut out = Vec::new();
662        self.write_canonical(&mut out);
663        out
664    }
665
666    /// Canonical fingerprint — auto-canonicalises first.
667    #[must_use]
668    pub fn fingerprint(&self) -> Fingerprint {
669        let canonical = self.clone().canonicalize();
670        Fingerprint::of(&canonical.canonical_bytes())
671    }
672
673    fn write_canonical(&self, out: &mut Vec<u8>) {
674        const T_ALWAYS: u8 = 0x30;
675        const T_NEVER:  u8 = 0x31;
676        const T_EQTO:   u8 = 0x32;
677        const T_NOT:    u8 = 0x33;
678
679        match self {
680            Self::Always => out.push(T_ALWAYS),
681            Self::Never => out.push(T_NEVER),
682            Self::EqTo { b } => {
683                out.push(T_EQTO);
684                out.push(u8::from(*b));
685            }
686            Self::Not { child } => {
687                out.push(T_NOT);
688                child.write_canonical(out);
689            }
690        }
691    }
692}
693
694impl std::ops::Not for BoolConstraint {
695    type Output = Self;
696    fn not(self) -> Self {
697        Self::Not {
698            child: Box::new(self),
699        }
700    }
701}
702
703// ---------------------------------------------------------------------------
704// StringConstraint.
705// ---------------------------------------------------------------------------
706
707/// String-value constraint.
708#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
709#[serde(tag = "op", rename_all = "snake_case")]
710pub enum StringConstraint {
711    /// Always satisfied.
712    Always,
713    /// Never satisfied.
714    Never,
715    /// Value must match this regex.
716    Regex {
717        /// The compiled pattern.
718        pattern: RegexPattern,
719    },
720    /// `min <= value.len() <= max` (byte length).
721    LengthRange {
722        /// Inclusive lower bound on byte length.
723        min: u32,
724        /// Inclusive upper bound on byte length.
725        max: u32,
726    },
727    /// `!value.is_empty()`.
728    NonEmpty,
729    /// Value is in this set.
730    InSet {
731        /// The permitted values.
732        values: BTreeSet<String>,
733    },
734    /// Value is not in this set.
735    NotInSet {
736        /// The forbidden values.
737        values: BTreeSet<String>,
738    },
739    /// All children must hold.
740    And {
741        /// Conjuncts.
742        children: Vec<Self>,
743    },
744    /// At least one child must hold.
745    Or {
746        /// Disjuncts.
747        children: Vec<Self>,
748    },
749    /// Child must not hold.
750    Not {
751        /// The negated child.
752        child: Box<Self>,
753    },
754}
755
756impl StringConstraint {
757    /// Test a candidate value.
758    #[must_use]
759    pub fn test(&self, value: &str) -> bool {
760        match self {
761            Self::Always => true,
762            Self::Never => false,
763            Self::Regex { pattern } => pattern.is_match(value),
764            Self::LengthRange { min, max } => {
765                let len = u32::try_from(value.len()).unwrap_or(u32::MAX);
766                len >= *min && len <= *max
767            }
768            Self::NonEmpty => !value.is_empty(),
769            Self::InSet { values } => values.contains(value),
770            Self::NotInSet { values } => !values.contains(value),
771            Self::And { children } => children.iter().all(|c| c.test(value)),
772            Self::Or { children } => children.iter().any(|c| c.test(value)),
773            Self::Not { child } => !child.test(value),
774        }
775    }
776
777    /// Conjunction, flattening nested `And`.
778    #[must_use]
779    pub fn and(self, rhs: Self) -> Self {
780        let mut children = Vec::new();
781        absorb_and_string(&mut children, self);
782        absorb_and_string(&mut children, rhs);
783        Self::And { children }
784    }
785
786    /// Disjunction, flattening nested `Or`.
787    #[must_use]
788    pub fn or(self, rhs: Self) -> Self {
789        let mut children = Vec::new();
790        absorb_or_string(&mut children, self);
791        absorb_or_string(&mut children, rhs);
792        Self::Or { children }
793    }
794
795    /// Same rule set as [`IntConstraint::canonicalize`].
796    #[must_use]
797    pub fn canonicalize(self) -> Self {
798        match self {
799            Self::Not { child } => match child.canonicalize() {
800                Self::Not { child: inner } => *inner,
801                Self::Always => Self::Never,
802                Self::Never => Self::Always,
803                other => Self::Not {
804                    child: Box::new(other),
805                },
806            },
807            Self::And { children } => {
808                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
809                for c in children {
810                    match c.canonicalize() {
811                        Self::Always => {}
812                        Self::Never => return Self::Never,
813                        Self::And { children: sub } => flat.extend(sub),
814                        other => flat.push(other),
815                    }
816                }
817                flat.sort_by_cached_key(Self::canonical_bytes);
818                dedup_partial_eq(&mut flat);
819                match flat.len() {
820                    0 => Self::Always,
821                    1 => flat.pop().expect("len == 1"),
822                    _ => Self::And { children: flat },
823                }
824            }
825            Self::Or { children } => {
826                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
827                for c in children {
828                    match c.canonicalize() {
829                        Self::Never => {}
830                        Self::Always => return Self::Always,
831                        Self::Or { children: sub } => flat.extend(sub),
832                        other => flat.push(other),
833                    }
834                }
835                flat.sort_by_cached_key(Self::canonical_bytes);
836                dedup_partial_eq(&mut flat);
837                match flat.len() {
838                    0 => Self::Never,
839                    1 => flat.pop().expect("len == 1"),
840                    _ => Self::Or { children: flat },
841                }
842            }
843            leaf => leaf,
844        }
845    }
846
847    /// Canonical byte form of this constraint *as written*.
848    #[must_use]
849    pub fn canonical_bytes(&self) -> Vec<u8> {
850        let mut out = Vec::new();
851        self.write_canonical(&mut out);
852        out
853    }
854
855    /// Canonical fingerprint — auto-canonicalises first. Regex leaves
856    /// hash their source pattern (not the compiled automaton).
857    #[must_use]
858    pub fn fingerprint(&self) -> Fingerprint {
859        let canonical = self.clone().canonicalize();
860        Fingerprint::of(&canonical.canonical_bytes())
861    }
862
863    fn write_canonical(&self, out: &mut Vec<u8>) {
864        const T_ALWAYS:      u8 = 0x40;
865        const T_NEVER:       u8 = 0x41;
866        const T_REGEX:       u8 = 0x42;
867        const T_LENGTHRANGE: u8 = 0x43;
868        const T_NONEMPTY:    u8 = 0x44;
869        const T_INSET:       u8 = 0x45;
870        const T_NOTINSET:    u8 = 0x46;
871        const T_AND:         u8 = 0x47;
872        const T_OR:          u8 = 0x48;
873        const T_NOT:         u8 = 0x49;
874
875        match self {
876            Self::Always => out.push(T_ALWAYS),
877            Self::Never => out.push(T_NEVER),
878            Self::Regex { pattern } => {
879                out.push(T_REGEX);
880                write_str_len_prefixed(out, pattern.as_str());
881            }
882            Self::LengthRange { min, max } => {
883                out.push(T_LENGTHRANGE);
884                write_u32_le(out, *min);
885                write_u32_le(out, *max);
886            }
887            Self::NonEmpty => out.push(T_NONEMPTY),
888            Self::InSet { values } => {
889                out.push(T_INSET);
890                write_string_set(out, values);
891            }
892            Self::NotInSet { values } => {
893                out.push(T_NOTINSET);
894                write_string_set(out, values);
895            }
896            Self::And { children } => {
897                out.push(T_AND);
898                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
899                for c in children {
900                    c.write_canonical(out);
901                }
902            }
903            Self::Or { children } => {
904                out.push(T_OR);
905                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
906                for c in children {
907                    c.write_canonical(out);
908                }
909            }
910            Self::Not { child } => {
911                out.push(T_NOT);
912                child.write_canonical(out);
913            }
914        }
915    }
916}
917
918impl std::ops::Not for StringConstraint {
919    type Output = Self;
920    fn not(self) -> Self {
921        Self::Not {
922            child: Box::new(self),
923        }
924    }
925}
926
927fn absorb_and_string(into: &mut Vec<StringConstraint>, c: StringConstraint) {
928    match c {
929        StringConstraint::And { children } => into.extend(children),
930        other => into.push(other),
931    }
932}
933
934fn absorb_or_string(into: &mut Vec<StringConstraint>, c: StringConstraint) {
935    match c {
936        StringConstraint::Or { children } => into.extend(children),
937        other => into.push(other),
938    }
939}
940
941// ---------------------------------------------------------------------------
942// SelectionConstraint.
943// ---------------------------------------------------------------------------
944
945/// Constraint over a selection value (an ordered set of items).
946#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
947#[serde(tag = "op", rename_all = "snake_case")]
948pub enum SelectionConstraint {
949    /// Always satisfied.
950    Always,
951    /// Never satisfied.
952    Never,
953    /// Every required item must be selected.
954    RequireAll {
955        /// The items that must all be present.
956        items: BTreeSet<SelectionItem>,
957    },
958    /// At least one required item must be selected.
959    RequireAny {
960        /// The items — at least one must be present.
961        items: BTreeSet<SelectionItem>,
962    },
963    /// None of these items may be selected.
964    ForbidAll {
965        /// The forbidden items.
966        items: BTreeSet<SelectionItem>,
967    },
968    /// `selection.len() >= n`.
969    MinSize {
970        /// The inclusive minimum size.
971        n: u32,
972    },
973    /// `selection.len() <= n`.
974    MaxSize {
975        /// The inclusive maximum size.
976        n: u32,
977    },
978    /// All children must hold.
979    And {
980        /// Conjuncts.
981        children: Vec<Self>,
982    },
983    /// At least one child must hold.
984    Or {
985        /// Disjuncts.
986        children: Vec<Self>,
987    },
988    /// Child must not hold.
989    Not {
990        /// The negated child.
991        child: Box<Self>,
992    },
993}
994
995impl SelectionConstraint {
996    /// Test a candidate selection.
997    #[must_use]
998    pub fn test(&self, selection: &IndexSet<SelectionItem>) -> bool {
999        match self {
1000            Self::Always => true,
1001            Self::Never => false,
1002            Self::RequireAll { items } => items.iter().all(|i| selection.contains(i)),
1003            Self::RequireAny { items } => items.iter().any(|i| selection.contains(i)),
1004            Self::ForbidAll { items } => !items.iter().any(|i| selection.contains(i)),
1005            Self::MinSize { n } => selection.len() >= *n as usize,
1006            Self::MaxSize { n } => selection.len() <= *n as usize,
1007            Self::And { children } => children.iter().all(|c| c.test(selection)),
1008            Self::Or { children } => children.iter().any(|c| c.test(selection)),
1009            Self::Not { child } => !child.test(selection),
1010        }
1011    }
1012
1013    /// Conjunction, flattening nested `And`.
1014    #[must_use]
1015    pub fn and(self, rhs: Self) -> Self {
1016        let mut children = Vec::new();
1017        absorb_and_selection(&mut children, self);
1018        absorb_and_selection(&mut children, rhs);
1019        Self::And { children }
1020    }
1021
1022    /// Disjunction, flattening nested `Or`.
1023    #[must_use]
1024    pub fn or(self, rhs: Self) -> Self {
1025        let mut children = Vec::new();
1026        absorb_or_selection(&mut children, self);
1027        absorb_or_selection(&mut children, rhs);
1028        Self::Or { children }
1029    }
1030
1031    /// Same rule set as [`IntConstraint::canonicalize`].
1032    #[must_use]
1033    pub fn canonicalize(self) -> Self {
1034        match self {
1035            Self::Not { child } => match child.canonicalize() {
1036                Self::Not { child: inner } => *inner,
1037                Self::Always => Self::Never,
1038                Self::Never => Self::Always,
1039                other => Self::Not {
1040                    child: Box::new(other),
1041                },
1042            },
1043            Self::And { children } => {
1044                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
1045                for c in children {
1046                    match c.canonicalize() {
1047                        Self::Always => {}
1048                        Self::Never => return Self::Never,
1049                        Self::And { children: sub } => flat.extend(sub),
1050                        other => flat.push(other),
1051                    }
1052                }
1053                flat.sort_by_cached_key(Self::canonical_bytes);
1054                dedup_partial_eq(&mut flat);
1055                match flat.len() {
1056                    0 => Self::Always,
1057                    1 => flat.pop().expect("len == 1"),
1058                    _ => Self::And { children: flat },
1059                }
1060            }
1061            Self::Or { children } => {
1062                let mut flat: Vec<Self> = Vec::with_capacity(children.len());
1063                for c in children {
1064                    match c.canonicalize() {
1065                        Self::Never => {}
1066                        Self::Always => return Self::Always,
1067                        Self::Or { children: sub } => flat.extend(sub),
1068                        other => flat.push(other),
1069                    }
1070                }
1071                flat.sort_by_cached_key(Self::canonical_bytes);
1072                dedup_partial_eq(&mut flat);
1073                match flat.len() {
1074                    0 => Self::Never,
1075                    1 => flat.pop().expect("len == 1"),
1076                    _ => Self::Or { children: flat },
1077                }
1078            }
1079            leaf => leaf,
1080        }
1081    }
1082
1083    /// Canonical byte form of this constraint *as written*.
1084    #[must_use]
1085    pub fn canonical_bytes(&self) -> Vec<u8> {
1086        let mut out = Vec::new();
1087        self.write_canonical(&mut out);
1088        out
1089    }
1090
1091    /// Canonical fingerprint — auto-canonicalises first.
1092    #[must_use]
1093    pub fn fingerprint(&self) -> Fingerprint {
1094        let canonical = self.clone().canonicalize();
1095        Fingerprint::of(&canonical.canonical_bytes())
1096    }
1097
1098    fn write_canonical(&self, out: &mut Vec<u8>) {
1099        const T_ALWAYS:     u8 = 0x50;
1100        const T_NEVER:      u8 = 0x51;
1101        const T_REQUIREALL: u8 = 0x52;
1102        const T_REQUIREANY: u8 = 0x53;
1103        const T_FORBIDALL:  u8 = 0x54;
1104        const T_MINSIZE:    u8 = 0x55;
1105        const T_MAXSIZE:    u8 = 0x56;
1106        const T_AND:        u8 = 0x57;
1107        const T_OR:         u8 = 0x58;
1108        const T_NOT:        u8 = 0x59;
1109
1110        match self {
1111            Self::Always => out.push(T_ALWAYS),
1112            Self::Never => out.push(T_NEVER),
1113            Self::RequireAll { items } => {
1114                out.push(T_REQUIREALL);
1115                write_selection_item_set(out, items);
1116            }
1117            Self::RequireAny { items } => {
1118                out.push(T_REQUIREANY);
1119                write_selection_item_set(out, items);
1120            }
1121            Self::ForbidAll { items } => {
1122                out.push(T_FORBIDALL);
1123                write_selection_item_set(out, items);
1124            }
1125            Self::MinSize { n } => {
1126                out.push(T_MINSIZE);
1127                write_u32_le(out, *n);
1128            }
1129            Self::MaxSize { n } => {
1130                out.push(T_MAXSIZE);
1131                write_u32_le(out, *n);
1132            }
1133            Self::And { children } => {
1134                out.push(T_AND);
1135                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
1136                for c in children {
1137                    c.write_canonical(out);
1138                }
1139            }
1140            Self::Or { children } => {
1141                out.push(T_OR);
1142                write_u32_le(out, u32::try_from(children.len()).expect("fits in u32"));
1143                for c in children {
1144                    c.write_canonical(out);
1145                }
1146            }
1147            Self::Not { child } => {
1148                out.push(T_NOT);
1149                child.write_canonical(out);
1150            }
1151        }
1152    }
1153}
1154
1155impl std::ops::Not for SelectionConstraint {
1156    type Output = Self;
1157    fn not(self) -> Self {
1158        Self::Not {
1159            child: Box::new(self),
1160        }
1161    }
1162}
1163
1164fn absorb_and_selection(into: &mut Vec<SelectionConstraint>, c: SelectionConstraint) {
1165    match c {
1166        SelectionConstraint::And { children } => into.extend(children),
1167        other => into.push(other),
1168    }
1169}
1170
1171fn absorb_or_selection(into: &mut Vec<SelectionConstraint>, c: SelectionConstraint) {
1172    match c {
1173        SelectionConstraint::Or { children } => into.extend(children),
1174        other => into.push(other),
1175    }
1176}
1177
1178// ---------------------------------------------------------------------------
1179// Outer Constraint enum.
1180// ---------------------------------------------------------------------------
1181
1182/// Kind-tagged constraint for dispatch against a [`Value`].
1183#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1184#[serde(tag = "kind", rename_all = "snake_case")]
1185pub enum Constraint {
1186    /// Constraint over an `i64`.
1187    Integer(IntConstraint),
1188    /// Constraint over an `f64`.
1189    Double(DoubleConstraint),
1190    /// Constraint over a `bool`.
1191    Boolean(BoolConstraint),
1192    /// Constraint over a `String`.
1193    String(StringConstraint),
1194    /// Constraint over a selection.
1195    Selection(SelectionConstraint),
1196}
1197
1198impl Constraint {
1199    /// Test a candidate value against this constraint. Kind-mismatched
1200    /// values always fail.
1201    #[must_use]
1202    pub fn test(&self, value: &Value) -> bool {
1203        match (self, value) {
1204            (Self::Integer(c), Value::Integer(v)) => c.test(v.value),
1205            (Self::Double(c), Value::Double(v)) => c.test(v.value),
1206            (Self::Boolean(c), Value::Boolean(v)) => c.test(v.value),
1207            (Self::String(c), Value::String(v)) => c.test(&v.value),
1208            (Self::Selection(c), Value::Selection(v)) => c.test(&v.items),
1209            _ => false,
1210        }
1211    }
1212
1213    /// Canonicalise the contained per-kind constraint.
1214    #[must_use]
1215    pub fn canonicalize(self) -> Self {
1216        match self {
1217            Self::Integer(c) => Self::Integer(c.canonicalize()),
1218            Self::Double(c) => Self::Double(c.canonicalize()),
1219            Self::Boolean(c) => Self::Boolean(c.canonicalize()),
1220            Self::String(c) => Self::String(c.canonicalize()),
1221            Self::Selection(c) => Self::Selection(c.canonicalize()),
1222        }
1223    }
1224
1225    /// Canonical byte form. Delegates to the per-kind encoding — the
1226    /// first byte already identifies the kind because each kind uses a
1227    /// disjoint variant-tag range.
1228    #[must_use]
1229    pub fn canonical_bytes(&self) -> Vec<u8> {
1230        match self {
1231            Self::Integer(c) => c.canonical_bytes(),
1232            Self::Double(c) => c.canonical_bytes(),
1233            Self::Boolean(c) => c.canonical_bytes(),
1234            Self::String(c) => c.canonical_bytes(),
1235            Self::Selection(c) => c.canonical_bytes(),
1236        }
1237    }
1238
1239    /// Canonical fingerprint. Delegates to the per-kind fingerprint,
1240    /// which auto-canonicalises.
1241    #[must_use]
1242    pub fn fingerprint(&self) -> Fingerprint {
1243        match self {
1244            Self::Integer(c) => c.fingerprint(),
1245            Self::Double(c) => c.fingerprint(),
1246            Self::Boolean(c) => c.fingerprint(),
1247            Self::String(c) => c.fingerprint(),
1248            Self::Selection(c) => c.fingerprint(),
1249        }
1250    }
1251}
1252
1253// ---------------------------------------------------------------------------
1254// Tests.
1255// ---------------------------------------------------------------------------
1256
1257#[cfg(test)]
1258mod tests {
1259    use std::ops::Not;
1260
1261    use super::*;
1262    use crate::names::ParameterName;
1263
1264    fn pname(s: &str) -> ParameterName {
1265        ParameterName::new(s).unwrap()
1266    }
1267
1268    // ---------- IntConstraint ----------
1269
1270    #[test]
1271    fn int_min_and_max_test() {
1272        assert!(IntConstraint::Min { n: 5 }.test(5));
1273        assert!(IntConstraint::Min { n: 5 }.test(100));
1274        assert!(!IntConstraint::Min { n: 5 }.test(4));
1275        assert!(IntConstraint::Max { n: 5 }.test(5));
1276        assert!(IntConstraint::Max { n: 5 }.test(-1));
1277        assert!(!IntConstraint::Max { n: 5 }.test(6));
1278    }
1279
1280    #[test]
1281    fn int_range_and_multiple_test() {
1282        let c = IntConstraint::Range { min: 1, max: 10 };
1283        assert!(c.test(1));
1284        assert!(c.test(10));
1285        assert!(!c.test(11));
1286        let m = IntConstraint::multiple(3).unwrap();
1287        assert!(m.test(9));
1288        assert!(!m.test(10));
1289        // Multiple(0) short-circuits to false, never panics.
1290        assert!(!IntConstraint::Multiple { n: 0 }.test(0));
1291    }
1292
1293    #[test]
1294    fn int_inset_notinset_test() {
1295        let s: BTreeSet<i64> = [1, 2, 3].into_iter().collect();
1296        let in_set = IntConstraint::InSet { values: s.clone() };
1297        let not_in = IntConstraint::NotInSet { values: s };
1298        assert!(in_set.test(2));
1299        assert!(!in_set.test(4));
1300        assert!(not_in.test(4));
1301        assert!(!not_in.test(2));
1302    }
1303
1304    #[test]
1305    fn int_and_or_not_flatten_and_test() {
1306        let c = IntConstraint::Min { n: 0 }
1307            .and(IntConstraint::Max { n: 10 })
1308            .and(IntConstraint::multiple(2).unwrap());
1309        // `and` flattens.
1310        match &c {
1311            IntConstraint::And { children } => assert_eq!(children.len(), 3),
1312            other => panic!("expected And, got {other:?}"),
1313        }
1314        assert!(c.test(4));
1315        assert!(!c.test(5));
1316        assert!(!c.test(-1));
1317
1318        let n = IntConstraint::Min { n: 0 }.not();
1319        assert!(n.test(-1));
1320        assert!(!n.test(0));
1321    }
1322
1323    #[test]
1324    fn int_canonicalize_collapses_identities() {
1325        // And(Always, x) -> x
1326        let c = IntConstraint::Always
1327            .and(IntConstraint::Min { n: 0 })
1328            .canonicalize();
1329        assert_eq!(c, IntConstraint::Min { n: 0 });
1330
1331        // And(Never, x) -> Never
1332        let c = IntConstraint::Never
1333            .and(IntConstraint::Min { n: 0 })
1334            .canonicalize();
1335        assert_eq!(c, IntConstraint::Never);
1336
1337        // Or(Always, x) -> Always
1338        let c = IntConstraint::Always
1339            .or(IntConstraint::Min { n: 0 })
1340            .canonicalize();
1341        assert_eq!(c, IntConstraint::Always);
1342
1343        // Or(Never, x) -> x
1344        let c = IntConstraint::Never
1345            .or(IntConstraint::Min { n: 0 })
1346            .canonicalize();
1347        assert_eq!(c, IntConstraint::Min { n: 0 });
1348    }
1349
1350    #[test]
1351    fn int_canonicalize_flattens_nested() {
1352        let c = IntConstraint::And {
1353            children: vec![
1354                IntConstraint::Min { n: 0 },
1355                IntConstraint::And {
1356                    children: vec![
1357                        IntConstraint::Max { n: 10 },
1358                        IntConstraint::multiple(2).unwrap(),
1359                    ],
1360                },
1361            ],
1362        }
1363        .canonicalize();
1364        match c {
1365            IntConstraint::And { children } => assert_eq!(children.len(), 3),
1366            other => panic!("expected And, got {other:?}"),
1367        }
1368    }
1369
1370    #[test]
1371    fn int_canonicalize_peels_double_negation() {
1372        let c = IntConstraint::Min { n: 0 }.not().not().canonicalize();
1373        assert_eq!(c, IntConstraint::Min { n: 0 });
1374    }
1375
1376    #[test]
1377    fn int_canonicalize_dedups_adjacent() {
1378        let c = IntConstraint::And {
1379            children: vec![
1380                IntConstraint::Min { n: 0 },
1381                IntConstraint::Min { n: 0 },
1382                IntConstraint::Max { n: 10 },
1383            ],
1384        }
1385        .canonicalize();
1386        match c {
1387            IntConstraint::And { children } => assert_eq!(children.len(), 2),
1388            other => panic!("expected And, got {other:?}"),
1389        }
1390    }
1391
1392    #[test]
1393    fn int_canonicalize_single_child_collapses() {
1394        let c = IntConstraint::And {
1395            children: vec![IntConstraint::Min { n: 0 }],
1396        }
1397        .canonicalize();
1398        assert_eq!(c, IntConstraint::Min { n: 0 });
1399
1400        let c = IntConstraint::Or {
1401            children: vec![],
1402        }
1403        .canonicalize();
1404        assert_eq!(c, IntConstraint::Never);
1405
1406        let c = IntConstraint::And {
1407            children: vec![],
1408        }
1409        .canonicalize();
1410        assert_eq!(c, IntConstraint::Always);
1411    }
1412
1413    #[test]
1414    fn int_canonicalize_not_folds_identities() {
1415        assert_eq!(
1416            IntConstraint::Always.not().canonicalize(),
1417            IntConstraint::Never
1418        );
1419        assert_eq!(
1420            IntConstraint::Never.not().canonicalize(),
1421            IntConstraint::Always
1422        );
1423    }
1424
1425    // ---------- DoubleConstraint ----------
1426
1427    #[test]
1428    fn double_range_test_and_nan_safe() {
1429        let c = DoubleConstraint::Range {
1430            min: 0.0,
1431            max: 1.0,
1432        };
1433        assert!(c.test(0.5));
1434        assert!(!c.test(1.5));
1435        assert!(!c.test(f64::NAN)); // NaN never satisfies.
1436    }
1437
1438    #[test]
1439    fn double_canonicalize_double_negation() {
1440        let c = DoubleConstraint::Min { n: 0.0 }
1441            .not()
1442            .not()
1443            .canonicalize();
1444        assert_eq!(c, DoubleConstraint::Min { n: 0.0 });
1445    }
1446
1447    // ---------- BoolConstraint ----------
1448
1449    #[test]
1450    fn bool_eq_to_test() {
1451        assert!(BoolConstraint::EqTo { b: true }.test(true));
1452        assert!(!BoolConstraint::EqTo { b: true }.test(false));
1453    }
1454
1455    #[test]
1456    fn bool_not_flips_eq_to_under_canonicalize() {
1457        let c = BoolConstraint::EqTo { b: true }.not().canonicalize();
1458        assert_eq!(c, BoolConstraint::EqTo { b: false });
1459    }
1460
1461    // ---------- StringConstraint ----------
1462
1463    #[test]
1464    fn string_regex_test() {
1465        let c = StringConstraint::Regex {
1466            pattern: RegexPattern::new("^[a-z]+$").unwrap(),
1467        };
1468        assert!(c.test("abc"));
1469        assert!(!c.test("abc1"));
1470    }
1471
1472    #[test]
1473    fn string_length_range_and_nonempty_test() {
1474        let lr = StringConstraint::LengthRange { min: 1, max: 5 };
1475        assert!(lr.test("a"));
1476        assert!(lr.test("abcde"));
1477        assert!(!lr.test(""));
1478        assert!(!lr.test("abcdef"));
1479        assert!(StringConstraint::NonEmpty.test("x"));
1480        assert!(!StringConstraint::NonEmpty.test(""));
1481    }
1482
1483    #[test]
1484    fn string_inset_test() {
1485        let set: BTreeSet<String> = ["red".into(), "blue".into()].into_iter().collect();
1486        let c = StringConstraint::InSet { values: set };
1487        assert!(c.test("red"));
1488        assert!(!c.test("green"));
1489    }
1490
1491    #[test]
1492    fn string_and_or_not_combine() {
1493        let c = StringConstraint::NonEmpty
1494            .and(StringConstraint::LengthRange { min: 1, max: 3 });
1495        assert!(c.test("ab"));
1496        assert!(!c.test(""));
1497        assert!(!c.test("abcd"));
1498    }
1499
1500    // ---------- SelectionConstraint ----------
1501
1502    fn sel(xs: &[&str]) -> IndexSet<SelectionItem> {
1503        xs.iter().map(|s| SelectionItem::new(*s).unwrap()).collect()
1504    }
1505
1506    fn sitems(xs: &[&str]) -> BTreeSet<SelectionItem> {
1507        xs.iter().map(|s| SelectionItem::new(*s).unwrap()).collect()
1508    }
1509
1510    #[test]
1511    fn selection_require_all_and_any() {
1512        let c = SelectionConstraint::RequireAll {
1513            items: sitems(&["a", "b"]),
1514        };
1515        assert!(c.test(&sel(&["a", "b", "c"])));
1516        assert!(!c.test(&sel(&["a", "c"])));
1517
1518        let c = SelectionConstraint::RequireAny {
1519            items: sitems(&["a", "b"]),
1520        };
1521        assert!(c.test(&sel(&["b"])));
1522        assert!(!c.test(&sel(&["x"])));
1523    }
1524
1525    #[test]
1526    fn selection_forbid_all_and_sizes() {
1527        let c = SelectionConstraint::ForbidAll {
1528            items: sitems(&["z"]),
1529        };
1530        assert!(c.test(&sel(&["a"])));
1531        assert!(!c.test(&sel(&["z"])));
1532
1533        let c = SelectionConstraint::MinSize { n: 2 };
1534        assert!(c.test(&sel(&["a", "b"])));
1535        assert!(!c.test(&sel(&["a"])));
1536
1537        let c = SelectionConstraint::MaxSize { n: 2 };
1538        assert!(c.test(&sel(&["a", "b"])));
1539        assert!(!c.test(&sel(&["a", "b", "c"])));
1540    }
1541
1542    // ---------- outer Constraint ----------
1543
1544    #[test]
1545    fn outer_dispatches_by_kind() {
1546        let c = Constraint::Integer(IntConstraint::Min { n: 0 });
1547        let good = Value::integer(pname("n"), 5, None);
1548        let bad = Value::integer(pname("n"), -1, None);
1549        let wrong_kind = Value::boolean(pname("n"), true, None);
1550        assert!(c.test(&good));
1551        assert!(!c.test(&bad));
1552        assert!(!c.test(&wrong_kind));
1553    }
1554
1555    #[test]
1556    fn outer_canonicalize_delegates() {
1557        let c = Constraint::Integer(IntConstraint::Min { n: 0 }.not().not());
1558        let canonical = c.canonicalize();
1559        assert_eq!(
1560            canonical,
1561            Constraint::Integer(IntConstraint::Min { n: 0 })
1562        );
1563    }
1564
1565    // ---------- serde roundtrips ----------
1566
1567    #[test]
1568    fn int_constraint_serde_roundtrip() {
1569        let c = IntConstraint::Min { n: 0 }
1570            .and(IntConstraint::Max { n: 10 })
1571            .not();
1572        let json = serde_json::to_string(&c).unwrap();
1573        let back: IntConstraint = serde_json::from_str(&json).unwrap();
1574        assert_eq!(c, back);
1575    }
1576
1577    #[test]
1578    fn string_constraint_with_regex_serde_roundtrip() {
1579        let c = StringConstraint::Regex {
1580            pattern: RegexPattern::new("^foo$").unwrap(),
1581        };
1582        let json = serde_json::to_string(&c).unwrap();
1583        let back: StringConstraint = serde_json::from_str(&json).unwrap();
1584        assert_eq!(c, back);
1585    }
1586
1587    #[test]
1588    fn outer_constraint_serde_roundtrip() {
1589        let c = Constraint::Boolean(BoolConstraint::EqTo { b: true });
1590        let json = serde_json::to_string(&c).unwrap();
1591        let back: Constraint = serde_json::from_str(&json).unwrap();
1592        assert_eq!(c, back);
1593    }
1594
1595    // ---------- canonical_bytes / fingerprint ----------
1596
1597    #[test]
1598    fn int_canonical_bytes_deterministic() {
1599        let a = IntConstraint::Range { min: 1, max: 10 }.canonical_bytes();
1600        let b = IntConstraint::Range { min: 1, max: 10 }.canonical_bytes();
1601        assert_eq!(a, b);
1602    }
1603
1604    #[test]
1605    fn int_canonical_bytes_distinguish_variants() {
1606        let min = IntConstraint::Min { n: 5 }.canonical_bytes();
1607        let max = IntConstraint::Max { n: 5 }.canonical_bytes();
1608        assert_ne!(min, max);
1609    }
1610
1611    #[test]
1612    fn int_fingerprint_stable_under_child_reordering() {
1613        let ab = IntConstraint::Min { n: 0 }
1614            .and(IntConstraint::Max { n: 10 })
1615            .fingerprint();
1616        let ba = IntConstraint::Max { n: 10 }
1617            .and(IntConstraint::Min { n: 0 })
1618            .fingerprint();
1619        assert_eq!(ab, ba, "canonicalise should reorder And children");
1620    }
1621
1622    #[test]
1623    fn int_fingerprint_stable_under_nested_and_flattening() {
1624        let flat = IntConstraint::Min { n: 0 }
1625            .and(IntConstraint::Max { n: 10 })
1626            .and(IntConstraint::multiple(2).unwrap())
1627            .fingerprint();
1628        let nested = IntConstraint::Min { n: 0 }
1629            .and(IntConstraint::Max { n: 10 }.and(IntConstraint::multiple(2).unwrap()))
1630            .fingerprint();
1631        assert_eq!(flat, nested);
1632    }
1633
1634    #[test]
1635    fn int_fingerprint_stable_under_double_negation() {
1636        let a = IntConstraint::Min { n: 0 }.fingerprint();
1637        let b = IntConstraint::Min { n: 0 }.not().not().fingerprint();
1638        assert_eq!(a, b);
1639    }
1640
1641    #[test]
1642    fn int_fingerprint_distinguishes_different_constraints() {
1643        let a = IntConstraint::Min { n: 0 }.fingerprint();
1644        let b = IntConstraint::Min { n: 1 }.fingerprint();
1645        assert_ne!(a, b);
1646    }
1647
1648    #[test]
1649    fn double_fingerprint_nan_normalises() {
1650        let a = DoubleConstraint::Min { n: f64::NAN }.fingerprint();
1651        let b = DoubleConstraint::Min {
1652            n: f64::from_bits(f64::NAN.to_bits() ^ 1),
1653        }
1654        .fingerprint();
1655        assert_eq!(a, b);
1656    }
1657
1658    #[test]
1659    fn canonicalize_sorts_and_dedups_and_children() {
1660        let c = IntConstraint::And {
1661            children: vec![
1662                IntConstraint::Max { n: 10 },
1663                IntConstraint::Min { n: 0 },
1664                IntConstraint::Max { n: 10 }, // duplicate, should dedup
1665            ],
1666        }
1667        .canonicalize();
1668        let children = match c {
1669            IntConstraint::And { children } => children,
1670            other => panic!("expected And, got {other:?}"),
1671        };
1672        assert_eq!(children.len(), 2);
1673        // After canonical sort, the canonically-smaller child comes
1674        // first. Min has tag 0x12, Max has tag 0x13, so Min sorts first.
1675        match &children[0] {
1676            IntConstraint::Min { n: 0 } => {}
1677            other => panic!("expected Min {{ n: 0 }} first, got {other:?}"),
1678        }
1679    }
1680
1681    #[test]
1682    fn bool_fingerprint_double_negation_matches() {
1683        let a = BoolConstraint::EqTo { b: true }.fingerprint();
1684        let b = BoolConstraint::EqTo { b: true }.not().not().fingerprint();
1685        assert_eq!(a, b);
1686    }
1687
1688    #[test]
1689    fn string_fingerprint_regex_hashes_source() {
1690        let a = StringConstraint::Regex {
1691            pattern: RegexPattern::new("^abc$").unwrap(),
1692        }
1693        .fingerprint();
1694        let b = StringConstraint::Regex {
1695            pattern: RegexPattern::new("^abc$").unwrap(),
1696        }
1697        .fingerprint();
1698        assert_eq!(a, b);
1699        let c = StringConstraint::Regex {
1700            pattern: RegexPattern::new("^abd$").unwrap(),
1701        }
1702        .fingerprint();
1703        assert_ne!(a, c);
1704    }
1705
1706    #[test]
1707    fn selection_fingerprint_stable_under_and_reordering() {
1708        let req = SelectionConstraint::RequireAll {
1709            items: sitems(&["a", "b"]),
1710        };
1711        let max = SelectionConstraint::MaxSize { n: 3 };
1712        let ab = req.clone().and(max.clone()).fingerprint();
1713        let ba = max.and(req).fingerprint();
1714        assert_eq!(ab, ba);
1715    }
1716
1717    #[test]
1718    fn outer_constraint_fingerprint_delegates() {
1719        let int_c = IntConstraint::Min { n: 0 };
1720        let direct = int_c.fingerprint();
1721        let outer = Constraint::Integer(int_c).fingerprint();
1722        assert_eq!(direct, outer);
1723    }
1724
1725    #[test]
1726    fn outer_constraint_kinds_do_not_collide() {
1727        // Integer Always and Double Always use distinct first bytes,
1728        // so their canonical forms (and fingerprints) differ.
1729        let i = Constraint::Integer(IntConstraint::Always).fingerprint();
1730        let d = Constraint::Double(DoubleConstraint::Always).fingerprint();
1731        let b = Constraint::Boolean(BoolConstraint::Always).fingerprint();
1732        let s = Constraint::String(StringConstraint::Always).fingerprint();
1733        let sel = Constraint::Selection(SelectionConstraint::Always).fingerprint();
1734        let all = [i, d, b, s, sel];
1735        for (a_i, a) in all.iter().enumerate() {
1736            for (b_i, b) in all.iter().enumerate() {
1737                if a_i != b_i {
1738                    assert_ne!(a, b, "kinds {a_i} and {b_i} collided");
1739                }
1740            }
1741        }
1742    }
1743}