Skip to main content

meta_language/grammar/inference/
semantic.rs

1//! Semantic invariant mining over inferred grammars.
2//!
3//! This module is a clean-room, deterministic implementation of the D8
4//! inference layer. It keeps the public model grammar-aware and query-shaped,
5//! while the current backend mines observations from the positive corpus until a
6//! public derivation-tree parser is available from the CFG runtime.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::fmt;
10
11mod observation;
12
13use observation::{Observation, ObservedValue};
14
15use crate::grammar::Grammar;
16use crate::link_network::LinkType;
17use crate::query::LinkQuery;
18use crate::semantics::{ProbabilisticTruthValue, Probability, TruthValue};
19
20const DEF_SLOT: &str = "def";
21const USE_SLOT: &str = "use";
22const LEFT_SLOT: &str = "left";
23const RIGHT_SLOT: &str = "right";
24const FIELD_SLOT: &str = "field";
25const BODY_SLOT: &str = "body";
26const TARGET_SLOT: &str = "target";
27
28const DEF_BEFORE_USE_SLOTS: &[&str] = &[DEF_SLOT, USE_SLOT];
29const EQUAL_COUNT_SLOTS: &[&str] = &[LEFT_SLOT, RIGHT_SLOT];
30const LENGTH_FIELD_SLOTS: &[&str] = &[FIELD_SLOT, BODY_SLOT];
31const SINGLE_TARGET_SLOTS: &[&str] = &[TARGET_SLOT];
32
33/// A grammar-aware atomic predicate over derivation-tree observations.
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum ConstraintAtom {
36    /// Every use must match a previously seen definition.
37    DefBeforeUse {
38        /// Non-terminal that contributes definitions.
39        def: NonTerminalRef,
40        /// Non-terminal that contributes uses.
41        use_: NonTerminalRef,
42    },
43    /// The number of left and right matches must be equal.
44    EqualCount {
45        /// Left-hand non-terminal.
46        left: NonTerminalRef,
47        /// Right-hand non-terminal.
48        right: NonTerminalRef,
49    },
50    /// A numeric field must equal the measured length of a body.
51    LengthField {
52        /// Numeric length field.
53        field: NonTerminalRef,
54        /// Body whose length is measured.
55        body: NonTerminalRef,
56        /// Unit used for measuring the body.
57        unit: LengthUnit,
58    },
59    /// Values matched at the target must be pairwise distinct.
60    Unique {
61        /// Non-terminal whose values must be distinct.
62        target: NonTerminalRef,
63    },
64    /// Values matched at the target must be non-decreasing in document order.
65    Ordered {
66        /// Non-terminal whose values must be ordered.
67        target: NonTerminalRef,
68    },
69}
70
71/// A reference to a non-terminal plus the structural query that locates it.
72#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct NonTerminalRef {
74    /// Rule name in the inferred grammar.
75    pub rule: String,
76    /// Query used by derivation-tree backends to extract instances.
77    pub query: LinkQuery,
78}
79
80impl NonTerminalRef {
81    /// Builds a reference whose query is keyed by the rule term.
82    #[must_use]
83    pub fn new(rule: impl Into<String>) -> Self {
84        let rule = rule.into();
85        Self {
86            query: LinkQuery::by_type(LinkType::Grammar).with_term(rule.clone()),
87            rule,
88        }
89    }
90
91    /// Builds a reference with an explicit structural query.
92    #[must_use]
93    pub fn with_query(rule: impl Into<String>, query: LinkQuery) -> Self {
94        Self {
95            rule: rule.into(),
96            query,
97        }
98    }
99}
100
101/// Unit used by [`ConstraintAtom::LengthField`].
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub enum LengthUnit {
104    /// Count comma-separated elements, falling back to characters for scalar
105    /// bodies.
106    Elements,
107    /// Count UTF-8 bytes.
108    Bytes,
109    /// Count Unicode scalar values.
110    Chars,
111}
112
113/// One conjunctive clause in a disjunctive-normal-form constraint.
114#[derive(Clone, Debug, Default, PartialEq, Eq)]
115pub struct ConstraintClause {
116    /// Atoms that must all hold.
117    pub atoms: Vec<ConstraintAtom>,
118}
119
120impl ConstraintClause {
121    /// Builds a clause from atoms.
122    #[must_use]
123    pub const fn new(atoms: Vec<ConstraintAtom>) -> Self {
124        Self { atoms }
125    }
126
127    /// Evaluates the clause over one input.
128    #[must_use]
129    pub fn evaluate(&self, grammar: &Grammar, input: &str) -> TruthValue {
130        let observation = Observation::from_grammar(grammar, input);
131        evaluate_clause_observation(&observation, self)
132    }
133}
134
135/// A ranked disjunctive-normal-form semantic constraint.
136#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct SemanticConstraint {
138    /// Disjunction of clauses. An empty list is treated as trivially true.
139    pub clauses: Vec<ConstraintClause>,
140    /// Deterministic ranking score; larger values are more specific.
141    pub specificity: u32,
142    /// Fraction of the positive corpus satisfied by the DNF.
143    pub recall: Probability,
144}
145
146impl SemanticConstraint {
147    /// Builds a constraint value.
148    #[must_use]
149    pub const fn new(
150        clauses: Vec<ConstraintClause>,
151        specificity: u32,
152        recall: Probability,
153    ) -> Self {
154        Self {
155            clauses,
156            specificity,
157            recall,
158        }
159    }
160
161    /// Builds the trivially true constraint.
162    #[must_use]
163    pub const fn trivially_true() -> Self {
164        Self {
165            clauses: Vec::new(),
166            specificity: 0,
167            recall: Probability::ONE,
168        }
169    }
170
171    /// Evaluates the DNF over one input.
172    #[must_use]
173    pub fn evaluate(&self, grammar: &Grammar, input: &str) -> TruthValue {
174        evaluate_constraint(grammar, input, self)
175    }
176
177    /// Evaluates the DNF as a probabilistic truth value.
178    #[must_use]
179    pub fn evaluate_probabilistic(
180        &self,
181        grammar: &Grammar,
182        input: &str,
183    ) -> ProbabilisticTruthValue {
184        evaluate_probabilistic(grammar, input, self)
185    }
186}
187
188/// Function type used by a constraint-pattern template.
189pub type ConstraintInstantiator = fn(&BTreeMap<&'static str, String>) -> Vec<ConstraintAtom>;
190
191/// A reusable invariant template with named non-terminal slots.
192#[derive(Clone, Copy)]
193pub struct ConstraintPattern {
194    /// Stable template name.
195    pub name: &'static str,
196    /// Slot names the instantiator binds to grammar rules.
197    pub slots: &'static [&'static str],
198    /// Builds concrete atoms from a slot binding.
199    pub instantiate: ConstraintInstantiator,
200}
201
202impl fmt::Debug for ConstraintPattern {
203    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
204        formatter
205            .debug_struct("ConstraintPattern")
206            .field("name", &self.name)
207            .field("slots", &self.slots)
208            .finish_non_exhaustive()
209    }
210}
211
212/// Configuration for semantic invariant mining.
213#[derive(Clone, Debug)]
214pub struct SemanticInferenceConfig {
215    /// Pattern catalog to instantiate.
216    pub catalog: Vec<ConstraintPattern>,
217    /// Maximum grammar path depth considered by augmentation.
218    pub k_path_depth: usize,
219    /// Maximum number of augmented mutants to consider.
220    pub max_augmented: usize,
221    /// Minimum positive-corpus recall for a clause.
222    pub min_recall: Probability,
223}
224
225impl Default for SemanticInferenceConfig {
226    fn default() -> Self {
227        Self {
228            catalog: default_pattern_catalog(),
229            k_path_depth: 3,
230            max_augmented: 128,
231            min_recall: Probability::ONE,
232        }
233    }
234}
235
236/// Returns the built-in semantic-invariant pattern catalog.
237#[must_use]
238pub fn default_pattern_catalog() -> Vec<ConstraintPattern> {
239    vec![
240        ConstraintPattern {
241            name: "def-before-use",
242            slots: DEF_BEFORE_USE_SLOTS,
243            instantiate: instantiate_def_before_use,
244        },
245        ConstraintPattern {
246            name: "equal-count",
247            slots: EQUAL_COUNT_SLOTS,
248            instantiate: instantiate_equal_count,
249        },
250        ConstraintPattern {
251            name: "length-field",
252            slots: LENGTH_FIELD_SLOTS,
253            instantiate: instantiate_length_field,
254        },
255        ConstraintPattern {
256            name: "unique",
257            slots: SINGLE_TARGET_SLOTS,
258            instantiate: instantiate_unique,
259        },
260        ConstraintPattern {
261            name: "ordered",
262            slots: SINGLE_TARGET_SLOTS,
263            instantiate: instantiate_ordered,
264        },
265    ]
266}
267
268/// Mines a ranked DNF of semantic constraints from a grammar and positive corpus.
269#[must_use]
270pub fn mine_semantic_constraints(
271    grammar: &Grammar,
272    positive_examples: &[String],
273    config: &SemanticInferenceConfig,
274) -> SemanticConstraint {
275    if grammar.rules().is_empty() || positive_examples.is_empty() || config.catalog.is_empty() {
276        return SemanticConstraint::trivially_true();
277    }
278
279    let observations = positive_examples
280        .iter()
281        .map(|example| Observation::from_grammar(grammar, example))
282        .collect::<Vec<_>>();
283    let candidates = instantiate_candidates(grammar, &config.catalog);
284    let mut surviving = candidates
285        .into_iter()
286        .filter(|atom| holds_on_positive_corpus(&observations, atom))
287        .filter(|atom| discriminates_augmented_variant(&observations, atom, config))
288        .collect::<Vec<_>>();
289
290    surviving.sort_by_key(atom_sort_key);
291    surviving.dedup();
292    build_semantic_constraint(surviving, &observations, config.min_recall)
293}
294
295/// Evaluates one atom over one input.
296#[must_use]
297pub fn evaluate_atom(grammar: &Grammar, input: &str, atom: &ConstraintAtom) -> TruthValue {
298    let observation = Observation::from_grammar(grammar, input);
299    evaluate_atom_observation(&observation, atom)
300}
301
302/// Evaluates one clause over one input.
303#[must_use]
304pub fn evaluate_clause(grammar: &Grammar, input: &str, clause: &ConstraintClause) -> TruthValue {
305    let observation = Observation::from_grammar(grammar, input);
306    evaluate_clause_observation(&observation, clause)
307}
308
309/// Evaluates a semantic DNF over one input.
310#[must_use]
311pub fn evaluate_constraint(
312    grammar: &Grammar,
313    input: &str,
314    constraint: &SemanticConstraint,
315) -> TruthValue {
316    if constraint.clauses.is_empty() {
317        return TruthValue::True;
318    }
319
320    let observation = Observation::from_grammar(grammar, input);
321    constraint
322        .clauses
323        .iter()
324        .map(|clause| evaluate_clause_observation(&observation, clause))
325        .fold(TruthValue::False, TruthValue::or)
326}
327
328/// Evaluates a semantic DNF as a probabilistic truth value.
329#[must_use]
330pub fn evaluate_probabilistic(
331    grammar: &Grammar,
332    input: &str,
333    constraint: &SemanticConstraint,
334) -> ProbabilisticTruthValue {
335    ProbabilisticTruthValue::new(probability_for_truth(evaluate_constraint(
336        grammar, input, constraint,
337    )))
338}
339
340fn instantiate_def_before_use(bindings: &BTreeMap<&'static str, String>) -> Vec<ConstraintAtom> {
341    let Some(def) = bindings.get(DEF_SLOT) else {
342        return Vec::new();
343    };
344    let Some(use_) = bindings.get(USE_SLOT) else {
345        return Vec::new();
346    };
347    vec![ConstraintAtom::DefBeforeUse {
348        def: NonTerminalRef::new(def),
349        use_: NonTerminalRef::new(use_),
350    }]
351}
352
353fn instantiate_equal_count(bindings: &BTreeMap<&'static str, String>) -> Vec<ConstraintAtom> {
354    let Some(left) = bindings.get(LEFT_SLOT) else {
355        return Vec::new();
356    };
357    let Some(right) = bindings.get(RIGHT_SLOT) else {
358        return Vec::new();
359    };
360    vec![ConstraintAtom::EqualCount {
361        left: NonTerminalRef::new(left),
362        right: NonTerminalRef::new(right),
363    }]
364}
365
366fn instantiate_length_field(bindings: &BTreeMap<&'static str, String>) -> Vec<ConstraintAtom> {
367    let Some(field) = bindings.get(FIELD_SLOT) else {
368        return Vec::new();
369    };
370    let Some(body) = bindings.get(BODY_SLOT) else {
371        return Vec::new();
372    };
373    vec![ConstraintAtom::LengthField {
374        field: NonTerminalRef::new(field),
375        body: NonTerminalRef::new(body),
376        unit: LengthUnit::Bytes,
377    }]
378}
379
380fn instantiate_unique(bindings: &BTreeMap<&'static str, String>) -> Vec<ConstraintAtom> {
381    let Some(target) = bindings.get(TARGET_SLOT) else {
382        return Vec::new();
383    };
384    vec![ConstraintAtom::Unique {
385        target: NonTerminalRef::new(target),
386    }]
387}
388
389fn instantiate_ordered(bindings: &BTreeMap<&'static str, String>) -> Vec<ConstraintAtom> {
390    let Some(target) = bindings.get(TARGET_SLOT) else {
391        return Vec::new();
392    };
393    vec![ConstraintAtom::Ordered {
394        target: NonTerminalRef::new(target),
395    }]
396}
397
398fn instantiate_candidates(grammar: &Grammar, catalog: &[ConstraintPattern]) -> Vec<ConstraintAtom> {
399    let rule_names = grammar
400        .rule_names()
401        .into_iter()
402        .map(str::to_owned)
403        .collect::<Vec<_>>();
404    let mut atoms = Vec::new();
405
406    for pattern in catalog {
407        enumerate_bindings(pattern, &rule_names, 0, &mut BTreeMap::new(), &mut atoms);
408    }
409
410    atoms.sort_by_key(atom_sort_key);
411    atoms.dedup();
412    atoms
413}
414
415fn enumerate_bindings(
416    pattern: &ConstraintPattern,
417    rule_names: &[String],
418    slot_index: usize,
419    bindings: &mut BTreeMap<&'static str, String>,
420    atoms: &mut Vec<ConstraintAtom>,
421) {
422    if slot_index == pattern.slots.len() {
423        if binding_is_compatible(pattern.name, bindings) {
424            atoms.extend((pattern.instantiate)(bindings));
425        }
426        return;
427    }
428
429    let slot = pattern.slots[slot_index];
430    for rule_name in rule_names {
431        bindings.insert(slot, rule_name.clone());
432        enumerate_bindings(pattern, rule_names, slot_index + 1, bindings, atoms);
433    }
434    bindings.remove(slot);
435}
436
437fn binding_is_compatible(pattern_name: &str, bindings: &BTreeMap<&'static str, String>) -> bool {
438    match pattern_name {
439        "def-before-use" => {
440            let Some(def) = bindings.get(DEF_SLOT) else {
441                return false;
442            };
443            let Some(use_) = bindings.get(USE_SLOT) else {
444                return false;
445            };
446            def != use_
447                && has_any(def, &["def", "decl", "bind", "let", "var"])
448                && has_any(use_, &["use", "ref", "call"])
449        }
450        "equal-count" => {
451            let Some(left) = bindings.get(LEFT_SLOT) else {
452                return false;
453            };
454            let Some(right) = bindings.get(RIGHT_SLOT) else {
455                return false;
456            };
457            left != right
458                && (has_pair(left, right, "open", "close")
459                    || has_pair(left, right, "left", "right")
460                    || has_pair(left, right, "start", "end")
461                    || has_pair(left, right, "begin", "end"))
462        }
463        "length-field" => {
464            let Some(field) = bindings.get(FIELD_SLOT) else {
465                return false;
466            };
467            let Some(body) = bindings.get(BODY_SLOT) else {
468                return false;
469            };
470            field != body
471                && has_any(field, &["len", "length", "size", "count", "field"])
472                && has_any(body, &["body", "payload", "data", "content"])
473        }
474        "unique" => bindings
475            .get(TARGET_SLOT)
476            .is_some_and(|target| has_any(target, &["id", "name", "symbol", "item", "key"])),
477        "ordered" => bindings
478            .get(TARGET_SLOT)
479            .is_some_and(|target| has_any(target, &["number", "index", "order", "rank", "seq"])),
480        _ => true,
481    }
482}
483
484fn has_pair(left: &str, right: &str, left_marker: &str, right_marker: &str) -> bool {
485    has_any(left, &[left_marker]) && has_any(right, &[right_marker])
486}
487
488fn has_any(value: &str, needles: &[&str]) -> bool {
489    let lower = value.to_ascii_lowercase();
490    needles.iter().any(|needle| lower.contains(needle))
491}
492
493fn holds_on_positive_corpus(observations: &[Observation], atom: &ConstraintAtom) -> bool {
494    let mut saw_true = false;
495    for observation in observations {
496        match evaluate_atom_observation(observation, atom) {
497            TruthValue::True => saw_true = true,
498            TruthValue::Unknown => {}
499            TruthValue::False | TruthValue::Both => return false,
500        }
501    }
502    saw_true
503}
504
505fn discriminates_augmented_variant(
506    observations: &[Observation],
507    atom: &ConstraintAtom,
508    config: &SemanticInferenceConfig,
509) -> bool {
510    if config.k_path_depth == 0 || config.max_augmented == 0 {
511        return false;
512    }
513
514    observations
515        .iter()
516        .any(|observation| atom_has_discriminating_mutation(observation, atom))
517}
518
519fn atom_has_discriminating_mutation(observation: &Observation, atom: &ConstraintAtom) -> bool {
520    match atom {
521        ConstraintAtom::DefBeforeUse { def, use_ } => {
522            !observation.values(def).is_empty() && !observation.values(use_).is_empty()
523        }
524        ConstraintAtom::EqualCount { left, right } => {
525            let left_count = observation.values(left).len();
526            let right_count = observation.values(right).len();
527            left_count > 0 && left_count == right_count
528        }
529        ConstraintAtom::LengthField { field, body, .. } => {
530            !observation.values(field).is_empty() && !observation.values(body).is_empty()
531        }
532        ConstraintAtom::Unique { target } => observation.values(target).len() > 1,
533        ConstraintAtom::Ordered { target } => {
534            let values = observation.values(target);
535            values.len() > 1
536                && values.windows(2).any(|pair| {
537                    comparable_value(&pair[0].value) != comparable_value(&pair[1].value)
538                })
539        }
540    }
541}
542
543fn build_semantic_constraint(
544    atoms: Vec<ConstraintAtom>,
545    observations: &[Observation],
546    min_recall: Probability,
547) -> SemanticConstraint {
548    if atoms.is_empty() {
549        return SemanticConstraint::trivially_true();
550    }
551
552    let mut groups = BTreeMap::<Vec<u8>, Vec<ConstraintAtom>>::new();
553    for atom in atoms {
554        let signature = observations
555            .iter()
556            .map(|observation| truth_signature(evaluate_atom_observation(observation, &atom)))
557            .collect::<Vec<_>>();
558        groups.entry(signature).or_default().push(atom);
559    }
560
561    let mut clauses = groups
562        .into_values()
563        .map(|mut clause_atoms| {
564            clause_atoms.sort_by_key(atom_sort_key);
565            ConstraintClause::new(clause_atoms)
566        })
567        .filter(|clause| clause_recall(clause, observations) >= min_recall)
568        .collect::<Vec<_>>();
569
570    clauses.sort_by(|left, right| {
571        clause_specificity(right, observations)
572            .cmp(&clause_specificity(left, observations))
573            .then_with(|| clause_sort_key(left).cmp(&clause_sort_key(right)))
574    });
575
576    let specificity = clauses
577        .iter()
578        .map(|clause| clause_specificity(clause, observations))
579        .fold(0_u32, u32::saturating_add);
580    let recall = constraint_recall(&clauses, observations);
581
582    SemanticConstraint::new(clauses, specificity, recall)
583}
584
585const fn truth_signature(value: TruthValue) -> u8 {
586    match value {
587        TruthValue::Both => 0,
588        TruthValue::False => 1,
589        TruthValue::Unknown => 2,
590        TruthValue::True => 3,
591    }
592}
593
594fn clause_recall(clause: &ConstraintClause, observations: &[Observation]) -> Probability {
595    let satisfied = observations
596        .iter()
597        .filter(|observation| truth_is_satisfied(evaluate_clause_observation(observation, clause)))
598        .count();
599    probability_from_counts(satisfied, observations.len())
600}
601
602fn constraint_recall(clauses: &[ConstraintClause], observations: &[Observation]) -> Probability {
603    if observations.is_empty() {
604        return Probability::ONE;
605    }
606    if clauses.is_empty() {
607        return Probability::ONE;
608    }
609
610    let satisfied = observations
611        .iter()
612        .filter(|observation| {
613            let truth = clauses
614                .iter()
615                .map(|clause| evaluate_clause_observation(observation, clause))
616                .fold(TruthValue::False, TruthValue::or);
617            truth_is_satisfied(truth)
618        })
619        .count();
620    probability_from_counts(satisfied, observations.len())
621}
622
623fn probability_from_counts(numerator: usize, denominator: usize) -> Probability {
624    if denominator == 0 {
625        return Probability::ONE;
626    }
627    let numerator = u64::try_from(numerator).unwrap_or(u64::MAX);
628    let denominator = u64::try_from(denominator).unwrap_or(u64::MAX);
629    Probability::from_ratio(numerator, denominator).unwrap_or(Probability::ZERO)
630}
631
632const fn truth_is_satisfied(value: TruthValue) -> bool {
633    matches!(value, TruthValue::True | TruthValue::Unknown)
634}
635
636fn evaluate_clause_observation(observation: &Observation, clause: &ConstraintClause) -> TruthValue {
637    clause
638        .atoms
639        .iter()
640        .map(|atom| evaluate_atom_observation(observation, atom))
641        .fold(TruthValue::True, TruthValue::and)
642}
643
644fn evaluate_atom_observation(observation: &Observation, atom: &ConstraintAtom) -> TruthValue {
645    match atom {
646        ConstraintAtom::DefBeforeUse { def, use_ } => {
647            evaluate_def_before_use(observation.values(def), observation.values(use_))
648        }
649        ConstraintAtom::EqualCount { left, right } => {
650            evaluate_equal_count(observation.values(left), observation.values(right))
651        }
652        ConstraintAtom::LengthField { field, body, unit } => {
653            evaluate_length_field(observation.values(field), observation.values(body), *unit)
654        }
655        ConstraintAtom::Unique { target } => evaluate_unique(observation.values(target)),
656        ConstraintAtom::Ordered { target } => evaluate_ordered(observation.values(target)),
657    }
658}
659
660fn evaluate_def_before_use(defs: &[ObservedValue], uses: &[ObservedValue]) -> TruthValue {
661    if defs.is_empty() || uses.is_empty() {
662        return TruthValue::Unknown;
663    }
664
665    let mut defs = defs.to_vec();
666    defs.sort_by_key(|value| value.position);
667    let mut uses = uses.to_vec();
668    uses.sort_by_key(|value| value.position);
669
670    let mut def_index = 0;
671    let mut seen = BTreeSet::new();
672    for use_value in uses {
673        while def_index < defs.len() && defs[def_index].position < use_value.position {
674            seen.insert(defs[def_index].value.as_str());
675            def_index += 1;
676        }
677        if !seen.contains(use_value.value.as_str()) {
678            return TruthValue::False;
679        }
680    }
681
682    TruthValue::True
683}
684
685const fn evaluate_equal_count(left: &[ObservedValue], right: &[ObservedValue]) -> TruthValue {
686    if left.is_empty() || right.is_empty() {
687        return TruthValue::Unknown;
688    }
689    if left.len() == right.len() {
690        TruthValue::True
691    } else {
692        TruthValue::False
693    }
694}
695
696fn evaluate_length_field(
697    fields: &[ObservedValue],
698    bodies: &[ObservedValue],
699    unit: LengthUnit,
700) -> TruthValue {
701    if fields.is_empty() || bodies.is_empty() {
702        return TruthValue::Unknown;
703    }
704
705    for field in fields {
706        let Ok(expected) = field.value.parse::<usize>() else {
707            return TruthValue::Unknown;
708        };
709        let Some(body) = nearest_following_body(field.position, bodies).or_else(|| bodies.first())
710        else {
711            return TruthValue::Unknown;
712        };
713        if measure_body(&body.value, unit) != expected {
714            return TruthValue::False;
715        }
716    }
717
718    TruthValue::True
719}
720
721fn nearest_following_body(position: usize, bodies: &[ObservedValue]) -> Option<&ObservedValue> {
722    bodies
723        .iter()
724        .filter(|body| body.position > position)
725        .min_by_key(|body| body.position)
726}
727
728fn measure_body(body: &str, unit: LengthUnit) -> usize {
729    match unit {
730        LengthUnit::Elements => {
731            let elements = body
732                .split(',')
733                .filter(|element| !element.trim().is_empty())
734                .count();
735            if elements > 1 {
736                elements
737            } else {
738                body.chars().count()
739            }
740        }
741        LengthUnit::Bytes => body.len(),
742        LengthUnit::Chars => body.chars().count(),
743    }
744}
745
746fn evaluate_unique(values: &[ObservedValue]) -> TruthValue {
747    if values.is_empty() {
748        return TruthValue::Unknown;
749    }
750    let mut seen = BTreeSet::new();
751    for value in values {
752        if !seen.insert(value.value.as_str()) {
753            return TruthValue::False;
754        }
755    }
756    TruthValue::True
757}
758
759fn evaluate_ordered(values: &[ObservedValue]) -> TruthValue {
760    if values.is_empty() {
761        return TruthValue::Unknown;
762    }
763
764    let mut ordered = values.to_vec();
765    ordered.sort_by_key(|value| value.position);
766    if ordered
767        .windows(2)
768        .all(|pair| comparable_value(&pair[0].value) <= comparable_value(&pair[1].value))
769    {
770        TruthValue::True
771    } else {
772        TruthValue::False
773    }
774}
775
776fn comparable_value(value: &str) -> ComparableValue<'_> {
777    value
778        .parse::<i64>()
779        .map_or(ComparableValue::Text(value), ComparableValue::Number)
780}
781
782#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
783enum ComparableValue<'a> {
784    Number(i64),
785    Text(&'a str),
786}
787
788fn probability_for_truth(value: TruthValue) -> Probability {
789    match value {
790        TruthValue::True => Probability::ONE,
791        TruthValue::False => Probability::ZERO,
792        TruthValue::Unknown | TruthValue::Both => {
793            Probability::from_basis_points(5_000).expect("5,000 basis points is in range")
794        }
795    }
796}
797
798fn atom_sort_key(atom: &ConstraintAtom) -> String {
799    format!("{atom:?}")
800}
801
802fn clause_sort_key(clause: &ConstraintClause) -> String {
803    clause
804        .atoms
805        .iter()
806        .map(atom_sort_key)
807        .collect::<Vec<_>>()
808        .join("\n")
809}
810
811fn clause_specificity(clause: &ConstraintClause, observations: &[Observation]) -> u32 {
812    clause
813        .atoms
814        .iter()
815        .map(|atom| atom_specificity(atom, observations))
816        .fold(0_u32, u32::saturating_add)
817}
818
819fn atom_specificity(atom: &ConstraintAtom, observations: &[Observation]) -> u32 {
820    let rules = atom_rules(atom);
821    let touched = observations
822        .iter()
823        .map(|observation| {
824            rules
825                .iter()
826                .map(|rule| {
827                    observation
828                        .values_by_rule
829                        .get(rule.as_str())
830                        .map_or(0, Vec::len)
831                })
832                .sum::<usize>()
833        })
834        .sum::<usize>();
835    let touched = u32::try_from(touched).unwrap_or(u32::MAX);
836    touched.saturating_add(1)
837}
838
839fn atom_rules(atom: &ConstraintAtom) -> Vec<&String> {
840    match atom {
841        ConstraintAtom::DefBeforeUse { def, use_ } => vec![&def.rule, &use_.rule],
842        ConstraintAtom::EqualCount { left, right } => vec![&left.rule, &right.rule],
843        ConstraintAtom::LengthField { field, body, .. } => vec![&field.rule, &body.rule],
844        ConstraintAtom::Unique { target } | ConstraintAtom::Ordered { target } => {
845            vec![&target.rule]
846        }
847    }
848}