kibi-core 0.14.1 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/logic_ir.pl CHANGED
@@ -10,15 +10,21 @@
10
10
  logic_rule_semantic_key/2,
11
11
  logic_derive/2,
12
12
  logic_rule_conflict/3,
13
+ logic_rule_conflict/4,
13
14
  logic_rule_conflict_witness/3,
15
+ logic_rule_missing_keys/4,
16
+ functional_predicate_declarations/1,
14
17
  logic_atom_signature/2,
15
- logic_rules_stratified/1
18
+ logic_rules_stratified/1,
19
+ logic_rule_fold_exceptions/2,
20
+ logic_rule_property_form/2
16
21
  ]).
17
22
 
18
23
  :- use_module(library(http/json)).
19
24
  :- use_module(library(aggregate)).
20
25
  :- use_module(library(clpfd)).
21
- :- use_module('kb.pl', [kb_entity/3, predicate_fact/5]).
26
+ :- use_module('intervals.pl', [numeric_constraints_satisfiable/2, numeric_constraint_entailed/3, numeric_constraint_holds/3]).
27
+ :- use_module('kb.pl', [kb_entity/3, predicate_fact/5, predicate_schema_keys/4]).
22
28
 
23
29
  %% logic_rule_from_props(+Props, -Rule)
24
30
  % Props is an entity property list from a fact_kind=rule fact.
@@ -364,9 +370,14 @@ logic_rule_fact(FactId, Rule) :-
364
370
  % The JSON decoder keeps variable names as data terms for validation and
365
371
  % rendering. Evaluation reifies those names into fresh Prolog variables so
366
372
  % ordinary unification can be used without ever evaluating caller source.
367
- materialize_rule(
373
+ materialize_rule(Rule, Materialized) :-
374
+ materialize_rule_environment(Rule, Materialized, _Environment).
375
+
376
+ % Environment maps each declared variable name to its fresh Prolog variable.
377
+ materialize_rule_environment(
368
378
  rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, Schema, Variables),
369
- rule(Kind, Modality, MaterialHead, MaterialBody, MaterialExceptions, Scope, From, To, Schema, Variables)
379
+ rule(Kind, Modality, MaterialHead, MaterialBody, MaterialExceptions, Scope, From, To, Schema, Variables),
380
+ Environment
370
381
  ) :-
371
382
  variable_environment(Variables, Environment),
372
383
  materialize_head(Head, Environment, MaterialHead),
@@ -487,17 +498,670 @@ temporal_end(timestamp(Value), Value).
487
498
  temporal_end(interval(_, End), End).
488
499
 
489
500
  %% logic_rule_conflict(+RuleA, +RuleB, -Status)
