From 16a9b534adec2c53b50f92a43c1623918b1c59c0 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 7 Mar 2024 22:10:42 +0100 Subject: refactor: move terms and DNF into logic subproject --- subprojects/logic/build.gradle.kts | 14 + .../java/tools/refinery/logic/AbstractDomain.java | 37 +++ .../tools/refinery/logic/AnyAbstractDomain.java | 12 + .../main/java/tools/refinery/logic/Constraint.java | 82 +++++ .../refinery/logic/InvalidQueryException.java | 23 ++ .../refinery/logic/dnf/AbstractQueryBuilder.java | 175 ++++++++++ .../java/tools/refinery/logic/dnf/AnyQuery.java | 16 + .../refinery/logic/dnf/ClausePostProcessor.java | 362 +++++++++++++++++++++ .../main/java/tools/refinery/logic/dnf/Dnf.java | 235 +++++++++++++ .../java/tools/refinery/logic/dnf/DnfBuilder.java | 225 +++++++++++++ .../java/tools/refinery/logic/dnf/DnfClause.java | 37 +++ .../tools/refinery/logic/dnf/DnfPostProcessor.java | 112 +++++++ .../java/tools/refinery/logic/dnf/DnfUtils.java | 24 ++ .../refinery/logic/dnf/FunctionalDependency.java | 22 ++ .../tools/refinery/logic/dnf/FunctionalQuery.java | 126 +++++++ .../refinery/logic/dnf/FunctionalQueryBuilder.java | 29 ++ .../refinery/logic/dnf/InvalidClauseException.java | 35 ++ .../main/java/tools/refinery/logic/dnf/Query.java | 202 ++++++++++++ .../tools/refinery/logic/dnf/QueryBuilder.java | 27 ++ .../tools/refinery/logic/dnf/RelationalQuery.java | 77 +++++ .../refinery/logic/dnf/SymbolicParameter.java | 53 +++ .../logic/dnf/callback/ClauseCallback0.java | 15 + .../logic/dnf/callback/ClauseCallback1Data0.java | 16 + .../logic/dnf/callback/ClauseCallback1Data1.java | 16 + .../logic/dnf/callback/ClauseCallback2Data0.java | 16 + .../logic/dnf/callback/ClauseCallback2Data1.java | 17 + .../logic/dnf/callback/ClauseCallback2Data2.java | 16 + .../logic/dnf/callback/ClauseCallback3Data0.java | 16 + .../logic/dnf/callback/ClauseCallback3Data1.java | 17 + .../logic/dnf/callback/ClauseCallback3Data2.java | 17 + .../logic/dnf/callback/ClauseCallback3Data3.java | 16 + .../logic/dnf/callback/ClauseCallback4Data0.java | 16 + .../logic/dnf/callback/ClauseCallback4Data1.java | 17 + .../logic/dnf/callback/ClauseCallback4Data2.java | 17 + .../logic/dnf/callback/ClauseCallback4Data3.java | 17 + .../logic/dnf/callback/ClauseCallback4Data4.java | 16 + .../dnf/callback/FunctionalQueryCallback0.java | 14 + .../dnf/callback/FunctionalQueryCallback1.java | 15 + .../dnf/callback/FunctionalQueryCallback2.java | 15 + .../dnf/callback/FunctionalQueryCallback3.java | 16 + .../dnf/callback/FunctionalQueryCallback4.java | 16 + .../logic/dnf/callback/QueryCallback0.java | 13 + .../logic/dnf/callback/QueryCallback1.java | 14 + .../logic/dnf/callback/QueryCallback2.java | 14 + .../logic/dnf/callback/QueryCallback3.java | 14 + .../logic/dnf/callback/QueryCallback4.java | 14 + .../logic/equality/DeepDnfEqualityChecker.java | 77 +++++ .../logic/equality/DnfEqualityChecker.java | 17 + .../logic/equality/LiteralEqualityHelper.java | 27 ++ .../logic/equality/LiteralHashCodeHelper.java | 17 + .../SubstitutingLiteralEqualityHelper.java | 59 ++++ .../SubstitutingLiteralHashCodeHelper.java | 42 +++ .../logic/literal/AbstractCallLiteral.java | 135 ++++++++ .../logic/literal/AbstractCountLiteral.java | 107 ++++++ .../refinery/logic/literal/AbstractLiteral.java | 34 ++ .../refinery/logic/literal/AggregationLiteral.java | 143 ++++++++ .../refinery/logic/literal/AssignLiteral.java | 88 +++++ .../refinery/logic/literal/BooleanLiteral.java | 69 ++++ .../tools/refinery/logic/literal/CallLiteral.java | 134 ++++++++ .../tools/refinery/logic/literal/CallPolarity.java | 39 +++ .../tools/refinery/logic/literal/CanNegate.java | 10 + .../tools/refinery/logic/literal/CheckLiteral.java | 95 ++++++ .../tools/refinery/logic/literal/Connectivity.java | 18 + .../refinery/logic/literal/ConstantLiteral.java | 73 +++++ .../tools/refinery/logic/literal/CountLiteral.java | 45 +++ .../refinery/logic/literal/EquivalenceLiteral.java | 100 ++++++ .../refinery/logic/literal/LeftJoinLiteral.java | 140 ++++++++ .../java/tools/refinery/logic/literal/Literal.java | 32 ++ .../tools/refinery/logic/literal/Literals.java | 22 ++ .../tools/refinery/logic/literal/Reduction.java | 31 ++ .../literal/RepresentativeElectionLiteral.java | 119 +++++++ .../logic/rewriter/AbstractRecursiveRewriter.java | 26 ++ .../rewriter/ClauseInputParameterResolver.java | 160 +++++++++ .../refinery/logic/rewriter/CompositeRewriter.java | 29 ++ .../tools/refinery/logic/rewriter/DnfRewriter.java | 24 ++ .../logic/rewriter/DuplicateDnfRemover.java | 98 ++++++ .../logic/rewriter/InputParameterResolver.java | 51 +++ .../logic/substitution/MapBasedSubstitution.java | 18 + .../logic/substitution/RenewingSubstitution.java | 20 ++ .../logic/substitution/StatelessSubstitution.java | 23 ++ .../refinery/logic/substitution/Substitution.java | 29 ++ .../logic/substitution/SubstitutionBuilder.java | 79 +++++ .../tools/refinery/logic/term/AbstractTerm.java | 47 +++ .../java/tools/refinery/logic/term/Aggregator.java | 18 + .../tools/refinery/logic/term/AnyDataVariable.java | 55 ++++ .../java/tools/refinery/logic/term/AnyTerm.java | 24 ++ .../tools/refinery/logic/term/AssignedValue.java | 13 + .../java/tools/refinery/logic/term/BinaryTerm.java | 106 ++++++ .../tools/refinery/logic/term/ConstantTerm.java | 67 ++++ .../tools/refinery/logic/term/DataVariable.java | 103 ++++++ .../logic/term/ExtremeValueAggregator.java | 108 ++++++ .../tools/refinery/logic/term/NodeVariable.java | 71 ++++ .../java/tools/refinery/logic/term/Parameter.java | 68 ++++ .../refinery/logic/term/ParameterDirection.java | 22 ++ .../refinery/logic/term/StatefulAggregate.java | 22 ++ .../refinery/logic/term/StatefulAggregator.java | 28 ++ .../refinery/logic/term/StatelessAggregator.java | 25 ++ .../main/java/tools/refinery/logic/term/Term.java | 26 ++ .../java/tools/refinery/logic/term/UnaryTerm.java | 74 +++++ .../java/tools/refinery/logic/term/Variable.java | 88 +++++ .../refinery/logic/term/bool/BoolAndTerm.java | 31 ++ .../refinery/logic/term/bool/BoolBinaryTerm.java | 15 + .../refinery/logic/term/bool/BoolNotTerm.java | 31 ++ .../tools/refinery/logic/term/bool/BoolOrTerm.java | 31 ++ .../tools/refinery/logic/term/bool/BoolTerms.java | 35 ++ .../refinery/logic/term/bool/BoolXorTerm.java | 31 ++ .../cardinalityinterval/CardinalityDomain.java | 69 ++++ .../cardinalityinterval/CardinalityInterval.java | 30 ++ .../cardinalityinterval/CardinalityIntervals.java | 54 +++ .../EmptyCardinalityInterval.java | 74 +++++ .../NonEmptyCardinalityInterval.java | 109 +++++++ .../logic/term/comparable/ComparisonTerm.java | 19 ++ .../refinery/logic/term/comparable/EqTerm.java | 30 ++ .../logic/term/comparable/GreaterEqTerm.java | 30 ++ .../logic/term/comparable/GreaterTerm.java | 30 ++ .../refinery/logic/term/comparable/LessEqTerm.java | 30 ++ .../refinery/logic/term/comparable/LessTerm.java | 30 ++ .../refinery/logic/term/comparable/NotEqTerm.java | 30 ++ .../tools/refinery/logic/term/int_/IntAddTerm.java | 31 ++ .../refinery/logic/term/int_/IntBinaryTerm.java | 15 + .../tools/refinery/logic/term/int_/IntDivTerm.java | 31 ++ .../tools/refinery/logic/term/int_/IntMaxTerm.java | 31 ++ .../tools/refinery/logic/term/int_/IntMinTerm.java | 31 ++ .../refinery/logic/term/int_/IntMinusTerm.java | 30 ++ .../tools/refinery/logic/term/int_/IntMulTerm.java | 31 ++ .../refinery/logic/term/int_/IntPlusTerm.java | 30 ++ .../tools/refinery/logic/term/int_/IntPowTerm.java | 43 +++ .../tools/refinery/logic/term/int_/IntSubTerm.java | 31 ++ .../refinery/logic/term/int_/IntSumAggregator.java | 40 +++ .../tools/refinery/logic/term/int_/IntTerms.java | 95 ++++++ .../refinery/logic/term/int_/IntUnaryTerm.java | 15 + .../refinery/logic/term/int_/RealToIntTerm.java | 31 ++ .../refinery/logic/term/real/IntToRealTerm.java | 31 ++ .../refinery/logic/term/real/RealAddTerm.java | 31 ++ .../refinery/logic/term/real/RealBinaryTerm.java | 15 + .../refinery/logic/term/real/RealDivTerm.java | 31 ++ .../refinery/logic/term/real/RealMaxTerm.java | 31 ++ .../refinery/logic/term/real/RealMinTerm.java | 31 ++ .../refinery/logic/term/real/RealMinusTerm.java | 30 ++ .../refinery/logic/term/real/RealMulTerm.java | 31 ++ .../refinery/logic/term/real/RealPlusTerm.java | 30 ++ .../refinery/logic/term/real/RealPowTerm.java | 31 ++ .../refinery/logic/term/real/RealSubTerm.java | 31 ++ .../logic/term/real/RealSumAggregator.java | 90 +++++ .../tools/refinery/logic/term/real/RealTerms.java | 95 ++++++ .../refinery/logic/term/real/RealUnaryTerm.java | 15 + .../refinery/logic/term/truthvalue/TruthValue.java | 76 +++++ .../logic/term/truthvalue/TruthValueDomain.java | 69 ++++ .../uppercardinality/FiniteUpperCardinality.java | 83 +++++ .../UnboundedUpperCardinality.java | 66 ++++ .../term/uppercardinality/UpperCardinalities.java | 38 +++ .../term/uppercardinality/UpperCardinality.java | 32 ++ .../uppercardinality/UpperCardinalityAddTerm.java | 30 ++ .../UpperCardinalityBinaryTerm.java | 16 + .../uppercardinality/UpperCardinalityMaxTerm.java | 30 ++ .../uppercardinality/UpperCardinalityMinTerm.java | 30 ++ .../uppercardinality/UpperCardinalityMulTerm.java | 30 ++ .../UpperCardinalitySumAggregator.java | 84 +++++ .../uppercardinality/UpperCardinalityTerms.java | 71 ++++ .../refinery/logic/util/CycleDetectingMapper.java | 61 ++++ .../logic/valuation/MapBasedValuation.java | 22 ++ .../logic/valuation/RestrictedValuation.java | 21 ++ .../logic/valuation/SubstitutedValuation.java | 16 + .../tools/refinery/logic/valuation/Valuation.java | 37 +++ .../refinery/logic/valuation/ValuationBuilder.java | 40 +++ .../dnf/DnfBuilderLiteralEliminationTest.java | 257 +++++++++++++++ .../dnf/DnfBuilderVariableUnificationTest.java | 322 ++++++++++++++++++ .../logic/dnf/DnfToDefinitionStringTest.java | 154 +++++++++ .../tools/refinery/logic/dnf/HashCodeTest.java | 64 ++++ .../refinery/logic/dnf/TopologicalSortTest.java | 111 +++++++ .../refinery/logic/dnf/VariableDirectionTest.java | 247 ++++++++++++++ .../logic/literal/AggregationLiteralTest.java | 89 +++++ .../refinery/logic/literal/CallLiteralTest.java | 94 ++++++ .../logic/rewriter/DuplicateDnfRemoverTest.java | 162 +++++++++ .../logic/rewriter/InputParameterResolverTest.java | 225 +++++++++++++ .../refinery/logic/term/TermSubstitutionTest.java | 97 ++++++ .../logic/term/bool/BoolTermsEvaluateTest.java | 76 +++++ .../CardinalityIntervalTest.java | 127 ++++++++ .../CardinalityIntervalsTest.java | 27 ++ .../EmptyCardinalityIntervalTest.java | 19 ++ .../FiniteCardinalityIntervalTest.java | 27 ++ .../logic/term/int_/IntTermsEvaluateTest.java | 260 +++++++++++++++ .../logic/term/real/RealTermEvaluateTest.java | 239 ++++++++++++++ .../FiniteUpperCardinalityTest.java | 17 + .../uppercardinality/UpperCardinalitiesTest.java | 30 ++ .../UpperCardinalitySumAggregatorStreamTest.java | 54 +++ .../UpperCardinalitySumAggregatorTest.java | 79 +++++ .../UpperCardinalityTermsEvaluateTest.java | 103 ++++++ .../uppercardinality/UpperCardinalityTest.java | 115 +++++++ .../refinery/logic/tests/FakeFunctionView.java | 57 ++++ .../refinery/logic/tests/FakeKeyOnlyView.java | 21 ++ .../logic/tests/StructurallyEqualToRawTest.java | 155 +++++++++ .../logic/tests/StructurallyEqualToTest.java | 123 +++++++ .../MismatchDescribingDnfEqualityChecker.java | 68 ++++ .../tools/refinery/logic/tests/QueryMatchers.java | 46 +++ .../refinery/logic/tests/StructurallyEqualTo.java | 41 +++ .../logic/tests/StructurallyEqualToRaw.java | 51 +++ 197 files changed, 11636 insertions(+) create mode 100644 subprojects/logic/build.gradle.kts create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/AbstractDomain.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/AnyAbstractDomain.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/Constraint.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/InvalidQueryException.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/AbstractQueryBuilder.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/AnyQuery.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/ClausePostProcessor.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/Dnf.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfBuilder.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfClause.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfPostProcessor.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfUtils.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalDependency.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQuery.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQueryBuilder.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/InvalidClauseException.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/Query.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/QueryBuilder.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/RelationalQuery.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/SymbolicParameter.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data2.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data2.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data3.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data2.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data3.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data4.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback2.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback3.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback4.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback0.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback1.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback2.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback3.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback4.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/DeepDnfEqualityChecker.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/DnfEqualityChecker.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralEqualityHelper.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralHashCodeHelper.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralEqualityHelper.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralHashCodeHelper.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCallLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCountLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/AggregationLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/AssignLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/BooleanLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/CallLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/CallPolarity.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/CanNegate.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/CheckLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/Connectivity.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/ConstantLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/CountLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/EquivalenceLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/LeftJoinLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/Literal.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/Literals.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/Reduction.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/literal/RepresentativeElectionLiteral.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/AbstractRecursiveRewriter.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/ClauseInputParameterResolver.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/CompositeRewriter.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DnfRewriter.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DuplicateDnfRemover.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/rewriter/InputParameterResolver.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/substitution/MapBasedSubstitution.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/substitution/RenewingSubstitution.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/substitution/StatelessSubstitution.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/substitution/Substitution.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/substitution/SubstitutionBuilder.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/AbstractTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/Aggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/AnyDataVariable.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/AnyTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/AssignedValue.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/BinaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/ConstantTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/DataVariable.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/ExtremeValueAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/NodeVariable.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/Parameter.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/ParameterDirection.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregate.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/StatelessAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/Term.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/UnaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/Variable.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolAndTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolBinaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolNotTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolOrTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolTerms.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolXorTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityDomain.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityInterval.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervals.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityInterval.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/NonEmptyCardinalityInterval.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/ComparisonTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/EqTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterEqTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessEqTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/NotEqTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntAddTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntBinaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntDivTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMaxTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinusTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMulTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPlusTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPowTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSubTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSumAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntTerms.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntUnaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/int_/RealToIntTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/IntToRealTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealAddTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealBinaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealDivTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMaxTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinusTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMulTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPlusTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPowTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSubTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSumAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealTerms.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealUnaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValueDomain.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinality.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UnboundedUpperCardinality.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalities.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinality.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityAddTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityBinaryTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMaxTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMinTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMulTerm.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregator.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTerms.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/util/CycleDetectingMapper.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/valuation/MapBasedValuation.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/valuation/RestrictedValuation.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/valuation/SubstitutedValuation.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/valuation/Valuation.java create mode 100644 subprojects/logic/src/main/java/tools/refinery/logic/valuation/ValuationBuilder.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderLiteralEliminationTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderVariableUnificationTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfToDefinitionStringTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/HashCodeTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/TopologicalSortTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/dnf/VariableDirectionTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/literal/AggregationLiteralTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/literal/CallLiteralTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/rewriter/DuplicateDnfRemoverTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/rewriter/InputParameterResolverTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/TermSubstitutionTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/bool/BoolTermsEvaluateTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalsTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityIntervalTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/FiniteCardinalityIntervalTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/int_/IntTermsEvaluateTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/real/RealTermEvaluateTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinalityTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitiesTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorStreamTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTermsEvaluateTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeFunctionView.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeKeyOnlyView.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToRawTest.java create mode 100644 subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToTest.java create mode 100644 subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/MismatchDescribingDnfEqualityChecker.java create mode 100644 subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/QueryMatchers.java create mode 100644 subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualTo.java create mode 100644 subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualToRaw.java (limited to 'subprojects/logic') diff --git a/subprojects/logic/build.gradle.kts b/subprojects/logic/build.gradle.kts new file mode 100644 index 00000000..57de8e5d --- /dev/null +++ b/subprojects/logic/build.gradle.kts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ + +plugins { + id("tools.refinery.gradle.java-library") + id("tools.refinery.gradle.java-test-fixtures") +} + +dependencies { + testFixturesApi(libs.hamcrest) +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/AbstractDomain.java b/subprojects/logic/src/main/java/tools/refinery/logic/AbstractDomain.java new file mode 100644 index 00000000..607caa48 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/AbstractDomain.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic; + +import java.util.Objects; +import java.util.Optional; + +public non-sealed interface AbstractDomain extends AnyAbstractDomain { + @Override + Class abstractType(); + + @Override + Class concreteType(); + + A toAbstract(C concreteValue); + + Optional toConcrete(A abstractValue); + + default boolean isConcrete(A abstractValue) { + return toConcrete(abstractValue).isPresent(); + } + + default boolean isRefinement(A originalValue, A refinedValue) { + return Objects.equals(commonRefinement(originalValue, refinedValue), refinedValue); + } + + A commonRefinement(A leftValue, A rightValue); + + A commonAncestor(A leftValue, A rightValue); + + A unknown(); + + boolean isError(A abstractValue); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/AnyAbstractDomain.java b/subprojects/logic/src/main/java/tools/refinery/logic/AnyAbstractDomain.java new file mode 100644 index 00000000..a296f4b2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/AnyAbstractDomain.java @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic; + +public sealed interface AnyAbstractDomain permits AbstractDomain { + Class abstractType(); + + Class concreteType(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/Constraint.java b/subprojects/logic/src/main/java/tools/refinery/logic/Constraint.java new file mode 100644 index 00000000..89c8760b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/Constraint.java @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.literal.*; +import tools.refinery.logic.term.*; + +import java.util.List; + +public interface Constraint { + String name(); + + List getParameters(); + + default int arity() { + return getParameters().size(); + } + + default boolean invalidIndex(int i) { + return i < 0 || i >= arity(); + } + + default Reduction getReduction() { + return Reduction.NOT_REDUCIBLE; + } + + default boolean equals(LiteralEqualityHelper helper, Constraint other) { + return equals(other); + } + + default String toReferenceString() { + return name(); + } + + default CallLiteral call(CallPolarity polarity, List arguments) { + return new CallLiteral(polarity, this, arguments); + } + + default CallLiteral call(CallPolarity polarity, Variable... arguments) { + return call(polarity, List.of(arguments)); + } + + default CallLiteral call(Variable... arguments) { + return call(CallPolarity.POSITIVE, arguments); + } + + default CallLiteral callTransitive(NodeVariable left, NodeVariable right) { + return call(CallPolarity.TRANSITIVE, List.of(left, right)); + } + + default AssignedValue count(List arguments) { + return targetVariable -> new CountLiteral(targetVariable, this, arguments); + } + + default AssignedValue count(Variable... arguments) { + return count(List.of(arguments)); + } + + default AssignedValue aggregateBy(DataVariable inputVariable, Aggregator aggregator, + List arguments) { + return targetVariable -> new AggregationLiteral<>(targetVariable, aggregator, inputVariable, this, arguments); + } + + default AssignedValue aggregateBy(DataVariable inputVariable, Aggregator aggregator, + Variable... arguments) { + return aggregateBy(inputVariable, aggregator, List.of(arguments)); + } + + default AssignedValue leftJoinBy(DataVariable placeholderVariable, T defaultValue, + List arguments) { + return targetVariable -> new LeftJoinLiteral<>(targetVariable, placeholderVariable, defaultValue, this, + arguments); + } + + default AssignedValue leftJoinBy(DataVariable inputVariable, T defaultValue, Variable... arguments) { + return leftJoinBy(inputVariable, defaultValue, List.of(arguments)); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/InvalidQueryException.java b/subprojects/logic/src/main/java/tools/refinery/logic/InvalidQueryException.java new file mode 100644 index 00000000..b5460e0d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/InvalidQueryException.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic; + +public class InvalidQueryException extends RuntimeException { + public InvalidQueryException() { + } + + public InvalidQueryException(String message) { + super(message); + } + + public InvalidQueryException(String message, Throwable cause) { + super(message, cause); + } + + public InvalidQueryException(Throwable cause) { + super(cause); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AbstractQueryBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AbstractQueryBuilder.java new file mode 100644 index 00000000..68712b98 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AbstractQueryBuilder.java @@ -0,0 +1,175 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.dnf.callback.*; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +public abstract class AbstractQueryBuilder> { + protected final DnfBuilder dnfBuilder; + + protected AbstractQueryBuilder(DnfBuilder dnfBuilder) { + this.dnfBuilder = dnfBuilder; + } + + protected abstract T self(); + + public NodeVariable parameter() { + return dnfBuilder.parameter(); + } + + public NodeVariable parameter(String name) { + return dnfBuilder.parameter(name); + } + + public NodeVariable parameter(ParameterDirection direction) { + return dnfBuilder.parameter(direction); + } + + public NodeVariable parameter(String name, ParameterDirection direction) { + return dnfBuilder.parameter(name, direction); + } + + public T parameter(NodeVariable variable) { + dnfBuilder.parameter(variable); + return self(); + } + + public T parameter(NodeVariable variable, ParameterDirection direction) { + dnfBuilder.parameter(variable, direction); + return self(); + } + + public T parameters(NodeVariable... variables) { + dnfBuilder.parameters(variables); + return self(); + } + + public T parameters(List variables) { + dnfBuilder.parameters(variables); + return self(); + } + + public T parameters(List variables, ParameterDirection direction) { + dnfBuilder.parameters(variables, direction); + return self(); + } + + public T symbolicParameters(List parameters) { + dnfBuilder.symbolicParameters(parameters); + return self(); + } + + public T functionalDependencies(Collection> functionalDependencies) { + dnfBuilder.functionalDependencies(functionalDependencies); + return self(); + } + + public T functionalDependency(FunctionalDependency functionalDependency) { + dnfBuilder.functionalDependency(functionalDependency); + return self(); + } + + public T functionalDependency(Set forEach, Set unique) { + dnfBuilder.functionalDependency(forEach, unique); + return self(); + } + + public T clause(ClauseCallback0 callback) { + dnfBuilder.clause(callback); + return self(); + } + + public T clause(ClauseCallback1Data0 callback) { + dnfBuilder.clause(callback); + return self(); + } + + public T clause(Class type1, ClauseCallback1Data1 callback) { + dnfBuilder.clause(type1, callback); + return self(); + } + + public T clause(ClauseCallback2Data0 callback) { + dnfBuilder.clause(callback); + return self(); + } + + public T clause(Class type1, ClauseCallback2Data1 callback) { + dnfBuilder.clause(type1, callback); + return self(); + } + + public T clause(Class type1, Class type2, ClauseCallback2Data2 callback) { + dnfBuilder.clause(type1, type2, callback); + return self(); + } + + public T clause(ClauseCallback3Data0 callback) { + dnfBuilder.clause(callback); + return self(); + } + + public T clause(Class type1, ClauseCallback3Data1 callback) { + dnfBuilder.clause(type1, callback); + return self(); + } + + public T clause(Class type1, Class type2, ClauseCallback3Data2 callback) { + dnfBuilder.clause(type1, type2, callback); + return self(); + } + + public T clause(Class type1, Class type2, Class type3, + ClauseCallback3Data3 callback) { + dnfBuilder.clause(type1, type2, type3, callback); + return self(); + } + + public T clause(ClauseCallback4Data0 callback) { + dnfBuilder.clause(callback); + return self(); + } + + public T clause(Class type1, ClauseCallback4Data1 callback) { + dnfBuilder.clause(type1, callback); + return self(); + } + + public T clause(Class type1, Class type2, ClauseCallback4Data2 callback) { + dnfBuilder.clause(type1, type2, callback); + return self(); + } + + public T clause(Class type1, Class type2, Class type3, + ClauseCallback4Data3 callback) { + dnfBuilder.clause(type1, type2, type3, callback); + return self(); + } + + public T clause(Class type1, Class type2, Class type3, Class type4, + ClauseCallback4Data4 callback) { + dnfBuilder.clause(type1, type2, type3, type4, callback); + return self(); + } + + public T clause(Literal... literals) { + dnfBuilder.clause(literals); + return self(); + } + + public T clause(Collection literals) { + dnfBuilder.clause(literals); + return self(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AnyQuery.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AnyQuery.java new file mode 100644 index 00000000..aebfd73f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/AnyQuery.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +public sealed interface AnyQuery permits Query { + String name(); + + int arity(); + + Class valueType(); + + Dnf getDnf(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/ClausePostProcessor.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/ClausePostProcessor.java new file mode 100644 index 00000000..00d15a0c --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/ClausePostProcessor.java @@ -0,0 +1,362 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.jetbrains.annotations.NotNull; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.literal.*; +import tools.refinery.logic.substitution.MapBasedSubstitution; +import tools.refinery.logic.substitution.StatelessSubstitution; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.*; +import java.util.function.Function; + +class ClausePostProcessor { + private final Map parameters; + private final List literals; + private final Map representatives = new LinkedHashMap<>(); + private final Map> equivalencePartition = new HashMap<>(); + private List substitutedLiterals; + private final Set existentiallyQuantifiedVariables = new LinkedHashSet<>(); + private Set positiveVariables; + private Map> variableToLiteralInputMap; + private PriorityQueue literalsWithAllInputsBound; + private LinkedHashSet topologicallySortedLiterals; + + public ClausePostProcessor(Map parameters, List literals) { + this.parameters = parameters; + this.literals = literals; + } + + public Result postProcessClause() { + mergeEquivalentNodeVariables(); + substitutedLiterals = new ArrayList<>(literals.size()); + keepParameterEquivalences(); + substituteLiterals(); + computeExistentiallyQuantifiedVariables(); + computePositiveVariables(); + validatePositiveRepresentatives(); + validatePrivateVariables(); + topologicallySortLiterals(); + var filteredLiterals = new ArrayList(topologicallySortedLiterals.size()); + for (var literal : topologicallySortedLiterals) { + var reducedLiteral = literal.reduce(); + if (BooleanLiteral.FALSE.equals(reducedLiteral)) { + return ConstantResult.ALWAYS_FALSE; + } else if (!BooleanLiteral.TRUE.equals(reducedLiteral)) { + filteredLiterals.add(reducedLiteral); + } + } + if (filteredLiterals.isEmpty()) { + return ConstantResult.ALWAYS_TRUE; + } + if (hasContradictoryCall(filteredLiterals)) { + return ConstantResult.ALWAYS_FALSE; + } + var clause = new DnfClause(Collections.unmodifiableSet(positiveVariables), + Collections.unmodifiableList(filteredLiterals)); + return new ClauseResult(clause); + } + + private void mergeEquivalentNodeVariables() { + for (var literal : literals) { + if (isPositiveEquivalence(literal)) { + var equivalenceLiteral = (EquivalenceLiteral) literal; + mergeVariables(equivalenceLiteral.getLeft(), equivalenceLiteral.getRight()); + } + } + } + + private static boolean isPositiveEquivalence(Literal literal) { + return literal instanceof EquivalenceLiteral equivalenceLiteral && equivalenceLiteral.isPositive(); + } + + private void mergeVariables(Variable left, Variable right) { + var leftRepresentative = getRepresentative(left); + var rightRepresentative = getRepresentative(right); + var leftInfo = parameters.get(leftRepresentative); + var rightInfo = parameters.get(rightRepresentative); + if (leftInfo != null && (rightInfo == null || leftInfo.index() <= rightInfo.index())) { + // Prefer the variable occurring earlier in the parameter list as a representative. + doMergeVariables(leftRepresentative, rightRepresentative); + } else { + doMergeVariables(rightRepresentative, leftRepresentative); + } + } + + private void doMergeVariables(Variable parentRepresentative, Variable newChildRepresentative) { + var parentSet = getEquivalentVariables(parentRepresentative); + var childSet = getEquivalentVariables(newChildRepresentative); + parentSet.addAll(childSet); + equivalencePartition.remove(newChildRepresentative); + for (var childEquivalentNodeVariable : childSet) { + representatives.put(childEquivalentNodeVariable, parentRepresentative); + } + } + + private Variable getRepresentative(Variable variable) { + return representatives.computeIfAbsent(variable, Function.identity()); + } + + private Set getEquivalentVariables(Variable variable) { + var representative = getRepresentative(variable); + if (!representative.equals(variable)) { + throw new AssertionError("NodeVariable %s already has a representative %s" + .formatted(variable, representative)); + } + return equivalencePartition.computeIfAbsent(variable, key -> { + var set = HashSet.newHashSet(1); + set.add(key); + return set; + }); + } + + private void keepParameterEquivalences() { + for (var pair : representatives.entrySet()) { + var left = pair.getKey(); + var right = pair.getValue(); + if (!left.equals(right) && parameters.containsKey(left) && parameters.containsKey(right)) { + substitutedLiterals.add(new EquivalenceLiteral(true, left, right)); + } + } + } + + private void substituteLiterals() { + Substitution substitution; + if (representatives.isEmpty()) { + substitution = null; + } else { + substitution = new MapBasedSubstitution(Collections.unmodifiableMap(representatives), + StatelessSubstitution.IDENTITY); + } + for (var literal : literals) { + if (isPositiveEquivalence(literal)) { + // We already retained all equivalences that cannot be replaced with substitutions in + // {@link#keepParameterEquivalences()}. + continue; + } + var substitutedLiteral = substitution == null ? literal : literal.substitute(substitution); + substitutedLiterals.add(substitutedLiteral); + } + } + + private void computeExistentiallyQuantifiedVariables() { + for (var literal : substitutedLiterals) { + existentiallyQuantifiedVariables.addAll(literal.getOutputVariables()); + } + } + + private void computePositiveVariables() { + positiveVariables = new LinkedHashSet<>(); + for (var pair : parameters.entrySet()) { + var variable = pair.getKey(); + if (pair.getValue().direction() == ParameterDirection.IN) { + // Inputs count as positive, because they are already bound when we evaluate literals. + positiveVariables.add(variable); + } else if (!existentiallyQuantifiedVariables.contains(variable)) { + throw new InvalidQueryException("Unbound %s parameter %s" + .formatted(ParameterDirection.OUT, variable)); + } + } + positiveVariables.addAll(existentiallyQuantifiedVariables); + } + + private void validatePositiveRepresentatives() { + for (var pair : equivalencePartition.entrySet()) { + var representative = pair.getKey(); + if (!positiveVariables.contains(representative)) { + var variableSet = pair.getValue(); + throw new InvalidQueryException("Variables %s were merged by equivalence but are not bound" + .formatted(variableSet)); + } + } + } + + private void validatePrivateVariables() { + var negativeVariablesMap = new HashMap(); + for (var literal : substitutedLiterals) { + for (var variable : literal.getPrivateVariables(positiveVariables)) { + var oldLiteral = negativeVariablesMap.put(variable, literal); + if (oldLiteral != null) { + throw new InvalidQueryException("Unbound variable %s appears in multiple literals %s and %s" + .formatted(variable, oldLiteral, literal)); + } + } + } + } + + private void topologicallySortLiterals() { + topologicallySortedLiterals = LinkedHashSet.newLinkedHashSet(substitutedLiterals.size()); + variableToLiteralInputMap = new HashMap<>(); + literalsWithAllInputsBound = new PriorityQueue<>(); + int size = substitutedLiterals.size(); + for (int i = 0; i < size; i++) { + var literal = substitutedLiterals.get(i); + var sortableLiteral = new SortableLiteral(i, literal); + sortableLiteral.enqueue(); + } + while (!literalsWithAllInputsBound.isEmpty()) { + var variable = literalsWithAllInputsBound.remove(); + variable.addToSortedLiterals(); + } + if (!variableToLiteralInputMap.isEmpty()) { + throw new InvalidQueryException("Unbound input variables %s" + .formatted(variableToLiteralInputMap.keySet())); + } + } + + private boolean hasContradictoryCall(Collection filteredLiterals) { + var positiveCalls = new HashMap>(); + for (var literal : filteredLiterals) { + if (literal instanceof CallLiteral callLiteral && callLiteral.getPolarity() == CallPolarity.POSITIVE) { + var callsOfTarget = positiveCalls.computeIfAbsent(callLiteral.getTarget(), key -> new HashSet<>()); + callsOfTarget.add(callLiteral); + } + } + for (var literal : filteredLiterals) { + if (literal instanceof CallLiteral callLiteral && callLiteral.getPolarity() == CallPolarity.NEGATIVE) { + var callsOfTarget = positiveCalls.get(callLiteral.getTarget()); + if (contradicts(callLiteral, callsOfTarget)) { + return true; + } + } + } + return false; + } + + private boolean contradicts(CallLiteral negativeCall, Collection positiveCalls) { + if (positiveCalls == null) { + return false; + } + for (var positiveCall : positiveCalls) { + if (contradicts(negativeCall, positiveCall)) { + return true; + } + } + return false; + } + + private boolean contradicts(CallLiteral negativeCall, CallLiteral positiveCall) { + var privateVariables = negativeCall.getPrivateVariables(positiveVariables); + var negativeArguments = negativeCall.getArguments(); + var positiveArguments = positiveCall.getArguments(); + int arity = negativeArguments.size(); + for (int i = 0; i < arity; i++) { + var negativeArgument = negativeArguments.get(i); + if (privateVariables.contains(negativeArgument)) { + continue; + } + var positiveArgument = positiveArguments.get(i); + if (!negativeArgument.equals(positiveArgument)) { + return false; + } + } + return true; + } + + private class SortableLiteral implements Comparable { + private final int index; + private final Literal literal; + private final Set remainingInputs; + + private SortableLiteral(int index, Literal literal) { + this.index = index; + this.literal = literal; + remainingInputs = new HashSet<>(literal.getInputVariables(positiveVariables)); + for (var pair : parameters.entrySet()) { + if (pair.getValue().direction() == ParameterDirection.IN) { + remainingInputs.remove(pair.getKey()); + } + } + } + + public void enqueue() { + if (allInputsBound()) { + addToAllInputsBoundQueue(); + } else { + addToVariableToLiteralInputMap(); + } + } + + private void bindVariable(Variable input) { + if (!remainingInputs.remove(input)) { + throw new AssertionError("Already processed input %s of literal %s".formatted(input, literal)); + } + if (allInputsBound()) { + addToAllInputsBoundQueue(); + } + } + + private boolean allInputsBound() { + return remainingInputs.isEmpty(); + } + + private void addToVariableToLiteralInputMap() { + for (var inputVariable : remainingInputs) { + var literalSetForInput = variableToLiteralInputMap.computeIfAbsent( + inputVariable, key -> new HashSet<>()); + literalSetForInput.add(this); + } + } + + private void addToAllInputsBoundQueue() { + literalsWithAllInputsBound.add(this); + } + + public void addToSortedLiterals() { + if (!allInputsBound()) { + throw new AssertionError("Inputs %s of %s are not yet bound".formatted(remainingInputs, literal)); + } + // Add literal if we haven't yet added a duplicate of this literal. + topologicallySortedLiterals.add(literal); + for (var variable : literal.getOutputVariables()) { + var literalSetForInput = variableToLiteralInputMap.remove(variable); + if (literalSetForInput == null) { + continue; + } + for (var targetSortableLiteral : literalSetForInput) { + targetSortableLiteral.bindVariable(variable); + } + } + } + + @Override + public int compareTo(@NotNull ClausePostProcessor.SortableLiteral other) { + return Integer.compare(index, other.index); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SortableLiteral that = (SortableLiteral) o; + return index == that.index && Objects.equals(literal, that.literal); + } + + @Override + public int hashCode() { + return Objects.hash(index, literal); + } + } + + public sealed interface Result permits ClauseResult, ConstantResult { + } + + public record ClauseResult(DnfClause clause) implements Result { + } + + public enum ConstantResult implements Result { + ALWAYS_TRUE, + ALWAYS_FALSE + } + + public record ParameterInfo(ParameterDirection direction, int index) { + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Dnf.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Dnf.java new file mode 100644 index 00000000..0fc2a1cc --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Dnf.java @@ -0,0 +1,235 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.DnfEqualityChecker; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.SubstitutingLiteralEqualityHelper; +import tools.refinery.logic.equality.SubstitutingLiteralHashCodeHelper; +import tools.refinery.logic.literal.Reduction; +import tools.refinery.logic.term.Parameter; +import tools.refinery.logic.term.Variable; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +public final class Dnf implements Constraint { + private static final String INDENTATION = " "; + + private final String name; + private final String uniqueName; + private final List symbolicParameters; + private final List> functionalDependencies; + private final List clauses; + + Dnf(String name, List symbolicParameters, + List> functionalDependencies, List clauses) { + validateFunctionalDependencies(symbolicParameters, functionalDependencies); + this.name = name; + this.uniqueName = DnfUtils.generateUniqueName(name); + this.symbolicParameters = symbolicParameters; + this.functionalDependencies = functionalDependencies; + this.clauses = clauses; + } + + private static void validateFunctionalDependencies( + Collection symbolicParameters, + Collection> functionalDependencies) { + var parameterSet = symbolicParameters.stream().map(SymbolicParameter::getVariable).collect(Collectors.toSet()); + for (var functionalDependency : functionalDependencies) { + validateParameters(symbolicParameters, parameterSet, functionalDependency.forEach(), functionalDependency); + validateParameters(symbolicParameters, parameterSet, functionalDependency.unique(), functionalDependency); + } + } + + private static void validateParameters(Collection symbolicParameters, + Set parameterSet, Collection toValidate, + FunctionalDependency functionalDependency) { + for (var variable : toValidate) { + if (!parameterSet.contains(variable)) { + throw new InvalidQueryException( + "Variable %s of functional dependency %s does not appear in the parameter list %s" + .formatted(variable, functionalDependency, symbolicParameters)); + } + } + } + + @Override + public String name() { + return name == null ? uniqueName : name; + } + + public boolean isExplicitlyNamed() { + return name != null; + } + + public String getUniqueName() { + return uniqueName; + } + + public List getSymbolicParameters() { + return symbolicParameters; + } + + public List getParameters() { + return Collections.unmodifiableList(symbolicParameters); + } + + public List> getFunctionalDependencies() { + return functionalDependencies; + } + + @Override + public int arity() { + return symbolicParameters.size(); + } + + public List getClauses() { + return clauses; + } + + public RelationalQuery asRelation() { + return new RelationalQuery(this); + } + + public FunctionalQuery asFunction(Class type) { + return new FunctionalQuery<>(this, type); + } + + @Override + public Reduction getReduction() { + if (clauses.isEmpty()) { + return Reduction.ALWAYS_FALSE; + } + for (var clause : clauses) { + if (clause.literals().isEmpty()) { + return Reduction.ALWAYS_TRUE; + } + } + return Reduction.NOT_REDUCIBLE; + } + + public boolean equalsWithSubstitution(DnfEqualityChecker callEqualityChecker, Dnf other) { + if (arity() != other.arity()) { + return false; + } + for (int i = 0; i < arity(); i++) { + if (!symbolicParameters.get(i).getDirection().equals(other.getSymbolicParameters().get(i).getDirection())) { + return false; + } + } + int numClauses = clauses.size(); + if (numClauses != other.clauses.size()) { + return false; + } + for (int i = 0; i < numClauses; i++) { + var literalEqualityHelper = new SubstitutingLiteralEqualityHelper(callEqualityChecker, symbolicParameters, + other.symbolicParameters); + if (!clauses.get(i).equalsWithSubstitution(literalEqualityHelper, other.clauses.get(i))) { + return false; + } + } + return true; + } + + @Override + public boolean equals(LiteralEqualityHelper helper, Constraint other) { + if (other instanceof Dnf otherDnf) { + return helper.dnfEqual(this, otherDnf); + } + return false; + } + + public int hashCodeWithSubstitution() { + var helper = new SubstitutingLiteralHashCodeHelper(); + int result = 0; + for (var symbolicParameter : symbolicParameters) { + result = result * 31 + symbolicParameter.hashCodeWithSubstitution(helper); + } + for (var clause : clauses) { + result = result * 31 + clause.hashCodeWithSubstitution(helper); + } + return result; + } + + @Override + public String toString() { + return "%s/%d".formatted(name(), arity()); + } + + @Override + public String toReferenceString() { + return "@Dnf " + name(); + } + + public String toDefinitionString() { + var builder = new StringBuilder(); + builder.append("pred ").append(name()).append("("); + var parameterIterator = symbolicParameters.iterator(); + if (parameterIterator.hasNext()) { + builder.append(parameterIterator.next()); + while (parameterIterator.hasNext()) { + builder.append(", ").append(parameterIterator.next()); + } + } + builder.append(") <->"); + var clauseIterator = clauses.iterator(); + if (clauseIterator.hasNext()) { + appendClause(clauseIterator.next(), builder); + while (clauseIterator.hasNext()) { + builder.append("\n;"); + appendClause(clauseIterator.next(), builder); + } + } else { + builder.append("\n").append(INDENTATION).append(""); + } + builder.append(".\n"); + return builder.toString(); + } + + private static void appendClause(DnfClause clause, StringBuilder builder) { + var iterator = clause.literals().iterator(); + if (!iterator.hasNext()) { + builder.append("\n").append(INDENTATION).append(""); + return; + } + builder.append("\n").append(INDENTATION).append(iterator.next()); + while (iterator.hasNext()) { + builder.append(",\n").append(INDENTATION).append(iterator.next()); + } + } + + public static DnfBuilder builder() { + return builder(null); + } + + public static DnfBuilder builder(String name) { + return new DnfBuilder(name); + } + + public static DnfBuilder builderFrom(Dnf original) { + var builder = builder(original.name()); + builder.symbolicParameters(original.getSymbolicParameters()); + builder.functionalDependencies(original.getFunctionalDependencies()); + return builder; + } + + public static Dnf of(Consumer callback) { + return of(null, callback); + } + + public static Dnf of(String name, Consumer callback) { + var builder = builder(name); + callback.accept(builder); + return builder.build(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfBuilder.java new file mode 100644 index 00000000..b58c5c45 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfBuilder.java @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.dnf.callback.*; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.*; + +import java.util.*; + +@SuppressWarnings("UnusedReturnValue") +public final class DnfBuilder { + private final String name; + private final Set parameterVariables = new LinkedHashSet<>(); + private final List parameters = new ArrayList<>(); + private final List> functionalDependencies = new ArrayList<>(); + private final List> clauses = new ArrayList<>(); + + DnfBuilder(String name) { + this.name = name; + } + + public NodeVariable parameter() { + return parameter((String) null); + } + + public NodeVariable parameter(String name) { + return parameter(name, ParameterDirection.OUT); + } + + public NodeVariable parameter(ParameterDirection direction) { + return parameter((String) null, direction); + } + + public NodeVariable parameter(String name, ParameterDirection direction) { + var variable = Variable.of(name); + parameter(variable, direction); + return variable; + } + + public DataVariable parameter(Class type) { + return parameter(null, type); + } + + public DataVariable parameter(String name, Class type) { + return parameter(name, type, ParameterDirection.OUT); + } + + public DataVariable parameter(Class type, ParameterDirection direction) { + return parameter(null, type, direction); + } + + public DataVariable parameter(String name, Class type, ParameterDirection direction) { + var variable = Variable.of(name, type); + parameter(variable, direction); + return variable; + } + + public Variable parameter(Parameter parameter) { + return parameter(null, parameter); + } + + public Variable parameter(String name, Parameter parameter) { + var type = parameter.tryGetType(); + if (type.isPresent()) { + return parameter(name, type.get(), parameter.getDirection()); + } + return parameter(name, parameter.getDirection()); + } + + public DnfBuilder parameter(Variable variable) { + return parameter(variable, ParameterDirection.OUT); + } + + public DnfBuilder parameter(Variable variable, ParameterDirection direction) { + return symbolicParameter(new SymbolicParameter(variable, direction)); + } + + public DnfBuilder parameters(Variable... variables) { + return parameters(List.of(variables)); + } + + public DnfBuilder parameters(Collection variables) { + return parameters(variables, ParameterDirection.OUT); + } + + public DnfBuilder parameters(Collection variables, ParameterDirection direction) { + for (var variable : variables) { + parameter(variable, direction); + } + return this; + } + + public DnfBuilder symbolicParameter(SymbolicParameter symbolicParameter) { + var variable = symbolicParameter.getVariable(); + if (!parameterVariables.add(variable)) { + throw new InvalidQueryException("Variable %s is already on the parameter list %s" + .formatted(variable, parameters)); + } + parameters.add(symbolicParameter); + return this; + } + + public DnfBuilder symbolicParameters(SymbolicParameter... symbolicParameters) { + return symbolicParameters(List.of(symbolicParameters)); + } + + public DnfBuilder symbolicParameters(Collection symbolicParameters) { + for (var symbolicParameter : symbolicParameters) { + symbolicParameter(symbolicParameter); + } + return this; + } + + public DnfBuilder functionalDependencies(Collection> functionalDependencies) { + this.functionalDependencies.addAll(functionalDependencies); + return this; + } + + public DnfBuilder functionalDependency(FunctionalDependency functionalDependency) { + functionalDependencies.add(functionalDependency); + return this; + } + + public DnfBuilder functionalDependency(Set forEach, Set unique) { + return functionalDependency(new FunctionalDependency<>(Set.copyOf(forEach), Set.copyOf(unique))); + } + + public DnfBuilder clause(ClauseCallback0 callback) { + return clause(callback.toLiterals()); + } + + public DnfBuilder clause(ClauseCallback1Data0 callback) { + return clause(callback.toLiterals(Variable.of("v1"))); + } + + public DnfBuilder clause(Class type1, ClauseCallback1Data1 callback) { + return clause(callback.toLiterals(Variable.of("d1", type1))); + } + + public DnfBuilder clause(ClauseCallback2Data0 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"))); + } + + public DnfBuilder clause(Class type1, ClauseCallback2Data1 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("d1", type1))); + } + + public DnfBuilder clause(Class type1, Class type2, ClauseCallback2Data2 callback) { + return clause(callback.toLiterals(Variable.of("d1", type1), Variable.of("d2", type2))); + } + + public DnfBuilder clause(ClauseCallback3Data0 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"), Variable.of("v3"))); + } + + public DnfBuilder clause(Class type1, ClauseCallback3Data1 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"), Variable.of("d1", type1))); + } + + public DnfBuilder clause(Class type1, Class type2, ClauseCallback3Data2 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("d1", type1), Variable.of("d2", type2))); + } + + public DnfBuilder clause(Class type1, Class type2, Class type3, + ClauseCallback3Data3 callback) { + return clause(callback.toLiterals(Variable.of("d1", type1), Variable.of("d2", type2), + Variable.of("d3", type3))); + } + + public DnfBuilder clause(ClauseCallback4Data0 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"), Variable.of("v3"), Variable.of("v4"))); + } + + public DnfBuilder clause(Class type1, ClauseCallback4Data1 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"), Variable.of("v3"), Variable.of("d1", + type1))); + } + + public DnfBuilder clause(Class type1, Class type2, ClauseCallback4Data2 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("v2"), Variable.of("d1", type1), + Variable.of("d2", type2))); + } + + public DnfBuilder clause(Class type1, Class type2, Class type3, + ClauseCallback4Data3 callback) { + return clause(callback.toLiterals(Variable.of("v1"), Variable.of("d1", type1), Variable.of("d2", type2), + Variable.of("d3", type3))); + } + + public DnfBuilder clause(Class type1, Class type2, Class type3, Class type4, + ClauseCallback4Data4 callback) { + return clause(callback.toLiterals(Variable.of("d1", type1), Variable.of("d2", type2), + Variable.of("d3", type3), Variable.of("d4", type4))); + } + + public DnfBuilder clause(Literal... literals) { + clause(List.of(literals)); + return this; + } + + public DnfBuilder clause(Collection literals) { + clauses.add(List.copyOf(literals)); + return this; + } + + void output(DataVariable outputVariable) { + // Copy parameter variables to exclude the newly added {@code outputVariable}. + var fromParameters = Set.copyOf(parameterVariables); + parameter(outputVariable, ParameterDirection.OUT); + functionalDependency(fromParameters, Set.of(outputVariable)); + } + + public Dnf build() { + var postProcessor = new DnfPostProcessor(parameters, clauses); + var postProcessedClauses = postProcessor.postProcessClauses(); + return new Dnf(name, Collections.unmodifiableList(parameters), + Collections.unmodifiableList(functionalDependencies), + Collections.unmodifiableList(postProcessedClauses)); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfClause.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfClause.java new file mode 100644 index 00000000..92755d4d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfClause.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.Variable; + +import java.util.List; +import java.util.Set; + +public record DnfClause(Set positiveVariables, List literals) { + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, DnfClause other) { + int size = literals.size(); + if (size != other.literals.size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!literals.get(i).equalsWithSubstitution(helper, other.literals.get(i))) { + return false; + } + } + return true; + } + + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + int result = 0; + for (var literal : literals) { + result = result * 31 + literal.hashCodeWithSubstitution(helper); + } + return result; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfPostProcessor.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfPostProcessor.java new file mode 100644 index 00000000..87d07187 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfPostProcessor.java @@ -0,0 +1,112 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.DnfEqualityChecker; +import tools.refinery.logic.equality.SubstitutingLiteralEqualityHelper; +import tools.refinery.logic.equality.SubstitutingLiteralHashCodeHelper; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.*; + +class DnfPostProcessor { + private final List parameters; + private final List> clauses; + + public DnfPostProcessor(List parameters, List> clauses) { + this.parameters = parameters; + this.clauses = clauses; + } + + public List postProcessClauses() { + var parameterInfoMap = getParameterInfoMap(); + var postProcessedClauses = LinkedHashSet.newLinkedHashSet(clauses.size()); + int index = 0; + for (var literals : clauses) { + var postProcessor = new ClausePostProcessor(parameterInfoMap, literals); + ClausePostProcessor.Result result; + try { + result = postProcessor.postProcessClause(); + } catch (InvalidQueryException e) { + throw new InvalidClauseException(index, e); + } + if (result instanceof ClausePostProcessor.ClauseResult clauseResult) { + postProcessedClauses.add(new CanonicalClause(clauseResult.clause())); + } else if (result instanceof ClausePostProcessor.ConstantResult constantResult) { + switch (constantResult) { + case ALWAYS_TRUE -> { + var inputVariables = getInputVariables(); + return List.of(new DnfClause(inputVariables, List.of())); + } + case ALWAYS_FALSE -> { + // Skip this clause because it can never match. + } + default -> throw new IllegalStateException("Unexpected ClausePostProcessor.ConstantResult: " + + constantResult); + } + } else { + throw new IllegalStateException("Unexpected ClausePostProcessor.Result: " + result); + } + index++; + } + return postProcessedClauses.stream().map(CanonicalClause::getDnfClause).toList(); + } + + private Map getParameterInfoMap() { + var mutableParameterInfoMap = new LinkedHashMap(); + int arity = parameters.size(); + for (int i = 0; i < arity; i++) { + var parameter = parameters.get(i); + mutableParameterInfoMap.put(parameter.getVariable(), + new ClausePostProcessor.ParameterInfo(parameter.getDirection(), i)); + } + return Collections.unmodifiableMap(mutableParameterInfoMap); + } + + private Set getInputVariables() { + var inputParameters = new LinkedHashSet(); + for (var parameter : parameters) { + if (parameter.getDirection() == ParameterDirection.IN) { + inputParameters.add(parameter.getVariable()); + } + } + return Collections.unmodifiableSet(inputParameters); + } + + private class CanonicalClause { + private final DnfClause dnfClause; + + public CanonicalClause(DnfClause dnfClause) { + this.dnfClause = dnfClause; + } + + public DnfClause getDnfClause() { + return dnfClause; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + var otherCanonicalClause = (CanonicalClause) obj; + var helper = new SubstitutingLiteralEqualityHelper(DnfEqualityChecker.DEFAULT, parameters, parameters); + return dnfClause.equalsWithSubstitution(helper, otherCanonicalClause.dnfClause); + } + + @Override + public int hashCode() { + var helper = new SubstitutingLiteralHashCodeHelper(parameters); + return dnfClause.hashCodeWithSubstitution(helper); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfUtils.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfUtils.java new file mode 100644 index 00000000..02a619a6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/DnfUtils.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import java.util.UUID; + +public final class DnfUtils { + private DnfUtils() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static String generateUniqueName(String originalName) { + UUID uuid = UUID.randomUUID(); + String uniqueString = "_" + uuid.toString().replace('-', '_'); + if (originalName == null) { + return uniqueString; + } else { + return originalName + uniqueString; + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalDependency.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalDependency.java new file mode 100644 index 00000000..c3fc87ab --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalDependency.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; + +import java.util.HashSet; +import java.util.Set; + +public record FunctionalDependency(Set forEach, Set unique) { + public FunctionalDependency { + var uniqueForEach = new HashSet<>(unique); + uniqueForEach.retainAll(forEach); + if (!uniqueForEach.isEmpty()) { + throw new InvalidQueryException("Variables %s appear on both sides of the functional dependency" + .formatted(uniqueForEach)); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQuery.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQuery.java new file mode 100644 index 00000000..1df63fbd --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQuery.java @@ -0,0 +1,126 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.literal.CallPolarity; +import tools.refinery.logic.term.Aggregator; +import tools.refinery.logic.term.AssignedValue; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Variable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class FunctionalQuery extends Query { + private final Class type; + + FunctionalQuery(Dnf dnf, Class type) { + super(dnf); + var parameters = dnf.getSymbolicParameters(); + int outputIndex = dnf.arity() - 1; + for (int i = 0; i < outputIndex; i++) { + var parameter = parameters.get(i); + var parameterType = parameter.tryGetType(); + if (parameterType.isPresent()) { + throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead" + .formatted(parameter, dnf, parameterType.get().getName())); + } + } + var outputParameter = parameters.get(outputIndex); + var outputParameterType = outputParameter.tryGetType(); + if (outputParameterType.isEmpty() || !outputParameterType.get().equals(type)) { + throw new InvalidQueryException("Expected parameter %s of %s to be %s, but got %s instead".formatted( + outputParameter, dnf, type, outputParameterType.map(Class::getName).orElse("node"))); + } + this.type = type; + } + + @Override + public int arity() { + return getDnf().arity() - 1; + } + + @Override + public Class valueType() { + return type; + } + + @Override + public T defaultValue() { + return null; + } + + @Override + protected FunctionalQuery withDnfInternal(Dnf newDnf) { + return newDnf.asFunction(type); + } + + @Override + public FunctionalQuery withDnf(Dnf newDnf) { + return (FunctionalQuery) super.withDnf(newDnf); + } + + public AssignedValue call(List arguments) { + return targetVariable -> { + var argumentsWithTarget = new ArrayList(arguments.size() + 1); + argumentsWithTarget.addAll(arguments); + argumentsWithTarget.add(targetVariable); + return getDnf().call(CallPolarity.POSITIVE, argumentsWithTarget); + }; + } + + public AssignedValue call(NodeVariable... arguments) { + return call(List.of(arguments)); + } + + public AssignedValue aggregate(Aggregator aggregator, List arguments) { + return targetVariable -> { + var placeholderVariable = Variable.of(type); + var argumentsWithPlaceholder = new ArrayList(arguments.size() + 1); + argumentsWithPlaceholder.addAll(arguments); + argumentsWithPlaceholder.add(placeholderVariable); + return getDnf() + .aggregateBy(placeholderVariable, aggregator, argumentsWithPlaceholder) + .toLiteral(targetVariable); + }; + } + + public AssignedValue aggregate(Aggregator aggregator, NodeVariable... arguments) { + return aggregate(aggregator, List.of(arguments)); + } + + public AssignedValue leftJoin(T defaultValue, List arguments) { + return targetVariable -> { + var placeholderVariable = Variable.of(type); + var argumentsWithPlaceholder = new ArrayList(arguments.size() + 1); + argumentsWithPlaceholder.addAll(arguments); + argumentsWithPlaceholder.add(placeholderVariable); + return getDnf() + .leftJoinBy(placeholderVariable, defaultValue, argumentsWithPlaceholder) + .toLiteral(targetVariable); + }; + } + + public AssignedValue leftJoin(T defaultValue, NodeVariable... arguments) { + return leftJoin(defaultValue, List.of(arguments)); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + FunctionalQuery that = (FunctionalQuery) o; + return Objects.equals(type, that.type); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQueryBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQueryBuilder.java new file mode 100644 index 00000000..476f3c83 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/FunctionalQueryBuilder.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.term.DataVariable; + +public final class FunctionalQueryBuilder extends AbstractQueryBuilder> { + private final DataVariable outputVariable; + private final Class type; + + FunctionalQueryBuilder(DataVariable outputVariable, DnfBuilder dnfBuilder, Class type) { + super(dnfBuilder); + this.outputVariable = outputVariable; + this.type = type; + } + + @Override + protected FunctionalQueryBuilder self() { + return this; + } + + public FunctionalQuery build() { + dnfBuilder.output(outputVariable); + return dnfBuilder.build().asFunction(type); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/InvalidClauseException.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/InvalidClauseException.java new file mode 100644 index 00000000..51a42d02 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/InvalidClauseException.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; + +public class InvalidClauseException extends InvalidQueryException { + private final int clauseIndex; + + public InvalidClauseException(int clauseIndex) { + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, String message) { + super(message); + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, String message, Throwable cause) { + super(message, cause); + this.clauseIndex = clauseIndex; + } + + public InvalidClauseException(int clauseIndex, Throwable cause) { + super(cause); + this.clauseIndex = clauseIndex; + } + + public int getClauseIndex() { + return clauseIndex; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Query.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Query.java new file mode 100644 index 00000000..1f913ea0 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/Query.java @@ -0,0 +1,202 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.dnf.callback.*; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.Objects; + +public abstract sealed class Query implements AnyQuery permits FunctionalQuery, RelationalQuery { + private static final String OUTPUT_VARIABLE_NAME = "output"; + + private final Dnf dnf; + + protected Query(Dnf dnf) { + for (var parameter : dnf.getSymbolicParameters()) { + if (parameter.getDirection() != ParameterDirection.OUT) { + throw new IllegalArgumentException("Query parameter %s with direction %s is not allowed" + .formatted(parameter.getVariable(), parameter.getDirection())); + } + } + this.dnf = dnf; + } + + @Override + public String name() { + return dnf.name(); + } + + @Override + public Dnf getDnf() { + return dnf; + } + + // Allow redeclaration of the method with refined return type. + @SuppressWarnings("squid:S3038") + @Override + public abstract Class valueType(); + + public abstract T defaultValue(); + + public Query withDnf(Dnf newDnf) { + if (dnf.equals(newDnf)) { + return this; + } + int arity = dnf.arity(); + if (newDnf.arity() != arity) { + throw new IllegalArgumentException("Arity of %s and %s do not match".formatted(dnf, newDnf)); + } + var parameters = dnf.getParameters(); + var newParameters = newDnf.getParameters(); + for (int i = 0; i < arity; i++) { + var parameter = parameters.get(i); + var newParameter = newParameters.get(i); + if (!parameter.matches(newParameter)) { + throw new IllegalArgumentException("Parameter #%d mismatch: %s does not match %s" + .formatted(i, parameter, newParameter)); + } + } + return withDnfInternal(newDnf); + } + + protected abstract Query withDnfInternal(Dnf newDnf); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Query that = (Query) o; + return Objects.equals(dnf, that.dnf); + } + + @Override + public int hashCode() { + return Objects.hash(dnf); + } + + @Override + public String toString() { + return dnf.toString(); + } + + public static QueryBuilder builder() { + return builder(null); + } + + public static QueryBuilder builder(String name) { + return new QueryBuilder(name); + } + + public static RelationalQuery of(QueryCallback0 callback) { + return of(null, callback); + } + + public static RelationalQuery of(String name, QueryCallback0 callback) { + var builder = builder(name); + callback.accept(builder); + return builder.build(); + } + + public static RelationalQuery of(QueryCallback1 callback) { + return of(null, callback); + } + + public static RelationalQuery of(String name, QueryCallback1 callback) { + var builder = builder(name); + callback.accept(builder, builder.parameter("p1")); + return builder.build(); + } + + public static RelationalQuery of(QueryCallback2 callback) { + return of(null, callback); + } + + public static RelationalQuery of(String name, QueryCallback2 callback) { + var builder = builder(name); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2")); + return builder.build(); + } + + public static RelationalQuery of(QueryCallback3 callback) { + return of(null, callback); + } + + public static RelationalQuery of(String name, QueryCallback3 callback) { + var builder = builder(name); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2"), builder.parameter("p3")); + return builder.build(); + } + + public static RelationalQuery of(QueryCallback4 callback) { + return of(null, callback); + } + + public static RelationalQuery of(String name, QueryCallback4 callback) { + var builder = builder(name); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2"), builder.parameter("p3"), + builder.parameter("p4")); + return builder.build(); + } + + public static FunctionalQuery of(Class type, FunctionalQueryCallback0 callback) { + return of(null, type, callback); + } + + public static FunctionalQuery of(String name, Class type, FunctionalQueryCallback0 callback) { + var outputVariable = Variable.of(OUTPUT_VARIABLE_NAME, type); + var builder = builder(name).output(outputVariable); + callback.accept(builder, outputVariable); + return builder.build(); + } + + public static FunctionalQuery of(Class type, FunctionalQueryCallback1 callback) { + return of(null, type, callback); + } + + public static FunctionalQuery of(String name, Class type, FunctionalQueryCallback1 callback) { + var outputVariable = Variable.of(OUTPUT_VARIABLE_NAME, type); + var builder = builder(name).output(outputVariable); + callback.accept(builder, builder.parameter("p1"), outputVariable); + return builder.build(); + } + + public static FunctionalQuery of(Class type, FunctionalQueryCallback2 callback) { + return of(null, type, callback); + } + + public static FunctionalQuery of(String name, Class type, FunctionalQueryCallback2 callback) { + var outputVariable = Variable.of(OUTPUT_VARIABLE_NAME, type); + var builder = builder(name).output(outputVariable); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2"), outputVariable); + return builder.build(); + } + + public static FunctionalQuery of(Class type, FunctionalQueryCallback3 callback) { + return of(null, type, callback); + } + + public static FunctionalQuery of(String name, Class type, FunctionalQueryCallback3 callback) { + var outputVariable = Variable.of(OUTPUT_VARIABLE_NAME, type); + var builder = builder(name).output(outputVariable); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2"), builder.parameter("p3"), + outputVariable); + return builder.build(); + } + + public static FunctionalQuery of(Class type, FunctionalQueryCallback4 callback) { + return of(null, type, callback); + } + + public static FunctionalQuery of(String name, Class type, FunctionalQueryCallback4 callback) { + var outputVariable = Variable.of(OUTPUT_VARIABLE_NAME, type); + var builder = builder(name).output(outputVariable); + callback.accept(builder, builder.parameter("p1"), builder.parameter("p2"), builder.parameter("p3"), + builder.parameter("p4"), outputVariable); + return builder.build(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/QueryBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/QueryBuilder.java new file mode 100644 index 00000000..a74295ba --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/QueryBuilder.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.term.DataVariable; + +public final class QueryBuilder extends AbstractQueryBuilder { + QueryBuilder(String name) { + super(Dnf.builder(name)); + } + + @Override + protected QueryBuilder self() { + return this; + } + + public FunctionalQueryBuilder output(DataVariable outputVariable) { + return new FunctionalQueryBuilder<>(outputVariable, dnfBuilder, outputVariable.getType()); + } + + public RelationalQuery build() { + return dnfBuilder.build().asRelation(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/RelationalQuery.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/RelationalQuery.java new file mode 100644 index 00000000..dc2b8eb6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/RelationalQuery.java @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.literal.CallLiteral; +import tools.refinery.logic.literal.CallPolarity; +import tools.refinery.logic.term.AssignedValue; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collections; +import java.util.List; + +public final class RelationalQuery extends Query { + RelationalQuery(Dnf dnf) { + super(dnf); + for (var parameter : dnf.getSymbolicParameters()) { + var parameterType = parameter.tryGetType(); + if (parameterType.isPresent()) { + throw new InvalidQueryException("Expected parameter %s of %s to be a node variable, got %s instead" + .formatted(parameter, dnf, parameterType.get().getName())); + } + } + } + + @Override + public int arity() { + return getDnf().arity(); + } + + @Override + public Class valueType() { + return Boolean.class; + } + + @Override + public Boolean defaultValue() { + return false; + } + + @Override + protected RelationalQuery withDnfInternal(Dnf newDnf) { + return newDnf.asRelation(); + } + + @Override + public RelationalQuery withDnf(Dnf newDnf) { + return (RelationalQuery) super.withDnf(newDnf); + } + + public CallLiteral call(CallPolarity polarity, List arguments) { + return getDnf().call(polarity, Collections.unmodifiableList(arguments)); + } + + public CallLiteral call(CallPolarity polarity, NodeVariable... arguments) { + return getDnf().call(polarity, arguments); + } + + public CallLiteral call(NodeVariable... arguments) { + return getDnf().call(arguments); + } + + public CallLiteral callTransitive(NodeVariable left, NodeVariable right) { + return getDnf().callTransitive(left, right); + } + + public AssignedValue count(List arguments) { + return getDnf().count(Collections.unmodifiableList(arguments)); + } + + public AssignedValue count(NodeVariable... arguments) { + return getDnf().count(arguments); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/SymbolicParameter.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/SymbolicParameter.java new file mode 100644 index 00000000..acc775a7 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/SymbolicParameter.java @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.term.Parameter; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.Objects; + +public final class SymbolicParameter extends Parameter { + private final Variable variable; + + public SymbolicParameter(Variable variable, ParameterDirection direction) { + super(variable.tryGetType().orElse(null), direction); + this.variable = variable; + } + + public Variable getVariable() { + return variable; + } + + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCode(), helper.getVariableHashCode(variable)); + } + + @Override + public String toString() { + var direction = getDirection(); + if (direction == ParameterDirection.OUT) { + return variable.toString(); + } + return "%s %s".formatted(getDirection(), variable); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + SymbolicParameter that = (SymbolicParameter) o; + return Objects.equals(variable, that.variable); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), variable); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback0.java new file mode 100644 index 00000000..2e276030 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback0.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback0 { + Collection toLiterals(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data0.java new file mode 100644 index 00000000..f2e174b0 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data0.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback1Data0 { + Collection toLiterals(NodeVariable v1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data1.java new file mode 100644 index 00000000..4e6b5a06 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback1Data1.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback1Data1 { + Collection toLiterals(DataVariable d1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data0.java new file mode 100644 index 00000000..42b7cb08 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data0.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback2Data0 { + Collection toLiterals(NodeVariable v1, NodeVariable v2); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data1.java new file mode 100644 index 00000000..59e53744 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data1.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback2Data1 { + Collection toLiterals(NodeVariable v1, DataVariable x1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data2.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data2.java new file mode 100644 index 00000000..d9550d49 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback2Data2.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback2Data2 { + Collection toLiterals(DataVariable x1, DataVariable x2); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data0.java new file mode 100644 index 00000000..e36f05db --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data0.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback3Data0 { + Collection toLiterals(NodeVariable v1, NodeVariable v2, NodeVariable v3); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data1.java new file mode 100644 index 00000000..7227bada --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data1.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback3Data1 { + Collection toLiterals(NodeVariable v1, NodeVariable v2, DataVariable d1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data2.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data2.java new file mode 100644 index 00000000..7d842655 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data2.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback3Data2 { + Collection toLiterals(NodeVariable v1, DataVariable d1, DataVariable d2); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data3.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data3.java new file mode 100644 index 00000000..3bc895d5 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback3Data3.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback3Data3 { + Collection toLiterals(DataVariable d1, DataVariable d2, DataVariable d3); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data0.java new file mode 100644 index 00000000..164c3208 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data0.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback4Data0 { + Collection toLiterals(NodeVariable v1, NodeVariable v2, NodeVariable v3, NodeVariable v4); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data1.java new file mode 100644 index 00000000..d7454135 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data1.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback4Data1 { + Collection toLiterals(NodeVariable v1, NodeVariable v2, NodeVariable v3, DataVariable d1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data2.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data2.java new file mode 100644 index 00000000..829dbcf8 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data2.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback4Data2 { + Collection toLiterals(NodeVariable v1, NodeVariable v2, DataVariable d1, DataVariable d2); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data3.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data3.java new file mode 100644 index 00000000..50da829b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data3.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback4Data3 { + Collection toLiterals(NodeVariable v1, DataVariable d1, DataVariable d2, DataVariable d3); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data4.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data4.java new file mode 100644 index 00000000..46f8fbe9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/ClauseCallback4Data4.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; + +import java.util.Collection; + +@FunctionalInterface +public interface ClauseCallback4Data4 { + Collection toLiterals(DataVariable d1, DataVariable d2, DataVariable d3, DataVariable d4); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback0.java new file mode 100644 index 00000000..689a5b53 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback0.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.FunctionalQueryBuilder; +import tools.refinery.logic.term.DataVariable; + +@FunctionalInterface +public interface FunctionalQueryCallback0 { + void accept(FunctionalQueryBuilder builder, DataVariable output); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback1.java new file mode 100644 index 00000000..b7d69a50 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback1.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.FunctionalQueryBuilder; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface FunctionalQueryCallback1 { + void accept(FunctionalQueryBuilder builder, NodeVariable p1, DataVariable output); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback2.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback2.java new file mode 100644 index 00000000..38fb265b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback2.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.FunctionalQueryBuilder; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface FunctionalQueryCallback2 { + void accept(FunctionalQueryBuilder builder, NodeVariable p1, NodeVariable p2, DataVariable output); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback3.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback3.java new file mode 100644 index 00000000..04451657 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback3.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.FunctionalQueryBuilder; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface FunctionalQueryCallback3 { + void accept(FunctionalQueryBuilder builder, NodeVariable p1, NodeVariable p2, NodeVariable p3, + DataVariable output); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback4.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback4.java new file mode 100644 index 00000000..a39c389d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/FunctionalQueryCallback4.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.FunctionalQueryBuilder; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface FunctionalQueryCallback4 { + void accept(FunctionalQueryBuilder builder, NodeVariable p1, NodeVariable p2, NodeVariable p3, NodeVariable p4, + DataVariable output); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback0.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback0.java new file mode 100644 index 00000000..bc9a6ee4 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback0.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.QueryBuilder; + +@FunctionalInterface +public interface QueryCallback0 { + void accept(QueryBuilder builder); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback1.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback1.java new file mode 100644 index 00000000..80763171 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback1.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.QueryBuilder; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface QueryCallback1 { + void accept(QueryBuilder builder, NodeVariable p1); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback2.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback2.java new file mode 100644 index 00000000..54423104 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback2.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.QueryBuilder; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface QueryCallback2 { + void accept(QueryBuilder builder, NodeVariable p1, NodeVariable p2); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback3.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback3.java new file mode 100644 index 00000000..eb68d493 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback3.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.QueryBuilder; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface QueryCallback3 { + void accept(QueryBuilder builder, NodeVariable p1, NodeVariable p2, NodeVariable p3); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback4.java b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback4.java new file mode 100644 index 00000000..59690af9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/dnf/callback/QueryCallback4.java @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf.callback; + +import tools.refinery.logic.dnf.QueryBuilder; +import tools.refinery.logic.term.NodeVariable; + +@FunctionalInterface +public interface QueryCallback4 { + void accept(QueryBuilder builder, NodeVariable p1, NodeVariable p2, NodeVariable p3, NodeVariable p4); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/DeepDnfEqualityChecker.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/DeepDnfEqualityChecker.java new file mode 100644 index 00000000..a49ef080 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/DeepDnfEqualityChecker.java @@ -0,0 +1,77 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.DnfClause; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.util.CycleDetectingMapper; + +import java.util.List; + +public class DeepDnfEqualityChecker implements DnfEqualityChecker { + private final CycleDetectingMapper mapper = new CycleDetectingMapper<>(this::doCheckEqual); + + @Override + public boolean dnfEqual(Dnf left, Dnf right) { + return mapper.map(new Pair(left, right)); + } + + public boolean dnfEqualRaw(List symbolicParameters, + List> clauses, Dnf other) { + int arity = symbolicParameters.size(); + if (arity != other.arity()) { + return false; + } + for (int i = 0; i < arity; i++) { + if (!symbolicParameters.get(i).getDirection().equals(other.getSymbolicParameters().get(i).getDirection())) { + return false; + } + } + int numClauses = clauses.size(); + if (numClauses != other.getClauses().size()) { + return false; + } + for (int i = 0; i < numClauses; i++) { + var literalEqualityHelper = new SubstitutingLiteralEqualityHelper(this, symbolicParameters, + other.getSymbolicParameters()); + if (!equalsWithSubstitutionRaw(literalEqualityHelper, clauses.get(i), other.getClauses().get(i))) { + return false; + } + } + return true; + } + + private boolean equalsWithSubstitutionRaw(LiteralEqualityHelper helper, List literals, + DnfClause other) { + int size = literals.size(); + if (size != other.literals().size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!literals.get(i).equalsWithSubstitution(helper, other.literals().get(i))) { + return false; + } + } + return true; + } + + protected boolean doCheckEqual(Pair pair) { + return pair.left.equalsWithSubstitution(this, pair.right); + } + + protected List getInProgress() { + return mapper.getInProgress(); + } + + protected record Pair(Dnf left, Dnf right) { + @Override + public String toString() { + return "(%s, %s)".formatted(left.name(), right.name()); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/DnfEqualityChecker.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/DnfEqualityChecker.java new file mode 100644 index 00000000..3162b019 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/DnfEqualityChecker.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.dnf.Dnf; + +import java.util.Objects; + +@FunctionalInterface +public interface DnfEqualityChecker { + DnfEqualityChecker DEFAULT = Objects::equals; + + boolean dnfEqual(Dnf left, Dnf right); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralEqualityHelper.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralEqualityHelper.java new file mode 100644 index 00000000..ea62061e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralEqualityHelper.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.term.Variable; + +import java.util.Objects; + +public interface LiteralEqualityHelper extends DnfEqualityChecker { + LiteralEqualityHelper DEFAULT = new LiteralEqualityHelper() { + @Override + public boolean variableEqual(Variable left, Variable right) { + return Objects.equals(left, right); + } + + @Override + public boolean dnfEqual(Dnf left, Dnf right) { + return DnfEqualityChecker.DEFAULT.dnfEqual(left, right); + } + }; + + boolean variableEqual(Variable left, Variable right); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralHashCodeHelper.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralHashCodeHelper.java new file mode 100644 index 00000000..fccadd08 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/LiteralHashCodeHelper.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.term.Variable; + +import java.util.Objects; + +@FunctionalInterface +public interface LiteralHashCodeHelper { + LiteralHashCodeHelper DEFAULT = Objects::hashCode; + + int getVariableHashCode(Variable variable); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralEqualityHelper.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralEqualityHelper.java new file mode 100644 index 00000000..7a16b5c9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralEqualityHelper.java @@ -0,0 +1,59 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.term.Variable; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SubstitutingLiteralEqualityHelper implements LiteralEqualityHelper { + private final DnfEqualityChecker dnfEqualityChecker; + private final Map leftToRight; + private final Map rightToLeft; + + public SubstitutingLiteralEqualityHelper(DnfEqualityChecker dnfEqualityChecker, + List leftParameters, + List rightParameters) { + this.dnfEqualityChecker = dnfEqualityChecker; + var arity = leftParameters.size(); + if (arity != rightParameters.size()) { + throw new IllegalArgumentException("Parameter lists have unequal length"); + } + leftToRight = new HashMap<>(arity); + rightToLeft = new HashMap<>(arity); + for (int i = 0; i < arity; i++) { + if (!variableEqual(leftParameters.get(i).getVariable(), rightParameters.get(i).getVariable())) { + throw new IllegalArgumentException("Parameter lists cannot be unified: duplicate parameter " + i); + } + } + } + + @Override + public boolean dnfEqual(Dnf left, Dnf right) { + return dnfEqualityChecker.dnfEqual(left, right); + } + + @Override + public boolean variableEqual(Variable left, Variable right) { + if (left.tryGetType().equals(right.tryGetType()) && + checkMapping(leftToRight, left, right) && + checkMapping(rightToLeft, right, left)) { + leftToRight.put(left, right); + rightToLeft.put(right, left); + return true; + } + return false; + } + + private static boolean checkMapping(Map map, Variable key, Variable expectedValue) { + var currentValue = map.get(key); + return currentValue == null || currentValue.equals(expectedValue); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralHashCodeHelper.java b/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralHashCodeHelper.java new file mode 100644 index 00000000..64bd9784 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/equality/SubstitutingLiteralHashCodeHelper.java @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.equality; + +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.term.Variable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class SubstitutingLiteralHashCodeHelper implements LiteralHashCodeHelper { + private final Map assignedHashCodes = new LinkedHashMap<>(); + + // 0 is for {@code null}, so we start with 1. + private int next = 1; + + public SubstitutingLiteralHashCodeHelper() { + this(List.of()); + } + + public SubstitutingLiteralHashCodeHelper(List parameters) { + for (var parameter : parameters) { + getVariableHashCode(parameter.getVariable()); + } + } + + @Override + public int getVariableHashCode(Variable variable) { + if (variable == null) { + return 0; + } + return assignedHashCodes.computeIfAbsent(variable, key -> { + int sequenceNumber = next; + next++; + return variable.hashCodeWithSubstitution(sequenceNumber); + }); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCallLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCallLiteral.java new file mode 100644 index 00000000..9ae84547 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCallLiteral.java @@ -0,0 +1,135 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.*; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public abstract class AbstractCallLiteral extends tools.refinery.logic.literal.AbstractLiteral { + private final Constraint target; + private final List arguments; + private final Set inArguments; + private final Set outArguments; + + // Use exhaustive switch over enums. + @SuppressWarnings("squid:S1301") + protected AbstractCallLiteral(Constraint target, List arguments) { + int arity = target.arity(); + if (arguments.size() != arity) { + throw new InvalidQueryException("%s needs %d arguments, but got %s".formatted(target.name(), + target.arity(), arguments.size())); + } + this.target = target; + this.arguments = arguments; + var mutableInArguments = new LinkedHashSet(); + var mutableOutArguments = new LinkedHashSet(); + var parameters = target.getParameters(); + for (int i = 0; i < arity; i++) { + var argument = arguments.get(i); + var parameter = parameters.get(i); + if (!parameter.isAssignable(argument)) { + throw new InvalidQueryException("Argument %d of %s is not assignable to parameter %s" + .formatted(i, target, parameter)); + } + switch (parameter.getDirection()) { + case IN -> { + mutableOutArguments.remove(argument); + mutableInArguments.add(argument); + } + case OUT -> { + if (!mutableInArguments.contains(argument)) { + mutableOutArguments.add(argument); + } + } + } + } + inArguments = Collections.unmodifiableSet(mutableInArguments); + outArguments = Collections.unmodifiableSet(mutableOutArguments); + } + + public Constraint getTarget() { + return target; + } + + public List getArguments() { + return arguments; + } + + protected Set getArgumentsOfDirection(ParameterDirection direction) { + return switch (direction) { + case IN -> inArguments; + case OUT -> outArguments; + }; + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + var inputVariables = new LinkedHashSet<>(getArgumentsOfDirection(ParameterDirection.OUT)); + inputVariables.retainAll(positiveVariablesInClause); + inputVariables.addAll(getArgumentsOfDirection(ParameterDirection.IN)); + return Collections.unmodifiableSet(inputVariables); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + var privateVariables = new LinkedHashSet<>(getArgumentsOfDirection(ParameterDirection.OUT)); + privateVariables.removeAll(positiveVariablesInClause); + return Collections.unmodifiableSet(privateVariables); + } + + @Override + public tools.refinery.logic.literal.Literal substitute(Substitution substitution) { + var substitutedArguments = arguments.stream().map(substitution::getSubstitute).toList(); + return doSubstitute(substitution, substitutedArguments); + } + + protected abstract tools.refinery.logic.literal.Literal doSubstitute(Substitution substitution, List substitutedArguments); + + public AbstractCallLiteral withTarget(Constraint newTarget) { + if (Objects.equals(target, newTarget)) { + return this; + } + return withArguments(newTarget, arguments); + } + + public abstract AbstractCallLiteral withArguments(Constraint newTarget, List newArguments); + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, tools.refinery.logic.literal.Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherCallLiteral = (AbstractCallLiteral) other; + var arity = arguments.size(); + if (arity != otherCallLiteral.arguments.size()) { + return false; + } + for (int i = 0; i < arity; i++) { + if (!helper.variableEqual(arguments.get(i), otherCallLiteral.arguments.get(i))) { + return false; + } + } + return target.equals(helper, otherCallLiteral.target); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + int result = super.hashCodeWithSubstitution(helper) * 31 + target.hashCode(); + for (var argument : arguments) { + result = result * 31 + helper.getVariableHashCode(argument); + } + return result; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCountLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCountLiteral.java new file mode 100644 index 00000000..ee932598 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractCountLiteral.java @@ -0,0 +1,107 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.Variable; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public abstract class AbstractCountLiteral extends AbstractCallLiteral { + private final Class resultType; + private final DataVariable resultVariable; + + protected AbstractCountLiteral(Class resultType, DataVariable resultVariable, Constraint target, + List arguments) { + super(target, arguments); + if (!resultVariable.getType().equals(resultType)) { + throw new InvalidQueryException("Count result variable %s must be of type %s, got %s instead".formatted( + resultVariable, resultType, resultVariable.getType().getName())); + } + if (arguments.contains(resultVariable)) { + throw new InvalidQueryException("Count result variable %s must not appear in the argument list" + .formatted(resultVariable)); + } + this.resultType = resultType; + this.resultVariable = resultVariable; + } + + public Class getResultType() { + return resultType; + } + + public DataVariable getResultVariable() { + return resultVariable; + } + + @Override + public Set getOutputVariables() { + return Set.of(resultVariable); + } + + protected abstract T zero(); + + protected abstract T one(); + + @Override + public Literal reduce() { + var reduction = getTarget().getReduction(); + return switch (reduction) { + case ALWAYS_FALSE -> getResultVariable().assign(new ConstantTerm<>(resultType, zero())); + // The only way a constant {@code true} predicate can be called in a negative position is to have all of + // its arguments bound as input variables. Thus, there will only be a single match. + case ALWAYS_TRUE -> getResultVariable().assign(new ConstantTerm<>(resultType, one())); + case NOT_REDUCIBLE -> this; + }; + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherCountLiteral = (AbstractCountLiteral) other; + return Objects.equals(resultType, otherCountLiteral.resultType) && + helper.variableEqual(resultVariable, otherCountLiteral.resultVariable); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), resultType, + helper.getVariableHashCode(resultVariable)); + } + + protected abstract String operatorName(); + + @Override + public String toString() { + var builder = new StringBuilder(); + builder.append(resultVariable); + builder.append(" is "); + builder.append(operatorName()); + builder.append(' '); + builder.append(getTarget().toReferenceString()); + builder.append('('); + var argumentIterator = getArguments().iterator(); + if (argumentIterator.hasNext()) { + builder.append(argumentIterator.next()); + while (argumentIterator.hasNext()) { + builder.append(", ").append(argumentIterator.next()); + } + } + builder.append(')'); + return builder.toString(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractLiteral.java new file mode 100644 index 00000000..79100e40 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AbstractLiteral.java @@ -0,0 +1,34 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; + +public abstract class AbstractLiteral implements Literal { + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + return other != null && getClass() == other.getClass(); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractLiteral that = (AbstractLiteral) o; + return equalsWithSubstitution(LiteralEqualityHelper.DEFAULT, that); + } + + @Override + public int hashCode() { + return hashCodeWithSubstitution(LiteralHashCodeHelper.DEFAULT); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/AggregationLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AggregationLiteral.java new file mode 100644 index 00000000..d2cc23f9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AggregationLiteral.java @@ -0,0 +1,143 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.*; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public class AggregationLiteral extends AbstractCallLiteral { + private final DataVariable resultVariable; + private final DataVariable inputVariable; + private final Aggregator aggregator; + + public AggregationLiteral(DataVariable resultVariable, Aggregator aggregator, + DataVariable inputVariable, Constraint target, List arguments) { + super(target, arguments); + if (!inputVariable.getType().equals(aggregator.getInputType())) { + throw new InvalidQueryException("Input variable %s must of type %s, got %s instead".formatted( + inputVariable, aggregator.getInputType().getName(), inputVariable.getType().getName())); + } + if (!getArgumentsOfDirection(ParameterDirection.OUT).contains(inputVariable)) { + throw new InvalidQueryException("Input variable %s must be bound with direction %s in the argument list" + .formatted(inputVariable, ParameterDirection.OUT)); + } + if (!resultVariable.getType().equals(aggregator.getResultType())) { + throw new InvalidQueryException("Result variable %s must of type %s, got %s instead".formatted( + resultVariable, aggregator.getResultType().getName(), resultVariable.getType().getName())); + } + if (arguments.contains(resultVariable)) { + throw new InvalidQueryException("Result variable %s must not appear in the argument list".formatted( + resultVariable)); + } + this.resultVariable = resultVariable; + this.inputVariable = inputVariable; + this.aggregator = aggregator; + } + + public DataVariable getResultVariable() { + return resultVariable; + } + + public DataVariable getInputVariable() { + return inputVariable; + } + + public Aggregator getAggregator() { + return aggregator; + } + + @Override + public Set getOutputVariables() { + return Set.of(resultVariable); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + if (positiveVariablesInClause.contains(inputVariable)) { + throw new InvalidQueryException("Aggregation variable %s must not be bound".formatted(inputVariable)); + } + return super.getInputVariables(positiveVariablesInClause); + } + + @Override + public Literal reduce() { + var reduction = getTarget().getReduction(); + return switch (reduction) { + case ALWAYS_FALSE -> { + var emptyValue = aggregator.getEmptyResult(); + yield emptyValue == null ? BooleanLiteral.FALSE : + resultVariable.assign(new ConstantTerm<>(resultVariable.getType(), emptyValue)); + } + case ALWAYS_TRUE -> throw new InvalidQueryException("Trying to aggregate over an infinite set"); + case NOT_REDUCIBLE -> this; + }; + } + + @Override + protected Literal doSubstitute(Substitution substitution, List substitutedArguments) { + return new AggregationLiteral<>(substitution.getTypeSafeSubstitute(resultVariable), aggregator, + substitution.getTypeSafeSubstitute(inputVariable), getTarget(), substitutedArguments); + } + + @Override + public AbstractCallLiteral withArguments(Constraint newTarget, List newArguments) { + return new AggregationLiteral<>(resultVariable, aggregator, inputVariable, newTarget, newArguments); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherAggregationLiteral = (AggregationLiteral) other; + return helper.variableEqual(resultVariable, otherAggregationLiteral.resultVariable) && + aggregator.equals(otherAggregationLiteral.aggregator) && + helper.variableEqual(inputVariable, otherAggregationLiteral.inputVariable); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(resultVariable), + helper.getVariableHashCode(inputVariable), aggregator); + } + + @Override + public String toString() { + var builder = new StringBuilder(); + builder.append(resultVariable); + builder.append(" is "); + builder.append(getTarget().toReferenceString()); + builder.append("("); + var argumentIterator = getArguments().iterator(); + if (argumentIterator.hasNext()) { + var argument = argumentIterator.next(); + if (inputVariable.equals(argument)) { + builder.append("@Aggregate(\"").append(aggregator).append("\") "); + } + builder.append(argument); + while (argumentIterator.hasNext()) { + builder.append(", "); + argument = argumentIterator.next(); + if (inputVariable.equals(argument)) { + builder.append("@Aggregate(\"").append(aggregator).append("\") "); + } + builder.append(argument); + } + } + builder.append(")"); + return builder.toString(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/AssignLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AssignLiteral.java new file mode 100644 index 00000000..4929e74c --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/AssignLiteral.java @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.Variable; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public class AssignLiteral extends AbstractLiteral { + private final DataVariable variable; + private final Term term; + + public AssignLiteral(DataVariable variable, Term term) { + if (!term.getType().equals(variable.getType())) { + throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted( + term, variable.getType().getName(), term.getType().getName())); + } + var inputVariables = term.getInputVariables(); + if (inputVariables.contains(variable)) { + throw new InvalidQueryException("Result variable %s must not appear in the term %s".formatted( + variable, term)); + } + this.variable = variable; + this.term = term; + } + + public DataVariable getVariable() { + return variable; + } + + public Term getTerm() { + return term; + } + + @Override + public Set getOutputVariables() { + return Set.of(variable); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Collections.unmodifiableSet(term.getInputVariables()); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Literal substitute(Substitution substitution) { + return new AssignLiteral<>(substitution.getTypeSafeSubstitute(variable), term.substitute(substitution)); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (other == null || getClass() != other.getClass()) { + return false; + } + var otherAssignLiteral = (AssignLiteral) other; + return helper.variableEqual(variable, otherAssignLiteral.variable) && + term.equalsWithSubstitution(helper, otherAssignLiteral.term); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(variable), + term.hashCodeWithSubstitution(helper)); + } + + @Override + public String toString() { + return "%s is (%s)".formatted(variable, term); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/BooleanLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/BooleanLiteral.java new file mode 100644 index 00000000..fd1dbf91 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/BooleanLiteral.java @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Variable; + +import java.util.Set; + +public enum BooleanLiteral implements CanNegate { + TRUE(true), + FALSE(false); + + private final boolean value; + + BooleanLiteral(boolean value) { + this.value = value; + } + + @Override + public Set getOutputVariables() { + return Set.of(); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Literal substitute(Substitution substitution) { + // No variables to substitute. + return this; + } + + @Override + public BooleanLiteral negate() { + return fromBoolean(!value); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + return equals(other); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return hashCode(); + } + + @Override + public String toString() { + return Boolean.toString(value); + } + + public static BooleanLiteral fromBoolean(boolean value) { + return value ? TRUE : FALSE; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallLiteral.java new file mode 100644 index 00000000..d93afdc7 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallLiteral.java @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.*; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public final class CallLiteral extends AbstractCallLiteral implements CanNegate { + private final CallPolarity polarity; + + public CallLiteral(CallPolarity polarity, Constraint target, List arguments) { + super(target, arguments); + var parameters = target.getParameters(); + int arity = target.arity(); + if (polarity.isTransitive()) { + if (arity != 2) { + throw new InvalidQueryException("Transitive closures can only take binary relations"); + } + if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) { + throw new InvalidQueryException("Transitive closures can only be computed over nodes"); + } + if (parameters.get(0).getDirection() != ParameterDirection.OUT || + parameters.get(1).getDirection() != ParameterDirection.OUT) { + throw new InvalidQueryException("Transitive closures cannot take input parameters"); + } + } + this.polarity = polarity; + } + + public CallPolarity getPolarity() { + return polarity; + } + + @Override + protected Literal doSubstitute(Substitution substitution, List substitutedArguments) { + return new CallLiteral(polarity, getTarget(), substitutedArguments); + } + + @Override + public Set getOutputVariables() { + if (polarity.isPositive()) { + return getArgumentsOfDirection(ParameterDirection.OUT); + } + return Set.of(); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + if (polarity.isPositive()) { + return getArgumentsOfDirection(ParameterDirection.IN); + } + return super.getInputVariables(positiveVariablesInClause); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + if (polarity.isPositive()) { + return Set.of(); + } + return super.getPrivateVariables(positiveVariablesInClause); + } + + @Override + public Literal reduce() { + var reduction = getTarget().getReduction(); + var negatedReduction = polarity.isPositive() ? reduction : reduction.negate(); + return switch (negatedReduction) { + case ALWAYS_TRUE -> BooleanLiteral.TRUE; + case ALWAYS_FALSE -> BooleanLiteral.FALSE; + default -> this; + }; + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherCallLiteral = (CallLiteral) other; + return polarity.equals(otherCallLiteral.polarity); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), polarity); + } + + @Override + public CallLiteral negate() { + return new CallLiteral(polarity.negate(), getTarget(), getArguments()); + } + + @Override + public AbstractCallLiteral withArguments(Constraint newTarget, List newArguments) { + return new CallLiteral(polarity, newTarget, newArguments); + } + + @Override + public String toString() { + var builder = new StringBuilder(); + if (!polarity.isPositive()) { + builder.append("!("); + } + builder.append(getTarget().toReferenceString()); + if (polarity.isTransitive()) { + builder.append("+"); + } + builder.append("("); + var argumentIterator = getArguments().iterator(); + if (argumentIterator.hasNext()) { + builder.append(argumentIterator.next()); + while (argumentIterator.hasNext()) { + builder.append(", ").append(argumentIterator.next()); + } + } + builder.append(")"); + if (!polarity.isPositive()) { + builder.append(")"); + } + return builder.toString(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallPolarity.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallPolarity.java new file mode 100644 index 00000000..4f3bca15 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CallPolarity.java @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.InvalidQueryException; + +public enum CallPolarity { + POSITIVE(true, false), + NEGATIVE(false, false), + TRANSITIVE(true, true); + + private final boolean positive; + + private final boolean transitive; + + CallPolarity(boolean positive, boolean transitive) { + this.positive = positive; + this.transitive = transitive; + } + + public boolean isPositive() { + return positive; + } + + public boolean isTransitive() { + return transitive; + } + + public CallPolarity negate() { + return switch (this) { + case POSITIVE -> NEGATIVE; + case NEGATIVE -> POSITIVE; + case TRANSITIVE -> throw new InvalidQueryException("Transitive polarity cannot be negated"); + }; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/CanNegate.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CanNegate.java new file mode 100644 index 00000000..649f0505 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CanNegate.java @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +public interface CanNegate> extends Literal { + T negate(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/CheckLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CheckLiteral.java new file mode 100644 index 00000000..86bf7eba --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CheckLiteral.java @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.term.bool.BoolNotTerm; +import tools.refinery.logic.term.bool.BoolTerms; + +import java.util.Collections; +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public class CheckLiteral extends AbstractLiteral implements CanNegate { + private final Term term; + + public CheckLiteral(Term term) { + if (!term.getType().equals(Boolean.class)) { + throw new InvalidQueryException("Term %s must be of type %s, got %s instead".formatted( + term, Boolean.class.getName(), term.getType().getName())); + } + this.term = term; + } + + public Term getTerm() { + return term; + } + + @Override + public Set getOutputVariables() { + return Set.of(); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Collections.unmodifiableSet(term.getInputVariables()); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Literal substitute(Substitution substitution) { + return new CheckLiteral(term.substitute(substitution)); + } + + @Override + public CheckLiteral negate() { + if (term instanceof BoolNotTerm notTerm) { + return new CheckLiteral(notTerm.getBody()); + } + return new CheckLiteral(BoolTerms.not(term)); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (other == null || getClass() != other.getClass()) { + return false; + } + var otherAssumeLiteral = (CheckLiteral) other; + return term.equalsWithSubstitution(helper, otherAssumeLiteral.term); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), term.hashCodeWithSubstitution(helper)); + } + + @Override + public Literal reduce() { + if (term instanceof ConstantTerm constantTerm) { + // Return {@link BooleanLiteral#FALSE} for {@code false} or {@code null} literals. + return Boolean.TRUE.equals(constantTerm.getValue()) ? BooleanLiteral.TRUE : + BooleanLiteral.FALSE; + } + return this; + } + + @Override + public String toString() { + return "(%s)".formatted(term); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/Connectivity.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Connectivity.java new file mode 100644 index 00000000..c0465716 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Connectivity.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import java.util.Locale; + +public enum Connectivity { + WEAK, + STRONG; + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/ConstantLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/ConstantLiteral.java new file mode 100644 index 00000000..688ddfa0 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/ConstantLiteral.java @@ -0,0 +1,73 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Variable; + +import java.util.Objects; +import java.util.Set; + +public class ConstantLiteral extends AbstractLiteral { + private final NodeVariable variable; + private final int nodeId; + + public ConstantLiteral(NodeVariable variable, int nodeId) { + this.variable = variable; + this.nodeId = nodeId; + } + + public NodeVariable getVariable() { + return variable; + } + + public int getNodeId() { + return nodeId; + } + + + @Override + public Set getOutputVariables() { + return Set.of(variable); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public ConstantLiteral substitute(Substitution substitution) { + return new ConstantLiteral(substitution.getTypeSafeSubstitute(variable), nodeId); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (other.getClass() != getClass()) { + return false; + } + var otherConstantLiteral = (ConstantLiteral) other; + return helper.variableEqual(variable, otherConstantLiteral.variable) && nodeId == otherConstantLiteral.nodeId; + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(variable), nodeId); + } + + @Override + public String toString() { + return "%s === @Constant %d".formatted(variable, nodeId); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/CountLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CountLiteral.java new file mode 100644 index 00000000..19879692 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/CountLiteral.java @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.Variable; + +import java.util.List; + +public class CountLiteral extends AbstractCountLiteral { + public CountLiteral(DataVariable resultVariable, Constraint target, List arguments) { + super(Integer.class, resultVariable, target, arguments); + } + + @Override + protected Integer zero() { + return 0; + } + + @Override + protected Integer one() { + return 1; + } + + @Override + protected Literal doSubstitute(Substitution substitution, List substitutedArguments) { + return new CountLiteral(substitution.getTypeSafeSubstitute(getResultVariable()), getTarget(), + substitutedArguments); + } + + @Override + public AbstractCallLiteral withArguments(Constraint newTarget, List newArguments) { + return new CountLiteral(getResultVariable(), newTarget, newArguments); + } + + @Override + protected String operatorName() { + return "count"; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/EquivalenceLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/EquivalenceLiteral.java new file mode 100644 index 00000000..48e85b38 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/EquivalenceLiteral.java @@ -0,0 +1,100 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Variable; + +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public final class EquivalenceLiteral extends AbstractLiteral implements CanNegate { + private final boolean positive; + private final Variable left; + private final Variable right; + + public EquivalenceLiteral(boolean positive, Variable left, Variable right) { + if (!left.tryGetType().equals(right.tryGetType())) { + throw new InvalidQueryException("Variables %s and %s of different type cannot be equivalent" + .formatted(left, right)); + } + this.positive = positive; + this.left = left; + this.right = right; + } + + public boolean isPositive() { + return positive; + } + + public Variable getLeft() { + return left; + } + + public Variable getRight() { + return right; + } + + @Override + public Set getOutputVariables() { + return Set.of(left); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Set.of(right); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public EquivalenceLiteral negate() { + return new EquivalenceLiteral(!positive, left, right); + } + + @Override + public EquivalenceLiteral substitute(Substitution substitution) { + return new EquivalenceLiteral(positive, substitution.getSubstitute(left), + substitution.getSubstitute(right)); + } + + @Override + public Literal reduce() { + if (left.equals(right)) { + return positive ? BooleanLiteral.TRUE : BooleanLiteral.FALSE; + } + return this; + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (other.getClass() != getClass()) { + return false; + } + var otherEquivalenceLiteral = (EquivalenceLiteral) other; + return helper.variableEqual(left, otherEquivalenceLiteral.left) && + helper.variableEqual(right, otherEquivalenceLiteral.right); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(left), + helper.getVariableHashCode(right)); + } + + @Override + public String toString() { + return "%s %s %s".formatted(left, positive ? "===" : "!==", right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/LeftJoinLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/LeftJoinLiteral.java new file mode 100644 index 00000000..593904c5 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/LeftJoinLiteral.java @@ -0,0 +1,140 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.*; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public class LeftJoinLiteral extends AbstractCallLiteral { + private final DataVariable resultVariable; + private final DataVariable placeholderVariable; + private final T defaultValue; + + public LeftJoinLiteral(DataVariable resultVariable, DataVariable placeholderVariable, + T defaultValue, Constraint target, List arguments) { + super(target, arguments); + this.resultVariable = resultVariable; + this.placeholderVariable = placeholderVariable; + this.defaultValue = defaultValue; + if (defaultValue == null) { + throw new InvalidQueryException("Default value must not be null"); + } + if (!resultVariable.getType().isInstance(defaultValue)) { + throw new InvalidQueryException("Default value %s must be assignable to result variable %s type %s" + .formatted(defaultValue, resultVariable, resultVariable.getType().getName())); + } + if (!getArgumentsOfDirection(ParameterDirection.OUT).contains(placeholderVariable)) { + throw new InvalidQueryException( + "Placeholder variable %s must be bound with direction %s in the argument list" + .formatted(resultVariable, ParameterDirection.OUT)); + } + if (arguments.contains(resultVariable)) { + throw new InvalidQueryException("Result variable must not appear in the argument list"); + } + } + + public DataVariable getResultVariable() { + return resultVariable; + } + + public DataVariable getPlaceholderVariable() { + return placeholderVariable; + } + + public T getDefaultValue() { + return defaultValue; + } + + @Override + public Set getOutputVariables() { + return Set.of(resultVariable); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + var inputVariables = new LinkedHashSet<>(getArguments()); + inputVariables.remove(placeholderVariable); + return Collections.unmodifiableSet(inputVariables); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(placeholderVariable); + } + + @Override + public Literal reduce() { + var reduction = getTarget().getReduction(); + return switch (reduction) { + case ALWAYS_FALSE -> resultVariable.assign(new ConstantTerm<>(resultVariable.getType(), defaultValue)); + case ALWAYS_TRUE -> throw new InvalidQueryException("Trying to left join an infinite set"); + case NOT_REDUCIBLE -> this; + }; + } + + @Override + protected Literal doSubstitute(Substitution substitution, List substitutedArguments) { + return new LeftJoinLiteral<>(substitution.getTypeSafeSubstitute(resultVariable), + substitution.getTypeSafeSubstitute(placeholderVariable), defaultValue, getTarget(), + substitutedArguments); + } + + @Override + public AbstractCallLiteral withArguments(Constraint newTarget, List newArguments) { + return new LeftJoinLiteral<>(resultVariable, placeholderVariable, defaultValue, newTarget, newArguments); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherLeftJoinLiteral = (LeftJoinLiteral) other; + return helper.variableEqual(resultVariable, otherLeftJoinLiteral.resultVariable) && + helper.variableEqual(placeholderVariable, otherLeftJoinLiteral.placeholderVariable) && + Objects.equals(defaultValue, otherLeftJoinLiteral.defaultValue); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), helper.getVariableHashCode(resultVariable), + helper.getVariableHashCode(placeholderVariable), defaultValue); + } + + @Override + public String toString() { + var builder = new StringBuilder(); + var argumentIterator = getArguments().iterator(); + if (argumentIterator.hasNext()) { + appendArgument(builder, argumentIterator.next()); + while (argumentIterator.hasNext()) { + builder.append(", "); + appendArgument(builder, argumentIterator.next()); + } + } + builder.append(")"); + return builder.toString(); + } + + private void appendArgument(StringBuilder builder, Variable argument) { + if (placeholderVariable.equals(argument)) { + builder.append("@Default(").append(defaultValue).append(") "); + argument = resultVariable; + } + builder.append(argument); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literal.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literal.java new file mode 100644 index 00000000..08a0241c --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literal.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Variable; + +import java.util.Set; + +public interface Literal { + Set getOutputVariables(); + + Set getInputVariables(Set positiveVariablesInClause); + + Set getPrivateVariables(Set positiveVariablesInClause); + + Literal substitute(Substitution substitution); + + default Literal reduce() { + return this; + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other); + + int hashCodeWithSubstitution(LiteralHashCodeHelper helper); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literals.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literals.java new file mode 100644 index 00000000..2f7ddb5d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Literals.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.term.Term; + +public final class Literals { + private Literals() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static > T not(CanNegate literal) { + return literal.negate(); + } + + public static CheckLiteral check(Term term) { + return new CheckLiteral(term); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/Reduction.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Reduction.java new file mode 100644 index 00000000..1f7d9a2f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/Reduction.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +public enum Reduction { + /** + * Signifies that a literal should be preserved in the clause. + */ + NOT_REDUCIBLE, + + /** + * Signifies that the literal may be omitted from the cause (if the model being queried is nonempty). + */ + ALWAYS_TRUE, + + /** + * Signifies that the clause with the literal may be omitted entirely. + */ + ALWAYS_FALSE; + + public Reduction negate() { + return switch (this) { + case NOT_REDUCIBLE -> NOT_REDUCIBLE; + case ALWAYS_TRUE -> ALWAYS_FALSE; + case ALWAYS_FALSE -> ALWAYS_TRUE; + }; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/literal/RepresentativeElectionLiteral.java b/subprojects/logic/src/main/java/tools/refinery/logic/literal/RepresentativeElectionLiteral.java new file mode 100644 index 00000000..ff9a6bed --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/literal/RepresentativeElectionLiteral.java @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.List; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractLiteral}. +@SuppressWarnings("squid:S2160") +public class RepresentativeElectionLiteral extends AbstractCallLiteral { + private final Connectivity connectivity; + + public RepresentativeElectionLiteral(Connectivity connectivity, Constraint target, NodeVariable specific, + NodeVariable representative) { + this(connectivity, target, List.of(specific, representative)); + } + + private RepresentativeElectionLiteral(Connectivity connectivity, Constraint target, List arguments) { + super(target, arguments); + this.connectivity = connectivity; + var parameters = target.getParameters(); + int arity = target.arity(); + if (arity != 2) { + throw new InvalidQueryException("SCCs can only take binary relations"); + } + if (parameters.get(0).isDataVariable() || parameters.get(1).isDataVariable()) { + throw new InvalidQueryException("SCCs can only be computed over nodes"); + } + if (parameters.get(0).getDirection() != ParameterDirection.OUT || + parameters.get(1).getDirection() != ParameterDirection.OUT) { + throw new InvalidQueryException("SCCs cannot take input parameters"); + } + } + + public Connectivity getConnectivity() { + return connectivity; + } + + @Override + protected Literal doSubstitute(Substitution substitution, List substitutedArguments) { + return new RepresentativeElectionLiteral(connectivity, getTarget(), substitutedArguments); + } + + @Override + public Set getOutputVariables() { + return getArgumentsOfDirection(ParameterDirection.OUT); + } + + @Override + public Set getInputVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Set getPrivateVariables(Set positiveVariablesInClause) { + return Set.of(); + } + + @Override + public Literal reduce() { + var reduction = getTarget().getReduction(); + return switch (reduction) { + case ALWAYS_FALSE -> BooleanLiteral.FALSE; + case ALWAYS_TRUE -> throw new InvalidQueryException( + "Trying to elect representatives over an infinite set"); + case NOT_REDUCIBLE -> this; + }; + } + + @Override + public AbstractCallLiteral withArguments(Constraint newTarget, List newArguments) { + return new RepresentativeElectionLiteral(connectivity, newTarget, newArguments); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, Literal other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherRepresentativeElectionLiteral = (RepresentativeElectionLiteral) other; + return connectivity.equals(otherRepresentativeElectionLiteral.connectivity); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return super.hashCodeWithSubstitution(helper) * 31 + connectivity.hashCode(); + } + + @Override + public String toString() { + var builder = new StringBuilder(); + builder.append("@Representative(\""); + builder.append(connectivity); + builder.append("\") "); + builder.append(getTarget().toReferenceString()); + builder.append("("); + var argumentIterator = getArguments().iterator(); + if (argumentIterator.hasNext()) { + builder.append(argumentIterator.next()); + while (argumentIterator.hasNext()) { + builder.append(", ").append(argumentIterator.next()); + } + } + builder.append(")"); + return builder.toString(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/AbstractRecursiveRewriter.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/AbstractRecursiveRewriter.java new file mode 100644 index 00000000..073705f8 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/AbstractRecursiveRewriter.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.equality.DnfEqualityChecker; +import tools.refinery.logic.util.CycleDetectingMapper; + +public abstract class AbstractRecursiveRewriter implements DnfRewriter { + private final CycleDetectingMapper mapper = new CycleDetectingMapper<>(Dnf::name, this::map); + + @Override + public Dnf rewrite(Dnf dnf) { + return mapper.map(dnf); + } + + protected Dnf map(Dnf dnf) { + var result = doRewrite(dnf); + return dnf.equalsWithSubstitution(DnfEqualityChecker.DEFAULT, result) ? dnf : result; + } + + protected abstract Dnf doRewrite(Dnf dnf); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/ClauseInputParameterResolver.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/ClauseInputParameterResolver.java new file mode 100644 index 00000000..83fd44c5 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/ClauseInputParameterResolver.java @@ -0,0 +1,160 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.DnfClause; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.literal.*; +import tools.refinery.logic.term.ParameterDirection; + +import java.util.*; + +class ClauseInputParameterResolver { + private final InputParameterResolver rewriter; + private final String dnfName; + private final int clauseIndex; + private final Set positiveVariables = new LinkedHashSet<>(); + private final List inlinedLiterals = new ArrayList<>(); + private final Deque workList; + private int helperIndex = 0; + + public ClauseInputParameterResolver(InputParameterResolver rewriter, List context, DnfClause clause, + String dnfName, int clauseIndex) { + this.rewriter = rewriter; + this.dnfName = dnfName; + this.clauseIndex = clauseIndex; + workList = new ArrayDeque<>(clause.literals().size() + context.size()); + for (var literal : context) { + workList.addLast(literal); + } + for (var literal : clause.literals()) { + workList.addLast(literal); + } + } + + public List rewriteClause() { + while (!workList.isEmpty()) { + var literal = workList.removeFirst(); + processLiteral(literal); + } + return inlinedLiterals; + } + + private void processLiteral(Literal literal) { + if (!(literal instanceof AbstractCallLiteral abstractCallLiteral) || + !(abstractCallLiteral.getTarget() instanceof Dnf targetDnf)) { + markAsDone(literal); + return; + } + boolean hasInputParameter = hasInputParameter(targetDnf); + if (!hasInputParameter) { + targetDnf = rewriter.rewrite(targetDnf); + } + if (inlinePositiveClause(abstractCallLiteral, targetDnf)) { + return; + } + if (eliminateDoubleNegation(abstractCallLiteral, targetDnf)) { + return; + } + if (hasInputParameter) { + rewriteWithCurrentContext(abstractCallLiteral, targetDnf); + return; + } + markAsDone(abstractCallLiteral.withTarget(targetDnf)); + } + + private void markAsDone(Literal literal) { + positiveVariables.addAll(literal.getOutputVariables()); + inlinedLiterals.add(literal); + } + + private boolean inlinePositiveClause(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) { + var targetLiteral = getSingleLiteral(abstractCallLiteral, targetDnf, CallPolarity.POSITIVE); + if (targetLiteral == null) { + return false; + } + var substitution = asSubstitution(abstractCallLiteral, targetDnf); + var substitutedLiteral = targetLiteral.substitute(substitution); + workList.addFirst(substitutedLiteral); + return true; + } + + private boolean eliminateDoubleNegation(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) { + var targetLiteral = getSingleLiteral(abstractCallLiteral, targetDnf, CallPolarity.NEGATIVE); + if (!(targetLiteral instanceof CallLiteral targetCallLiteral) || + targetCallLiteral.getPolarity() != CallPolarity.NEGATIVE) { + return false; + } + var substitution = asSubstitution(abstractCallLiteral, targetDnf); + var substitutedLiteral = (CallLiteral) targetCallLiteral.substitute(substitution); + workList.addFirst(substitutedLiteral.negate()); + return true; + } + + private void rewriteWithCurrentContext(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf) { + var contextBuilder = Dnf.builder("%s#clause%d#helper%d".formatted(dnfName, clauseIndex, helperIndex)); + helperIndex++; + contextBuilder.parameters(positiveVariables, ParameterDirection.OUT); + contextBuilder.clause(inlinedLiterals); + var contextDnf = contextBuilder.build(); + var contextCall = new CallLiteral(CallPolarity.POSITIVE, contextDnf, List.copyOf(positiveVariables)); + inlinedLiterals.clear(); + var substitution = Substitution.builder().renewing().build(); + var context = new ArrayList(); + context.add(contextCall.substitute(substitution)); + int arity = targetDnf.arity(); + for (int i = 0; i < arity; i++) { + var parameter = targetDnf.getSymbolicParameters().get(i).getVariable(); + var argument = abstractCallLiteral.getArguments().get(i); + context.add(new EquivalenceLiteral(true, parameter, substitution.getSubstitute(argument))); + } + var rewrittenDnf = rewriter.rewriteWithContext(context, targetDnf); + workList.addFirst(abstractCallLiteral.withTarget(rewrittenDnf)); + workList.addFirst(contextCall); + } + + private static boolean hasInputParameter(Dnf targetDnf) { + for (var parameter : targetDnf.getParameters()) { + if (parameter.getDirection() != ParameterDirection.OUT) { + return true; + } + } + return false; + } + + private static Literal getSingleLiteral(AbstractCallLiteral abstractCallLiteral, Dnf targetDnf, + CallPolarity polarity) { + if (!(abstractCallLiteral instanceof CallLiteral callLiteral) || + callLiteral.getPolarity() != polarity) { + return null; + } + var clauses = targetDnf.getClauses(); + if (clauses.size() != 1) { + return null; + } + var targetLiterals = clauses.get(0).literals(); + if (targetLiterals.size() != 1) { + return null; + } + return targetLiterals.get(0); + } + + private static Substitution asSubstitution(AbstractCallLiteral callLiteral, Dnf targetDnf) { + var builder = Substitution.builder().renewing(); + var arguments = callLiteral.getArguments(); + var parameters = targetDnf.getSymbolicParameters(); + int arity = arguments.size(); + if (parameters.size() != arity) { + throw new IllegalArgumentException("Call %s of %s arity mismatch".formatted(callLiteral, targetDnf)); + } + for (int i = 0; i < arity; i++) { + builder.putChecked(parameters.get(i).getVariable(), arguments.get(i)); + } + return builder.build(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/CompositeRewriter.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/CompositeRewriter.java new file mode 100644 index 00000000..2e681093 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/CompositeRewriter.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.Dnf; + +import java.util.ArrayList; +import java.util.List; + +public class CompositeRewriter implements DnfRewriter { + private final List rewriterList = new ArrayList<>(); + + public void addFirst(DnfRewriter rewriter) { + rewriterList.add(rewriter); + } + + @Override + public Dnf rewrite(Dnf dnf) { + Dnf rewrittenDnf = dnf; + for (int i = rewriterList.size() - 1; i >= 0; i--) { + var rewriter = rewriterList.get(i); + rewrittenDnf = rewriter.rewrite(rewrittenDnf); + } + return rewrittenDnf; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DnfRewriter.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DnfRewriter.java new file mode 100644 index 00000000..f4605895 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DnfRewriter.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.AnyQuery; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.Query; + +@FunctionalInterface +public interface DnfRewriter { + Dnf rewrite(Dnf dnf); + + default AnyQuery rewrite(AnyQuery query) { + return rewrite((Query) query); + } + + default Query rewrite(Query query) { + var rewrittenDnf = rewrite(query.getDnf()); + return query.withDnf(rewrittenDnf); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DuplicateDnfRemover.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DuplicateDnfRemover.java new file mode 100644 index 00000000..43317338 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/DuplicateDnfRemover.java @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.DnfClause; +import tools.refinery.logic.dnf.Query; +import tools.refinery.logic.equality.DnfEqualityChecker; +import tools.refinery.logic.literal.AbstractCallLiteral; +import tools.refinery.logic.literal.Literal; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DuplicateDnfRemover extends AbstractRecursiveRewriter { + private final Map dnfCache = new HashMap<>(); + private final Map> queryCache = new HashMap<>(); + + @Override + protected Dnf map(Dnf dnf) { + var result = super.map(dnf); + return dnfCache.computeIfAbsent(new CanonicalDnf(result), CanonicalDnf::getDnf); + } + + @Override + protected Dnf doRewrite(Dnf dnf) { + var builder = Dnf.builderFrom(dnf); + for (var clause : dnf.getClauses()) { + builder.clause(rewriteClause(clause)); + } + return builder.build(); + } + + private List rewriteClause(DnfClause clause) { + var originalLiterals = clause.literals(); + var literals = new ArrayList(originalLiterals.size()); + for (var literal : originalLiterals) { + var rewrittenLiteral = literal; + if (literal instanceof AbstractCallLiteral abstractCallLiteral && + abstractCallLiteral.getTarget() instanceof Dnf targetDnf) { + var rewrittenTarget = rewrite(targetDnf); + rewrittenLiteral = abstractCallLiteral.withTarget(rewrittenTarget); + } + literals.add(rewrittenLiteral); + } + return literals; + } + + @Override + public Query rewrite(Query query) { + var rewrittenDnf = rewrite(query.getDnf()); + // {@code withDnf} will always return the appropriate type. + @SuppressWarnings("unchecked") + var rewrittenQuery = (Query) queryCache.computeIfAbsent(rewrittenDnf, query::withDnf); + return rewrittenQuery; + } + + private static class CanonicalDnf { + private final Dnf dnf; + private final int hash; + + public CanonicalDnf(Dnf dnf) { + this.dnf = dnf; + hash = dnf.hashCodeWithSubstitution(); + } + + public Dnf getDnf() { + return dnf; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + var otherCanonicalDnf = (CanonicalDnf) obj; + return dnf.equalsWithSubstitution(DnfEqualityChecker.DEFAULT, otherCanonicalDnf.dnf); + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public String toString() { + return dnf.name(); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/InputParameterResolver.java b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/InputParameterResolver.java new file mode 100644 index 00000000..89aa43c7 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/rewriter/InputParameterResolver.java @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.DnfBuilder; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.ParameterDirection; + +import java.util.HashSet; +import java.util.List; + +public class InputParameterResolver extends AbstractRecursiveRewriter { + @Override + protected Dnf doRewrite(Dnf dnf) { + return rewriteWithContext(List.of(), dnf); + } + + Dnf rewriteWithContext(List context, Dnf dnf) { + var dnfName = dnf.name(); + var builder = Dnf.builder(dnfName); + createSymbolicParameters(context, dnf, builder); + builder.functionalDependencies(dnf.getFunctionalDependencies()); + var clauses = dnf.getClauses(); + int clauseCount = clauses.size(); + for (int i = 0; i < clauseCount; i++) { + var clause = clauses.get(i); + var clauseRewriter = new ClauseInputParameterResolver(this, context, clause, dnfName, i); + builder.clause(clauseRewriter.rewriteClause()); + } + return builder.build(); + } + + private static void createSymbolicParameters(List context, Dnf dnf, DnfBuilder builder) { + var positiveInContext = new HashSet(); + for (var literal : context) { + positiveInContext.addAll(literal.getOutputVariables()); + } + for (var symbolicParameter : dnf.getSymbolicParameters()) { + var variable = symbolicParameter.getVariable(); + var isOutput = symbolicParameter.getDirection() == ParameterDirection.OUT || + positiveInContext.contains(variable); + var direction = isOutput ? ParameterDirection.OUT : ParameterDirection.IN; + builder.parameter(variable, direction); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/substitution/MapBasedSubstitution.java b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/MapBasedSubstitution.java new file mode 100644 index 00000000..2977bd57 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/MapBasedSubstitution.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.substitution; + +import tools.refinery.logic.term.Variable; + +import java.util.Map; + +public record MapBasedSubstitution(Map map, Substitution fallback) implements Substitution { + @Override + public Variable getSubstitute(Variable variable) { + var value = map.get(variable); + return value == null ? fallback.getSubstitute(variable) : value; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/substitution/RenewingSubstitution.java b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/RenewingSubstitution.java new file mode 100644 index 00000000..ca40ee50 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/RenewingSubstitution.java @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.substitution; + +import tools.refinery.logic.term.Variable; + +import java.util.HashMap; +import java.util.Map; + +public class RenewingSubstitution implements Substitution { + private final Map alreadyRenewed = new HashMap<>(); + + @Override + public Variable getSubstitute(Variable variable) { + return alreadyRenewed.computeIfAbsent(variable, Variable::renew); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/substitution/StatelessSubstitution.java b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/StatelessSubstitution.java new file mode 100644 index 00000000..ab36d3db --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/StatelessSubstitution.java @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.substitution; + +import tools.refinery.logic.term.Variable; + +public enum StatelessSubstitution implements Substitution { + FAILING { + @Override + public Variable getSubstitute(Variable variable) { + throw new IllegalArgumentException("No substitute for " + variable); + } + }, + IDENTITY { + @Override + public Variable getSubstitute(Variable variable) { + return variable; + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/substitution/Substitution.java b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/Substitution.java new file mode 100644 index 00000000..8fc700bc --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/Substitution.java @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.substitution; + +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Variable; + +@FunctionalInterface +public interface Substitution { + Variable getSubstitute(Variable variable); + + default NodeVariable getTypeSafeSubstitute(NodeVariable variable) { + var substitute = getSubstitute(variable); + return substitute.asNodeVariable(); + } + + default DataVariable getTypeSafeSubstitute(DataVariable variable) { + var substitute = getSubstitute(variable); + return substitute.asDataVariable(variable.getType()); + } + + static SubstitutionBuilder builder() { + return new SubstitutionBuilder(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/substitution/SubstitutionBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/SubstitutionBuilder.java new file mode 100644 index 00000000..f30b1a5d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/substitution/SubstitutionBuilder.java @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.substitution; + +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Variable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("UnusedReturnValue") +public class SubstitutionBuilder { + private final Map map = new HashMap<>(); + private Substitution fallback; + + SubstitutionBuilder() { + total(); + } + + public SubstitutionBuilder put(NodeVariable original, NodeVariable substitute) { + return putChecked(original, substitute); + } + + public SubstitutionBuilder put(DataVariable original, DataVariable substitute) { + return putChecked(original, substitute); + } + + public SubstitutionBuilder putChecked(Variable original, Variable substitute) { + if (!original.tryGetType().equals(substitute.tryGetType())) { + throw new IllegalArgumentException("Cannot substitute variable %s of sort %s with variable %s of sort %s" + .formatted(original, original.tryGetType().map(Class::getName).orElse("node"), substitute, + substitute.tryGetType().map(Class::getName).orElse("node"))); + } + if (map.containsKey(original)) { + throw new IllegalArgumentException("Already has substitution for variable %s".formatted(original)); + } + map.put(original, substitute); + return this; + } + + public SubstitutionBuilder putManyChecked(List originals, List substitutes) { + int size = originals.size(); + if (size != substitutes.size()) { + throw new IllegalArgumentException("Cannot substitute %d variables %s with %d variables %s" + .formatted(size, originals, substitutes.size(), substitutes)); + } + for (int i = 0; i < size; i++) { + putChecked(originals.get(i), substitutes.get(i)); + } + return this; + } + + public SubstitutionBuilder fallback(Substitution newFallback) { + fallback = newFallback; + return this; + } + + public SubstitutionBuilder total() { + return fallback(StatelessSubstitution.FAILING); + } + + public SubstitutionBuilder partial() { + return fallback(StatelessSubstitution.IDENTITY); + } + + public SubstitutionBuilder renewing() { + return fallback(new RenewingSubstitution()); + } + + public Substitution build() { + return map.isEmpty() ? fallback : new MapBasedSubstitution(Collections.unmodifiableMap(map), fallback); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/AbstractTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/AbstractTerm.java new file mode 100644 index 00000000..293f7e9e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/AbstractTerm.java @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; + +import java.util.Objects; + +public abstract class AbstractTerm implements Term { + private final Class type; + + protected AbstractTerm(Class type) { + this.type = type; + } + + @Override + public Class getType() { + return type; + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + return other != null && getClass() == other.getClass() && type.equals(other.getType()); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(getClass(), type); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractTerm that = (AbstractTerm) o; + return equalsWithSubstitution(LiteralEqualityHelper.DEFAULT, that); + } + + @Override + public int hashCode() { + return hashCodeWithSubstitution(LiteralHashCodeHelper.DEFAULT); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/Aggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/Aggregator.java new file mode 100644 index 00000000..40189ebc --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/Aggregator.java @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import java.util.stream.Stream; + +public interface Aggregator { + Class getResultType(); + + Class getInputType(); + + R aggregateStream(Stream stream); + + R getEmptyResult(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyDataVariable.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyDataVariable.java new file mode 100644 index 00000000..3376bdc9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyDataVariable.java @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import org.jetbrains.annotations.Nullable; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; + +import java.util.Optional; +import java.util.Set; + +public abstract sealed class AnyDataVariable extends Variable implements AnyTerm permits DataVariable { + protected AnyDataVariable(String name) { + super(name); + } + + @Override + public Optional> tryGetType() { + return Optional.of(getType()); + } + + @Override + public boolean isNodeVariable() { + return false; + } + + @Override + public boolean isDataVariable() { + return true; + } + + @Override + public NodeVariable asNodeVariable() { + throw new InvalidQueryException("%s is a data variable".formatted(this)); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + return other instanceof AnyDataVariable dataVariable && helper.variableEqual(this, dataVariable); + } + + @Override + public Set getInputVariables() { + return Set.of(this); + } + + @Override + public abstract AnyDataVariable renew(@Nullable String name); + + @Override + public abstract AnyDataVariable renew(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyTerm.java new file mode 100644 index 00000000..58b4cb1d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/AnyTerm.java @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; + +import java.util.Set; + +public sealed interface AnyTerm permits AnyDataVariable, Term { + Class getType(); + + AnyTerm substitute(Substitution substitution); + + boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other); + + int hashCodeWithSubstitution(LiteralHashCodeHelper helper); + + Set getInputVariables(); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/AssignedValue.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/AssignedValue.java new file mode 100644 index 00000000..78900cd6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/AssignedValue.java @@ -0,0 +1,13 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.literal.Literal; + +@FunctionalInterface +public interface AssignedValue { + Literal toLiteral(DataVariable targetVariable); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/BinaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/BinaryTerm.java new file mode 100644 index 00000000..356fd807 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/BinaryTerm.java @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.valuation.Valuation; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}. +@SuppressWarnings("squid:S2160") +public abstract class BinaryTerm extends AbstractTerm { + private final Class leftType; + private final Class rightType; + private final Term left; + private final Term right; + + protected BinaryTerm(Class type, Class leftType, Class rightType, Term left, Term right) { + super(type); + if (!left.getType().equals(leftType)) { + throw new InvalidQueryException("Expected left %s to be of type %s, got %s instead".formatted( + left, leftType.getName(), left.getType().getName())); + } + if (!right.getType().equals(rightType)) { + throw new InvalidQueryException("Expected right %s to be of type %s, got %s instead".formatted( + right, rightType.getName(), right.getType().getName())); + } + this.leftType = leftType; + this.rightType = rightType; + this.left = left; + this.right = right; + } + + public Class getLeftType() { + return leftType; + } + + public Class getRightType() { + return rightType; + } + + public Term getLeft() { + return left; + } + + public Term getRight() { + return right; + } + + @Override + public R evaluate(Valuation valuation) { + var leftValue = left.evaluate(valuation); + if (leftValue == null) { + return null; + } + var rightValue = right.evaluate(valuation); + if (rightValue == null) { + return null; + } + return doEvaluate(leftValue, rightValue); + } + + protected abstract R doEvaluate(T1 leftValue, T2 rightValue); + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherBinaryTerm = (BinaryTerm) other; + return leftType.equals(otherBinaryTerm.leftType) && + rightType.equals(otherBinaryTerm.rightType) && + left.equalsWithSubstitution(helper, otherBinaryTerm.left) && + right.equalsWithSubstitution(helper, otherBinaryTerm.right); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), leftType.hashCode(), rightType.hashCode(), + left.hashCodeWithSubstitution(helper), right.hashCodeWithSubstitution(helper)); + } + + @Override + public Term substitute(Substitution substitution) { + return doSubstitute(substitution, left.substitute(substitution), right.substitute(substitution)); + } + + public abstract Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight); + + @Override + public Set getInputVariables() { + var inputVariables = new HashSet<>(left.getInputVariables()); + inputVariables.addAll(right.getInputVariables()); + return Collections.unmodifiableSet(inputVariables); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/ConstantTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/ConstantTerm.java new file mode 100644 index 00000000..69c7e5f6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/ConstantTerm.java @@ -0,0 +1,67 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.valuation.Valuation; + +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}. +@SuppressWarnings("squid:S2160") +public final class ConstantTerm extends AbstractTerm { + private final T value; + + public ConstantTerm(Class type, T value) { + super(type); + if (value != null && !type.isInstance(value)) { + throw new InvalidQueryException("Value %s is not an instance of %s".formatted(value, type.getName())); + } + this.value = value; + } + + public T getValue() { + return value; + } + + @Override + public T evaluate(Valuation valuation) { + return getValue(); + } + + @Override + public Term substitute(Substitution substitution) { + return this; + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherConstantTerm = (ConstantTerm) other; + return Objects.equals(value, otherConstantTerm.value); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), Objects.hash(value)); + } + + @Override + public Set getInputVariables() { + return Set.of(); + } + + @Override + public String toString() { + return value.toString(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/DataVariable.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/DataVariable.java new file mode 100644 index 00000000..7ef69d05 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/DataVariable.java @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import org.jetbrains.annotations.Nullable; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.literal.EquivalenceLiteral; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.valuation.Valuation; + +import java.util.Objects; + +public final class DataVariable extends AnyDataVariable implements Term { + private final Class type; + + DataVariable(String name, Class type) { + super(name); + this.type = type; + } + + @Override + public Class getType() { + return type; + } + + @Override + public DataVariable renew(@Nullable String name) { + return new DataVariable<>(name, type); + } + + @Override + public DataVariable renew() { + return renew(getExplicitName()); + } + + @Override + public DataVariable asDataVariable(Class newType) { + if (!getType().equals(newType)) { + throw new InvalidQueryException("%s is not of type %s but of type %s" + .formatted(this, newType.getName(), getType().getName())); + } + @SuppressWarnings("unchecked") + var result = (DataVariable) this; + return result; + } + + @Override + public T evaluate(Valuation valuation) { + return valuation.getValue(this); + } + + @Override + public Term substitute(Substitution substitution) { + return substitution.getTypeSafeSubstitute(this); + } + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + return other instanceof DataVariable dataVariable && helper.variableEqual(this, dataVariable); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return helper.getVariableHashCode(this); + } + + @Override + public int hashCodeWithSubstitution(int sequenceNumber) { + return Objects.hash(type, sequenceNumber); + } + + public Literal assign(AssignedValue value) { + return value.toLiteral(this); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + DataVariable that = (DataVariable) o; + return type.equals(that.type); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), type); + } + + public EquivalenceLiteral isEquivalent(DataVariable other) { + return new EquivalenceLiteral(true, this, other); + } + + public EquivalenceLiteral notEquivalent(DataVariable other) { + return new EquivalenceLiteral(false, this, other); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/ExtremeValueAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/ExtremeValueAggregator.java new file mode 100644 index 00000000..31c61b6a --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/ExtremeValueAggregator.java @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import java.util.Comparator; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; + +public class ExtremeValueAggregator implements StatefulAggregator { + private final Class type; + private final T emptyResult; + private final Comparator comparator; + + public ExtremeValueAggregator(Class type, T emptyResult) { + this(type, emptyResult, null); + } + + public ExtremeValueAggregator(Class type, T emptyResult, Comparator comparator) { + this.type = type; + this.emptyResult = emptyResult; + this.comparator = comparator; + } + + @Override + public Class getResultType() { + return getInputType(); + } + + @Override + public Class getInputType() { + return type; + } + + @Override + public StatefulAggregate createEmptyAggregate() { + return new Aggregate(); + } + + @Override + public T getEmptyResult() { + return emptyResult; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExtremeValueAggregator that = (ExtremeValueAggregator) o; + return type.equals(that.type) && Objects.equals(emptyResult, that.emptyResult) && Objects.equals(comparator, + that.comparator); + } + + @Override + public int hashCode() { + return Objects.hash(type, emptyResult, comparator); + } + + private class Aggregate implements StatefulAggregate { + private final SortedMap values; + + private Aggregate() { + values = new TreeMap<>(comparator); + } + + private Aggregate(Aggregate other) { + values = new TreeMap<>(other.values); + } + + @Override + public void add(T value) { + values.compute(value, (ignoredValue, currentCount) -> currentCount == null ? 1 : currentCount + 1); + } + + @Override + public void remove(T value) { + values.compute(value, (theValue, currentCount) -> { + if (currentCount == null || currentCount <= 0) { + throw new IllegalStateException("Invalid count %d for value %s".formatted(currentCount, theValue)); + } + return currentCount.equals(1) ? null : currentCount - 1; + }); + } + + @Override + public T getResult() { + return isEmpty() ? emptyResult : values.firstKey(); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public StatefulAggregate deepCopy() { + return new Aggregate(this); + } + + @Override + public boolean contains(T value) { + return StatefulAggregate.super.contains(value); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/NodeVariable.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/NodeVariable.java new file mode 100644 index 00000000..1a5f2657 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/NodeVariable.java @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import org.jetbrains.annotations.Nullable; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.literal.ConstantLiteral; +import tools.refinery.logic.literal.EquivalenceLiteral; + +import java.util.Optional; + +public final class NodeVariable extends Variable { + NodeVariable(@Nullable String name) { + super(name); + } + + @Override + public Optional> tryGetType() { + return Optional.empty(); + } + + @Override + public NodeVariable renew(@Nullable String name) { + return of(name); + } + + @Override + public NodeVariable renew() { + return renew(getExplicitName()); + } + + @Override + public boolean isNodeVariable() { + return true; + } + + @Override + public boolean isDataVariable() { + return false; + } + + @Override + public NodeVariable asNodeVariable() { + return this; + } + + @Override + public DataVariable asDataVariable(Class type) { + throw new InvalidQueryException("%s is a node variable".formatted(this)); + } + + @Override + public int hashCodeWithSubstitution(int sequenceNumber) { + return sequenceNumber; + } + + public ConstantLiteral isConstant(int value) { + return new ConstantLiteral(this, value); + } + + public EquivalenceLiteral isEquivalent(NodeVariable other) { + return new EquivalenceLiteral(true, this, other); + } + + public EquivalenceLiteral notEquivalent(NodeVariable other) { + return new EquivalenceLiteral(false, this, other); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/Parameter.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/Parameter.java new file mode 100644 index 00000000..d4a651e1 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/Parameter.java @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import java.util.Objects; +import java.util.Optional; + +public class Parameter { + public static final Parameter NODE_OUT = new Parameter(null); + + private final Class dataType; + private final ParameterDirection direction; + + public Parameter(Class dataType) { + this(dataType, ParameterDirection.OUT); + } + + public Parameter(Class dataType, ParameterDirection direction) { + this.dataType = dataType; + this.direction = direction; + } + + public boolean isNodeVariable() { + return dataType == null; + } + + public boolean isDataVariable() { + return !isNodeVariable(); + } + + public Optional> tryGetType() { + return Optional.ofNullable(dataType); + } + + public ParameterDirection getDirection() { + return direction; + } + + public boolean matches(Parameter other) { + return Objects.equals(dataType, other.dataType) && direction == other.direction; + } + + public boolean isAssignable(Variable variable) { + if (variable instanceof AnyDataVariable dataVariable) { + return dataVariable.getType().equals(dataType); + } else if (variable instanceof NodeVariable) { + return !isDataVariable(); + } else { + throw new IllegalArgumentException("Unknown variable " + variable); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Parameter parameter = (Parameter) o; + return matches(parameter); + } + + @Override + public int hashCode() { + return Objects.hash(dataType, direction); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/ParameterDirection.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/ParameterDirection.java new file mode 100644 index 00000000..75ee06e6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/ParameterDirection.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +public enum ParameterDirection { + OUT("out"), + IN("in"); + + private final String name; + + ParameterDirection(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregate.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregate.java new file mode 100644 index 00000000..75e7ecff --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregate.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +public interface StatefulAggregate { + void add(T value); + + void remove(T value); + + R getResult(); + + boolean isEmpty(); + + StatefulAggregate deepCopy(); + + default boolean contains(T value) { + throw new UnsupportedOperationException(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregator.java new file mode 100644 index 00000000..79d32535 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatefulAggregator.java @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import java.util.stream.Stream; + +public interface StatefulAggregator extends Aggregator { + StatefulAggregate createEmptyAggregate(); + + @Override + default R aggregateStream(Stream stream) { + var accumulator = createEmptyAggregate(); + var iterator = stream.iterator(); + while (iterator.hasNext()) { + var value = iterator.next(); + accumulator.add(value); + } + return accumulator.getResult(); + } + + @Override + default R getEmptyResult() { + return createEmptyAggregate().getResult(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/StatelessAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatelessAggregator.java new file mode 100644 index 00000000..37db262e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/StatelessAggregator.java @@ -0,0 +1,25 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import java.util.stream.Stream; + +public interface StatelessAggregator extends Aggregator { + R add(R current, T value); + + R remove(R current, T value); + + @Override + default R aggregateStream(Stream stream) { + var accumulator = getEmptyResult(); + var iterator = stream.iterator(); + while (iterator.hasNext()) { + var value = iterator.next(); + accumulator = add(accumulator, value); + } + return accumulator; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/Term.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/Term.java new file mode 100644 index 00000000..0eaa36d8 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/Term.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.literal.AssignLiteral; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.valuation.Valuation; + +public non-sealed interface Term extends AnyTerm, AssignedValue { + @Override + Class getType(); + + T evaluate(Valuation valuation); + + @Override + Term substitute(Substitution substitution); + + @Override + default Literal toLiteral(DataVariable targetVariable) { + return new AssignLiteral<>(targetVariable, this); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/UnaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/UnaryTerm.java new file mode 100644 index 00000000..e173de3e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/UnaryTerm.java @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.equality.LiteralEqualityHelper; +import tools.refinery.logic.equality.LiteralHashCodeHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.valuation.Valuation; + +import java.util.Objects; +import java.util.Set; + +// {@link Object#equals(Object)} is implemented by {@link AbstractTerm}. +@SuppressWarnings("squid:S2160") +public abstract class UnaryTerm extends AbstractTerm { + private final Class bodyType; + private final Term body; + + protected UnaryTerm(Class type, Class bodyType, Term body) { + super(type); + if (!body.getType().equals(bodyType)) { + throw new InvalidQueryException("Expected body %s to be of type %s, got %s instead".formatted(body, + bodyType.getName(), body.getType().getName())); + } + this.bodyType = bodyType; + this.body = body; + } + + public Class getBodyType() { + return bodyType; + } + + public Term getBody() { + return body; + } + + @Override + public R evaluate(Valuation valuation) { + var bodyValue = body.evaluate(valuation); + return bodyValue == null ? null : doEvaluate(bodyValue); + } + + protected abstract R doEvaluate(T bodyValue); + + @Override + public boolean equalsWithSubstitution(LiteralEqualityHelper helper, AnyTerm other) { + if (!super.equalsWithSubstitution(helper, other)) { + return false; + } + var otherUnaryTerm = (UnaryTerm) other; + return bodyType.equals(otherUnaryTerm.bodyType) && body.equalsWithSubstitution(helper, otherUnaryTerm.body); + } + + @Override + public int hashCodeWithSubstitution(LiteralHashCodeHelper helper) { + return Objects.hash(super.hashCodeWithSubstitution(helper), bodyType, body.hashCodeWithSubstitution(helper)); + } + + @Override + public Term substitute(Substitution substitution) { + return doSubstitute(substitution, body.substitute(substitution)); + } + + protected abstract Term doSubstitute(Substitution substitution, Term substitutedBody); + + @Override + public Set getInputVariables() { + return body.getInputVariables(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/Variable.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/Variable.java new file mode 100644 index 00000000..28aad21b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/Variable.java @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import org.jetbrains.annotations.Nullable; +import tools.refinery.logic.dnf.DnfUtils; + +import java.util.Objects; +import java.util.Optional; + +public abstract sealed class Variable permits AnyDataVariable, NodeVariable { + private final String explicitName; + private final String uniqueName; + + protected Variable(String name) { + this.explicitName = name; + uniqueName = DnfUtils.generateUniqueName(name); + } + + public abstract Optional> tryGetType(); + + public String getName() { + return explicitName == null ? uniqueName : explicitName; + } + + protected String getExplicitName() { + return explicitName; + } + + public boolean isExplicitlyNamed() { + return explicitName != null; + } + + public String getUniqueName() { + return uniqueName; + } + + public abstract Variable renew(@Nullable String name); + + public abstract Variable renew(); + + public abstract boolean isNodeVariable(); + + public abstract boolean isDataVariable(); + + public abstract NodeVariable asNodeVariable(); + + public abstract DataVariable asDataVariable(Class type); + + public abstract int hashCodeWithSubstitution(int sequenceNumber); + + @Override + public String toString() { + return getName(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Variable variable = (Variable) o; + return Objects.equals(uniqueName, variable.uniqueName); + } + + @Override + public int hashCode() { + return Objects.hash(uniqueName); + } + + public static NodeVariable of(@Nullable String name) { + return new NodeVariable(name); + } + + public static NodeVariable of() { + return of((String) null); + } + + public static DataVariable of(@Nullable String name, Class type) { + return new DataVariable<>(name, type); + } + + public static DataVariable of(Class type) { + return of(null, type); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolAndTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolAndTerm.java new file mode 100644 index 00000000..88c2081b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolAndTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class BoolAndTerm extends BoolBinaryTerm { + public BoolAndTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new BoolAndTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Boolean doEvaluate(Boolean leftValue, Boolean rightValue) { + return leftValue && rightValue; + } + + @Override + public String toString() { + return "(%s && %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolBinaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolBinaryTerm.java new file mode 100644 index 00000000..6d53fb43 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolBinaryTerm.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.term.BinaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class BoolBinaryTerm extends BinaryTerm { + protected BoolBinaryTerm(Term left, Term right) { + super(Boolean.class, Boolean.class, Boolean.class, left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolNotTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolNotTerm.java new file mode 100644 index 00000000..ec80484f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolNotTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.UnaryTerm; + +public class BoolNotTerm extends UnaryTerm { + protected BoolNotTerm(Term body) { + super(Boolean.class, Boolean.class, body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new BoolNotTerm(substitutedBody); + } + + @Override + protected Boolean doEvaluate(Boolean bodyValue) { + return !bodyValue; + } + + @Override + public String toString() { + return "(!%s)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolOrTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolOrTerm.java new file mode 100644 index 00000000..10c61bf2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolOrTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class BoolOrTerm extends BoolBinaryTerm { + public BoolOrTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new BoolOrTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Boolean doEvaluate(Boolean leftValue, Boolean rightValue) { + return leftValue || rightValue; + } + + @Override + public String toString() { + return "(%s || %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolTerms.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolTerms.java new file mode 100644 index 00000000..5bdc3207 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolTerms.java @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.Term; + +public final class BoolTerms { + private BoolTerms() { + throw new IllegalArgumentException("This is a static utility class and should not be instantiated directly"); + } + + public static Term constant(Boolean value) { + return new ConstantTerm<>(Boolean.class, value); + } + + public static Term not(Term body) { + return new BoolNotTerm(body); + } + + public static Term and(Term left, Term right) { + return new BoolAndTerm(left, right); + } + + public static Term or(Term left, Term right) { + return new BoolOrTerm(left, right); + } + + public static Term xor(Term left, Term right) { + return new BoolXorTerm(left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolXorTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolXorTerm.java new file mode 100644 index 00000000..3d5247a7 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/bool/BoolXorTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class BoolXorTerm extends BoolBinaryTerm { + public BoolXorTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new BoolXorTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Boolean doEvaluate(Boolean leftValue, Boolean rightValue) { + return leftValue ^ rightValue; + } + + @Override + public String toString() { + return "(%s ^^ %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityDomain.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityDomain.java new file mode 100644 index 00000000..508a454b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityDomain.java @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import tools.refinery.logic.term.uppercardinality.FiniteUpperCardinality; +import tools.refinery.logic.AbstractDomain; + +import java.util.Optional; + +// Singleton pattern, because there is only one domain for truth values. +@SuppressWarnings("squid:S6548") +public class CardinalityDomain implements AbstractDomain { + public static final CardinalityDomain INSTANCE = new CardinalityDomain(); + + private CardinalityDomain() { + } + + @Override + public Class abstractType() { + return CardinalityInterval.class; + } + + @Override + public Class concreteType() { + return Integer.class; + } + + @Override + public CardinalityInterval toAbstract(Integer concreteValue) { + return CardinalityIntervals.exactly(concreteValue); + } + + @Override + public Optional toConcrete(CardinalityInterval abstractValue) { + return isConcrete(abstractValue) ? Optional.of(abstractValue.lowerBound()) : Optional.empty(); + } + + @Override + public boolean isConcrete(CardinalityInterval abstractValue) { + if (!(abstractValue instanceof NonEmptyCardinalityInterval nonEmptyValue) || + !((nonEmptyValue.upperBound()) instanceof FiniteUpperCardinality finiteUpperCardinality)) { + return false; + } + return nonEmptyValue.lowerBound() == finiteUpperCardinality.finiteUpperBound(); + } + + @Override + public CardinalityInterval commonRefinement(CardinalityInterval leftValue, CardinalityInterval rightValue) { + return leftValue.meet(rightValue); + } + + @Override + public CardinalityInterval commonAncestor(CardinalityInterval leftValue, CardinalityInterval rightValue) { + return leftValue.join(rightValue); + } + + @Override + public CardinalityInterval unknown() { + return CardinalityIntervals.SET; + } + + @Override + public boolean isError(CardinalityInterval abstractValue) { + return abstractValue.isEmpty(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityInterval.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityInterval.java new file mode 100644 index 00000000..dbf30def --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityInterval.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import tools.refinery.logic.term.uppercardinality.UpperCardinality; + +public sealed interface CardinalityInterval permits NonEmptyCardinalityInterval, EmptyCardinalityInterval { + int lowerBound(); + + UpperCardinality upperBound(); + + boolean isEmpty(); + + CardinalityInterval min(CardinalityInterval other); + + CardinalityInterval max(CardinalityInterval other); + + CardinalityInterval add(CardinalityInterval other); + + CardinalityInterval take(int count); + + CardinalityInterval multiply(CardinalityInterval other); + + CardinalityInterval meet(CardinalityInterval other); + + CardinalityInterval join(CardinalityInterval other); +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervals.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervals.java new file mode 100644 index 00000000..cb64cc0d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervals.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import tools.refinery.logic.term.uppercardinality.UpperCardinalities; +import tools.refinery.logic.term.uppercardinality.UpperCardinality; + +public final class CardinalityIntervals { + public static final CardinalityInterval NONE = exactly(0); + + public static final CardinalityInterval ONE = exactly(1); + + public static final CardinalityInterval LONE = atMost(1); + + public static final CardinalityInterval SET = atLeast(0); + + public static final CardinalityInterval SOME = atLeast(1); + + public static final CardinalityInterval ERROR = EmptyCardinalityInterval.INSTANCE; + + private CardinalityIntervals() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static CardinalityInterval between(int lowerBound, UpperCardinality upperBound) { + if (upperBound.compareToInt(lowerBound) < 0) { + return ERROR; + } + return new NonEmptyCardinalityInterval(lowerBound, upperBound); + } + + public static CardinalityInterval between(int lowerBound, int upperBound) { + return between(lowerBound, UpperCardinalities.atMost(upperBound)); + } + + public static CardinalityInterval atMost(UpperCardinality upperBound) { + return new NonEmptyCardinalityInterval(0, upperBound); + } + + public static CardinalityInterval atMost(int upperBound) { + return atMost(UpperCardinalities.atMost(upperBound)); + } + + public static CardinalityInterval atLeast(int lowerBound) { + return new NonEmptyCardinalityInterval(lowerBound, UpperCardinalities.UNBOUNDED); + } + + public static CardinalityInterval exactly(int lowerBound) { + return new NonEmptyCardinalityInterval(lowerBound, UpperCardinalities.atMost(lowerBound)); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityInterval.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityInterval.java new file mode 100644 index 00000000..29a7f69d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityInterval.java @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import tools.refinery.logic.term.uppercardinality.UpperCardinalities; +import tools.refinery.logic.term.uppercardinality.UpperCardinality; + +// Singleton implementation, because there is only a single empty interval. +@SuppressWarnings("squid:S6548") +public final class EmptyCardinalityInterval implements CardinalityInterval { + static final EmptyCardinalityInterval INSTANCE = new EmptyCardinalityInterval(); + + private EmptyCardinalityInterval() { + // Singleton constructor. + } + + @Override + public int lowerBound() { + return 1; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public UpperCardinality upperBound() { + return UpperCardinalities.ZERO; + } + + @Override + public CardinalityInterval min(CardinalityInterval other) { + return this; + } + + @Override + public CardinalityInterval max(CardinalityInterval other) { + return this; + } + + @Override + public CardinalityInterval add(CardinalityInterval other) { + return this; + } + + @Override + public CardinalityInterval take(int count) { + return this; + } + + @Override + public CardinalityInterval multiply(CardinalityInterval other) { + return this; + } + + @Override + public CardinalityInterval meet(CardinalityInterval other) { + return this; + } + + @Override + public CardinalityInterval join(CardinalityInterval other) { + return other; + } + + @Override + public String toString() { + return "error"; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/NonEmptyCardinalityInterval.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/NonEmptyCardinalityInterval.java new file mode 100644 index 00000000..0919fc36 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/cardinalityinterval/NonEmptyCardinalityInterval.java @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import tools.refinery.logic.term.uppercardinality.FiniteUpperCardinality; +import tools.refinery.logic.term.uppercardinality.UpperCardinality; + +import java.util.Objects; +import java.util.function.BinaryOperator; +import java.util.function.IntBinaryOperator; + +public record NonEmptyCardinalityInterval(int lowerBound, UpperCardinality upperBound) implements CardinalityInterval { + public NonEmptyCardinalityInterval { + if (lowerBound < 0) { + throw new IllegalArgumentException("lowerBound must not be negative"); + } + if (upperBound.compareToInt(lowerBound) < 0) { + throw new IllegalArgumentException("lowerBound must not be larger than upperBound"); + } + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public CardinalityInterval min(CardinalityInterval other) { + return lift(other, Math::min, UpperCardinality::min); + } + + @Override + public CardinalityInterval max(CardinalityInterval other) { + return lift(other, Math::max, UpperCardinality::max); + } + + @Override + public CardinalityInterval add(CardinalityInterval other) { + return lift(other, Integer::sum, UpperCardinality::add); + } + + @Override + public CardinalityInterval multiply(CardinalityInterval other) { + return lift(other, (a, b) -> a * b, UpperCardinality::multiply); + } + + @Override + public CardinalityInterval meet(CardinalityInterval other) { + return lift(other, Math::max, UpperCardinality::min); + } + + @Override + public CardinalityInterval join(CardinalityInterval other) { + return lift(other, Math::min, UpperCardinality::max, this); + } + + @Override + public CardinalityInterval take(int count) { + int newLowerBound = Math.max(lowerBound - count, 0); + var newUpperBound = upperBound.take(count); + if (newUpperBound == null) { + return CardinalityIntervals.ERROR; + } + return CardinalityIntervals.between(newLowerBound, newUpperBound); + } + + private CardinalityInterval lift(CardinalityInterval other, IntBinaryOperator lowerOperator, + BinaryOperator upperOperator, + CardinalityInterval whenEmpty) { + if (other instanceof NonEmptyCardinalityInterval nonEmptyOther) { + return CardinalityIntervals.between(lowerOperator.applyAsInt(lowerBound, nonEmptyOther.lowerBound), + upperOperator.apply(upperBound, nonEmptyOther.upperBound)); + } + if (other instanceof EmptyCardinalityInterval) { + return whenEmpty; + } + throw new IllegalArgumentException("Unknown CardinalityInterval: " + other); + } + + private CardinalityInterval lift(CardinalityInterval other, IntBinaryOperator lowerOperator, + BinaryOperator upperOperator) { + return lift(other, lowerOperator, upperOperator, CardinalityIntervals.ERROR); + } + + @Override + public String toString() { + if (upperBound instanceof FiniteUpperCardinality finiteUpperCardinality && + finiteUpperCardinality.finiteUpperBound() == lowerBound) { + return "[%d]".formatted(lowerBound); + } + return "[%d..%s]".formatted(lowerBound, upperBound); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + NonEmptyCardinalityInterval that = (NonEmptyCardinalityInterval) o; + return lowerBound == that.lowerBound && Objects.equals(upperBound, that.upperBound); + } + + @Override + public int hashCode() { + return lowerBound * 31 + upperBound.hashCode(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/ComparisonTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/ComparisonTerm.java new file mode 100644 index 00000000..0401f984 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/ComparisonTerm.java @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.term.BinaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class ComparisonTerm extends BinaryTerm { + protected ComparisonTerm(Class argumentType, Term left, Term right) { + super(Boolean.class, argumentType, argumentType, left, right); + } + + public Class getArgumentType() { + return getLeftType(); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/EqTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/EqTerm.java new file mode 100644 index 00000000..7a1315e6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/EqTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class EqTerm extends ComparisonTerm { + public EqTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return leftValue.equals(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new EqTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s == %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterEqTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterEqTerm.java new file mode 100644 index 00000000..38323999 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterEqTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class GreaterEqTerm> extends ComparisonTerm { + public GreaterEqTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return leftValue.compareTo(rightValue) >= 0; + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new GreaterEqTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s >= %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterTerm.java new file mode 100644 index 00000000..7174372e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/GreaterTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class GreaterTerm> extends ComparisonTerm { + public GreaterTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return leftValue.compareTo(rightValue) > 0; + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new GreaterTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s > %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessEqTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessEqTerm.java new file mode 100644 index 00000000..7a07b7b2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessEqTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class LessEqTerm> extends ComparisonTerm { + public LessEqTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return leftValue.compareTo(rightValue) <= 0; + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new LessEqTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s <= %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessTerm.java new file mode 100644 index 00000000..b402f54d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/LessTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class LessTerm> extends ComparisonTerm { + public LessTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return leftValue.compareTo(rightValue) < 0; + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new LessTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s < %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/NotEqTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/NotEqTerm.java new file mode 100644 index 00000000..d2c697f4 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/comparable/NotEqTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.comparable; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class NotEqTerm extends ComparisonTerm { + public NotEqTerm(Class argumentType, Term left, Term right) { + super(argumentType, left, right); + } + + @Override + protected Boolean doEvaluate(T leftValue, T rightValue) { + return !leftValue.equals(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new NotEqTerm<>(getArgumentType(), substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s != %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntAddTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntAddTerm.java new file mode 100644 index 00000000..73533d9b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntAddTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntAddTerm extends IntBinaryTerm { + public IntAddTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntAddTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return leftValue + rightValue; + } + + @Override + public String toString() { + return "(%s + %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntBinaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntBinaryTerm.java new file mode 100644 index 00000000..9543c21f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntBinaryTerm.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.term.BinaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class IntBinaryTerm extends BinaryTerm { + protected IntBinaryTerm(Term left, Term right) { + super(Integer.class, Integer.class, Integer.class, left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntDivTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntDivTerm.java new file mode 100644 index 00000000..f9fa70ef --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntDivTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntDivTerm extends IntBinaryTerm { + public IntDivTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntDivTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return rightValue == 0 ? null : leftValue / rightValue; + } + + @Override + public String toString() { + return "(%s / %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMaxTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMaxTerm.java new file mode 100644 index 00000000..f7b315df --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMaxTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntMaxTerm extends IntBinaryTerm { + public IntMaxTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntMaxTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return Math.max(rightValue, leftValue); + } + + @Override + public String toString() { + return "max(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinTerm.java new file mode 100644 index 00000000..73247db1 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntMinTerm extends IntBinaryTerm { + public IntMinTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntMinTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return Math.min(rightValue, leftValue); + } + + @Override + public String toString() { + return "min(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinusTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinusTerm.java new file mode 100644 index 00000000..006e1977 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMinusTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntMinusTerm extends IntUnaryTerm { + public IntMinusTerm(Term body) { + super(body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new IntMinusTerm(substitutedBody); + } + + @Override + protected Integer doEvaluate(Integer bodyValue) { + return -bodyValue; + } + + @Override + public String toString() { + return "(-%s)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMulTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMulTerm.java new file mode 100644 index 00000000..18fdcb45 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntMulTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntMulTerm extends IntBinaryTerm { + public IntMulTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntMulTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return leftValue * rightValue; + } + + @Override + public String toString() { + return "(%s * %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPlusTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPlusTerm.java new file mode 100644 index 00000000..1a905293 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPlusTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntPlusTerm extends IntUnaryTerm { + public IntPlusTerm(Term body) { + super(body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new IntPlusTerm(substitutedBody); + } + + @Override + protected Integer doEvaluate(Integer bodyValue) { + return bodyValue; + } + + @Override + public String toString() { + return "(+%s)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPowTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPowTerm.java new file mode 100644 index 00000000..dac490b0 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntPowTerm.java @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntPowTerm extends IntBinaryTerm { + public IntPowTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntPowTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return rightValue < 0 ? null : power(leftValue, rightValue); + } + + private static int power(int base, int exponent) { + int accum = 1; + while (exponent > 0) { + if (exponent % 2 == 1) { + accum = accum * base; + } + base = base * base; + exponent = exponent / 2; + } + return accum; + } + + @Override + public String toString() { + return "(%s ** %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSubTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSubTerm.java new file mode 100644 index 00000000..94326677 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSubTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntSubTerm extends IntBinaryTerm { + public IntSubTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new IntSubTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Integer doEvaluate(Integer leftValue, Integer rightValue) { + return leftValue - rightValue; + } + + @Override + public String toString() { + return "(%s - %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSumAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSumAggregator.java new file mode 100644 index 00000000..b42038ab --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntSumAggregator.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.term.StatelessAggregator; + +public final class IntSumAggregator implements StatelessAggregator { + public static final IntSumAggregator INSTANCE = new IntSumAggregator(); + + private IntSumAggregator() { + } + + @Override + public Class getResultType() { + return Integer.class; + } + + @Override + public Class getInputType() { + return Integer.class; + } + + @Override + public Integer getEmptyResult() { + return 0; + } + + @Override + public Integer add(Integer current, Integer value) { + return current + value; + } + + @Override + public Integer remove(Integer current, Integer value) { + return current - value; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntTerms.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntTerms.java new file mode 100644 index 00000000..b152a138 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntTerms.java @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.term.Aggregator; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.ExtremeValueAggregator; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.comparable.*; +import tools.refinery.logic.term.comparable.*; + +import java.util.Comparator; + +public final class IntTerms { + public static final Aggregator INT_SUM = IntSumAggregator.INSTANCE; + public static final Aggregator INT_MIN = new ExtremeValueAggregator<>(Integer.class, + Integer.MAX_VALUE); + public static final Aggregator INT_MAX = new ExtremeValueAggregator<>(Integer.class, + Integer.MIN_VALUE, Comparator.reverseOrder()); + + private IntTerms() { + throw new IllegalArgumentException("This is a static utility class and should not be instantiated directly"); + } + + public static Term constant(Integer value) { + return new ConstantTerm<>(Integer.class, value); + } + + public static Term plus(Term body) { + return new IntPlusTerm(body); + } + + public static Term minus(Term body) { + return new IntMinusTerm(body); + } + + public static Term add(Term left, Term right) { + return new IntAddTerm(left, right); + } + + public static Term sub(Term left, Term right) { + return new IntSubTerm(left, right); + } + + public static Term mul(Term left, Term right) { + return new IntMulTerm(left, right); + } + + public static Term div(Term left, Term right) { + return new IntDivTerm(left, right); + } + + public static Term pow(Term left, Term right) { + return new IntPowTerm(left, right); + } + + public static Term min(Term left, Term right) { + return new IntMinTerm(left, right); + } + + public static Term max(Term left, Term right) { + return new IntMaxTerm(left, right); + } + + public static Term eq(Term left, Term right) { + return new EqTerm<>(Integer.class, left, right); + } + + public static Term notEq(Term left, Term right) { + return new NotEqTerm<>(Integer.class, left, right); + } + + public static Term less(Term left, Term right) { + return new LessTerm<>(Integer.class, left, right); + } + + public static Term lessEq(Term left, Term right) { + return new LessEqTerm<>(Integer.class, left, right); + } + + public static Term greater(Term left, Term right) { + return new GreaterTerm<>(Integer.class, left, right); + } + + public static Term greaterEq(Term left, Term right) { + return new GreaterEqTerm<>(Integer.class, left, right); + } + + public static Term asInt(Term body) { + return new RealToIntTerm(body); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntUnaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntUnaryTerm.java new file mode 100644 index 00000000..20449e67 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/IntUnaryTerm.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.term.UnaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class IntUnaryTerm extends UnaryTerm { + protected IntUnaryTerm(Term body) { + super(Integer.class, Integer.class, body); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/RealToIntTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/RealToIntTerm.java new file mode 100644 index 00000000..7611af90 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/int_/RealToIntTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.UnaryTerm; +import tools.refinery.logic.substitution.Substitution; + +public class RealToIntTerm extends UnaryTerm { + protected RealToIntTerm(Term body) { + super(Integer.class, Double.class, body); + } + + @Override + protected Integer doEvaluate(Double bodyValue) { + return bodyValue.isNaN() ? null : bodyValue.intValue(); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new RealToIntTerm(substitutedBody); + } + + @Override + public String toString() { + return "(%s as int)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/IntToRealTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/IntToRealTerm.java new file mode 100644 index 00000000..23c6893c --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/IntToRealTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.term.UnaryTerm; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class IntToRealTerm extends UnaryTerm { + protected IntToRealTerm(Term body) { + super(Double.class, Integer.class, body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new IntToRealTerm(substitutedBody); + } + + @Override + protected Double doEvaluate(Integer bodyValue) { + return bodyValue.doubleValue(); + } + + @Override + public String toString() { + return "(%s as real)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealAddTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealAddTerm.java new file mode 100644 index 00000000..82b97228 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealAddTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealAddTerm extends RealBinaryTerm { + public RealAddTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealAddTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return leftValue + rightValue; + } + + @Override + public String toString() { + return "(%s + %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealBinaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealBinaryTerm.java new file mode 100644 index 00000000..3c23e767 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealBinaryTerm.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.term.BinaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class RealBinaryTerm extends BinaryTerm { + protected RealBinaryTerm(Term left, Term right) { + super(Double.class, Double.class, Double.class, left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealDivTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealDivTerm.java new file mode 100644 index 00000000..c22399e3 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealDivTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealDivTerm extends RealBinaryTerm { + public RealDivTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealDivTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return leftValue / rightValue; + } + + @Override + public String toString() { + return "(%s / %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMaxTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMaxTerm.java new file mode 100644 index 00000000..0c609c6d --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMaxTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealMaxTerm extends RealBinaryTerm { + public RealMaxTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealMaxTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return Math.max(leftValue, rightValue); + } + + @Override + public String toString() { + return "max(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinTerm.java new file mode 100644 index 00000000..af2c3a52 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealMinTerm extends RealBinaryTerm { + public RealMinTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealMinTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return Math.min(leftValue, rightValue); + } + + @Override + public String toString() { + return "min(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinusTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinusTerm.java new file mode 100644 index 00000000..e3e371c2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMinusTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealMinusTerm extends RealUnaryTerm { + public RealMinusTerm(Term body) { + super(body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new RealMinusTerm(substitutedBody); + } + + @Override + protected Double doEvaluate(Double bodyValue) { + return -bodyValue; + } + + @Override + public String toString() { + return "(-%s)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMulTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMulTerm.java new file mode 100644 index 00000000..ce8f2cd4 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealMulTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealMulTerm extends RealBinaryTerm { + public RealMulTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealMulTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return leftValue * rightValue; + } + + @Override + public String toString() { + return "(%s * %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPlusTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPlusTerm.java new file mode 100644 index 00000000..60efc5fd --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPlusTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealPlusTerm extends RealUnaryTerm { + public RealPlusTerm(Term body) { + super(body); + } + + @Override + protected Term doSubstitute(Substitution substitution, Term substitutedBody) { + return new RealPlusTerm(substitutedBody); + } + + @Override + protected Double doEvaluate(Double bodyValue) { + return bodyValue; + } + + @Override + public String toString() { + return "(+%s)".formatted(getBody()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPowTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPowTerm.java new file mode 100644 index 00000000..6cb50362 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealPowTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealPowTerm extends RealBinaryTerm { + public RealPowTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealPowTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return Math.pow(leftValue, rightValue); + } + + @Override + public String toString() { + return "(%s ** %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSubTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSubTerm.java new file mode 100644 index 00000000..32315e8e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSubTerm.java @@ -0,0 +1,31 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class RealSubTerm extends RealBinaryTerm { + public RealSubTerm(Term left, Term right) { + super(left, right); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, + Term substitutedRight) { + return new RealSubTerm(substitutedLeft, substitutedRight); + } + + @Override + protected Double doEvaluate(Double leftValue, Double rightValue) { + return leftValue - rightValue; + } + + @Override + public String toString() { + return "(%s - %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSumAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSumAggregator.java new file mode 100644 index 00000000..4b090188 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealSumAggregator.java @@ -0,0 +1,90 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.term.StatefulAggregate; +import tools.refinery.logic.term.StatefulAggregator; + +import java.util.Map; +import java.util.TreeMap; + +public final class RealSumAggregator implements StatefulAggregator { + public static final RealSumAggregator INSTANCE = new RealSumAggregator(); + + private RealSumAggregator() { + } + + @Override + public Class getResultType() { + return Double.class; + } + + @Override + public Class getInputType() { + return Double.class; + } + + @Override + public StatefulAggregate createEmptyAggregate() { + return new Aggregate(); + } + + @Override + public Double getEmptyResult() { + return 0d; + } + + private static class Aggregate implements StatefulAggregate { + private final Map values; + + public Aggregate() { + values = new TreeMap<>(); + } + + private Aggregate(Aggregate other) { + values = new TreeMap<>(other.values); + } + + @Override + public void add(Double value) { + values.compute(value, (ignoredValue, currentCount) -> currentCount == null ? 1 : currentCount + 1); + } + + @Override + public void remove(Double value) { + values.compute(value, (theValue, currentCount) -> { + if (currentCount == null || currentCount <= 0) { + throw new IllegalStateException("Invalid count %d for value %f".formatted(currentCount, theValue)); + } + return currentCount.equals(1) ? null : currentCount - 1; + }); + } + + @Override + public Double getResult() { + return values.entrySet() + .stream() + .mapToDouble(entry -> entry.getKey() * entry.getValue()) + .reduce(Double::sum) + .orElse(0d); + } + + @Override + public boolean isEmpty() { + return values.isEmpty(); + } + + @Override + public StatefulAggregate deepCopy() { + return new Aggregate(this); + } + + @Override + public boolean contains(Double value) { + return values.containsKey(value); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealTerms.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealTerms.java new file mode 100644 index 00000000..07dfa96b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealTerms.java @@ -0,0 +1,95 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.term.Aggregator; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.ExtremeValueAggregator; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.comparable.*; +import tools.refinery.logic.term.comparable.*; + +import java.util.Comparator; + +public final class RealTerms { + public static final Aggregator REAL_SUM = RealSumAggregator.INSTANCE; + public static final Aggregator REAL_MIN = new ExtremeValueAggregator<>(Double.class, + Double.POSITIVE_INFINITY); + public static final Aggregator REAL_MAX = new ExtremeValueAggregator<>(Double.class, + Double.NEGATIVE_INFINITY, Comparator.reverseOrder()); + + private RealTerms() { + throw new IllegalArgumentException("This is a static utility class and should not be instantiated directly"); + } + + public static Term constant(Double value) { + return new ConstantTerm<>(Double.class, value); + } + + public static Term plus(Term body) { + return new RealPlusTerm(body); + } + + public static Term minus(Term body) { + return new RealMinusTerm(body); + } + + public static Term add(Term left, Term right) { + return new RealAddTerm(left, right); + } + + public static Term sub(Term left, Term right) { + return new RealSubTerm(left, right); + } + + public static Term mul(Term left, Term right) { + return new RealMulTerm(left, right); + } + + public static Term div(Term left, Term right) { + return new RealDivTerm(left, right); + } + + public static Term pow(Term left, Term right) { + return new RealPowTerm(left, right); + } + + public static Term min(Term left, Term right) { + return new RealMinTerm(left, right); + } + + public static Term max(Term left, Term right) { + return new RealMaxTerm(left, right); + } + + public static Term eq(Term left, Term right) { + return new EqTerm<>(Double.class, left, right); + } + + public static Term notEq(Term left, Term right) { + return new NotEqTerm<>(Double.class, left, right); + } + + public static Term less(Term left, Term right) { + return new LessTerm<>(Double.class, left, right); + } + + public static Term lessEq(Term left, Term right) { + return new LessEqTerm<>(Double.class, left, right); + } + + public static Term greater(Term left, Term right) { + return new GreaterTerm<>(Double.class, left, right); + } + + public static Term greaterEq(Term left, Term right) { + return new GreaterEqTerm<>(Double.class, left, right); + } + + public static Term asReal(Term body) { + return new IntToRealTerm(body); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealUnaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealUnaryTerm.java new file mode 100644 index 00000000..14772411 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/real/RealUnaryTerm.java @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import tools.refinery.logic.term.UnaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class RealUnaryTerm extends UnaryTerm { + protected RealUnaryTerm(Term body) { + super(Double.class, Double.class, body); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java new file mode 100644 index 00000000..bbdd3f97 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.truthvalue; + +public enum TruthValue { + TRUE("true"), + + FALSE("false"), + + UNKNOWN("unknown"), + + ERROR("error"); + + private final String name; + + TruthValue(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static TruthValue toTruthValue(boolean value) { + return value ? TRUE : FALSE; + } + + public boolean isConsistent() { + return this != ERROR; + } + + public boolean isComplete() { + return this != UNKNOWN; + } + + public boolean isConcrete() { + return this == TRUE || this == FALSE; + } + + public boolean must() { + return this == TRUE || this == ERROR; + } + + public boolean may() { + return this == TRUE || this == UNKNOWN; + } + + public TruthValue not() { + return switch (this) { + case TRUE -> FALSE; + case FALSE -> TRUE; + default -> this; + }; + } + + public TruthValue merge(TruthValue other) { + return switch (this) { + case TRUE -> other == UNKNOWN || other == TRUE ? TRUE : ERROR; + case FALSE -> other == UNKNOWN || other == FALSE ? FALSE : ERROR; + case UNKNOWN -> other; + case ERROR -> ERROR; + }; + } + + public TruthValue join(TruthValue other) { + return switch (this) { + case TRUE -> other == ERROR || other == TRUE ? TRUE : UNKNOWN; + case FALSE -> other == ERROR || other == FALSE ? FALSE : UNKNOWN; + case UNKNOWN -> UNKNOWN; + case ERROR -> other; + }; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValueDomain.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValueDomain.java new file mode 100644 index 00000000..443f744f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValueDomain.java @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.truthvalue; + +import tools.refinery.logic.AbstractDomain; + +import java.util.Optional; + +// Singleton pattern, because there is only one domain for truth values. +@SuppressWarnings("squid:S6548") +public final class TruthValueDomain implements AbstractDomain { + public static final TruthValueDomain INSTANCE = new TruthValueDomain(); + + private TruthValueDomain() { + } + + @Override + public Class abstractType() { + return TruthValue.class; + } + + @Override + public Class concreteType() { + return Boolean.class; + } + + @Override + public TruthValue toAbstract(Boolean concreteValue) { + return TruthValue.toTruthValue(concreteValue); + } + + @Override + public Optional toConcrete(TruthValue abstractValue) { + return switch (abstractValue) { + case TRUE -> Optional.of(true); + case FALSE -> Optional.of(false); + default -> Optional.empty(); + }; + } + + @Override + public boolean isConcrete(TruthValue abstractValue) { + return abstractValue.isConcrete(); + } + + @Override + public TruthValue commonRefinement(TruthValue leftValue, TruthValue rightValue) { + return leftValue.merge(rightValue); + } + + @Override + public TruthValue commonAncestor(TruthValue leftValue, TruthValue rightValue) { + return leftValue.join(rightValue); + } + + @Override + public TruthValue unknown() { + return TruthValue.UNKNOWN; + } + + @Override + public boolean isError(TruthValue abstractValue) { + return !abstractValue.isConsistent(); + } +} + diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinality.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinality.java new file mode 100644 index 00000000..478d456b --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinality.java @@ -0,0 +1,83 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.IntBinaryOperator; + +public record FiniteUpperCardinality(int finiteUpperBound) implements UpperCardinality { + public FiniteUpperCardinality { + if (finiteUpperBound < 0) { + throw new IllegalArgumentException("finiteUpperBound must not be negative"); + } + } + + @Override + public UpperCardinality add(UpperCardinality other) { + return lift(other, Integer::sum); + } + + @Override + @Nullable + public UpperCardinality take(int count) { + if (finiteUpperBound < count) { + return null; + } + return new FiniteUpperCardinality(finiteUpperBound - count); + } + + @Override + public UpperCardinality multiply(UpperCardinality other) { + return lift(other, (a, b) -> a * b); + } + + @Override + public int compareTo(@NotNull UpperCardinality upperCardinality) { + if (upperCardinality instanceof FiniteUpperCardinality finiteUpperCardinality) { + return compareToInt(finiteUpperCardinality.finiteUpperBound); + } + if (upperCardinality instanceof UnboundedUpperCardinality) { + return -1; + } + throw new IllegalArgumentException("Unknown UpperCardinality: " + upperCardinality); + } + + @Override + public int compareToInt(int value) { + return Integer.compare(finiteUpperBound, value); + } + + @Override + public String toString() { + return Integer.toString(finiteUpperBound); + } + + private UpperCardinality lift(@NotNull UpperCardinality other, IntBinaryOperator operator) { + if (other instanceof FiniteUpperCardinality finiteUpperCardinality) { + return UpperCardinalities.atMost(operator.applyAsInt(finiteUpperBound, + finiteUpperCardinality.finiteUpperBound)); + } + if (other instanceof UnboundedUpperCardinality) { + return UpperCardinalities.UNBOUNDED; + } + throw new IllegalArgumentException("Unknown UpperCardinality: " + other); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FiniteUpperCardinality that = (FiniteUpperCardinality) o; + return finiteUpperBound == that.finiteUpperBound; + } + + @Override + public int hashCode() { + return finiteUpperBound; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UnboundedUpperCardinality.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UnboundedUpperCardinality.java new file mode 100644 index 00000000..b6ecc1bd --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UnboundedUpperCardinality.java @@ -0,0 +1,66 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.jetbrains.annotations.NotNull; + +// Singleton implementation, because there is only a single countable infinity. +@SuppressWarnings("squid:S6548") +public final class UnboundedUpperCardinality implements UpperCardinality { + static final UnboundedUpperCardinality INSTANCE = new UnboundedUpperCardinality(); + + private UnboundedUpperCardinality() { + // Singleton constructor. + } + + @Override + public UpperCardinality add(UpperCardinality other) { + return this; + } + + @Override + public UpperCardinality take(int count) { + return this; + } + + @Override + public UpperCardinality multiply(UpperCardinality other) { + return this; + } + + // This should always be greater than any finite cardinality. + @SuppressWarnings("ComparatorMethodParameterNotUsed") + @Override + public int compareTo(@NotNull UpperCardinality upperCardinality) { + if (upperCardinality instanceof FiniteUpperCardinality) { + return 1; + } + if (upperCardinality instanceof UnboundedUpperCardinality) { + return 0; + } + throw new IllegalArgumentException("Unknown UpperCardinality: " + upperCardinality); + } + + @Override + public int compareToInt(int value) { + return 1; + } + + @Override + public String toString() { + return "*"; + } + + @Override + public boolean equals(Object obj) { + return this == obj || (obj != null && getClass() == obj.getClass()); + } + + @Override + public int hashCode() { + return -1; + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalities.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalities.java new file mode 100644 index 00000000..edf0afd2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalities.java @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +public final class UpperCardinalities { + public static final UpperCardinality UNBOUNDED = UnboundedUpperCardinality.INSTANCE; + + public static final UpperCardinality ZERO; + + public static final UpperCardinality ONE; + + private static final FiniteUpperCardinality[] cache = new FiniteUpperCardinality[256]; + + static { + for (int i = 0; i < cache.length; i++) { + cache[i] = new FiniteUpperCardinality(i); + } + ZERO = cache[0]; + ONE = cache[1]; + } + + private UpperCardinalities() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static UpperCardinality atMost(int upperBound) { + if (upperBound < 0) { + return UNBOUNDED; + } + if (upperBound < cache.length) { + return cache[upperBound]; + } + return new FiniteUpperCardinality(upperBound); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinality.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinality.java new file mode 100644 index 00000000..5a3dcfb9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinality.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.jetbrains.annotations.Nullable; + +public sealed interface UpperCardinality extends Comparable permits FiniteUpperCardinality, + UnboundedUpperCardinality { + default UpperCardinality min(UpperCardinality other) { + return this.compareTo(other) <= 0 ? this : other; + } + + default UpperCardinality max(UpperCardinality other) { + return this.compareTo(other) >= 0 ? this : other; + } + + UpperCardinality add(UpperCardinality other); + + @Nullable + UpperCardinality take(int count); + + UpperCardinality multiply(UpperCardinality other); + + int compareToInt(int value); + + static UpperCardinality of(int upperBound) { + return UpperCardinalities.atMost(upperBound); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityAddTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityAddTerm.java new file mode 100644 index 00000000..5c849b0a --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityAddTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class UpperCardinalityAddTerm extends UpperCardinalityBinaryTerm { + protected UpperCardinalityAddTerm(Term left, Term right) { + super(left, right); + } + + @Override + protected UpperCardinality doEvaluate(UpperCardinality leftValue, UpperCardinality rightValue) { + return leftValue.add(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new UpperCardinalityAddTerm(substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s + %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityBinaryTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityBinaryTerm.java new file mode 100644 index 00000000..976d6115 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityBinaryTerm.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.term.BinaryTerm; +import tools.refinery.logic.term.Term; + +public abstract class UpperCardinalityBinaryTerm extends BinaryTerm { + protected UpperCardinalityBinaryTerm(Term left, Term right) { + super(UpperCardinality.class, UpperCardinality.class, UpperCardinality.class, left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMaxTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMaxTerm.java new file mode 100644 index 00000000..0a0c5ee4 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMaxTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class UpperCardinalityMaxTerm extends UpperCardinalityBinaryTerm { + protected UpperCardinalityMaxTerm(Term left, Term right) { + super(left, right); + } + + @Override + protected UpperCardinality doEvaluate(UpperCardinality leftValue, UpperCardinality rightValue) { + return leftValue.max(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new UpperCardinalityMaxTerm(substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "max(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMinTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMinTerm.java new file mode 100644 index 00000000..d6879595 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMinTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class UpperCardinalityMinTerm extends UpperCardinalityBinaryTerm { + protected UpperCardinalityMinTerm(Term left, Term right) { + super(left, right); + } + + @Override + protected UpperCardinality doEvaluate(UpperCardinality leftValue, UpperCardinality rightValue) { + return leftValue.min(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new UpperCardinalityMinTerm(substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "min(%s, %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMulTerm.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMulTerm.java new file mode 100644 index 00000000..cb0d685f --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityMulTerm.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.Term; + +public class UpperCardinalityMulTerm extends UpperCardinalityBinaryTerm { + protected UpperCardinalityMulTerm(Term left, Term right) { + super(left, right); + } + + @Override + protected UpperCardinality doEvaluate(UpperCardinality leftValue, UpperCardinality rightValue) { + return leftValue.multiply(rightValue); + } + + @Override + public Term doSubstitute(Substitution substitution, Term substitutedLeft, Term substitutedRight) { + return new UpperCardinalityMulTerm(substitutedLeft, substitutedRight); + } + + @Override + public String toString() { + return "(%s * %s)".formatted(getLeft(), getRight()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregator.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregator.java new file mode 100644 index 00000000..7c45e6ef --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregator.java @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.term.StatefulAggregate; +import tools.refinery.logic.term.StatefulAggregator; + +// Singleton implementation, since there is only one way to aggregate upper cardinalities. +@SuppressWarnings("squid:S6548") +public class UpperCardinalitySumAggregator implements StatefulAggregator { + public static final UpperCardinalitySumAggregator INSTANCE = new UpperCardinalitySumAggregator(); + + private UpperCardinalitySumAggregator() { + } + + @Override + public Class getResultType() { + return UpperCardinality.class; + } + + @Override + public Class getInputType() { + return UpperCardinality.class; + } + + @Override + public StatefulAggregate createEmptyAggregate() { + return new Aggregate(); + } + + private static class Aggregate implements StatefulAggregate { + private int sumFiniteUpperBounds; + private int countUnbounded; + + public Aggregate() { + this(0, 0); + } + + private Aggregate(int sumFiniteUpperBounds, int countUnbounded) { + this.sumFiniteUpperBounds = sumFiniteUpperBounds; + this.countUnbounded = countUnbounded; + } + + @Override + public void add(UpperCardinality value) { + if (value instanceof FiniteUpperCardinality finiteUpperCardinality) { + sumFiniteUpperBounds += finiteUpperCardinality.finiteUpperBound(); + } else if (value instanceof UnboundedUpperCardinality) { + countUnbounded += 1; + } else { + throw new IllegalArgumentException("Unknown UpperCardinality: " + value); + } + } + + @Override + public void remove(UpperCardinality value) { + if (value instanceof FiniteUpperCardinality finiteUpperCardinality) { + sumFiniteUpperBounds -= finiteUpperCardinality.finiteUpperBound(); + } else if (value instanceof UnboundedUpperCardinality) { + countUnbounded -= 1; + } else { + throw new IllegalArgumentException("Unknown UpperCardinality: " + value); + } + } + + @Override + public UpperCardinality getResult() { + return countUnbounded > 0 ? UpperCardinalities.UNBOUNDED : UpperCardinalities.atMost(sumFiniteUpperBounds); + } + + @Override + public boolean isEmpty() { + return sumFiniteUpperBounds == 0 && countUnbounded == 0; + } + + @Override + public StatefulAggregate deepCopy() { + return new Aggregate(sumFiniteUpperBounds, countUnbounded); + } + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTerms.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTerms.java new file mode 100644 index 00000000..7ec61cac --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTerms.java @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import tools.refinery.logic.term.Aggregator; +import tools.refinery.logic.term.ConstantTerm; +import tools.refinery.logic.term.ExtremeValueAggregator; +import tools.refinery.logic.term.Term; +import tools.refinery.logic.term.comparable.*; + +import java.util.Comparator; + +public final class UpperCardinalityTerms { + public static final Aggregator UPPER_CARDINALITY_SUM = + UpperCardinalitySumAggregator.INSTANCE; + public static final Aggregator UPPER_CARDINALITY_MIN = + new ExtremeValueAggregator<>(UpperCardinality.class, UpperCardinalities.UNBOUNDED); + public static final Aggregator UPPER_CARDINALITY_MAX = + new ExtremeValueAggregator<>(UpperCardinality.class, UpperCardinalities.ZERO, Comparator.reverseOrder()); + + private UpperCardinalityTerms() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + public static Term constant(UpperCardinality value) { + return new ConstantTerm<>(UpperCardinality.class, value); + } + + public static Term add(Term left, Term right) { + return new UpperCardinalityAddTerm(left, right); + } + + public static Term mul(Term left, Term right) { + return new UpperCardinalityMulTerm(left, right); + } + + public static Term min(Term left, Term right) { + return new UpperCardinalityMinTerm(left, right); + } + + public static Term max(Term left, Term right) { + return new UpperCardinalityMaxTerm(left, right); + } + + public static Term eq(Term left, Term right) { + return new EqTerm<>(UpperCardinality.class, left, right); + } + + public static Term notEq(Term left, Term right) { + return new NotEqTerm<>(UpperCardinality.class, left, right); + } + + public static Term less(Term left, Term right) { + return new LessTerm<>(UpperCardinality.class, left, right); + } + + public static Term lessEq(Term left, Term right) { + return new LessEqTerm<>(UpperCardinality.class, left, right); + } + + public static Term greater(Term left, Term right) { + return new GreaterTerm<>(UpperCardinality.class, left, right); + } + + public static Term greaterEq(Term left, Term right) { + return new GreaterEqTerm<>(UpperCardinality.class, left, right); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/util/CycleDetectingMapper.java b/subprojects/logic/src/main/java/tools/refinery/logic/util/CycleDetectingMapper.java new file mode 100644 index 00000000..8a9efdd6 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/util/CycleDetectingMapper.java @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.util; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class CycleDetectingMapper { + private static final String SEPARATOR = " -> "; + + private final Function getName; + + private final Function doMap; + + private final Set inProgress = new LinkedHashSet<>(); + + private final Map results = new HashMap<>(); + + public CycleDetectingMapper(Function doMap) { + this(Objects::toString, doMap); + } + + public CycleDetectingMapper(Function getName, Function doMap) { + this.getName = getName; + this.doMap = doMap; + } + + public R map(T input) { + if (inProgress.contains(input)) { + var path = inProgress.stream().map(getName).collect(Collectors.joining(SEPARATOR)); + throw new IllegalArgumentException("Circular reference %s%s%s detected".formatted(path, SEPARATOR, + getName.apply(input))); + } + // We can't use computeIfAbsent here, because translating referenced queries calls this method in a reentrant + // way, which would cause a ConcurrentModificationException with computeIfAbsent. + @SuppressWarnings("squid:S3824") + var result = results.get(input); + if (result == null) { + inProgress.add(input); + try { + result = doMap.apply(input); + results.put(input, result); + } finally { + inProgress.remove(input); + } + } + return result; + } + + public List getInProgress() { + return List.copyOf(inProgress); + } + + public R getAlreadyMapped(T input) { + return results.get(input); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/valuation/MapBasedValuation.java b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/MapBasedValuation.java new file mode 100644 index 00000000..be1862d2 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/MapBasedValuation.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.valuation; + +import tools.refinery.logic.term.AnyDataVariable; +import tools.refinery.logic.term.DataVariable; + +import java.util.Map; + +record MapBasedValuation(Map values) implements Valuation { + @Override + public T getValue(DataVariable variable) { + if (!values.containsKey(variable)) { + throw new IllegalArgumentException("No value for variable %s".formatted(variable)); + } + var value = values.get(variable); + return variable.getType().cast(value); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/valuation/RestrictedValuation.java b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/RestrictedValuation.java new file mode 100644 index 00000000..b6ac7cc9 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/RestrictedValuation.java @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.valuation; + +import tools.refinery.logic.term.AnyDataVariable; +import tools.refinery.logic.term.DataVariable; + +import java.util.Set; + +public record RestrictedValuation(Valuation valuation, Set allowedVariables) implements Valuation { + @Override + public T getValue(DataVariable variable) { + if (!allowedVariables.contains(variable)) { + throw new IllegalArgumentException("Variable %s is not in scope".formatted(variable)); + } + return valuation.getValue(variable); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/valuation/SubstitutedValuation.java b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/SubstitutedValuation.java new file mode 100644 index 00000000..38491481 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/SubstitutedValuation.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.valuation; + +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.DataVariable; + +public record SubstitutedValuation(Valuation originalValuation, Substitution substitution) implements Valuation { + @Override + public T getValue(DataVariable variable) { + return originalValuation.getValue(substitution.getTypeSafeSubstitute(variable)); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/valuation/Valuation.java b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/Valuation.java new file mode 100644 index 00000000..95ee887e --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/Valuation.java @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.valuation; + +import org.jetbrains.annotations.Nullable; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.AnyDataVariable; +import tools.refinery.logic.term.DataVariable; + +import java.util.Map; +import java.util.Set; + +public interface Valuation { + T getValue(DataVariable variable); + + default Valuation substitute(@Nullable Substitution substitution) { + if (substitution == null) { + return this; + } + return new SubstitutedValuation(this, substitution); + } + + default Valuation restrict(Set allowedVariables) { + return new RestrictedValuation(this, Set.copyOf(allowedVariables)); + } + + static ValuationBuilder builder() { + return new ValuationBuilder(); + } + + static Valuation empty() { + return new MapBasedValuation(Map.of()); + } +} diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/valuation/ValuationBuilder.java b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/ValuationBuilder.java new file mode 100644 index 00000000..46459ac7 --- /dev/null +++ b/subprojects/logic/src/main/java/tools/refinery/logic/valuation/ValuationBuilder.java @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.valuation; + +import tools.refinery.logic.term.AnyDataVariable; +import tools.refinery.logic.term.DataVariable; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class ValuationBuilder { + private final Map values = new HashMap<>(); + + ValuationBuilder() { + } + + public ValuationBuilder put(DataVariable variable, T value) { + return putChecked(variable, value); + } + + public ValuationBuilder putChecked(AnyDataVariable variable, Object value) { + if (value != null && !variable.getType().isInstance(value)) { + throw new IllegalArgumentException("Value %s is not an instance of %s" + .formatted(value, variable.getType().getName())); + } + if (values.containsKey(variable)) { + throw new IllegalArgumentException("Already has value for variable %s".formatted(variable)); + } + values.put(variable, value); + return this; + } + + public Valuation build() { + return new MapBasedValuation(Collections.unmodifiableMap(values)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderLiteralEliminationTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderLiteralEliminationTest.java new file mode 100644 index 00000000..d5a9ccad --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderLiteralEliminationTest.java @@ -0,0 +1,257 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.literal.BooleanLiteral; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.term.bool.BoolTerms; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static tools.refinery.logic.literal.Literals.check; +import static tools.refinery.logic.literal.Literals.not; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class DnfBuilderLiteralEliminationTest { + private final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private final NodeVariable p = Variable.of("p"); + private final NodeVariable q = Variable.of("q"); + private final Dnf trueDnf = Dnf.builder().parameter(p, ParameterDirection.IN).clause().build(); + private final Dnf falseDnf = Dnf.builder().parameter(p).build(); + + @Test + void eliminateTrueTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(BooleanLiteral.TRUE, friendView.call(p, q)) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateTrueAssumptionTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(check(BoolTerms.constant(true)), friendView.call(p, q)) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateFalseTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p), BooleanLiteral.FALSE) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @ParameterizedTest + @CsvSource(value = { + "false", + "null" + }, nullValues = "null") + void eliminateFalseAssumptionTest(Boolean value) { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p), check(BoolTerms.constant(value))) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysTrueTest() { + var actual = Dnf.builder() + .parameters(List.of(p, q), ParameterDirection.IN) + .clause(friendView.call(p, q)) + .clause(BooleanLiteral.TRUE) + .build(); + var expected = Dnf.builder().parameters(List.of(p, q), ParameterDirection.IN).clause().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysFalseTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q), BooleanLiteral.FALSE) + .build(); + var expected = Dnf.builder().parameters(p, q).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateTrueDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(trueDnf.call(q), friendView.call(p, q)) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateFalseDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p), falseDnf.call(q)) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysTrueDnfTest() { + var actual = Dnf.builder() + .parameters(List.of(p, q), ParameterDirection.IN) + .clause(friendView.call(p, q)) + .clause(trueDnf.call(q)) + .build(); + var expected = Dnf.builder().parameters(List.of(p, q), ParameterDirection.IN).clause().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysFalseDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q), falseDnf.call(q)) + .build(); + var expected = Dnf.builder().parameters(p, q).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateNotFalseDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(not(falseDnf.call(q)), friendView.call(p, q)) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void eliminateNotTrueDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p), not(trueDnf.call(q))) + .build(); + var expected = Dnf.builder().parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysNotFalseDnfTest() { + var actual = Dnf.builder() + .parameters(List.of(p, q), ParameterDirection.IN) + .clause(friendView.call(p, q)) + .clause(not(falseDnf.call(q))) + .build(); + var expected = Dnf.builder().parameters(List.of(p, q), ParameterDirection.IN).clause().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void alwaysNotTrueDnfTest() { + var actual = Dnf.builder() + .parameters(p, q) + .clause(friendView.call(p, q), not(trueDnf.call(q))) + .build(); + var expected = Dnf.builder().parameters(p, q).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void removeDuplicateTest() { + var actual = Dnf.of(builder -> builder.clause((p, q) -> List.of( + friendView.call(p, q), + friendView.call(p, q) + ))); + var expected = Dnf.of(builder -> builder.clause((p, q) -> List.of(friendView.call(p, q)))); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void removeContradictoryTest() { + var actual = Dnf.of(builder -> builder.clause((p, q) -> List.of( + friendView.call(p, q), + not(friendView.call(p, q)) + ))); + var expected = Dnf.builder().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void removeContradictoryUniversalTest() { + var actual = Dnf.of(builder -> builder.clause((p, q) -> List.of( + friendView.call(q, q), + friendView.call(p, q), + not(friendView.call(p, Variable.of())) + ))); + var expected = Dnf.builder().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void removeContradictoryExistentialUniversalTest() { + var actual = Dnf.of(builder -> builder.clause((p) -> List.of( + friendView.call(p, Variable.of()), + not(friendView.call(p, Variable.of())) + ))); + var expected = Dnf.builder().build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void removeContradictoryUniversalParameterTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + builder.clause((q) -> List.of( + friendView.call(q, q), + friendView.call(p, q), + not(friendView.call(p, Variable.of())) + )); + }); + var expected = Dnf.builder().parameter(p).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderVariableUnificationTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderVariableUnificationTest.java new file mode 100644 index 00000000..0e1f77e2 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfBuilderVariableUnificationTest.java @@ -0,0 +1,322 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class DnfBuilderVariableUnificationTest { + private final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private final Constraint childrenView = new FakeKeyOnlyView("children", 2); + + @Test + void equalToParameterTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + builder.clause(q -> List.of( + friendView.call(p, q), + p.isEquivalent(q) + )); + }); + + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(expectedP, ParameterDirection.OUT)), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); + } + + @Test + void equalToParameterReverseTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + builder.clause(q -> List.of( + friendView.call(p, q), + q.isEquivalent(p) + )); + }); + + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(expectedP, ParameterDirection.OUT)), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); + } + + @Test + void equalQuantifiedTest() { + var actual = Dnf.of(builder -> builder.clause((p, q) -> List.of( + friendView.call(p, q), + p.isEquivalent(q) + ))); + + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); + } + + @Test + void equalQuantifiedTransitiveTest() { + var actual = Dnf.of(builder -> builder.clause((p, q, r) -> List.of( + friendView.call(p, q), + p.isEquivalent(q), + childrenView.call(p, r), + q.isEquivalent(r) + ))); + + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP), childrenView.call(expectedP, expectedP)) + ) + )); + } + + @Test + void equalQuantifiedTransitiveRemoveDuplicateTest() { + var actual = Dnf.of(builder -> builder.clause((p, q, r) -> List.of( + friendView.call(p, q), + p.isEquivalent(q), + friendView.call(p, r), + q.isEquivalent(r) + ))); + + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); + } + + @Test + void parametersEqualTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause( + friendView.call(p, q), + p.isEquivalent(q) + ); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(expectedP, expectedP), expectedQ.isEquivalent(expectedP)) + ) + )); + } + + @Test + void parametersEqualTransitiveTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + var r = builder.parameter("r"); + builder.clause( + friendView.call(p, q), + childrenView.call(p, r), + p.isEquivalent(q), + r.isEquivalent(q) + ); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + var expectedR = Variable.of("r"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT), + new SymbolicParameter(expectedR, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + expectedR.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } + + @Test + void parameterAndQuantifiedEqualsTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause((r) -> List.of( + friendView.call(p, r), + p.isEquivalent(r), + childrenView.call(q, r), + q.isEquivalent(r) + )); + }); + + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } + + @Test + void parameterAndQuantifiedEqualsReverseFirstTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause((r) -> List.of( + friendView.call(p, r), + r.isEquivalent(p), + childrenView.call(q, r), + q.isEquivalent(r) + )); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } + + @Test + void parameterAndQuantifiedEqualsReverseSecondTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause((r) -> List.of( + friendView.call(p, r), + p.isEquivalent(r), + childrenView.call(q, r), + r.isEquivalent(q) + )); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } + + @Test + void parameterAndQuantifiedEqualsReverseBoth() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause((r) -> List.of( + friendView.call(p, r), + p.isEquivalent(r), + childrenView.call(q, r), + r.isEquivalent(q) + )); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } + + @Test + void parameterAndTwoQuantifiedEqualsTest() { + var actual = Dnf.of(builder -> { + var p = builder.parameter("p"); + var q = builder.parameter("q"); + builder.clause((r, s) -> List.of( + r.isEquivalent(s), + friendView.call(p, r), + p.isEquivalent(r), + childrenView.call(q, s), + q.isEquivalent(s) + )); + }); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfToDefinitionStringTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfToDefinitionStringTest.java new file mode 100644 index 00000000..dd624548 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/DnfToDefinitionStringTest.java @@ -0,0 +1,154 @@ +/* + * SPDX-FileCopyrightText: 2021-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static tools.refinery.logic.literal.Literals.not; + +class DnfToDefinitionStringTest { + private static final Constraint personView = new FakeKeyOnlyView("person", 1); + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + + @Test + void noClausesTest() { + var dnf = Dnf.builder("Example").parameter(p).build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + . + """)); + } + + @Test + void noParametersTest() { + var dnf = Dnf.builder("Example").build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example() <-> + . + """)); + } + + @Test + void emptyClauseTest() { + var dnf = Dnf.builder("Example").parameter(p, ParameterDirection.IN).clause().build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(in p) <-> + . + """)); + } + + @Test + void relationViewPositiveTest() { + var dnf = Dnf.builder("Example").parameter(p).clause(friendView.call(p, q)).build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + friend(p, q). + """)); + } + + @Test + void relationViewNegativeTest() { + var dnf = Dnf.builder("Example") + .parameter(p, ParameterDirection.IN) + .clause(not(friendView.call(p, q))) + .build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(in p) <-> + !(friend(p, q)). + """)); + } + + @Test + void relationViewTransitiveTest() { + var dnf = Dnf.builder("Example").parameter(p).clause(friendView.callTransitive(p, q)).build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + friend+(p, q). + """)); + } + + @Test + void multipleParametersTest() { + var dnf = Dnf.builder("Example").parameters(p, q).clause(friendView.call(p, q)).build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p, q) <-> + friend(p, q). + """)); + } + + @Test + void multipleLiteralsTest() { + var dnf = Dnf.builder("Example") + .parameter(p) + .clause( + personView.call(p), + personView.call(q), + friendView.call(p, q) + ) + .build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + person(p), + person(q), + friend(p, q). + """)); + } + + @Test + void multipleClausesTest() { + var dnf = Dnf.builder("Example") + .parameter(p) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p)) + .build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + friend(p, q) + ; + friend(q, p). + """)); + } + + @Test + void dnfTest() { + var r = Variable.of("r"); + var s = Variable.of("s"); + var called = Dnf.builder("Called").parameters(r, s).clause(friendView.call(r, s)).build(); + var dnf = Dnf.builder("Example") + .parameter(p) + .clause( + personView.call(p), + personView.call(q), + not(called.call(p, q)) + ) + .build(); + + assertThat(dnf.toDefinitionString(), is(""" + pred Example(p) <-> + person(p), + person(q), + !(@Dnf Called(p, q)). + """)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/HashCodeTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/HashCodeTest.java new file mode 100644 index 00000000..e140be1e --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/HashCodeTest.java @@ -0,0 +1,64 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +class HashCodeTest { + private static final Constraint personView = new FakeKeyOnlyView("Person", 1); + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + + @Test + void flatEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause(personView.call(q)).build(); + var actual = Dnf.builder("Actual").parameters(p).clause(personView.call(p)).build(); + + assertThat(actual.hashCodeWithSubstitution(), is(expected.hashCodeWithSubstitution())); + } + + @Test + void flatNotEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause(friendView.call(q, q)).build(); + var actual = Dnf.builder("Actual").parameters(p).clause(friendView.call(p, q)).build(); + + assertThat(actual.hashCodeWithSubstitution(), not(expected.hashCodeWithSubstitution())); + } + + @Test + void deepEqualsTest() { + var expected2 = Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build(); + var expected = Dnf.builder("Expected").parameters(q).clause( + expected2.call(q) + ).build(); + var actual = Dnf.builder("Actual").parameters(q).clause( + expected2.call(q) + ).build(); + + assertThat(actual.hashCodeWithSubstitution(), is(expected.hashCodeWithSubstitution())); + } + + @Test + void deepNotEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause( + Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + var actual = Dnf.builder("Actual").parameters(q).clause( + Dnf.builder("Actual2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + + assertThat(actual.hashCodeWithSubstitution(), not(expected.hashCodeWithSubstitution())); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/TopologicalSortTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/TopologicalSortTest.java new file mode 100644 index 00000000..8ea27cc9 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/TopologicalSortTest.java @@ -0,0 +1,111 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.logic.literal.Literals.not; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class TopologicalSortTest { + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final Dnf example = Dnf.of("example", builder -> { + var a = builder.parameter("a", ParameterDirection.IN); + var b = builder.parameter("b", ParameterDirection.IN); + var c = builder.parameter("c", ParameterDirection.OUT); + var d = builder.parameter("d", ParameterDirection.OUT); + builder.clause( + friendView.call(a, b), + friendView.call(b, c), + friendView.call(c, d) + ); + }); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + private static final NodeVariable r = Variable.of("r"); + private static final NodeVariable s = Variable.of("s"); + private static final NodeVariable t = Variable.of("t"); + + @Test + void topologicalSortTest() { + var actual = Dnf.builder("Actual") + .parameter(p, ParameterDirection.IN) + .parameter(q, ParameterDirection.OUT) + .clause( + not(friendView.call(p, q)), + example.call(p, q, r, s), + example.call(r, t, q, s), + friendView.call(r, t) + ) + .build(); + + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(p, ParameterDirection.IN), + new SymbolicParameter(q, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(r, t), + example.call(r, t, q, s), + not(friendView.call(p, q)), + example.call(p, q, r, s) + ) + ) + )); + } + + @Test + void missingInputTest() { + var builder = Dnf.builder("Actual") + .parameter(p, ParameterDirection.OUT) + .parameter(q, ParameterDirection.OUT) + .clause( + not(friendView.call(p, q)), + example.call(p, q, r, s), + example.call(r, t, q, s), + friendView.call(r, t) + ); + assertThrows(InvalidQueryException.class, builder::build); + } + + @Test + void missingVariableTest() { + var builder = Dnf.builder("Actual") + .parameter(p, ParameterDirection.IN) + .parameter(q, ParameterDirection.OUT) + .clause( + not(friendView.call(p, q)), + example.call(p, q, r, s), + example.call(r, t, q, s) + ); + assertThrows(InvalidQueryException.class, builder::build); + } + + @Test + void circularDependencyTest() { + var builder = Dnf.builder("Actual") + .parameter(p, ParameterDirection.IN) + .parameter(q, ParameterDirection.OUT) + .clause( + not(friendView.call(p, q)), + example.call(p, q, r, s), + example.call(r, t, q, s), + example.call(p, q, r, t) + ); + assertThrows(InvalidQueryException.class, builder::build); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/dnf/VariableDirectionTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/VariableDirectionTest.java new file mode 100644 index 00000000..f9f39b8a --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/dnf/VariableDirectionTest.java @@ -0,0 +1,247 @@ +/* + * SPDX-FileCopyrightText: 2023-2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.dnf; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.literal.BooleanLiteral; +import tools.refinery.logic.literal.Literal; +import tools.refinery.logic.term.DataVariable; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeFunctionView; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.logic.literal.Literals.not; +import static tools.refinery.logic.term.int_.IntTerms.INT_SUM; + +class VariableDirectionTest { + private static final Constraint personView = new FakeKeyOnlyView("Person", 1); + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final FakeFunctionView ageView = new FakeFunctionView<>("age", 1, Integer.class); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + private static final DataVariable x = Variable.of("x", Integer.class); + private static final DataVariable y = Variable.of("y", Integer.class); + private static final DataVariable z = Variable.of("z", Integer.class); + + @ParameterizedTest + @MethodSource("clausesWithVariableInput") + void unboundOutVariableTest(List clause) { + var builder = Dnf.builder().parameter(p, ParameterDirection.OUT).clause(clause); + assertThrows(InvalidClauseException.class, builder::build); + } + + @ParameterizedTest + @MethodSource("clausesWithVariableInput") + void unboundInVariableTest(List clause) { + var builder = Dnf.builder().parameter(p, ParameterDirection.IN).clause(clause); + var dnf = assertDoesNotThrow(builder::build); + var clauses = dnf.getClauses(); + if (!clauses.isEmpty()) { + assertThat(clauses.getFirst().positiveVariables(), hasItem(p)); + } + } + + @ParameterizedTest + @MethodSource("clausesWithVariableInput") + void boundPrivateVariableTest(List clause) { + var clauseWithBinding = new ArrayList(clause); + clauseWithBinding.add(personView.call(p)); + var builder = Dnf.builder().clause(clauseWithBinding); + var dnf = assertDoesNotThrow(builder::build); + var clauses = dnf.getClauses(); + if (!clauses.isEmpty()) { + assertThat(clauses.getFirst().positiveVariables(), hasItem(p)); + } + } + + static Stream clausesWithVariableInput() { + return Stream.concat( + clausesNotBindingVariable(), + literalToClauseArgumentStream(literalsWithRequiredVariableInput()) + ); + } + + @ParameterizedTest + @MethodSource("clausesNotBindingVariable") + void unboundPrivateVariableTest(List clause) { + var builder = Dnf.builder().clause(clause); + var dnf = assertDoesNotThrow(builder::build); + var clauses = dnf.getClauses(); + if (!clauses.isEmpty()) { + assertThat(clauses.getFirst().positiveVariables(), not(hasItem(p))); + } + } + + @ParameterizedTest + @MethodSource("clausesNotBindingVariable") + void unboundByEquivalencePrivateVariableTest(List clause) { + var r = Variable.of("r"); + var clauseWithEquivalence = new ArrayList(clause); + clauseWithEquivalence.add(r.isEquivalent(p)); + var builder = Dnf.builder().clause(clauseWithEquivalence); + assertThrows(InvalidClauseException.class, builder::build); + } + + static Stream clausesNotBindingVariable() { + return Stream.concat( + Stream.of( + Arguments.of(List.of()), + Arguments.of(List.of(BooleanLiteral.TRUE)), + Arguments.of(List.of(BooleanLiteral.FALSE)) + ), + literalToClauseArgumentStream(literalsWithPrivateVariable()) + ); + } + + @ParameterizedTest + @MethodSource("literalsWithPrivateVariable") + void unboundTwicePrivateVariableTest(Literal literal) { + var builder = Dnf.builder().clause(not(personView.call(p)), literal); + assertThrows(InvalidClauseException.class, builder::build); + } + + @ParameterizedTest + @MethodSource("literalsWithPrivateVariable") + void unboundTwiceByEquivalencePrivateVariableTest(Literal literal) { + var r = Variable.of("r"); + var builder = Dnf.builder().clause(not(personView.call(r)), r.isEquivalent(p), literal); + assertThrows(InvalidClauseException.class, builder::build); + } + + static Stream literalsWithPrivateVariable() { + var dnfWithOutput = Dnf.builder("WithOutput") + .parameter(p, ParameterDirection.OUT) + .parameter(q, ParameterDirection.OUT) + .clause(friendView.call(p, q)) + .build(); + var dnfWithOutputToAggregate = Dnf.builder("WithOutputToAggregate") + .parameter(p, ParameterDirection.OUT) + .parameter(q, ParameterDirection.OUT) + .parameter(x, ParameterDirection.OUT) + .clause( + friendView.call(p, q), + ageView.call(q, x) + ) + .build(); + + return Stream.of( + Arguments.of(not(friendView.call(p, q))), + Arguments.of(y.assign(friendView.count(p, q))), + Arguments.of(y.assign(ageView.aggregate(INT_SUM, p))), + Arguments.of(not(dnfWithOutput.call(p, q))), + Arguments.of(y.assign(dnfWithOutput.count(p, q))), + Arguments.of(y.assign(dnfWithOutputToAggregate.aggregateBy(z, INT_SUM, p, q, z))) + ); + } + + @ParameterizedTest + @MethodSource("literalsWithRequiredVariableInput") + void unboundPrivateVariableTest(Literal literal) { + var builder = Dnf.builder().clause(literal); + assertThrows(InvalidClauseException.class, builder::build); + } + + @ParameterizedTest + @MethodSource("literalsWithRequiredVariableInput") + void boundPrivateVariableInputTest(Literal literal) { + var builder = Dnf.builder().clause(personView.call(p), literal); + var dnf = assertDoesNotThrow(builder::build); + assertThat(dnf.getClauses().getFirst().positiveVariables(), hasItem(p)); + } + + static Stream literalsWithRequiredVariableInput() { + var dnfWithInput = Dnf.builder("WithInput") + .parameter(p, ParameterDirection.IN) + .parameter(q, ParameterDirection.OUT) + .clause(friendView.call(p, q)).build(); + var dnfWithInputToAggregate = Dnf.builder("WithInputToAggregate") + .parameter(p, ParameterDirection.IN) + .parameter(q, ParameterDirection.OUT) + .parameter(x, ParameterDirection.OUT) + .clause( + friendView.call(p, q), + ageView.call(q, x) + ).build(); + + return Stream.of( + Arguments.of(dnfWithInput.call(p, q)), + Arguments.of(dnfWithInput.call(p, p)), + Arguments.of(not(dnfWithInput.call(p, q))), + Arguments.of(not(dnfWithInput.call(p, p))), + Arguments.of(y.assign(dnfWithInput.count(p, q))), + Arguments.of(y.assign(dnfWithInput.count(p, p))), + Arguments.of(y.assign(dnfWithInputToAggregate.aggregateBy(z, INT_SUM, p, q, z))), + Arguments.of(y.assign(dnfWithInputToAggregate.aggregateBy(z, INT_SUM, p, p, z))) + ); + } + + @ParameterizedTest + @MethodSource("literalsWithVariableOutput") + void boundParameterTest(Literal literal) { + var builder = Dnf.builder().parameter(p, ParameterDirection.OUT).clause(literal); + var dnf = assertDoesNotThrow(builder::build); + assertThat(dnf.getClauses().getFirst().positiveVariables(), hasItem(p)); + } + + @ParameterizedTest + @MethodSource("literalsWithVariableOutput") + void boundTwiceParameterTest(Literal literal) { + var builder = Dnf.builder().parameter(p, ParameterDirection.IN).clause(literal); + var dnf = assertDoesNotThrow(builder::build); + assertThat(dnf.getClauses().getFirst().positiveVariables(), hasItem(p)); + } + + @ParameterizedTest + @MethodSource("literalsWithVariableOutput") + void boundPrivateVariableOutputTest(Literal literal) { + var dnfWithInput = Dnf.builder("WithInput") + .parameter(p, ParameterDirection.IN) + .clause(personView.call(p)) + .build(); + var builder = Dnf.builder().clause(dnfWithInput.call(p), literal); + var dnf = assertDoesNotThrow(builder::build); + assertThat(dnf.getClauses().getFirst().positiveVariables(), hasItem(p)); + } + + @ParameterizedTest + @MethodSource("literalsWithVariableOutput") + void boundTwicePrivateVariableOutputTest(Literal literal) { + var builder = Dnf.builder().clause(personView.call(p), literal); + var dnf = assertDoesNotThrow(builder::build); + assertThat(dnf.getClauses().getFirst().positiveVariables(), hasItem(p)); + } + + static Stream literalsWithVariableOutput() { + var dnfWithOutput = Dnf.builder("WithOutput") + .parameter(p, ParameterDirection.OUT) + .parameter(q, ParameterDirection.OUT) + .clause(friendView.call(p, q)) + .build(); + + return Stream.of( + Arguments.of(friendView.call(p, q)), + Arguments.of(dnfWithOutput.call(p, q)) + ); + } + + private static Stream literalToClauseArgumentStream(Stream literalArgumentsStream) { + return literalArgumentsStream.map(arguments -> Arguments.of(List.of(arguments.get()[0]))); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/literal/AggregationLiteralTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/literal/AggregationLiteralTest.java new file mode 100644 index 00000000..76639e18 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/literal/AggregationLiteralTest.java @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.InvalidQueryException; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.InvalidClauseException; +import tools.refinery.logic.term.*; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.logic.literal.Literals.not; +import static tools.refinery.logic.term.int_.IntTerms.INT_SUM; +import static tools.refinery.logic.term.int_.IntTerms.constant; + +class AggregationLiteralTest { + private static final NodeVariable p = Variable.of("p"); + private static final DataVariable x = Variable.of("x", Integer.class); + private static final DataVariable y = Variable.of("y", Integer.class); + private static final DataVariable z = Variable.of("z", Integer.class); + private static final Constraint fakeConstraint = new Constraint() { + @Override + public String name() { + return getClass().getName(); + } + + @Override + public List getParameters() { + return List.of( + new Parameter(null, ParameterDirection.OUT), + new Parameter(Integer.class, ParameterDirection.OUT) + ); + } + }; + + @Test + void parameterDirectionTest() { + var literal = x.assign(fakeConstraint.aggregateBy(y, INT_SUM, p, y)); + assertAll( + () -> assertThat(literal.getOutputVariables(), containsInAnyOrder(x)), + () -> assertThat(literal.getInputVariables(Set.of()), empty()), + () -> assertThat(literal.getInputVariables(Set.of(p)), containsInAnyOrder(p)), + () -> assertThat(literal.getPrivateVariables(Set.of()), containsInAnyOrder(p, y)), + () -> assertThat(literal.getPrivateVariables(Set.of(p)), containsInAnyOrder(y)) + ); + } + + @Test + void missingAggregationVariableTest() { + var aggregation = fakeConstraint.aggregateBy(y, INT_SUM, p, z); + assertThrows(InvalidQueryException.class, () -> x.assign(aggregation)); + } + + @Test + void circularAggregationVariableTest() { + var aggregation = fakeConstraint.aggregateBy(x, INT_SUM, p, x); + assertThrows(InvalidQueryException.class, () -> x.assign(aggregation)); + } + + @Test + void unboundTwiceVariableTest() { + var builder = Dnf.builder() + .clause( + not(fakeConstraint.call(p, y)), + x.assign(fakeConstraint.aggregateBy(y, INT_SUM, p, y)) + ); + assertThrows(InvalidClauseException.class, builder::build); + } + + @Test + void unboundBoundVariableTest() { + var builder = Dnf.builder() + .clause( + y.assign(constant(27)), + x.assign(fakeConstraint.aggregateBy(y, INT_SUM, p, y)) + ); + assertThrows(InvalidClauseException.class, builder::build); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/literal/CallLiteralTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/literal/CallLiteralTest.java new file mode 100644 index 00000000..0fb2e7c9 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/literal/CallLiteralTest.java @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.literal; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.Parameter; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.List; +import java.util.Set; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.junit.jupiter.api.Assertions.assertAll; +import static tools.refinery.logic.literal.Literals.not; + +class CallLiteralTest { + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + private static final NodeVariable r = Variable.of("r"); + private static final NodeVariable s = Variable.of("s"); + + private static final Constraint fakeConstraint = new Constraint() { + @Override + public String name() { + return getClass().getName(); + } + + @Override + public List getParameters() { + return List.of( + new Parameter(null, ParameterDirection.IN), + new Parameter(null, ParameterDirection.IN), + new Parameter(null, ParameterDirection.OUT), + new Parameter(null, ParameterDirection.OUT) + ); + } + }; + + @Test + void notRepeatedPositiveDirectionTest() { + var literal = fakeConstraint.call(p, q, r, s); + assertAll( + () -> assertThat(literal.getOutputVariables(), containsInAnyOrder(r, s)), + () -> assertThat(literal.getInputVariables(Set.of()), containsInAnyOrder(p, q)), + () -> assertThat(literal.getInputVariables(Set.of(p, q, r)), containsInAnyOrder(p, q)), + () -> assertThat(literal.getPrivateVariables(Set.of()), empty()), + () -> assertThat(literal.getPrivateVariables(Set.of(p, q, r)), empty()) + ); + } + + @Test + void notRepeatedNegativeDirectionTest() { + var literal = not(fakeConstraint.call(p, q, r, s)); + assertAll( + () -> assertThat(literal.getOutputVariables(), empty()), + () -> assertThat(literal.getInputVariables(Set.of()), containsInAnyOrder(p, q)), + () -> assertThat(literal.getInputVariables(Set.of(p, q, r)), containsInAnyOrder(p, q, r)), + () -> assertThat(literal.getPrivateVariables(Set.of()), containsInAnyOrder(r, s)), + () -> assertThat(literal.getPrivateVariables(Set.of(p, q, r)), containsInAnyOrder(s)) + ); + } + + @Test + void repeatedPositiveDirectionTest() { + var literal = fakeConstraint.call(p, p, q, q); + assertAll( + () -> assertThat(literal.getOutputVariables(), containsInAnyOrder(q)), + () -> assertThat(literal.getInputVariables(Set.of()), containsInAnyOrder(p)), + () -> assertThat(literal.getInputVariables(Set.of(p, q)), containsInAnyOrder(p)), + () -> assertThat(literal.getPrivateVariables(Set.of()), empty()), + () -> assertThat(literal.getPrivateVariables(Set.of(p, q)), empty()) + ); + } + + @Test + void repeatedNegativeDirectionTest() { + var literal = not(fakeConstraint.call(p, p, q, q)); + assertAll( + () -> assertThat(literal.getOutputVariables(), empty()), + () -> assertThat(literal.getInputVariables(Set.of()), containsInAnyOrder(p)), + () -> assertThat(literal.getInputVariables(Set.of(p, q)), containsInAnyOrder(p, q)), + () -> assertThat(literal.getPrivateVariables(Set.of()), containsInAnyOrder(q)), + () -> assertThat(literal.getPrivateVariables(Set.of(p, q)), empty()) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/DuplicateDnfRemoverTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/DuplicateDnfRemoverTest.java new file mode 100644 index 00000000..7b2ce8b2 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/DuplicateDnfRemoverTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.dnf.Query; +import tools.refinery.logic.literal.AbstractCallLiteral; +import tools.refinery.logic.literal.Reduction; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static tools.refinery.logic.literal.Literals.not; + +class DuplicateDnfRemoverTest { + private final static Constraint friendView = new FakeKeyOnlyView("friend", 2); + + private DuplicateDnfRemover sut; + + @BeforeEach + void beforeEach() { + sut = new DuplicateDnfRemover(); + } + + @Test + void removeDuplicateSimpleTest() { + var one = Query.of("One", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var two = Query.of("Two", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + + var oneResult = sut.rewrite(one); + var twoResult = sut.rewrite(two); + + assertThat(oneResult, is(twoResult)); + assertThat(one, is(oneResult)); + } + + @Test + void notDuplicateSimpleTest() { + var one = Query.of("One", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var two = Query.of("Two", (builder, x, y) -> builder.clause((z) -> List.of( + friendView.call(x, y), + friendView.call(y, z) + ))); + + var oneResult = sut.rewrite(one); + var twoResult = sut.rewrite(two); + + assertThat(one, is(oneResult)); + assertThat(two, is(twoResult)); + } + + @Test + void removeDuplicateRecursiveTest() { + var oneSubQuery = Query.of("OneSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var one = Query.of("One", (builder, x) -> builder.clause( + oneSubQuery.call(x, Variable.of()) + )); + var twoSubQuery = Query.of("TwoSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var two = Query.of("Two", (builder, x) -> builder.clause( + twoSubQuery.call(x, Variable.of()) + )); + + var oneResult = sut.rewrite(one); + var twoResult = sut.rewrite(two); + + assertThat(oneResult, is(twoResult)); + assertThat(one, is(oneResult)); + } + + @Test + void notDuplicateRecursiveTest() { + var oneSubQuery = Query.of("OneSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var one = Query.of("One", (builder, x) -> builder.clause( + oneSubQuery.call(x, Variable.of()) + )); + var twoSubQuery = Query.of("TwoSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var two = Query.of("Two", (builder, x) -> builder.clause( + twoSubQuery.call(Variable.of(), x) + )); + + var oneResult = sut.rewrite(one); + var twoResult = sut.rewrite(two); + + assertThat(one, is(oneResult)); + assertThat(oneResult, is(not(twoResult))); + + var oneCall = (AbstractCallLiteral) oneResult.getDnf().getClauses().getFirst().literals().getFirst(); + var twoCall = (AbstractCallLiteral) twoResult.getDnf().getClauses().getFirst().literals().getFirst(); + + assertThat(oneCall.getTarget(), is(twoCall.getTarget())); + } + + @Test + void removeContradictionTest() { + var oneSubQuery = Query.of("OneSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var twoSubQuery = Query.of("TwoSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var query = Query.of("Contradiction", (builder, x, y) -> builder.clause( + oneSubQuery.call(x, y), + not(twoSubQuery.call(x, y)) + )); + + var result = sut.rewrite(query); + + assertThat(result.getDnf().getReduction(), is(Reduction.ALWAYS_FALSE)); + } + + @Test + void removeQuantifiedContradictionTest() { + var oneSubQuery = Query.of("OneSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var twoSubQuery = Query.of("TwoSubQuery", (builder, x, y) -> builder.clause( + friendView.call(x, y), + friendView.call(y, x) + )); + var query = Query.of("Contradiction", (builder, x) -> builder.clause( + oneSubQuery.call(x, Variable.of()), + not(twoSubQuery.call(x, Variable.of())) + )); + + var result = sut.rewrite(query); + + assertThat(result.getDnf().getReduction(), is(Reduction.ALWAYS_FALSE)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/InputParameterResolverTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/InputParameterResolverTest.java new file mode 100644 index 00000000..5e5fdb64 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/rewriter/InputParameterResolverTest.java @@ -0,0 +1,225 @@ +/* + * SPDX-FileCopyrightText: 2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.rewriter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.Query; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; +import tools.refinery.logic.tests.FakeKeyOnlyView; + +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static tools.refinery.logic.literal.Literals.not; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class InputParameterResolverTest { + private final static Constraint personView = new FakeKeyOnlyView("Person", 1); + private final static Constraint friendView = new FakeKeyOnlyView("friend", 2); + + private InputParameterResolver sut; + + @BeforeEach + void beforeEach() { + sut = new InputParameterResolver(); + } + + @Test + void inlineSingleClauseTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + builder.clause(friendView.call(x, Variable.of())); + }); + var query = Query.of("Actual", (builder, x) -> builder.clause( + dnf.call(x), + personView.call(x) + )); + + var actual = sut.rewrite(query); + + var expected = Query.of("Expected", (builder, x) -> builder.clause( + friendView.call(x, Variable.of()), + personView.call(x) + )); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void inlineSingleClauseWIthInputTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.IN); + builder.clause(not(friendView.call(x, Variable.of()))); + }); + var query = Query.of("Actual", (builder, x) -> builder.clause( + dnf.call(x), + personView.call(x) + )); + + var actual = sut.rewrite(query); + + var expected = Query.of("Expected", (builder, x) -> builder.clause( + personView.call(x), + not(friendView.call(x, Variable.of())) + )); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void singleLiteralDemandSetTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.IN); + builder.clause(not(friendView.call(x, Variable.of()))); + builder.clause(not(friendView.call(Variable.of(), x))); + }); + var query = Query.of("Actual", (builder, x) -> builder.clause( + dnf.call(x), + personView.call(x) + )); + + var actual = sut.rewrite(query); + + var expectedSubQuery = Dnf.of("ExpectedSubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + builder.clause( + personView.call(x), + not(friendView.call(x, Variable.of())) + ); + builder.clause( + personView.call(x), + not(friendView.call(Variable.of(), x)) + ); + }); + var expected = Query.of("Expected", (builder, x) -> builder.clause( + personView.call(x), + expectedSubQuery.call(x) + )); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void multipleLiteralDemandSetTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.IN); + var y = builder.parameter("y", ParameterDirection.IN); + builder.clause(not(friendView.call(x, y))); + builder.clause(not(friendView.call(y, x))); + }); + var query = Query.of("Actual", (builder, p1) -> builder.clause(p2 -> List.of( + not(dnf.call(p1, p2)), + personView.call(p1), + personView.call(p2) + ))); + + var actual = sut.rewrite(query); + + var context = Dnf.of("Context", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + var y = builder.parameter("y", ParameterDirection.OUT); + builder.clause( + personView.call(x), + personView.call(y) + ); + }); + var expectedSubQuery = Dnf.of("ExpectedSubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + var y = builder.parameter("x", ParameterDirection.OUT); + builder.clause( + context.call(x, y), + not(friendView.call(x, y)) + ); + builder.clause( + context.call(x, y), + not(friendView.call(y, x)) + ); + }); + var expected = Query.of("Expected", (builder, p1) -> builder.clause(p2 -> List.of( + context.call(p1, p2), + not(expectedSubQuery.call(p1, p2)) + ))); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void multipleParameterDemandSetTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.IN); + var y = builder.parameter("y", ParameterDirection.IN); + builder.clause(not(friendView.call(x, y))); + builder.clause(not(friendView.call(y, x))); + }); + var query = Query.of("Actual", (builder, p1) -> builder.clause( + not(dnf.call(p1, p1)), + personView.call(p1) + )); + + var actual = sut.rewrite(query); + + var expectedSubQuery = Dnf.of("ExpectedSubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + var y = builder.parameter("y", ParameterDirection.OUT); + builder.clause( + y.isEquivalent(x), + personView.call(x), + not(friendView.call(x, x)) + ); + }); + var expected = Query.of("Expected", (builder, p1) -> builder.clause( + personView.call(p1), + not(expectedSubQuery.call(p1, p1)) + )); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void eliminateDoubleNegationTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.IN); + builder.clause(not(friendView.call(x, Variable.of()))); + }); + var query = Query.of("Actual", (builder, p1) -> builder.clause( + personView.call(p1), + not(dnf.call(p1)) + )); + + var actual = sut.rewrite(query); + + var expected = Query.of("Actual", (builder, p1) -> builder.clause( + personView.call(p1), + friendView.call(p1, Variable.of()) + )); + + assertThat(actual.getDnf(), structurallyEqualTo(expected.getDnf())); + } + + @Test + void identityWhenNoWorkToDoTest() { + var dnf = Dnf.of("SubQuery", builder -> { + var x = builder.parameter("x", ParameterDirection.OUT); + builder.clause( + personView.call(x), + not(friendView.call(x, Variable.of())) + ); + }); + var query = Query.of("Actual", (builder, p1) -> builder.clause( + personView.call(p1), + not(dnf.call(p1)) + )); + + var actual = sut.rewrite(query); + + assertThat(actual, is(query)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/TermSubstitutionTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/TermSubstitutionTest.java new file mode 100644 index 00000000..52b21692 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/TermSubstitutionTest.java @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.refinery.logic.equality.DnfEqualityChecker; +import tools.refinery.logic.equality.SubstitutingLiteralEqualityHelper; +import tools.refinery.logic.substitution.Substitution; +import tools.refinery.logic.term.bool.BoolTerms; +import tools.refinery.logic.term.int_.IntTerms; +import tools.refinery.logic.term.real.RealTerms; +import tools.refinery.logic.term.uppercardinality.UpperCardinality; +import tools.refinery.logic.term.uppercardinality.UpperCardinalityTerms; + +import java.util.List; +import java.util.stream.Stream; + +class TermSubstitutionTest { + private final static DataVariable intA = Variable.of("intA", Integer.class); + private final static DataVariable intB = Variable.of("intB", Integer.class); + private final static DataVariable realA = Variable.of("realA", Double.class); + private final static DataVariable realB = Variable.of("realB", Double.class); + private final static DataVariable boolA = Variable.of("boolA", Boolean.class); + private final static DataVariable boolB = Variable.of("boolB", Boolean.class); + private final static DataVariable upperCardinalityA = Variable.of("upperCardinalityA", + UpperCardinality.class); + private final static DataVariable upperCardinalityB = Variable.of("upperCardinalityB", + UpperCardinality.class); + private final static Substitution substitution = Substitution.builder() + .put(intA, intB) + .put(intB, intA) + .put(realA, realB) + .put(realB, realA) + .put(boolA, boolB) + .put(boolB, boolA) + .put(upperCardinalityA, upperCardinalityB) + .put(upperCardinalityB, upperCardinalityA) + .build(); + + @ParameterizedTest + @MethodSource + void substitutionTest(AnyTerm term) { + var substitutedTerm1 = term.substitute(substitution); + Assertions.assertNotEquals(term, substitutedTerm1, "Original term is not equal to substituted term"); + var helper = new SubstitutingLiteralEqualityHelper(DnfEqualityChecker.DEFAULT, List.of(), List.of()); + Assertions.assertTrue(term.equalsWithSubstitution(helper, substitutedTerm1), "Terms are equal by helper"); + // The {@link #substitution} is its own inverse. + var substitutedTerm2 = substitutedTerm1.substitute(substitution); + Assertions.assertEquals(term, substitutedTerm2, "Original term is not equal to back-substituted term"); + } + + static Stream substitutionTest() { + return Stream.of( + Arguments.of(IntTerms.plus(intA)), + Arguments.of(IntTerms.minus(intA)), + Arguments.of(IntTerms.add(intA, intB)), + Arguments.of(IntTerms.sub(intA, intB)), + Arguments.of(IntTerms.mul(intA, intB)), + Arguments.of(IntTerms.div(intA, intB)), + Arguments.of(IntTerms.pow(intA, intB)), + Arguments.of(IntTerms.min(intA, intB)), + Arguments.of(IntTerms.max(intA, intB)), + Arguments.of(IntTerms.eq(intA, intB)), + Arguments.of(IntTerms.notEq(intA, intB)), + Arguments.of(IntTerms.less(intA, intB)), + Arguments.of(IntTerms.lessEq(intA, intB)), + Arguments.of(IntTerms.greater(intA, intB)), + Arguments.of(IntTerms.greaterEq(intA, intB)), + Arguments.of(IntTerms.asInt(realA)), + Arguments.of(RealTerms.plus(realA)), + Arguments.of(RealTerms.minus(realA)), + Arguments.of(RealTerms.add(realA, realB)), + Arguments.of(RealTerms.sub(realA, realB)), + Arguments.of(RealTerms.mul(realA, realB)), + Arguments.of(RealTerms.div(realA, realB)), + Arguments.of(RealTerms.pow(realA, realB)), + Arguments.of(RealTerms.min(realA, realB)), + Arguments.of(RealTerms.max(realA, realB)), + Arguments.of(RealTerms.asReal(intA)), + Arguments.of(BoolTerms.not(boolA)), + Arguments.of(BoolTerms.and(boolA, boolB)), + Arguments.of(BoolTerms.or(boolA, boolB)), + Arguments.of(BoolTerms.xor(boolA, boolB)), + Arguments.of(RealTerms.eq(realA, realB)), + Arguments.of(UpperCardinalityTerms.add(upperCardinalityA, upperCardinalityB)), + Arguments.of(UpperCardinalityTerms.mul(upperCardinalityA, upperCardinalityB)), + Arguments.of(UpperCardinalityTerms.min(upperCardinalityA, upperCardinalityB)), + Arguments.of(UpperCardinalityTerms.max(upperCardinalityA, upperCardinalityB)) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/bool/BoolTermsEvaluateTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/bool/BoolTermsEvaluateTest.java new file mode 100644 index 00000000..7f65591f --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/bool/BoolTermsEvaluateTest.java @@ -0,0 +1,76 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.bool; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import tools.refinery.logic.term.bool.BoolTerms; +import tools.refinery.logic.valuation.Valuation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class BoolTermsEvaluateTest { + @ParameterizedTest(name = "!{0} == {1}") + @CsvSource(value = { + "false, true", + "true, false", + "null, null" + }, nullValues = "null") + void notTest(Boolean a, Boolean result) { + var term = BoolTerms.not(BoolTerms.constant(a)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} && {1} == {2}") + @CsvSource(value = { + "false, false, false", + "false, true, false", + "true, false, false", + "true, true, true", + "false, null, null", + "null, false, null", + "null, null, null" + }, nullValues = "null") + void andTest(Boolean a, Boolean b, Boolean result) { + var term = BoolTerms.and(BoolTerms.constant(a), BoolTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} || {1} == {2}") + @CsvSource(value = { + "false, false, false", + "false, true, true", + "true, false, true", + "true, true, true", + "true, null, null", + "null, true, null", + "null, null, null" + }, nullValues = "null") + void orTest(Boolean a, Boolean b, Boolean result) { + var term = BoolTerms.or(BoolTerms.constant(a), BoolTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} ^^ {1} == {2}") + @CsvSource(value = { + "false, false, false", + "false, true, true", + "true, false, true", + "true, true, false", + "false, null, null", + "null, false, null", + "null, null, null" + }, nullValues = "null") + void xorTest(Boolean a, Boolean b, Boolean result) { + var term = BoolTerms.xor(BoolTerms.constant(a), BoolTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalTest.java new file mode 100644 index 00000000..ee2dd61c --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalTest.java @@ -0,0 +1,127 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static tools.refinery.logic.term.cardinalityinterval.CardinalityIntervals.*; + +class CardinalityIntervalTest { + @ParameterizedTest(name = "min({0}, {1}) == {2}") + @MethodSource + void minTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.min(b), equalTo(expected)); + } + + static Stream minTest() { + return Stream.of( + Arguments.of(atMost(1), atMost(1), atMost(1)), + Arguments.of(atMost(1), between(2, 3), atMost(1)), + Arguments.of(atMost(1), atLeast(2), atMost(1)), + Arguments.of(atMost(1), ERROR, ERROR), + Arguments.of(atLeast(1), atLeast(2), atLeast(1)), + Arguments.of(atLeast(1), ERROR, ERROR), + Arguments.of(ERROR, atLeast(2), ERROR), + Arguments.of(ERROR, ERROR, ERROR) + ); + } + + @ParameterizedTest(name = "max({0}, {1}) == {2}") + @MethodSource + void maxTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.max(b), equalTo(expected)); + } + + static Stream maxTest() { + return Stream.of( + Arguments.of(atMost(1), atMost(1), atMost(1)), + Arguments.of(atMost(1), between(2, 3), between(2, 3)), + Arguments.of(atMost(1), atLeast(2), atLeast(2)), + Arguments.of(atMost(1), ERROR, ERROR), + Arguments.of(atLeast(1), atLeast(2), atLeast(2)), + Arguments.of(atLeast(1), ERROR, ERROR), + Arguments.of(ERROR, atLeast(2), ERROR), + Arguments.of(ERROR, ERROR, ERROR) + ); + } + + @ParameterizedTest(name = "{0} + {1} == {2}") + @MethodSource + void addTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.add(b), equalTo(expected)); + } + + static Stream addTest() { + return Stream.of( + Arguments.of(atMost(1), atMost(1), atMost(2)), + Arguments.of(atMost(1), between(2, 3), between(2, 4)), + Arguments.of(atMost(1), atLeast(2), atLeast(2)), + Arguments.of(atMost(1), ERROR, ERROR), + Arguments.of(atLeast(1), atLeast(2), atLeast(3)), + Arguments.of(atLeast(1), ERROR, ERROR), + Arguments.of(ERROR, atLeast(2), ERROR), + Arguments.of(ERROR, ERROR, ERROR) + ); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @MethodSource + void multiplyTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.multiply(b), equalTo(expected)); + } + + static Stream multiplyTest() { + return Stream.of( + Arguments.of(between(2, 3), between(4, 5), between(8, 15)), + Arguments.of(atLeast(2), between(4, 5), atLeast(8)), + Arguments.of(between(2, 3), atLeast(4), atLeast(8)), + Arguments.of(between(2, 3), ERROR, ERROR), + Arguments.of(ERROR, between(4, 5), ERROR), + Arguments.of(ERROR, ERROR, ERROR) + ); + } + + @ParameterizedTest(name = "{0} /\\ {1} == {2}") + @MethodSource + void meetTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.meet(b), equalTo(expected)); + } + + static Stream meetTest() { + return Stream.of( + Arguments.of(atMost(1), atMost(2), atMost(1)), + Arguments.of(atMost(2), between(1, 3), between(1, 2)), + Arguments.of(atMost(1), between(1, 3), exactly(1)), + Arguments.of(atMost(1), between(2, 3), ERROR), + Arguments.of(atMost(1), ERROR, ERROR), + Arguments.of(ERROR, atMost(1), ERROR), + Arguments.of(ERROR, ERROR, ERROR) + ); + } + + @ParameterizedTest(name = "{0} \\/ {1} == {2}") + @MethodSource + void joinTest(CardinalityInterval a, CardinalityInterval b, CardinalityInterval expected) { + assertThat(a.join(b), equalTo(expected)); + } + + static Stream joinTest() { + return Stream.of( + Arguments.of(atMost(1), atMost(2), atMost(2)), + Arguments.of(atMost(2), between(1, 3), atMost(3)), + Arguments.of(atMost(1), between(2, 3), atMost(3)), + Arguments.of(atMost(1), ERROR, atMost(1)), + Arguments.of(ERROR, atMost(1), atMost(1)), + Arguments.of(ERROR, ERROR, ERROR) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalsTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalsTest.java new file mode 100644 index 00000000..5441c837 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/CardinalityIntervalsTest.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.term.uppercardinality.UpperCardinalities; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class CardinalityIntervalsTest { + @Test + void betweenEmptyTest() { + var interval = CardinalityIntervals.between(2, 1); + assertThat(interval.isEmpty(), equalTo(true)); + } + + @Test + void betweenNegativeUpperBoundTest() { + var interval = CardinalityIntervals.between(0, -1); + assertThat(interval.upperBound(), equalTo(UpperCardinalities.UNBOUNDED)); + assertThat(interval.isEmpty(), equalTo(false)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityIntervalTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityIntervalTest.java new file mode 100644 index 00000000..0dbc7f61 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/EmptyCardinalityIntervalTest.java @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import org.junit.jupiter.api.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.lessThan; + +class EmptyCardinalityIntervalTest { + @Test + void inconsistentBoundsTest() { + assertThat(CardinalityIntervals.ERROR.upperBound().compareToInt(CardinalityIntervals.ERROR.lowerBound()), + lessThan(0)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/FiniteCardinalityIntervalTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/FiniteCardinalityIntervalTest.java new file mode 100644 index 00000000..588b25ab --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/cardinalityinterval/FiniteCardinalityIntervalTest.java @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.cardinalityinterval; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.term.uppercardinality.UpperCardinality; +import tools.refinery.logic.term.uppercardinality.UpperCardinalities; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FiniteCardinalityIntervalTest { + @Test + void invalidLowerBoundConstructorTest() { + assertThrows(IllegalArgumentException.class, () -> new NonEmptyCardinalityInterval(-1, + UpperCardinalities.UNBOUNDED)); + } + + @Test + void invalidUpperBoundConstructorTest() { + var upperCardinality = UpperCardinality.of(1); + assertThrows(IllegalArgumentException.class, () -> new NonEmptyCardinalityInterval(2, + upperCardinality)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/int_/IntTermsEvaluateTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/int_/IntTermsEvaluateTest.java new file mode 100644 index 00000000..55d9b740 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/int_/IntTermsEvaluateTest.java @@ -0,0 +1,260 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.int_; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; +import tools.refinery.logic.term.int_.IntTerms; +import tools.refinery.logic.term.real.RealTerms; +import tools.refinery.logic.valuation.Valuation; + +import java.util.stream.Stream; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class IntTermsEvaluateTest { + @ParameterizedTest(name = "+{0} == {1}") + @CsvSource(value = { + "2, 2", + "null, null" + }, nullValues = "null") + void plusTest(Integer a, Integer result) { + var term = IntTerms.plus(IntTerms.constant(a)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "-{0} == {1}") + @CsvSource(value = { + "2, -2", + "null, null" + }, nullValues = "null") + void minusTest(Integer a, Integer result) { + var term = IntTerms.minus(IntTerms.constant(a)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} + {1} == {2}") + @CsvSource(value = { + "1, 2, 3", + "null, 2, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void addTest(Integer a, Integer b, Integer result) { + var term = IntTerms.add(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} - {1} == {2}") + @CsvSource(value = { + "1, 3, -2", + "null, 3, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void subTest(Integer a, Integer b, Integer result) { + var term = IntTerms.sub(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @CsvSource(value = { + "2, 3, 6", + "null, 3, null", + "2, null, null", + "null, null, null" + }, nullValues = "null") + void mulTest(Integer a, Integer b, Integer result) { + var term = IntTerms.mul(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @CsvSource(value = { + "6, 3, 2", + "7, 3, 2", + "6, 0, null", + "null, 3, null", + "6, null, null", + "null, null, null" + }, nullValues = "null") + void divTest(Integer a, Integer b, Integer result) { + var term = IntTerms.div(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} ** {1} == {2}") + @CsvSource(value = { + "1, 0, 1", + "1, 3, 1", + "1, -3, null", + "2, 0, 1", + "2, 2, 4", + "2, 3, 8", + "2, 4, 16", + "2, 5, 32", + "2, 6, 64", + "2, -3, null", + "null, 3, null", + "2, null, null", + "null, null, null" + }, nullValues = "null") + void powTest(Integer a, Integer b, Integer result) { + var term = IntTerms.pow(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "min({0}, {1}) == {2}") + @CsvSource(value = { + "1, 2, 1", + "2, 1, 1", + "null, 2, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void minTest(Integer a, Integer b, Integer result) { + var term = IntTerms.min(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "max({0}, {1}) == {2}") + @CsvSource(value = { + "1, 2, 2", + "2, 1, 2", + "null, 2, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void maxTest(Integer a, Integer b, Integer result) { + var term = IntTerms.max(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} == {1}) == {2}") + @CsvSource(value = { + "1, 1, true", + "1, 2, false", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void eqTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.eq(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} != {1}) == {2}") + @CsvSource(value = { + "1, 1, false", + "1, 2, true", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void notEqTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.notEq(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} < {1}) == {2}") + @CsvSource(value = { + "1, -2, false", + "1, 1, false", + "1, 2, true", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void lessTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.less(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} <= {1}) == {2}") + @CsvSource(value = { + "1, -2, false", + "1, 1, true", + "1, 2, true", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void lessEqTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.lessEq(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} > {1}) == {2}") + @CsvSource(value = { + "1, -2, true", + "1, 1, false", + "1, 2, false", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void greaterTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.greater(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} >= {1}) == {2}") + @CsvSource(value = { + "1, -2, true", + "1, 1, true", + "1, 2, false", + "null, 1, null", + "1, null, null", + "null, null, null" + }, nullValues = "null") + void greaterEqTest(Integer a, Integer b, Boolean result) { + var term = IntTerms.greaterEq(IntTerms.constant(a), IntTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} as int == {1}") + @MethodSource + void asIntTest(Double a, Integer result) { + var term = IntTerms.asInt(RealTerms.constant(a)); + assertThat(term.getType(), is(Integer.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + static Stream asIntTest() { + return Stream.of( + Arguments.of(2.0, 2), + Arguments.of(2.1, 2), + Arguments.of(2.9, 2), + Arguments.of(-2.0, -2), + Arguments.of(-2.1, -2), + Arguments.of(-2.9, -2), + Arguments.of(0.0, 0), + Arguments.of(-0.0, 0), + Arguments.of(Double.POSITIVE_INFINITY, Integer.MAX_VALUE), + Arguments.of(Double.NEGATIVE_INFINITY, Integer.MIN_VALUE), + Arguments.of(Double.NaN, null), + Arguments.of(null, null) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/real/RealTermEvaluateTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/real/RealTermEvaluateTest.java new file mode 100644 index 00000000..042d1807 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/real/RealTermEvaluateTest.java @@ -0,0 +1,239 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.real; + +import org.hamcrest.Matcher; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import tools.refinery.logic.term.int_.IntTerms; +import tools.refinery.logic.term.real.RealTerms; +import tools.refinery.logic.valuation.Valuation; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +class RealTermEvaluateTest { + public static final double TOLERANCE = 1e-6; + + private static Matcher closeToOrNull(Double expected) { + return expected == null ? nullValue(Double.class) : closeTo(expected, TOLERANCE); + } + + @ParameterizedTest(name = "+{0} == {1}") + @CsvSource(value = { + "2.5, 2.5", + "null, null" + }, nullValues = "null") + void plusTest(Double a, Double result) { + var term = RealTerms.plus(RealTerms.constant(a)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "-{0} == {1}") + @CsvSource(value = { + "2.5, -2.5", + "null, null" + }, nullValues = "null") + void minusTest(Double a, Double result) { + var term = RealTerms.minus(RealTerms.constant(a)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "{0} + {1} == {2}") + @CsvSource(value = { + "1.2, 2.3, 3.5", + "null, 2.3, null", + "1.2, null, null", + "null, null, null" + }, nullValues = "null") + void addTest(Double a, Double b, Double result) { + var term = RealTerms.add(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "{0} - {1} == {2}") + @CsvSource(value = { + "1.2, 3.4, -2.2", + "null, 3.4, null", + "1.2, null, null", + "null, null, null" + }, nullValues = "null") + void subTest(Double a, Double b, Double result) { + var term = RealTerms.sub(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @CsvSource(value = { + "2.3, 3.4, 7.82", + "null, 3.4, null", + "2.3, null, null", + "null, null, null" + }, nullValues = "null") + void mulTest(Double a, Double b, Double result) { + var term = RealTerms.mul(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @CsvSource(value = { + "7.82, 3.4, 2.3", + "null, 3.4, null", + "7.82, null, null", + "null, null, null" + }, nullValues = "null") + void divTest(Double a, Double b, Double result) { + var term = RealTerms.div(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "{0} ** {1} == {2}") + @CsvSource(value = { + "2.0, 6.0, 64.0", + "null, 6.0, null", + "2.0, null, null", + "null, null, null" + }, nullValues = "null") + void powTest(Double a, Double b, Double result) { + var term = RealTerms.pow(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "min({0}, {1}) == {2}") + @CsvSource(value = { + "1.5, 2.7, 1.5", + "2.7, 1.5, 1.5", + "null, 2.7, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void minTest(Double a, Double b, Double result) { + var term = RealTerms.min(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "max({0}, {1}) == {2}") + @CsvSource(value = { + "1.5, 2.7, 2.7", + "2.7, 1.7, 2.7", + "null, 2.7, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void maxTest(Double a, Double b, Double result) { + var term = RealTerms.max(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } + + @ParameterizedTest(name = "({0} == {1}) == {2}") + @CsvSource(value = { + "1.5, 1.5, true", + "1.5, 2.7, false", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void eqTest(Double a, Double b, Boolean result) { + var term = RealTerms.eq(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} != {1}) == {2}") + @CsvSource(value = { + "1.5, 1.5, false", + "1.5, 2.7, true", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void notEqTest(Double a, Double b, Boolean result) { + var term = RealTerms.notEq(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} < {1}) == {2}") + @CsvSource(value = { + "1.5, -2.7, false", + "1.5, 1.5, false", + "1.5, 2.7, true", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void lessTest(Double a, Double b, Boolean result) { + var term = RealTerms.less(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} <= {1}) == {2}") + @CsvSource(value = { + "1.5, -2.7, false", + "1.5, 1.5, true", + "1.5, 2.7, true", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void lessEqTest(Double a, Double b, Boolean result) { + var term = RealTerms.lessEq(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} > {1}) == {2}") + @CsvSource(value = { + "1.5, -2.7, true", + "1.5, 1.5, false", + "1.5, 2.7, false", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void greaterTest(Double a, Double b, Boolean result) { + var term = RealTerms.greater(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "({0} >= {1}) == {2}") + @CsvSource(value = { + "1.5, -2.7, true", + "1.5, 1.5, true", + "1.5, 2.7, false", + "null, 1.5, null", + "1.5, null, null", + "null, null, null" + }, nullValues = "null") + void greaterEqTest(Double a, Double b, Boolean result) { + var term = RealTerms.greaterEq(RealTerms.constant(a), RealTerms.constant(b)); + assertThat(term.getType(), is(Boolean.class)); + assertThat(term.evaluate(Valuation.empty()), is(result)); + } + + @ParameterizedTest(name = "{0} as real == {1}") + @CsvSource(value = { + "0, 0.0", + "5, 5.0", + "null, null" + }, nullValues = "null") + void asRealTest(Integer a, Double result) { + var term = RealTerms.asReal(IntTerms.constant(a)); + assertThat(term.getType(), is(Double.class)); + assertThat(term.evaluate(Valuation.empty()), closeToOrNull(result)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinalityTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinalityTest.java new file mode 100644 index 00000000..8a57f029 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/FiniteUpperCardinalityTest.java @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FiniteUpperCardinalityTest { + @Test + void invalidConstructorTest() { + assertThrows(IllegalArgumentException.class, () -> new FiniteUpperCardinality(-1)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitiesTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitiesTest.java new file mode 100644 index 00000000..bdb6a833 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitiesTest.java @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; + +class UpperCardinalitiesTest { + @ParameterizedTest + @ValueSource(ints = {0, 1, 255, 256, 1000, Integer.MAX_VALUE}) + void valueOfBoundedTest(int value) { + var upperCardinality = UpperCardinalities.atMost(value); + assertThat(upperCardinality, instanceOf(FiniteUpperCardinality.class)); + assertThat(((FiniteUpperCardinality) upperCardinality).finiteUpperBound(), equalTo(value)); + } + + @Test + void valueOfUnboundedTest() { + var upperCardinality = UpperCardinalities.atMost(-1); + assertThat(upperCardinality, instanceOf(UnboundedUpperCardinality.class)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorStreamTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorStreamTest.java new file mode 100644 index 00000000..fc8522d4 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorStreamTest.java @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; + +class UpperCardinalitySumAggregatorStreamTest { + @ParameterizedTest + @MethodSource + void testStream(List list, UpperCardinality expected) { + var result = UpperCardinalitySumAggregator.INSTANCE.aggregateStream(list.stream()); + assertThat(result, Matchers.is(expected)); + } + + static Stream testStream() { + return Stream.of( + Arguments.of(List.of(), UpperCardinalities.ZERO), + Arguments.of(List.of(UpperCardinality.of(3)), UpperCardinality.of(3)), + Arguments.of( + List.of( + UpperCardinality.of(2), + UpperCardinality.of(3) + ), + UpperCardinality.of(5) + ), + Arguments.of(List.of(UpperCardinalities.UNBOUNDED), UpperCardinalities.UNBOUNDED), + Arguments.of( + List.of( + UpperCardinalities.UNBOUNDED, + UpperCardinalities.UNBOUNDED + ), + UpperCardinalities.UNBOUNDED + ), + Arguments.of( + List.of( + UpperCardinalities.UNBOUNDED, + UpperCardinality.of(3) + ), + UpperCardinalities.UNBOUNDED + ) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorTest.java new file mode 100644 index 00000000..e252b097 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalitySumAggregatorTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.refinery.logic.term.StatefulAggregate; + +import static org.hamcrest.MatcherAssert.assertThat; + +class UpperCardinalitySumAggregatorTest { + private StatefulAggregate accumulator; + + @BeforeEach + void beforeEach() { + accumulator = UpperCardinalitySumAggregator.INSTANCE.createEmptyAggregate(); + } + + @Test + void emptyAggregationTest() { + MatcherAssert.assertThat(accumulator.getResult(), Matchers.is(UpperCardinality.of(0))); + } + + @Test + void singleBoundedTest() { + accumulator.add(UpperCardinality.of(3)); + MatcherAssert.assertThat(accumulator.getResult(), Matchers.is(UpperCardinality.of(3))); + } + + @Test + void multipleBoundedTest() { + accumulator.add(UpperCardinality.of(2)); + accumulator.add(UpperCardinality.of(3)); + MatcherAssert.assertThat(accumulator.getResult(), Matchers.is(UpperCardinality.of(5))); + } + + @Test + void singleUnboundedTest() { + accumulator.add(UpperCardinalities.UNBOUNDED); + assertThat(accumulator.getResult(), Matchers.is(UpperCardinalities.UNBOUNDED)); + } + + @Test + void multipleUnboundedTest() { + accumulator.add(UpperCardinalities.UNBOUNDED); + accumulator.add(UpperCardinalities.UNBOUNDED); + assertThat(accumulator.getResult(), Matchers.is(UpperCardinalities.UNBOUNDED)); + } + + @Test + void removeBoundedTest() { + accumulator.add(UpperCardinality.of(2)); + accumulator.add(UpperCardinality.of(3)); + accumulator.remove(UpperCardinality.of(2)); + MatcherAssert.assertThat(accumulator.getResult(), Matchers.is(UpperCardinality.of(3))); + } + + @Test + void removeAllUnboundedTest() { + accumulator.add(UpperCardinalities.UNBOUNDED); + accumulator.add(UpperCardinality.of(3)); + accumulator.remove(UpperCardinalities.UNBOUNDED); + MatcherAssert.assertThat(accumulator.getResult(), Matchers.is(UpperCardinality.of(3))); + } + + @Test + void removeSomeUnboundedTest() { + accumulator.add(UpperCardinalities.UNBOUNDED); + accumulator.add(UpperCardinalities.UNBOUNDED); + accumulator.add(UpperCardinality.of(3)); + accumulator.remove(UpperCardinalities.UNBOUNDED); + assertThat(accumulator.getResult(), Matchers.is(UpperCardinalities.UNBOUNDED)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTermsEvaluateTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTermsEvaluateTest.java new file mode 100644 index 00000000..ab71b716 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTermsEvaluateTest.java @@ -0,0 +1,103 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import tools.refinery.logic.valuation.Valuation; + +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; + +class UpperCardinalityTermsEvaluateTest { + @ParameterizedTest(name = "min({0}, {1}) == {2}") + @MethodSource + void minTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + var term = UpperCardinalityTerms.min(UpperCardinalityTerms.constant(a), UpperCardinalityTerms.constant(b)); + assertThat(term.getType(), is(UpperCardinality.class)); + assertThat(term.evaluate(Valuation.empty()), Matchers.is(expected)); + } + + static Stream minTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(1), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(1), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinalities.UNBOUNDED, UpperCardinality.of(0)), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinality.of(1), null, null), + Arguments.of(null, UpperCardinality.of(1), null), + Arguments.of(null, null, null) + ); + } + + @ParameterizedTest(name = "max({0}, {1}) == {2}") + @MethodSource + void maxTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + var term = UpperCardinalityTerms.max(UpperCardinalityTerms.constant(a), UpperCardinalityTerms.constant(b)); + assertThat(term.getType(), is(UpperCardinality.class)); + assertThat(term.evaluate(Valuation.empty()), Matchers.is(expected)); + } + + static Stream maxTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(1), UpperCardinality.of(1)), + Arguments.of(UpperCardinality.of(1), UpperCardinality.of(0), UpperCardinality.of(1)), + Arguments.of(UpperCardinality.of(0), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(0), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinality.of(1), null, null), + Arguments.of(null, UpperCardinality.of(1), null), + Arguments.of(null, null, null) + ); + } + + @ParameterizedTest(name = "{0} + {1} == {2}") + @MethodSource + void addTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + var term = UpperCardinalityTerms.add(UpperCardinalityTerms.constant(a), UpperCardinalityTerms.constant(b)); + assertThat(term.getType(), is(UpperCardinality.class)); + assertThat(term.evaluate(Valuation.empty()), Matchers.is(expected)); + } + + static Stream addTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(2), UpperCardinality.of(3), UpperCardinality.of(5)), + Arguments.of(UpperCardinality.of(2), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(2), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinality.of(1), null, null), + Arguments.of(null, UpperCardinality.of(1), null), + Arguments.of(null, null, null) + ); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @MethodSource + void mulTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + var term = UpperCardinalityTerms.mul(UpperCardinalityTerms.constant(a), UpperCardinalityTerms.constant(b)); + assertThat(term.getType(), is(UpperCardinality.class)); + assertThat(term.evaluate(Valuation.empty()), Matchers.is(expected)); + } + + static Stream mulTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(2), UpperCardinality.of(3), UpperCardinality.of(6)), + Arguments.of(UpperCardinality.of(2), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(2), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinality.of(1), null, null), + Arguments.of(null, UpperCardinality.of(1), null), + Arguments.of(null, null, null) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTest.java new file mode 100644 index 00000000..70cb6695 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/term/uppercardinality/UpperCardinalityTest.java @@ -0,0 +1,115 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.term.uppercardinality; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +class UpperCardinalityTest { + @ParameterizedTest(name = "min({0}, {1}) == {2}") + @MethodSource + void minTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + assertThat(a.min(b), equalTo(expected)); + } + + static Stream minTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(1), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(1), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinalities.UNBOUNDED, UpperCardinality.of(0)), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED) + ); + } + + @ParameterizedTest(name = "max({0}, {1}) == {2}") + @MethodSource + void maxTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + assertThat(a.max(b), equalTo(expected)); + } + + static Stream maxTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(0), UpperCardinality.of(0)), + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(1), UpperCardinality.of(1)), + Arguments.of(UpperCardinality.of(1), UpperCardinality.of(0), UpperCardinality.of(1)), + Arguments.of(UpperCardinality.of(0), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(0), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED) + ); + } + + @ParameterizedTest(name = "{0} + {1} == {2}") + @MethodSource + void addTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + assertThat(a.add(b), equalTo(expected)); + } + + static Stream addTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(2), UpperCardinality.of(3), UpperCardinality.of(5)), + Arguments.of(UpperCardinality.of(2), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(2), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED) + ); + } + + @ParameterizedTest(name = "{0} * {1} == {2}") + @MethodSource + void multiplyTest(UpperCardinality a, UpperCardinality b, UpperCardinality expected) { + assertThat(a.multiply(b), equalTo(expected)); + } + + static Stream multiplyTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(2), UpperCardinality.of(3), UpperCardinality.of(6)), + Arguments.of(UpperCardinality.of(2), UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(2), UpperCardinalities.UNBOUNDED), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED) + ); + } + + @ParameterizedTest(name = "{0}.compareTo({1}) == {2}") + @MethodSource + void compareToTest(UpperCardinality a, UpperCardinality b, int expected) { + assertThat(a.compareTo(b), equalTo(expected)); + } + + static Stream compareToTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(0), 0), + Arguments.of(UpperCardinality.of(0), UpperCardinality.of(1), -1), + Arguments.of(UpperCardinality.of(1), UpperCardinality.of(0), 1), + Arguments.of(UpperCardinality.of(0), UpperCardinalities.UNBOUNDED, -1), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinality.of(0), 1), + Arguments.of(UpperCardinalities.UNBOUNDED, UpperCardinalities.UNBOUNDED, 0) + ); + } + + @ParameterizedTest(name = "{0}.compareToInt({1}) == {2}") + @MethodSource + void compareToIntTest(UpperCardinality a, int b, int expected) { + assertThat(a.compareToInt(b), equalTo(expected)); + } + + static Stream compareToIntTest() { + return Stream.of( + Arguments.of(UpperCardinality.of(3), -1, 1), + Arguments.of(UpperCardinality.of(3), 2, 1), + Arguments.of(UpperCardinality.of(3), 3, 0), + Arguments.of(UpperCardinality.of(3), 4, -1), + Arguments.of(UpperCardinalities.UNBOUNDED, -1, 1), + Arguments.of(UpperCardinalities.UNBOUNDED, 3, 1) + ); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeFunctionView.java b/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeFunctionView.java new file mode 100644 index 00000000..4a55f561 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeFunctionView.java @@ -0,0 +1,57 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public record FakeFunctionView(String name, int keyArity, Class valueType) implements Constraint { + @Override + public int arity() { + return keyArity + 1; + } + + @Override + public List getParameters() { + var parameters = new Parameter[keyArity + 1]; + Arrays.fill(parameters, Parameter.NODE_OUT); + parameters[keyArity] = new Parameter(valueType, ParameterDirection.OUT); + return List.of(parameters); + } + + public AssignedValue aggregate(Aggregator aggregator, List arguments) { + return targetVariable -> { + var placeholderVariable = Variable.of(valueType); + var argumentsWithPlaceholder = new ArrayList(arguments.size() + 1); + argumentsWithPlaceholder.addAll(arguments); + argumentsWithPlaceholder.add(placeholderVariable); + return aggregateBy(placeholderVariable, aggregator, argumentsWithPlaceholder).toLiteral(targetVariable); + }; + } + + public AssignedValue aggregate(Aggregator aggregator, NodeVariable... arguments) { + return aggregate(aggregator, List.of(arguments)); + } + + public AssignedValue leftJoin(T defaultValue, List arguments) { + return targetVariable -> { + var placeholderVariable = Variable.of(valueType); + var argumentsWithPlaceholder = new ArrayList(arguments.size() + 1); + argumentsWithPlaceholder.addAll(arguments); + argumentsWithPlaceholder.add(placeholderVariable); + return leftJoinBy(placeholderVariable, defaultValue, argumentsWithPlaceholder).toLiteral(targetVariable); + }; + } + + public AssignedValue leftJoin(T defaultValue, NodeVariable... arguments) { + return leftJoin(defaultValue, List.of(arguments)); + + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeKeyOnlyView.java b/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeKeyOnlyView.java new file mode 100644 index 00000000..7e09ddab --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/tests/FakeKeyOnlyView.java @@ -0,0 +1,21 @@ +/* + * SPDX-FileCopyrightText: 2024 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import tools.refinery.logic.Constraint; +import tools.refinery.logic.term.Parameter; + +import java.util.Arrays; +import java.util.List; + +public record FakeKeyOnlyView(String name, int arity) implements Constraint { + @Override + public List getParameters() { + var parameters = new Parameter[arity]; + Arrays.fill(parameters, Parameter.NODE_OUT); + return List.of(parameters); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToRawTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToRawTest.java new file mode 100644 index 00000000..52a22ce1 --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToRawTest.java @@ -0,0 +1,155 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class StructurallyEqualToRawTest { + private static final Constraint personView = new FakeKeyOnlyView("Person", 1); + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + + @Test + void flatEqualsTest() { + var actual = Dnf.builder("Actual").parameters(p).clause(personView.call(p)).build(); + + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of(List.of(personView.call(q))) + )); + } + + @Test + void flatNotEqualsTest() { + var actual = Dnf.builder("Actual").parameters(p).clause(friendView.call(p, q)).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of(List.of(friendView.call(q, q))) + ); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void deepEqualsTest() { + var actual = Dnf.builder("Actual").parameters(q).clause( + Dnf.builder("Actual2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of( + List.of( + Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build().call(q) + ) + ) + )); + } + + @Test + void deepNotEqualsTest() { + var actual = Dnf.builder("Actual").parameter(q).clause( + Dnf.builder("Actual2").parameters(p).clause(friendView.call(p, q)).build().call(q) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of( + List.of( + Dnf.builder("Expected2") + .parameters(p) + .clause(friendView.call(p, p)) + .build() + .call(q) + ) + ) + ); + var error = assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + assertThat(error.getMessage(), allOf(containsString("Expected2"), containsString("Actual2"))); + } + + @Test + void parameterListLengthMismatchTest() { + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(p, ParameterDirection.OUT)), + List.of(List.of(friendView.call(p, p))) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void parameterDirectionMismatchTest() { + var actual = Dnf.builder("Actual").parameter(p, ParameterDirection.IN).clause( + personView.call(p) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(p, ParameterDirection.OUT)), + List.of(List.of(personView.call(p))) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void clauseCountMismatchTest() { + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of( + new SymbolicParameter(p, ParameterDirection.OUT), + new SymbolicParameter(q, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(p, q)), + List.of(friendView.call(q, p)) + ) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void literalCountMismatchTest() { + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of( + new SymbolicParameter(p, ParameterDirection.OUT), + new SymbolicParameter(q, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(p, q), friendView.call(q, p)) + ) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } +} diff --git a/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToTest.java b/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToTest.java new file mode 100644 index 00000000..663b115a --- /dev/null +++ b/subprojects/logic/src/test/java/tools/refinery/logic/tests/StructurallyEqualToTest.java @@ -0,0 +1,123 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.junit.jupiter.api.Test; +import tools.refinery.logic.Constraint; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.term.NodeVariable; +import tools.refinery.logic.term.ParameterDirection; +import tools.refinery.logic.term.Variable; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.logic.tests.QueryMatchers.structurallyEqualTo; + +class StructurallyEqualToTest { + private static final Constraint personView = new FakeKeyOnlyView("Person", 1); + private static final Constraint friendView = new FakeKeyOnlyView("friend", 2); + private static final NodeVariable p = Variable.of("p"); + private static final NodeVariable q = Variable.of("q"); + + @Test + void flatEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause(personView.call(q)).build(); + var actual = Dnf.builder("Actual").parameters(p).clause(personView.call(p)).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void flatNotEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause(friendView.call(q, q)).build(); + var actual = Dnf.builder("Actual").parameters(p).clause(friendView.call(p, q)).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void deepEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause( + Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + var actual = Dnf.builder("Actual").parameters(q).clause( + Dnf.builder("Actual2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + + assertThat(actual, structurallyEqualTo(expected)); + } + + @Test + void deepNotEqualsTest() { + var expected = Dnf.builder("Expected").parameters(q).clause( + Dnf.builder("Expected2").parameters(p).clause(friendView.call(p, p)).build().call(q) + ).build(); + var actual = Dnf.builder("Actual").parameter(q).clause( + Dnf.builder("Actual2").parameters(p).clause(friendView.call(p, q)).build().call(q) + ).build(); + + var assertion = structurallyEqualTo(expected); + var error = assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + assertThat(error.getMessage(), containsString(" called from Expected/1 ")); + } + + @Test + void parameterListLengthMismatchTest() { + var expected = Dnf.builder("Expected").parameter(p).clause( + friendView.call(p, p) + ).build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void parameterDirectionMismatchTest() { + var expected = Dnf.builder("Expected").parameter(p, ParameterDirection.OUT).clause( + personView.call(p) + ).build(); + var actual = Dnf.builder("Actual").parameter(p, ParameterDirection.IN).clause( + personView.call(p) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void clauseCountMismatchTest() { + var expected = Dnf.builder("Expected") + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p)) + .build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void literalCountMismatchTest() { + var expected = Dnf.builder("Expected").parameters(p, q).clause( + friendView.call(p, q), + friendView.call(q, p) + ).build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } +} diff --git a/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/MismatchDescribingDnfEqualityChecker.java b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/MismatchDescribingDnfEqualityChecker.java new file mode 100644 index 00000000..aa73baec --- /dev/null +++ b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/MismatchDescribingDnfEqualityChecker.java @@ -0,0 +1,68 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.hamcrest.Description; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.equality.DeepDnfEqualityChecker; +import tools.refinery.logic.literal.Literal; + +import java.util.List; + +class MismatchDescribingDnfEqualityChecker extends DeepDnfEqualityChecker { + private final Description description; + private boolean raw; + private boolean needsDescription = true; + + MismatchDescribingDnfEqualityChecker(Description description) { + this.description = description; + } + + public boolean needsDescription() { + return needsDescription; + } + + @Override + public boolean dnfEqualRaw(List symbolicParameters, List> clauses, Dnf other) { + try { + raw = true; + boolean result = super.dnfEqualRaw(symbolicParameters, clauses, other); + if (!result && needsDescription) { + description.appendText("was ").appendText(other.toDefinitionString()); + } + return false; + } finally { + raw = false; + } + } + + @Override + protected boolean doCheckEqual(Pair pair) { + boolean result = super.doCheckEqual(pair); + if (!result && needsDescription) { + describeMismatch(pair); + // Only describe the first found (innermost) mismatch. + needsDescription = false; + } + return result; + } + + private void describeMismatch(Pair pair) { + var inProgress = getInProgress(); + int size = inProgress.size(); + if (size <= 1 && !raw) { + description.appendText("was ").appendText(pair.right().toDefinitionString()); + return; + } + var last = inProgress.get(size - 1); + description.appendText("expected ").appendText(last.left().toDefinitionString()); + for (int i = size - 2; i >= 0; i--) { + description.appendText(" called from ").appendText(inProgress.get(i).left().toString()); + } + description.appendText(" was not structurally equal to ").appendText(last.right().toDefinitionString()); + } +} diff --git a/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/QueryMatchers.java b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/QueryMatchers.java new file mode 100644 index 00000000..40332a8c --- /dev/null +++ b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/QueryMatchers.java @@ -0,0 +1,46 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.hamcrest.Matcher; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.literal.Literal; + +import java.util.List; + +public final class QueryMatchers { + private QueryMatchers() { + throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); + } + + /** + * Compare two {@link Dnf} instances up to renaming of variables. + * + * @param expected The expected {@link Dnf} instance. + * @return A Hamcrest matcher for equality up to renaming of variables. + */ + public static Matcher structurallyEqualTo(Dnf expected) { + return new StructurallyEqualTo(expected); + } + + /** + * Compare a {@link Dnf} instance to another predicate in DNF form without constructing it. + *

+ * This matcher should be used instead of {@link #structurallyEqualTo(Dnf)} when the validation and + * pre-processing associated with the {@link Dnf} constructor, i.e., validation of parameter directions, + * topological sorting of literals, and the reduction of trivial predicates is not desired. In particular, this + * matcher can be used to test for exact order of literal after pre-processing. + * + * @param expectedSymbolicParameters The expected list of symbolic parameters. + * @param expectedLiterals The expected clauses. Each clause is represented by a list of literals. + * @return A Hamcrest matcher for equality up to renaming of variables. + */ + public static Matcher structurallyEqualTo(List expectedSymbolicParameters, + List> expectedLiterals) { + return new StructurallyEqualToRaw(expectedSymbolicParameters, expectedLiterals); + } +} diff --git a/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualTo.java b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualTo.java new file mode 100644 index 00000000..257e6850 --- /dev/null +++ b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualTo.java @@ -0,0 +1,41 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.equality.DeepDnfEqualityChecker; + +public class StructurallyEqualTo extends TypeSafeMatcher { + private final Dnf expected; + + public StructurallyEqualTo(Dnf expected) { + this.expected = expected; + } + + @Override + protected boolean matchesSafely(Dnf item) { + var checker = new DeepDnfEqualityChecker(); + return checker.dnfEqual(expected, item); + } + + @Override + protected void describeMismatchSafely(Dnf item, Description mismatchDescription) { + var describingChecker = new MismatchDescribingDnfEqualityChecker(mismatchDescription); + if (describingChecker.dnfEqual(expected, item)) { + throw new IllegalStateException("Mismatched Dnf was matching on repeated comparison"); + } + if (describingChecker.needsDescription()) { + super.describeMismatchSafely(item, mismatchDescription); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("structurally equal to ").appendText(expected.toDefinitionString()); + } +} diff --git a/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualToRaw.java b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualToRaw.java new file mode 100644 index 00000000..944ab1ac --- /dev/null +++ b/subprojects/logic/src/testFixtures/java/tools/refinery/logic/tests/StructurallyEqualToRaw.java @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.logic.tests; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import tools.refinery.logic.dnf.Dnf; +import tools.refinery.logic.dnf.SymbolicParameter; +import tools.refinery.logic.equality.DeepDnfEqualityChecker; +import tools.refinery.logic.literal.Literal; + +import java.util.List; + +public class StructurallyEqualToRaw extends TypeSafeMatcher { + private final List expectedSymbolicParameters; + private final List> expectedClauses; + + public StructurallyEqualToRaw(List expectedSymbolicParameters, + List> expectedClauses) { + this.expectedSymbolicParameters = expectedSymbolicParameters; + this.expectedClauses = expectedClauses; + } + + @Override + protected boolean matchesSafely(Dnf item) { + var checker = new DeepDnfEqualityChecker(); + return checker.dnfEqualRaw(expectedSymbolicParameters, expectedClauses, item); + } + + @Override + protected void describeMismatchSafely(Dnf item, Description mismatchDescription) { + var describingChecker = new MismatchDescribingDnfEqualityChecker(mismatchDescription); + if (describingChecker.dnfEqualRaw(expectedSymbolicParameters, expectedClauses, item)) { + throw new IllegalStateException("Mismatched Dnf was matching on repeated comparison"); + } + if (describingChecker.needsDescription()) { + super.describeMismatchSafely(item, mismatchDescription); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("structurally equal to ") + .appendValueList("(", ", ", ")", expectedSymbolicParameters) + .appendText(" <-> ") + .appendValueList("", ", ", ".", expectedClauses); + } +} -- cgit v1.2.3-54-g00ecf