1use 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#[derive(Clone, Debug, PartialEq, Eq)]
35pub enum ConstraintAtom {
36 DefBeforeUse {
38 def: NonTerminalRef,
40 use_: NonTerminalRef,
42 },
43 EqualCount {
45 left: NonTerminalRef,
47 right: NonTerminalRef,
49 },
50 LengthField {
52 field: NonTerminalRef,
54 body: NonTerminalRef,
56 unit: LengthUnit,
58 },
59 Unique {
61 target: NonTerminalRef,
63 },
64 Ordered {
66 target: NonTerminalRef,
68 },
69}
70
71#[derive(Clone, Debug, PartialEq, Eq)]
73pub struct NonTerminalRef {
74 pub rule: String,
76 pub query: LinkQuery,
78}
79
80impl NonTerminalRef {
81 #[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 #[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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub enum LengthUnit {
104 Elements,
107 Bytes,
109 Chars,
111}
112
113#[derive(Clone, Debug, Default, PartialEq, Eq)]
115pub struct ConstraintClause {
116 pub atoms: Vec<ConstraintAtom>,
118}
119
120impl ConstraintClause {
121 #[must_use]
123 pub const fn new(atoms: Vec<ConstraintAtom>) -> Self {
124 Self { atoms }
125 }
126
127 #[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#[derive(Clone, Debug, PartialEq, Eq)]
137pub struct SemanticConstraint {
138 pub clauses: Vec<ConstraintClause>,
140 pub specificity: u32,
142 pub recall: Probability,
144}
145
146impl SemanticConstraint {
147 #[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 #[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 #[must_use]
173 pub fn evaluate(&self, grammar: &Grammar, input: &str) -> TruthValue {
174 evaluate_constraint(grammar, input, self)
175 }
176
177 #[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
188pub type ConstraintInstantiator = fn(&BTreeMap<&'static str, String>) -> Vec<ConstraintAtom>;
190
191#[derive(Clone, Copy)]
193pub struct ConstraintPattern {
194 pub name: &'static str,
196 pub slots: &'static [&'static str],
198 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#[derive(Clone, Debug)]
214pub struct SemanticInferenceConfig {
215 pub catalog: Vec<ConstraintPattern>,
217 pub k_path_depth: usize,
219 pub max_augmented: usize,
221 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#[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#[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#[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#[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#[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#[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}