490
- % Conservative symbolic conflict result. Identical opposing heads are a
491
- % proven conflict; different bodies remain unresolved unless their explicit
492
- % numeric/temporal constraints are disjoint.
493
- logic_rule_conflict(RuleA, RuleB, contradiction) :-
494
- opposing_rule_heads(RuleA, RuleB),
495
- compatible_rule_context(RuleA, RuleB),
496
- same_rule_body(RuleA, RuleB), !.
497
- logic_rule_conflict(RuleA, RuleB, unresolved) :-
498
- opposing_rule_heads(RuleA, RuleB),
499
- compatible_rule_context(RuleA, RuleB), !.
500
- logic_rule_conflict(_, _, disjoint).
501
+ % implements REQ-kibi-truthful-consistency
502
+ % Three-valued comparison of two opposing rules.
503
+ %
504
+ % contradiction neither rule carries an exception, and either the rules
505
+ % are variants of each other or every instance of one rule's
506
+ % body is an instance of the other's; in both cases the
507
+ % shared body must provably have an instance (every
508
+ % comparison translated exactly, the functional closure
509
+ % consistent, no negation, disjunction, count or temporal
510
+ % relation, and the constraints satisfiable with integer
511
+ % bounds for every variable any declaration types as int),
512
+ % so the opposing modalities are certain to collide.
513
+ % Variant bodies that cannot hold are disjoint; variant
514
+ % bodies the fragment cannot decide stay unresolved.
515
+ % disjoint the heads cannot denote the same action, the scopes or
516
+ % validity windows do not intersect, the bodies provably
517
+ % cannot hold together, or an exception of one rule is
518
+ % entailed by the other rule's body.
519
+ % unresolved the rules may overlap but the fragment cannot decide it.
520
+ %
521
+ % Rules are materialized before comparison: IR variables are data terms such
522
+ % as var('T', money) and must never be treated as ground values. Body atoms
523
+ % of the two rules are identified only through a declared functional
524
+ % dependency (a predicate_schema with key_arguments): two atoms of such a
525
+ % predicate whose key arguments are identical must agree on every other
526
+ % argument. Without a declaration a predicate is multivalued, so
527
+ % final_total(C, X), X > 0 and final_total(C, Y), Y =< 0 may both hold and the
528
+ % pair stays unresolved. A variable shared by both rules counts as integral
529
+ % for a disjoint verdict only when every declaration of it is int or integer.
530
+ % These assumptions are documented in docs/inference-rules.md.
531
+ logic_rule_conflict(RuleA, RuleB, Status) :-
532
+ functional_predicate_declarations(Functional),
533
+ logic_rule_conflict(RuleA, RuleB, Functional, Status).
534
+
535
+ %% logic_rule_conflict(+RuleA, +RuleB, +Functional, -Status)
536
+ % Functional is a list of functional(Namespace, Name, Arity, KeyPositions)
537
+ % declarations; logic_rule_conflict/3 reads them from predicate_schema facts.
538
+ logic_rule_conflict(RuleA, RuleB, Functional, Status) :-
539
+ ( catch(rule_pair_status(RuleA, RuleB, Functional, Status0), _, Status0 = unresolved)
540
+ -> Status = Status0
541
+ ; Status = disjoint
542
+ ).
543
+
544
+ functional_predicate_declarations(Functional) :-
545
+ catch(
546
+ findall(functional(Namespace, Name, Arity, Keys),
547
+ predicate_schema_keys(Namespace, Name, Arity, Keys),
548
+ Functional0),
549
+ _,
550
+ Functional0 = []
551
+ ),
552
+ sort(Functional0, Functional).
553
+
554
+ %% logic_rule_missing_keys(+RuleA, +RuleB, +Functional, -Missing)
555
+ % implements REQ-kibi-truthful-consistency
556
+ % For an opposing pair that is unresolved under the Functional declarations,
557
+ % the plain-conjunction body predicates without a key_arguments declaration
558
+ % whose declaration would decide the pair. Missing is a sorted list of
559
+ % missing_keys(Namespace, Name, Arity, KeySets): KeySets are the candidate
560
+ % key-position sets (non-empty proper subsets of the argument positions)
561
+ % that turn the pair into contradiction or disjoint when that one predicate
562
+ % is declared. When no single declaration decides the pair, declaring every
563
+ % such predicate keyed on all but its last argument (the value-last shape) is
564
+ % tried as well. Missing is empty when the pair is already decided or stays
565
+ % unresolved for another reason (an untranslatable comparison, negation,
566
+ % disjunction, counts, or genuinely overlapping bodies).
567
+ logic_rule_missing_keys(RuleA, RuleB, Functional, Missing) :-
568
+ logic_rule_conflict(RuleA, RuleB, Functional, Status),
569
+ ( Status == unresolved
570
+ -> rule_body_predicates(RuleA, PredicatesA),
571
+ rule_body_predicates(RuleB, PredicatesB),
572
+ append(PredicatesA, PredicatesB, Predicates0),
573
+ sort(Predicates0, Predicates),
574
+ exclude(declared_functional(Functional), Predicates, Undeclared),
575
+ convlist(deciding_key_sets(RuleA, RuleB, Functional), Undeclared, Single),
576
+ ( Single \== []
577
+ -> Missing = Single
578
+ ; value_last_declarations(Undeclared, Declarations),
579
+ Declarations \== [],
580
+ append(Declarations, Functional, Assumed),
581
+ logic_rule_conflict(RuleA, RuleB, Assumed, AssumedStatus),
582
+ AssumedStatus \== unresolved
583
+ -> findall(missing_keys(Namespace, Name, Arity, [Keys]),
584
+ member(functional(Namespace, Name, Arity, Keys), Declarations),
585
+ Missing)
586
+ ; Missing = []
587
+ )
588
+ ; Missing = []
589
+ ).
590
+
591
+ rule_body_predicates(rule(_, _, _, Body, _, _, _, _, _, _), Predicates) :-
592
+ expression_parts(Body, parts(Atoms, _, _)),
593
+ findall(predicate(Namespace, Name, Arity),
594
+ ( member(atom(Namespace, Name, Args, _, _), Atoms), length(Args, Arity), Arity >= 2 ),
595
+ Predicates).
596
+
597
+ declared_functional(Functional, predicate(Namespace, Name, Arity)) :-
598
+ memberchk(functional(Namespace, Name, Arity, _), Functional).
599
+
600
+ deciding_key_sets(RuleA, RuleB, Functional, predicate(Namespace, Name, Arity),
601
+ missing_keys(Namespace, Name, Arity, KeySets)) :-
602
+ Arity =< 6,
603
+ findall(Keys,
604
+ ( candidate_key_set(Arity, Keys),
605
+ logic_rule_conflict(RuleA, RuleB, [functional(Namespace, Name, Arity, Keys)|Functional], Status),
606
+ Status \== unresolved
607
+ ),
608
+ KeySets),
609
+ KeySets \== [].
610
+
611
+ % Non-empty proper subsets of 1..Arity, smallest first.
612
+ candidate_key_set(Arity, Keys) :-
613
+ numlist(1, Arity, Positions),
614
+ Max is Arity - 1,
615
+ between(1, Max, Size),
616
+ length(Keys, Size),
617
+ ordered_subset(Keys, Positions).
618
+
619
+ ordered_subset([], _).
620
+ ordered_subset([Position|Rest], [Position|Positions]) :-
621
+ ordered_subset(Rest, Positions).
622
+ ordered_subset(Subset, [_|Positions]) :-
623
+ Subset = [_|_],
624
+ ordered_subset(Subset, Positions).
625
+
626
+ value_last_declarations(Predicates, Declarations) :-
627
+ findall(functional(Namespace, Name, Arity, Keys),
628
+ ( member(predicate(Namespace, Name, Arity), Predicates),
629
+ Last is Arity - 1,
630
+ numlist(1, Last, Keys)
631
+ ),
632
+ Declarations).
633
+
634
+ % Fails only when the rules are certainly disjoint: the modalities do not
635
+ % oppose, the contexts do not intersect, or the heads do not unify.
636
+ rule_pair_status(RuleA00, RuleB00, Functional, Status) :-
637
+ logic_rule_fold_exceptions(RuleA00, RuleA0),
638
+ logic_rule_fold_exceptions(RuleB00, RuleB0),
639
+ RuleA0 = rule(_, ModalityA, _, _, _, ScopeA, FromA, ToA, _, VariablesA),
640
+ RuleB0 = rule(_, ModalityB, _, _, _, ScopeB, FromB, ToB, _, VariablesB),
641
+ conflicting_modality(ModalityA, ModalityB),
642
+ rule_context_compatible(ScopeA, FromA, ToA, ScopeB, FromB, ToB),
643
+ materialize_rule_environment(RuleA0, RuleA, EnvironmentA),
644
+ materialize_rule_environment(RuleB0, RuleB, EnvironmentB),
645
+ RuleA = rule(_, _, HeadA, BodyA, ExceptionsA, _, _, _, _, _),
646
+ RuleB = rule(_, _, HeadB, BodyB, ExceptionsB, _, _, _, _, _),
647
+ declared_types(VariablesA, EnvironmentA, DeclaredA),
648
+ declared_types(VariablesB, EnvironmentB, DeclaredB),
649
+ append(DeclaredA, DeclaredB, Declared),
650
+ ( ExceptionsA == [], ExceptionsB == [],
651
+ HeadA-BodyA =@= HeadB-BodyB
652
+ -> % Variant rules share every instance: identify them and decide
653
+ % whether that one body can hold at all.
654
+ HeadA-BodyA = HeadB-BodyB,
655
+ expression_parts(BodyA, Parts),
656
+ variant_body_status(Parts, Functional, Declared, Status)
657
+ ; HeadA = HeadB,
658
+ expression_parts(BodyA, PartsA),
659
+ expression_parts(BodyB, PartsB),
660
+ body_pair_status(PartsA, PartsB, ExceptionsA, ExceptionsB, Functional, Declared, Status)
661
+ ).
662
+
663
+ %% variant_body_status(+Parts, +Functional, +Declared, -Status)
664
+ % implements REQ-kibi-truthful-consistency
665
+ % The two rules apply to exactly the same instances, so they collide exactly
666
+ % when the shared body has an instance. contradiction needs that instance to
667
+ % be proven: the functional closure holds, every comparison was translated,
668
+ % nothing is left in Other (negation, disjunction, counts, temporal
669
+ % relations), and the translated constraints are satisfiable under the
670
+ % strict integer reading. disjoint needs the body to be provably empty under
671
+ % the loose integer reading. Anything in between stays unresolved.
672
+ variant_body_status(parts(Atoms, Comparisons, Other), Functional, Declared, Status) :-
673
+ ( functional_closure(Atoms, Functional)
674
+ -> translate_comparisons(Comparisons, Constraints, Translation),
675
+ integer_variables(Declared, Loose, Strict),
676
+ ( Translation == false
677
+ -> Status = disjoint
678
+ ; \+ numeric_constraints_satisfiable(Constraints, Loose)
679
+ -> Status = disjoint
680
+ ; Translation == complete,
681
+ Other == [],
682
+ numeric_constraints_satisfiable(Constraints, Strict)
683
+ -> Status = contradiction
684
+ ; Status = unresolved
685
+ )
686
+ ; Status = disjoint
687
+ ).
688
+
689
+ body_pair_status(PartsA, PartsB, ExceptionsA, ExceptionsB, Functional, Declared, Status) :-
690
+ ( parts_jointly_unsatisfiable(PartsA, PartsB, Functional, Declared)
691
+ -> Status = disjoint
692
+ ; exception_entailed(ExceptionsA, PartsB, Declared)
693
+ -> Status = disjoint
694
+ ; exception_entailed(ExceptionsB, PartsA, Declared)
695
+ -> Status = disjoint
696
+ ; ExceptionsA == [], ExceptionsB == [],
697
+ ( certain_subsumption(PartsA, PartsB, Functional, Declared)
698
+ ; certain_subsumption(PartsB, PartsA, Functional, Declared)
699
+ )
700
+ -> Status = contradiction
701
+ ; Status = unresolved
702
+ ).
703
+
704
+ rule_context_compatible(scope(AuthA, ScopeA, _), FromA, ToA, scope(AuthB, ScopeB, _), FromB, ToB) :-
705
+ (AuthA == '' ; AuthB == '' ; AuthA == AuthB),
706
+ (ScopeA == '' ; ScopeB == '' ; ScopeA == ScopeB),
707
+ (FromA == '' ; ToB == '' ; FromA @=< ToB),
708
+ (FromB == '' ; ToA == '' ; FromB @=< ToA).
709
+
710
+ conflicting_modality(assert, deny).
711
+ conflicting_modality(deny, assert).
712
+ conflicting_modality(oblige, forbid).
713
+ conflicting_modality(forbid, oblige).
714
+ conflicting_modality(permit, forbid).
715
+ conflicting_modality(forbid, permit).
716
+
717
+ %% declared_types(+Variables, +Environment, -Declared)
718
+ % Declared pairs each materialized rule variable with its declared type. The
719
+ % two rules' lists are appended, so after the heads, bodies or functional
720
+ % values are unified a single Prolog variable can carry several declarations.
721
+ declared_types(Variables, Environment, Declared) :-
722
+ convlist(declared_type(Environment), Variables, Declared).
723
+
724
+ declared_type(Environment, variable(Name, Type, _), Variable-Type) :-
725
+ memberchk(Name-Variable, Environment).
726
+
727
+ %% integer_variables(+Declared, -Loose, -Strict)
728
+ % implements REQ-kibi-truthful-consistency
729
+ % Variables declared int or integer range over the integers, so x > 0 and
730
+ % x < 1 cannot both hold for them. A variable shared by both rules may be
731
+ % declared int by one and number by the other; the fragment does not decide
732
+ % which declaration wins, so it reads both ways:
733
+ %
734
+ % Loose variables (compared by identity, after unification) whose every
735
+ % declaration is int or integer. Proofs of emptiness (disjoint)
736
+ % use it: fewer integral variables only make constraints easier to
737
+ % satisfy, so an empty Loose reading is empty under any reading.
738
+ % Strict variables with at least one int or integer declaration. Proofs
739
+ % of an instance (contradiction) use it: an integral witness
740
+ % satisfies every declaration at once.
741
+ %
742
+ % Called after functional closure and atom matching, so identifications made
743
+ % there are seen. convlist/3 and include/3 keep the variables; findall/3
744
+ % would copy them.
745
+ integer_variables(Declared, Loose, Strict) :-
746
+ foldl(collect_declared_variable, Declared, [], Variables),
747
+ include(every_declaration_integer(Declared), Variables, Loose),
748
+ include(some_declaration_integer(Declared), Variables, Strict).
749
+
750
+ collect_declared_variable(Variable-_, Seen, Seen1) :-
751
+ ( var(Variable),
752
+ \+ ( member(Known, Seen), Known == Variable )
753
+ -> Seen1 = [Variable|Seen]
754
+ ; Seen1 = Seen
755
+ ).
756
+
757
+ every_declaration_integer(Declared, Variable) :-
758
+ forall(( member(Other-Type, Declared), Other == Variable ), integer_type(Type)).
759
+
760
+ some_declaration_integer(Declared, Variable) :-
761
+ member(Other-Type, Declared),
762
+ Other == Variable,
763
+ integer_type(Type),
764
+ !.
765
+
766
+ integer_type(int).
767
+ integer_type(integer).
768
+
769
+ %% expression_parts(+Expression, -parts(Atoms, Comparisons, Other))
770
+ % Flatten a conjunctive body. Disjunctions, negations, counts and temporal
771
+ % relations are kept in Other: they can only make an analysis unresolved.
772
+ expression_parts(none, parts([], [], [])) :- !.
773
+ expression_parts(Expression, parts(Atoms, Comparisons, Other)) :-
774
+ flatten_conjunction(Expression, Items),
775
+ partition_items(Items, Atoms, Comparisons, Other).
776
+
777
+ flatten_conjunction(all(Items), Flat) :- !,
778
+ maplist(flatten_conjunction, Items, Nested),
779
+ append(Nested, Flat).
780
+ flatten_conjunction(Item, [Item]).
781
+
782
+ partition_items([], [], [], []).
783
+ partition_items([Item|Rest], [Item|Atoms], Comparisons, Other) :-
784
+ Item = atom(_, _, _, positive, _), !,
785
+ partition_items(Rest, Atoms, Comparisons, Other).
786
+ partition_items([Item|Rest], Atoms, [Item|Comparisons], Other) :-
787
+ Item = compare(_, _, _), !,
788
+ partition_items(Rest, Atoms, Comparisons, Other).
789
+ partition_items([Item|Rest], Atoms, Comparisons, [Item|Other]) :-
790
+ partition_items(Rest, Atoms, Comparisons, Other).
791
+
792
+ %% parts_jointly_unsatisfiable(+PartsA, +PartsB, +Functional, +Declared)
793
+ % True only when the two bodies provably cannot hold for the same instance:
794
+ % a declared functional dependency forces two provably different values, a
795
+ % comparison is false, or the translated comparisons admit no value under the
796
+ % loose integer reading. Comparisons that cannot be translated are left
797
+ % out, which can only make the remaining set easier to satisfy, so they never
798
+ % make the pair disjoint.
799
+ parts_jointly_unsatisfiable(parts(AtomsA, ComparisonsA, _), parts(AtomsB, ComparisonsB, _), Functional, Declared) :-
800
+ \+ \+ (
801
+ append(AtomsA, AtomsB, Atoms),
802
+ ( functional_closure(Atoms, Functional)
803
+ -> append(ComparisonsA, ComparisonsB, Comparisons),
804
+ translate_comparisons(Comparisons, Constraints, Status),
805
+ ( Status == false
806
+ -> true
807
+ ; integer_variables(Declared, Loose, _Strict),
808
+ \+ numeric_constraints_satisfiable(Constraints, Loose)
809
+ )
810
+ ; true
811
+ )
812
+ ).
813
+
814
+ %% functional_closure(+Atoms, +Functional)
815
+ % Unify the value arguments of every pair of atoms whose predicate declares
816
+ % key arguments and whose key arguments are identical, until nothing changes.
817
+ % Fails when two such atoms carry provably different ground values. Atoms of
818
+ % undeclared predicates are never identified.
819
+ functional_closure(Atoms, Functional) :-
820
+ atom_pairs(Atoms, Pairs),
821
+ foldl(identify_functional_pair(Functional), Pairs, false, Changed),
822
+ ( Changed == true
823
+ -> functional_closure(Atoms, Functional)
824
+ ; true
825
+ ).
826
+
827
+ atom_pairs([], []).
828
+ atom_pairs([Atom|Rest], Pairs) :-
829
+ maplist(atom_pair(Atom), Rest, Head),
830
+ atom_pairs(Rest, Tail),
831
+ append(Head, Tail, Pairs).
832
+
833
+ atom_pair(Left, Right, Left-Right).
834
+
835
+ identify_functional_pair(Functional, Left-Right, Changed0, Changed) :-
836
+ Left = atom(Namespace, Name, ArgsLeft, _, _),
837
+ Right = atom(NamespaceRight, NameRight, ArgsRight, _, _),
838
+ ( Namespace == NamespaceRight,
839
+ Name == NameRight,
840
+ length(ArgsLeft, Arity),
841
+ length(ArgsRight, Arity)
842
+ -> findall(Keys, member(functional(Namespace, Name, Arity, Keys), Functional), KeySets),
843
+ foldl(identify_by_keys(ArgsLeft, ArgsRight), KeySets, Changed0, Changed)
844
+ ; Changed = Changed0
845
+ ).
846
+
847
+ identify_by_keys(ArgsLeft, ArgsRight, Keys, Changed0, Changed) :-
848
+ ( forall(member(Position, Keys),
849
+ ( nth1(Position, ArgsLeft, KeyLeft),
850
+ nth1(Position, ArgsRight, KeyRight),
851
+ KeyLeft == KeyRight ))
852
+ -> identify_values(ArgsLeft, ArgsRight, Keys, 1, Changed0, Changed)
853
+ ; Changed = Changed0
854
+ ).
855
+
856
+ identify_values([], [], _, _, Changed, Changed).
857
+ identify_values([Left|LeftRest], [Right|RightRest], Keys, Position, Changed0, Changed) :-
858
+ ( memberchk(Position, Keys)
859
+ -> Changed1 = Changed0
860
+ ; identify_value(Left, Right, Changed0, Changed1)
861
+ ),
862
+ Next is Position + 1,
863
+ identify_values(LeftRest, RightRest, Keys, Next, Changed1, Changed).
864
+
865
+ identify_value(Left, Right, Changed0, Changed) :-
866
+ ( Left == Right
867
+ -> Changed = Changed0
868
+ ; ( var(Left) ; var(Right) )
869
+ -> Left = Right,
870
+ Changed = true
871
+ ; values_distinct(Left, Right)
872
+ -> fail
873
+ ; % Equal values in different spellings (1000 ms and 1 s), or values
874
+ % this fragment cannot compare: identify nothing.
875
+ Changed = Changed0
876
+ ).
877
+
878
+ values_distinct(Left, Right) :-
879
+ ir_ground(Left),
880
+ ir_ground(Right),
881
+ catch(term_value(Left, LeftValue), _, fail),
882
+ catch(term_value(Right, RightValue), _, fail),
883
+ ( number(LeftValue), number(RightValue)
884
+ -> LeftValue =\= RightValue
885
+ ; LeftValue \== RightValue
886
+ ).
887
+
888
+ %% certain_subsumption(+General, +Specific, +Functional, +Declared)
889
+ % Every instance of Specific's body is an instance of General's body, and
890
+ % Specific's body provably has an instance: both bodies are plain
891
+ % conjunctions, every Specific comparison was translated (none is unknown),
892
+ % and the translated comparisons are satisfiable under the strict integer
893
+ % reading. An untranslatable comparison such as X < Y can make Specific's
894
+ % body empty, so it blocks a contradiction verdict. The integer readings are
895
+ % taken after atom matching, which can identify a General variable with a
896
+ % Specific one.
897
+ certain_subsumption(General, Specific, Functional, Declared) :-
898
+ General = parts(GeneralAtoms, GeneralComparisons, []),
899
+ Specific = parts(SpecificAtoms, SpecificComparisons, []),
900
+ \+ \+ (
901
+ functional_closure(SpecificAtoms, Functional),
902
+ translate_comparisons(SpecificComparisons, Premises, complete),
903
+ match_atoms(GeneralAtoms, SpecificAtoms),
904
+ integer_variables(Declared, _Loose, Strict),
905
+ numeric_constraints_satisfiable(Premises, Strict),
906
+ forall(member(Comparison, GeneralComparisons),
907
+ comparison_entailed(Premises, Strict, Comparison))
908
+ ).
909
+
910
+ %% parts_subsume(+General, +Specific, +Declared)
911
+ % Every instance of Specific's body is an instance of General's body: each
912
+ % General atom matches a Specific atom without binding Specific's variables,
913
+ % and each General comparison is entailed by Specific's translated
914
+ % comparisons under the loose integer reading. Untranslated premises are
915
+ % dropped, which only weakens what is entailed.
916
+ parts_subsume(parts(GeneralAtoms, GeneralComparisons, []), parts(SpecificAtoms, SpecificComparisons, _), Declared) :-
917
+ match_atoms(GeneralAtoms, SpecificAtoms),
918
+ translate_comparisons(SpecificComparisons, Premises, _PremiseStatus),
919
+ integer_variables(Declared, Loose, _Strict),
920
+ forall(member(Comparison, GeneralComparisons), comparison_entailed(Premises, Loose, Comparison)).
921
+
922
+ match_atoms([], _).
923
+ match_atoms([Atom|Rest], Specific) :-
924
+ member(Candidate, Specific),
925
+ subsumes_term(Atom, Candidate),
926
+ Atom = Candidate,
927
+ match_atoms(Rest, Specific).
928
+
929
+ comparison_entailed(Premises, Integers, compare(Op, Left, Right)) :-
930
+ translate_comparison(compare(Op, Left, Right), Result),
931
+ ( Result == true
932
+ -> true
933
+ ; Result = constraint(Constraint)
934
+ -> numeric_constraint_entailed(Premises, Constraint, Integers)
935
+ ; fail
936
+ ).
937
+
938
+ %% exception_entailed(+Exceptions, +OtherParts, +Declared)
939
+ % An exception that holds whenever the other rule's body holds removes every
940
+ % shared instance, so the rules cannot collide.
941
+ exception_entailed(Exceptions, OtherParts, Declared) :-
942
+ member(Exception, Exceptions),
943
+ expression_parts(Exception, ExceptionParts),
944
+ ExceptionParts = parts(_, _, []),
945
+ \+ \+ parts_subsume(ExceptionParts, OtherParts, Declared), !.
946
+
947
+ %% translate_comparisons(+Comparisons, -Constraints, -Status)
948
+ % Status is false when a comparison is certainly false, unknown when some
949
+ % comparison could not be translated, and complete otherwise.
950
+ translate_comparisons(Comparisons, Constraints, Status) :-
951
+ foldl(translate_into, Comparisons, acc([], complete), acc(Reversed, Status)),
952
+ reverse(Reversed, Constraints).
953
+
954
+ translate_into(_, acc(Constraints, false), acc(Constraints, false)) :- !.
955
+ translate_into(Comparison, acc(Constraints, Status), acc(Constraints1, Status1)) :-
956
+ translate_comparison(Comparison, Result),
957
+ ( Result == true -> Constraints1 = Constraints, Status1 = Status
958
+ ; Result == false -> Constraints1 = Constraints, Status1 = false
959
+ ; Result = constraint(Constraint) -> Constraints1 = [Constraint|Constraints], Status1 = Status
960
+ ; Constraints1 = Constraints, Status1 = unknown
961
+ ).
962
+
963
+ translate_comparison(compare(Op, Left, Right), Result) :-
964
+ comparison_operand(Left, L),
965
+ comparison_operand(Right, R),
966
+ translate_operands(Op, L, R, Result).
967
+
968
+ translate_operands(Op, num(L), num(R), Result) :- !,
969
+ ( numeric_constraint_holds(Op, L, R) -> Result = true ; Result = false ).
970
+ translate_operands(Op, var(V), num(N), constraint(c(Op, V, N))) :- !.
971
+ translate_operands(Op, num(N), var(V), constraint(c(Flipped, V, N))) :- !,
972
+ flipped_operator(Op, Flipped).
973
+ % The same variable on both sides is decided exactly: X < X never holds and
974
+ % X =< X always does, whatever X denotes.
975
+ translate_operands(Op, var(L), var(R), Result) :-
976
+ L == R, !,
977
+ ( reflexive_operator(Op) -> Result = true ; Result = false ).
978
+ translate_operands(Op, ground(L), ground(R), Result) :- !,
979
+ ( catch(compare_terms(Op, L, R), _, fail) -> Result = true ; Result = false ).
980
+ translate_operands(_, _, _, unknown).
981
+
982
+ reflexive_operator(eq).
983
+ reflexive_operator(lte).
984
+ reflexive_operator(gte).
985
+
986
+ comparison_operand(Term, var(Term)) :- var(Term), !.
987
+ comparison_operand(Term, num(Value)) :-
988
+ ir_ground(Term),
989
+ catch(term_value(Term, Value), _, fail),
990
+ number(Value), !.
991
+ comparison_operand(Term, ground(Term)) :- ir_ground(Term), !.
992
+ comparison_operand(Term, other(Term)).
993
+
994
+ % IR terms keep variables as var(Name, Type) data until materialized.
995
+ ir_ground(Term) :- ground(Term), \+ sub_term(var(_, _), Term).
996
+
997
+ flipped_operator(eq, eq).
998
+ flipped_operator(neq, neq).
999
+ flipped_operator(lt, gt).
1000
+ flipped_operator(gt, lt).
1001
+ flipped_operator(lte, gte).
1002
+ flipped_operator(gte, lte).
1003
+
1004
+
1005
+ %% logic_rule_fold_exceptions(+Rule, -Folded)
1006
+ % implements REQ-kibi-truthful-consistency
1007
+ % "forbid H :- B unless E1, ..., En" fires exactly when B holds and no Ei
1008
+ % does. When every exception is a single comparison, its negation is again
1009
+ % a comparison (lt and gte, eq and neq, ...), so the rule is equivalent to
1010
+ % "forbid H :- B, not E1, ..., not En" without exceptions. This is how an
1011
+ % "only when C" requirement (forbid unless C) is compared: the conflict and
1012
+ % scenario checks then reason about one body. Rules with any other kind of
1013
+ % exception are returned unchanged.
1014
+ logic_rule_fold_exceptions(
1015
+ rule(Kind, Modality, Head, Body, Exceptions, Scope, From, To, Schema, Variables),
1016
+ rule(Kind, Modality, Head, FoldedBody, Remaining, Scope, From, To, Schema, Variables)
1017
+ ) :-
1018
+ ( Exceptions \== [],
1019
+ maplist(negated_comparison, Exceptions, Negated)
1020
+ -> fold_body(Body, Negated, FoldedBody),
1021
+ Remaining = []
1022
+ ; FoldedBody = Body,
1023
+ Remaining = Exceptions
1024
+ ).
1025
+
1026
+ negated_comparison(compare(Op, Left, Right), compare(Negated, Left, Right)) :-
1027
+ comparison_negation(Op, Negated).
1028
+
1029
+ comparison_negation(eq, neq).
1030
+ comparison_negation(neq, eq).
1031
+ comparison_negation(lt, gte).
1032
+ comparison_negation(gte, lt).
1033
+ comparison_negation(gt, lte).
1034
+ comparison_negation(lte, gt).
1035
+
1036
+ fold_body(none, [Single], Single) :- !.
1037
+ fold_body(none, Items, all(Items)) :- !.
1038
+ fold_body(all(Items0), Extra, all(Items)) :- !, append(Items0, Extra, Items).
1039
+ fold_body(Body, Extra, all([Body|Extra])).
1040
+
1041
+ %% logic_rule_property_form(+Rule, -Form)
1042
+ % implements REQ-kibi-scenario-feasibility-v2
1043
+ % Read a rule as a restriction over subject properties: the form scenario
1044
+ % feasibility shares with the property lane. A body atom reads a property
1045
+ % when its namespace is the property's subject_key and its name the
1046
+ % property_key; its last argument is the property's value and any earlier
1047
+ % arguments name the instance. After folding comparison exceptions
1048
+ % (logic_rule_fold_exceptions/2) Form is
1049
+ %
1050
+ % property_form(Modality, Head, ScopeName, From, To, Keys, Conditions)
1051
+ %
1052
+ % Keys are the Subject-Property pairs of every property atom in the body and
1053
+ % remaining exceptions, however nested. Conditions is
1054
+ % * a list of cond(Subject-Property, Op, Term, ValueType): the rule fires
1055
+ % exactly when every condition holds (an empty list fires whenever the
1056
+ % read properties have values);
1057
+ % * never: a comparison between two constants is false, so the rule never
1058
+ % fires;
1059
+ % * untranslatable: the body uses something this reading cannot decide (a
1060
+ % negation, disjunction, count or temporal relation, an atom that is not
1061
+ % a property read, two reads of one property with different values, a
1062
+ % property value reused as an instance, a comparison between two
1063
+ % properties, or an exception that is not a single comparison).
1064
+ logic_rule_property_form(Rule0, property_form(Modality, Head, ScopeName, From, To, Keys, Conditions)) :-
1065
+ logic_rule_fold_exceptions(Rule0, Rule),
1066
+ Rule = rule(_Kind, Modality, Head, Body, Exceptions, scope(_, ScopeName, _), From, To, _Schema, _Variables),
1067
+ findall(Key,
1068
+ ( member(Expression, [Body|Exceptions]),
1069
+ expression_property_key(Expression, Key)
1070
+ ),
1071
+ Keys0),
1072
+ sort(Keys0, Keys),
1073
+ expression_parts(Body, parts(Atoms, Comparisons, Other)),
1074
+ ( Exceptions == [],
1075
+ Other == [],
1076
+ maplist(property_read, Atoms, Reads),
1077
+ property_value_bindings(Reads, Head, Bindings, ConstantConditions)
1078
+ -> foldl(comparison_condition(Bindings), Comparisons, ConstantConditions, Conditions0),
1079
+ ( Conditions0 == untranslatable
1080
+ -> Conditions = untranslatable
1081
+ ; Conditions0 == never
1082
+ -> Conditions = never
1083
+ ; reverse(Conditions0, Conditions)
1084
+ )
1085
+ ; Conditions = untranslatable
1086
+ ).
1087
+
1088
+ expression_property_key(atom(Namespace, Name, Args, _, _), Namespace-Name) :-
1089
+ Namespace \== default,
1090
+ Args \== [].
1091
+ expression_property_key(all(Items), Key) :-
1092
+ member(Item, Items),
1093
+ expression_property_key(Item, Key).
1094
+ expression_property_key(any(Items), Key) :-
1095
+ member(Item, Items),
1096
+ expression_property_key(Item, Key).
1097
+ expression_property_key(not(Atom), Key) :-
1098
+ expression_property_key(Atom, Key).
1099
+ expression_property_key(count(Atom, _, _), Key) :-
1100
+ expression_property_key(Atom, Key).
1101
+
1102
+ property_read(atom(Namespace, Name, Args, positive, _), read(Namespace-Name, Value, Instance)) :-
1103
+ Namespace \== default,
1104
+ Args \== [],
1105
+ append(Instance, [Value], Args).
1106
+
1107
+ % Bindings maps each value variable name to its property and declared type.
1108
+ % A read with a constant value is a condition on that property.
1109
+ property_value_bindings(Reads, Head, Bindings, ConstantConditions) :-
1110
+ findall(Name, (member(read(_, _, Instance), Reads), member(var(Name, _), Instance)), InstanceNames),
1111
+ ( Head = atom(_, _, HeadArgs, _, _)
1112
+ -> findall(Name, member(var(Name, _), HeadArgs), HeadNames)
1113
+ ; HeadNames = []
1114
+ ),
1115
+ append(InstanceNames, HeadNames, IdentityNames),
1116
+ foldl(read_binding(IdentityNames), Reads, acc([], []), acc(Bindings, ConstantConditions)),
1117
+ % One property, one value: two reads of the same property must agree.
1118
+ forall(
1119
+ ( member(read(Key, ValueA, _), Reads),
1120
+ member(read(Key, ValueB, _), Reads)
1121
+ ),
1122
+ ValueA == ValueB
1123
+ ).
1124
+
1125
+ read_binding(IdentityNames, read(Key, var(Name, Type), _), acc(Bindings0, Conditions), acc(Bindings, Conditions)) :-
1126
+ !,
1127
+ \+ memberchk(Name, IdentityNames),
1128
+ ( memberchk(Name-OtherKey-_, Bindings0)
1129
+ -> OtherKey == Key,
1130
+ Bindings = Bindings0
1131
+ ; Bindings = [Name-Key-Type|Bindings0]
1132
+ ).
1133
+ read_binding(_, read(Key, Value, _), acc(Bindings, Conditions), acc(Bindings, [cond(Key, eq, Value, value)|Conditions])) :-
1134
+ constant_term(Value).
1135
+
1136
+ constant_term(number(_, _)).
1137
+ constant_term(duration(_, _)).
1138
+ constant_term(const(_, _)).
1139
+
1140
+ comparison_condition(_, _, untranslatable, untranslatable) :- !.
1141
+ comparison_condition(_, _, never, never) :- !.
1142
+ comparison_condition(Bindings, compare(Op, Left, Right), Conditions, Next) :-
1143
+ condition_operand(Bindings, Left, LeftOperand),
1144
+ condition_operand(Bindings, Right, RightOperand),
1145
+ ( LeftOperand = property(Key, Type), RightOperand = constant(Term)
1146
+ -> Next = [cond(Key, Op, Term, Type)|Conditions]
1147
+ ; LeftOperand = constant(Term), RightOperand = property(Key, Type)
1148
+ -> flipped_operator(Op, Flipped),
1149
+ Next = [cond(Key, Flipped, Term, Type)|Conditions]
1150
+ ; LeftOperand = constant(_), RightOperand = constant(_)
1151
+ -> ( catch(compare_terms(Op, Left, Right), _, fail)
1152
+ -> Next = Conditions
1153
+ ; catch(compare_terms(Op, Left, Right), _, true)
1154
+ -> Next = untranslatable
1155
+ ; Next = never
1156
+ )
1157
+ ; Next = untranslatable
1158
+ ).
1159
+
1160
+ condition_operand(Bindings, var(Name, _), property(Key, Type)) :-
1161
+ memberchk(Name-Key-Type, Bindings), !.
1162
+ condition_operand(_, Term, constant(Term)) :-
1163
+ constant_term(Term), !.
1164
+ condition_operand(_, Term, other(Term)).
501
1165
 
