test: Cut not_equal

This commit is contained in:
Tibo De Peuter 2025-04-15 16:40:52 +02:00
parent 229a8bbc3c
commit 2fcab52f65
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
7 changed files with 75 additions and 18 deletions

View file

@ -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
if (error.subs != null) {
yield(Result.failure(AppliedCut(newHeadSubs + error.subs)))
} else {
yield(Result.failure(AppliedCut()))
}
return@sequence
} else {
yield(Result.failure(error))

View file

@ -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.subs != null) {
// If it's a cut, yield the result with the left substitutions
yield(Result.failure(AppliedCut(it.subs)))
yield(Result.success(it.subs))
}
return@sequence
} else {
yield(Result.failure(it))

View file

@ -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,14 +56,18 @@ 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 (exception.subs != null) {
// If it's a cut, yield the result with the left substitutions
yield(Result.failure(AppliedCut(leftSubs + exception.subs)))
return@sequence
} else {
yield(Result.failure(AppliedCut()))
}
return@sequence
}
// 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))

View file

@ -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 {

View file

@ -1,5 +0,0 @@
package prolog.exceptions
import prolog.Substitutions
class AppliedCut(val subs: Substitutions): Exception()

View file

@ -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()

View file

@ -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"))