From 2fcab52f656baee962956553f81e5a34d713aeb9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 15 Apr 2025 16:40:52 +0200 Subject: [PATCH] test: Cut not_equal --- src/prolog/ast/logic/Clause.kt | 8 ++++-- src/prolog/ast/logic/Predicate.kt | 8 ++++-- src/prolog/builtins/controlOperators.kt | 20 +++++++------ src/prolog/builtins/unificationOperators.kt | 13 +++++++++ src/prolog/exceptions/AppliedCut.kt | 5 ---- src/prolog/flags/AppliedCut.kt | 11 ++++++++ .../prolog/builtins/ControlOperatorsTests.kt | 28 +++++++++++++++++++ 7 files changed, 75 insertions(+), 18 deletions(-) delete mode 100644 src/prolog/exceptions/AppliedCut.kt create mode 100644 src/prolog/flags/AppliedCut.kt diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index 98a5a6b..38aa3a0 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -7,7 +7,7 @@ import prolog.ast.terms.Functor import prolog.ast.terms.Goal import prolog.ast.terms.Head import prolog.builtins.True -import prolog.exceptions.AppliedCut +import prolog.flags.AppliedCut import prolog.logic.unifyLazy /** @@ -36,7 +36,11 @@ abstract class Clause(private val head: Head, private val body: Body) : Resolven onFailure = { error -> if (error is AppliedCut) { // Find single solution and return immediately - yield(Result.failure(AppliedCut(newHeadSubs + error.subs))) + if (error.subs != null) { + yield(Result.failure(AppliedCut(newHeadSubs + error.subs))) + } else { + yield(Result.failure(AppliedCut())) + } return@sequence } else { yield(Result.failure(error)) diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 9b8fb64..f25d3d5 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -4,7 +4,7 @@ import prolog.Answers import prolog.Substitutions import prolog.ast.terms.Functor import prolog.ast.terms.Goal -import prolog.exceptions.AppliedCut +import prolog.flags.AppliedCut /** * Collection of [Clause]s with the same [Functor]. @@ -62,8 +62,10 @@ class Predicate : Resolvent { }, onFailure = { if (it is AppliedCut) { - // If it's a cut, yield the result with the left substitutions - yield(Result.failure(AppliedCut(it.subs))) + if (it.subs != null) { + // If it's a cut, yield the result with the left substitutions + yield(Result.success(it.subs)) + } return@sequence } else { yield(Result.failure(it)) diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 36873b2..791d029 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -7,7 +7,7 @@ import prolog.ast.terms.Atom import prolog.ast.terms.Body import prolog.ast.terms.Goal import prolog.ast.logic.LogicOperator -import prolog.exceptions.AppliedCut +import prolog.flags.AppliedCut /** * Always fail. @@ -56,13 +56,17 @@ class Conjunction(private val left: LogicOperand, private val right: LogicOperan // If the right part fails, check if it's a cut onFailure = { exception -> if (exception is AppliedCut) { - // If it's a cut, yield the result with the left substitutions - yield(Result.failure(AppliedCut(leftSubs + exception.subs))) + if (exception.subs != null) { + // If it's a cut, yield the result with the left substitutions + yield(Result.failure(AppliedCut(leftSubs + exception.subs))) + } else { + yield(Result.failure(AppliedCut())) + } return@sequence - } else { - // If it's not a cut, yield the failure - yield(Result.failure(exception)) } + + // If it's not a cut, yield the failure + yield(Result.failure(exception)) } ) } @@ -71,7 +75,7 @@ class Conjunction(private val left: LogicOperand, private val right: LogicOperan onFailure = { exception -> // 1. If the left part is a cut, satisfy the right part ONCE, and stop searching for more solutions if (exception is AppliedCut) { - right.satisfy(subs + exception.subs).first().fold( + right.satisfy(subs + (exception.subs!!)).firstOrNull()?.fold( onSuccess = { // If the right part succeeds, yield the result with the left substitutions yield(Result.success(exception.subs + it)) @@ -81,7 +85,7 @@ class Conjunction(private val left: LogicOperand, private val right: LogicOperan // If the right part fails, yield the failure yield(Result.failure(it)) } - ) + ) ?: yield(Result.failure(AppliedCut())) } else { // 2. Any other failure should be returned as is yield(Result.failure(exception)) diff --git a/src/prolog/builtins/unificationOperators.kt b/src/prolog/builtins/unificationOperators.kt index c45de52..fc40df7 100644 --- a/src/prolog/builtins/unificationOperators.kt +++ b/src/prolog/builtins/unificationOperators.kt @@ -12,6 +12,19 @@ import prolog.ast.terms.Operator import prolog.ast.terms.Term import prolog.logic.applySubstitution import prolog.logic.equivalent +import prolog.logic.unifyLazy + +/** + * Unify Term1 with Term2. True if the unification succeeds. + */ +class Unify(private val term1: Term, private val term2: Term): Operator(Atom("="), term1, term2) { + override fun satisfy(subs: Substitutions): Answers = sequence { + val t1 = applySubstitution(term1, subs) + val t2 = applySubstitution(term2, subs) + + yieldAll(unifyLazy(t1, t2, subs)) + } +} class Equivalent(private val term1: Term, private val term2: Term) : Operator(Atom("=="), term1, term2) { override fun satisfy(subs: Substitutions): Answers = sequence { diff --git a/src/prolog/exceptions/AppliedCut.kt b/src/prolog/exceptions/AppliedCut.kt deleted file mode 100644 index d1d492a..0000000 --- a/src/prolog/exceptions/AppliedCut.kt +++ /dev/null @@ -1,5 +0,0 @@ -package prolog.exceptions - -import prolog.Substitutions - -class AppliedCut(val subs: Substitutions): Exception() diff --git a/src/prolog/flags/AppliedCut.kt b/src/prolog/flags/AppliedCut.kt new file mode 100644 index 0000000..02c851a --- /dev/null +++ b/src/prolog/flags/AppliedCut.kt @@ -0,0 +1,11 @@ +package prolog.flags + +import prolog.Substitutions + +/** + * An exception that indicates that a cut has been applied in the Prolog engine. + * + * @param subs The substitutions that were in effect when the cut was applied. + * If null, it means that the cut was applied on a failed branch. + */ +class AppliedCut(val subs: Substitutions? = null): Throwable() diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt index 9aaab2f..7b5be77 100644 --- a/tests/prolog/builtins/ControlOperatorsTests.kt +++ b/tests/prolog/builtins/ControlOperatorsTests.kt @@ -17,6 +17,8 @@ class ControlOperatorsTests { Program.clear() } + // See also: https://stackoverflow.com/a/23292126 + @Test fun `simple cut program`() { // First an example without cut @@ -149,6 +151,32 @@ class ControlOperatorsTests { assertEquals(1, result.size, "Expected 1 result") } + @Test + fun `not_equal cut test`() { + Program.load( + listOf( + Rule( + CompoundTerm(Atom("not_equal"), listOf(Variable("X"), Variable("Y"))), + Conjunction( + Unify(Variable("X"), Variable("Y")), + Conjunction(Cut(), Fail) + ) + ), + Fact(CompoundTerm(Atom("not_equal"), listOf(Variable("_1"), Variable("_2")))) + ) + ) + + var goal = CompoundTerm(Atom("not_equal"), listOf(Integer(1), Integer(1))) + var result = Program.query(goal).toList() + + assertTrue(result.none(), "Expected no results") + + goal = CompoundTerm(Atom("not_equal"), listOf(Integer(1), Integer(2))) + result = Program.query(goal).toList() + + assertEquals(1, result.size, "Expected 1 result") + } + @Test fun not_atom() { val success = Fact(Atom("a"))