502
1166
  %% logic_rules_stratified(+Rules)
503
1167
  % Check the finite dependency graph for a negated edge on a cycle. A single
@@ -574,56 +1238,6 @@ logic_rule_conflict_witness(RuleA, RuleB, Witness) :-
574
1238
  rule_schema_b: SchemaB
575
1239
  }.
576
1240
 
577
- opposing_rule_heads(rule(_, ModalityA, HeadA, _, _, _, _, _, _, _), rule(_, ModalityB, HeadB, _, _, _, _, _, _, _)) :-
578
- conflicting_modality(ModalityA, ModalityB),
579
- copy_term((HeadA, HeadB), (CopyA, CopyB)),
580
- CopyA = CopyB.
581
-
582
- conflicting_modality(assert, deny).
583
- conflicting_modality(deny, assert).
584
- conflicting_modality(oblige, forbid).
585
- conflicting_modality(forbid, oblige).
586
- conflicting_modality(permit, forbid).
587
- conflicting_modality(forbid, permit).
588
-
589
- compatible_rule_context(rule(_, _, _, BodyA, _, scope(AuthA, ScopeA, _), FromA, ToA, _, _), rule(_, _, _, BodyB, _, scope(AuthB, ScopeB, _), FromB, ToB, _, _)) :-
590
- (AuthA == '' ; AuthB == '' ; AuthA == AuthB),
591
- (ScopeA == '' ; ScopeB == '' ; ScopeA == ScopeB),
592
- (FromA == '' ; ToB == '' ; FromA @=< ToB),
593
- (FromB == '' ; ToA == '' ; FromB @=< ToA),
594
- bodies_may_overlap(BodyA, BodyB).
595
-
596
- bodies_may_overlap(none, _).
597
- bodies_may_overlap(_, none).
598
- bodies_may_overlap(BodyA, BodyB) :-
599
- BodyA \= none,
600
- BodyB \= none,
601
- \+ bodies_definitely_disjoint(BodyA, BodyB).
602
-
603
- bodies_definitely_disjoint(BodyA, _BodyB) :- expression_definitely_false(BodyA), !.
604
- bodies_definitely_disjoint(_BodyA, BodyB) :- expression_definitely_false(BodyB), !.
605
-
606
- % Only classify a body as impossible when its finite, ground comparison or
607
- % temporal constraint can be evaluated to false. Unknown atoms, variables,
608
- % and unsupported combinations remain unresolved rather than being treated as
609
- % disjoint by guesswork.
610
- expression_definitely_false(compare(Op, Left, Right)) :-
611
- ground(compare(Op, Left, Right)),
612
- \+ catch(compare_terms(Op, Left, Right), _, fail).
613
- expression_definitely_false(temporal(Relation, Left, Right)) :-
614
- ground(temporal(Relation, Left, Right)),
615
- \+ catch(temporal_holds(Relation, Left, Right), _, fail).
616
- expression_definitely_false(all(Items)) :-
617
- member(Item, Items),
618
- expression_definitely_false(Item), !.
619
- expression_definitely_false(any(Items)) :-
620
- Items \= [],
621
- maplist(expression_definitely_false, Items).
622
-
623
- same_rule_body(RuleA, RuleB) :-
624
- RuleA = rule(_, _, _, BodyA, ExceptionsA, ScopeA, FromA, ToA, _, _),
625
- RuleB = rule(_, _, _, BodyB, ExceptionsB, ScopeB, FromB, ToB, _, _),
626
- BodyA == BodyB, ExceptionsA == ExceptionsB, ScopeA == ScopeB, FromA == FromB, ToA == ToB.
627
1241
 
628
1242
  normalize_atom(Value, Atom) :- atom(Value), !, Atom = Value.
629
1243
  normalize_atom(Value, Atom) :- string(Value), atom_string(Atom, Value).