diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 97edd8f..5049fe3 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -65,6 +65,7 @@ open class Preprocessor { Functor.of("clause/2") -> ClauseOp(args[0] as Head, args[1] as Body) Functor.of("atomic/1") -> AtomicOp(args[0]) Functor.of("compound/1") -> CompoundOp(args[0]) + Functor.of("univ/2") -> Univ(args[0], args[1]) // Arithmetic Functor.of("inf/0") -> Integer(Int.MAX_VALUE) diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 0473200..98a7147 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -87,7 +87,7 @@ open class TermsGrammar : Tokens() { t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } } - protected val op700: Parser by (equivalent or notEquivalent or equals or notEquals or isOp) use { text } + protected val op700: Parser by (univOp or equivalent or notEquivalent or equals or notEquals or isOp) use { text } protected val term700: Parser by (term500 * optional(op700 * term500)) use { if (t2 == null) t1 else CompoundTerm(Atom(t2!!.t1), listOf(t1, t2!!.t2)) } diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index 018417f..3f54cba 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -20,6 +20,7 @@ abstract class Tokens : Grammar() { // 1000 protected val comma: Token by literalToken(",") // 700 + protected val univOp: Token by literalToken("=..") protected val notEquivalent: Token by literalToken("\\==") protected val equivalent: Token by literalToken("==") protected val equals: Token by literalToken("=") diff --git a/src/prolog/ast/lists/List.kt b/src/prolog/ast/lists/List.kt new file mode 100644 index 0000000..494aa08 --- /dev/null +++ b/src/prolog/ast/lists/List.kt @@ -0,0 +1,57 @@ +package prolog.ast.lists + +import prolog.Substitutions +import prolog.ast.terms.Term +import prolog.ast.arithmetic.Integer + +sealed class List : Term { + abstract val head: Term + open val tail: List? = null + val size: Integer + get() = when (this) { + is Empty -> Integer(0) + is Cons -> Integer(1 + tail.size.value) + } + + fun isEmpty(): Boolean = this is Empty + + /** + * The empty list [] + */ + object Empty : List() { + override val head = this + override fun applySubstitution(subs: Substitutions): Empty = this + override fun toString(): String = "[]" + } + + class Cons(override val head: Term, override var tail: List) : List() { + override fun applySubstitution(subs: Substitutions): List { + val newHead = head.applySubstitution(subs) + val newTail = tail.applySubstitution(subs) as List + return Cons(newHead, newTail) + } + + override fun toString(): String { + val values = mutableListOf() + var current: List? = this + while (current != null && current !is Empty) { + values.add(current.head) + current = current.tail + } + return "[${values.joinToString(", ")}]" + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (other !is List) return false + if (this is Empty && other is Empty) return true + if (this is Cons && other is Cons) { + return this.head == other.head && this.tail == other.tail + } + return false + } + + override fun hashCode(): Int = super.hashCode() +} diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index a2af1f3..d2aa5c9 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -39,28 +39,25 @@ abstract class Clause(var head: Head, var body: Body) : Term, Resolvent { // Filter the substitutions to only include those that map from head to goal val headToGoalSubs = headAndGoalSubs.filterKeys { it in headRenaming.values } val goalToHeadSubs = headAndGoalSubs.filterKeys { occurs(it as Variable, simplifiedGoal) } + val headResult = headToGoalSubs.filterKeys { key -> + goalToHeadSubs.any { occurs(key as Variable, it.value) } + } val newSubs = headRenaming + headToGoalSubs body.satisfy(newSubs).forEach { bodyAnswer -> bodyAnswer.fold( onSuccess = { bodySubs -> // If the body can be proven, yield the (combined) substitutions - val goalToHeadResult = goalToHeadSubs.mapValues { applySubstitution(it.value, bodySubs) } - val headResult = headToGoalSubs.filterKeys { key -> - goalToHeadSubs.any { - occurs( - key as Variable, - it.value - ) - } - } - yield(Result.success(goalToHeadResult + headResult)) + val goalResult = goalToHeadSubs.mapValues { applySubstitution(it.value, bodySubs) } + yield(Result.success(goalResult + headResult)) }, onFailure = { error -> if (error is AppliedCut) { // Find single solution and return immediately if (error.subs != null) { - var result = goalToHeadSubs.mapValues { applySubstitution(it.value, error.subs) } - yield(Result.failure(AppliedCut(result))) + val goalResult = goalToHeadSubs.mapValues { + applySubstitution(it.value, error.subs) + } + yield(Result.failure(AppliedCut(goalResult + headResult))) } else { yield(Result.failure(AppliedCut())) } diff --git a/src/prolog/ast/terms/Atom.kt b/src/prolog/ast/terms/Atom.kt index fccf9fc..42c12c5 100644 --- a/src/prolog/ast/terms/Atom.kt +++ b/src/prolog/ast/terms/Atom.kt @@ -11,7 +11,7 @@ open class Atom(val name: String) : Goal(), Head, Body, Resolvent { override fun solve(goal: Goal, subs: Substitutions): Answers = unifyLazy(goal, this, subs) - override fun applySubstitution(subs: Substitutions): Atom = Atom(name) + override fun applySubstitution(subs: Substitutions): Atom = this override fun toString(): String { return name diff --git a/src/prolog/builtins/analysisOperators.kt b/src/prolog/builtins/analysisOperators.kt index 4aff69e..2eb81a6 100644 --- a/src/prolog/builtins/analysisOperators.kt +++ b/src/prolog/builtins/analysisOperators.kt @@ -7,6 +7,9 @@ import prolog.ast.arithmetic.Integer import prolog.ast.terms.* import prolog.ast.logic.Clause import prolog.logic.* +import prolog.ast.lists.List +import prolog.ast.lists.List.Empty +import prolog.ast.lists.List.Cons /** * [True] when [Term] is a term with [Functor] Name/Arity. @@ -66,15 +69,13 @@ class Arg(private val arg: Term, private val term: Term, private val value: Term // Value will be unified with the successive arguments of term. // On successful unification, arg is unified with the argument number. // Backtracking yields alternative solutions. - var count = 0 - for (argument in t.arguments) { + for ((count, argument) in t.arguments.withIndex()) { unifyLazy(value, argument, subs).forEach { result -> result.map { val sub = arg to Integer(count + 1) yield(Result.success(it + sub)) } } - count++ } } @@ -94,6 +95,12 @@ class Arg(private val arg: Term, private val term: Term, private val value: Term } } } + + override fun applySubstitution(subs: Substitutions): Arg = Arg( + arg.applySubstitution(subs), + term.applySubstitution(subs), + value.applySubstitution(subs) + ) } /** @@ -118,11 +125,14 @@ class ClauseOp(private val head: Head, private val body: Body) : val clauseHead = clause.head val clauseBody = clause.body + val appliedHead = applySubstitution(head, subs) + val appliedBody = applySubstitution(body, subs) + // Unify the head of the clause with the head of the goal - unifyLazy(clauseHead, head, subs).forEach { result -> + unifyLazy(clauseHead, appliedHead, subs).forEach { result -> result.map { headSubs -> // Unify the body of the clause with the body of the goal - unifyLazy(clauseBody, body, headSubs).forEach { bodyResult -> + unifyLazy(clauseBody, appliedBody, headSubs).forEach { bodyResult -> bodyResult.map { bodySubs -> // Combine the substitutions from the head and body val combinedSubs = headSubs + bodySubs @@ -136,6 +146,11 @@ class ClauseOp(private val head: Head, private val body: Body) : yield(Result.success(emptyMap())) } } + + override fun applySubstitution(subs: Substitutions): ClauseOp = ClauseOp( + head.applySubstitution(subs) as Head, + body.applySubstitution(subs) as Body + ) } class AtomicOp(private val term: Term) : Operator(Atom("atomic"), null, term) { @@ -146,6 +161,8 @@ class AtomicOp(private val term: Term) : Operator(Atom("atomic"), null, term) { emptySequence() } } + + override fun applySubstitution(subs: Substitutions): AtomicOp = AtomicOp(term.applySubstitution(subs)) } class CompoundOp(private val term: Term) : Operator(Atom("compound"), null, term) { @@ -156,4 +173,75 @@ class CompoundOp(private val term: Term) : Operator(Atom("compound"), null, term emptySequence() } } + + override fun applySubstitution(subs: Substitutions): CompoundOp = CompoundOp(term.applySubstitution(subs)) +} + +open class Univ(private val term: Term, private val list: Term) : Operator(Atom("=.."), term, list) { + override fun satisfy(subs: Substitutions): Answers { + return when { + nonvariable(term, subs) && nonvariable(list, subs) -> { + val t = applySubstitution(term, subs) + val l = applySubstitution(list, subs) as List + unifyLazy(t, listToTerm(l), subs) + } + + variable(term, subs) && nonvariable(list, subs) -> { + val l = applySubstitution(list, subs) as List + val t = listToTerm(l) + unifyLazy(term, t, subs) + } + + nonvariable(term, subs) && variable(list, subs) -> { + val t = applySubstitution(term, subs) + val l = termToList(t) + unifyLazy(l, list, subs) + } + + else -> throw Exception("Arguments are not sufficiently instantiated") + } + } + + protected open fun listToTerm(list: List): Term { + return when { + list.size.value == 1 -> list.head + + list.size.value > 1 -> { + val head = list.head + val arguments = mutableListOf() + var tail: List? = list.tail + while (tail != null && tail !is Empty) { + arguments.add(tail.head) + tail = tail.tail + } + Structure(head as Atom, arguments) + } + + else -> throw IllegalStateException("List is empty") + } + } + + protected open fun termToList(term: Term): List { + return when (term) { + is Atom -> Cons(term, Empty) + + is Structure -> { + val head = term.functor.name + val arguments = term.arguments + // Construct the list by iterating over the arguments in reverse + var tail: List = Empty + for (i in arguments.size - 1 downTo 0) { + tail = Cons(arguments[i], tail) + } + return Cons(head, tail) + } + + else -> throw IllegalStateException("Term is not a valid structure") + } + } + + override fun applySubstitution(subs: Substitutions): Univ = Univ( + term.applySubstitution(subs), + list.applySubstitution(subs) + ) } diff --git a/src/prolog/builtins/listOperators.kt b/src/prolog/builtins/listOperators.kt new file mode 100644 index 0000000..4adbced --- /dev/null +++ b/src/prolog/builtins/listOperators.kt @@ -0,0 +1,27 @@ +package prolog.builtins + +import prolog.Answers +import prolog.Substitutions +import prolog.ast.lists.List +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty +import prolog.ast.terms.Atom +import prolog.ast.terms.Operator +import prolog.ast.terms.Term + +class Member(private val element: Term, private val list: List) : Operator(Atom("member"), element, list) { + private var solution: Operator = if (list is Empty) { + Unify(element, list) + } else if (list is Cons) { + Disjunction( + Unify(element, list.head), + Member(element, list.tail) + ) + } else { + throw IllegalArgumentException("Invalid list type: ${list::class.simpleName}") + } + + override fun satisfy(subs: Substitutions): Answers = solution.satisfy(subs) + + override fun toString(): String = "$element ∈ $list" +} diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 50560e0..23467c4 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -7,11 +7,15 @@ import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Number -import prolog.ast.logic.Clause +import prolog.ast.lists.List +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty import prolog.ast.logic.Fact import prolog.ast.logic.LogicOperator -import prolog.ast.logic.Rule -import prolog.ast.terms.* +import prolog.ast.terms.Atom +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.ast.terms.Variable // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { @@ -97,6 +101,31 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence } } + t1 is List && t2 is List -> { + if (equivalent(t1, t2, subs)) { + yield(Result.success(emptyMap())) + } else if (t1.size == t2.size) { + val head1 = t1.head + val head2 = t2.head + + unifyLazy(head1, head2, subs).forEach { headSubs -> + headSubs.map { headResult -> + if (t1 is Empty && t2 is Empty) { + yield(Result.success(headResult)) + } else if (t1 is Cons && t2 is Cons) { + val tail1 = t1.tail + val tail2 = t2.tail + unifyLazy(tail1, tail2, headResult).forEach { tailSubs -> + tailSubs.map { tailResult -> + yield(Result.success(headResult + tailResult)) + } + } + } + } + } + } + } + else -> {} } } @@ -122,6 +151,21 @@ fun equivalent(term1: Term, term2: Term, subs: Substitutions): Boolean { term1 is Variable && term2 is Variable -> term1 == term2 term1 is Variable -> term1 in subs && equivalent(subs[term1]!!, term2, subs) term2 is Variable -> term2 in subs && equivalent(subs[term2]!!, term1, subs) + term1 is List && term2 is List -> { + if (term1.isEmpty() && term2.isEmpty()) { + true + } else if (term1.isEmpty() || term2.isEmpty()) { + false + } else { + require(term1 is Cons && term2 is Cons) + val head1 = term1.head + val tail1 = term1.tail + val head2 = term2.head + val tail2 = term2.tail + equivalent(head1, head2, subs) && equivalent(tail1, tail2, subs) + } + } + else -> false } } diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt index d3b45e2..b39a0d8 100644 --- a/tests/parser/grammars/TermsGrammarTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -19,6 +19,8 @@ import prolog.ast.terms.Term import prolog.ast.terms.Variable import prolog.builtins.Is import prolog.logic.equivalent +import prolog.ast.lists.List.Empty +import prolog.ast.lists.List.Cons class TermsGrammarTests { private lateinit var parser: Grammar @@ -352,4 +354,49 @@ class TermsGrammarTests { ) } } + + @Nested + class Lists { + private lateinit var parser: Grammar + + @BeforeEach + fun setup() { + parser = TermsGrammar() as Grammar + } + + @Test + fun `parse empty list`() { + val input = "[]" + + val result = parser.parseToEnd(input) + + assertEquals(Empty, result, "Expected empty list") + } + + @Test + fun `parse non-empty list`() { + val input = "[a, b, c]" + + val result = parser.parseToEnd(input) + + assertEquals( + Cons(Atom("a"), Cons(Atom("b"), Cons(Atom("c"), Empty))), + result, + "Expected non-empty list" + ) + } + + @Test + fun `parse nested lists`() { + val input = "[a, [b, c], d]" + + val result = parser.parseToEnd(input) + + assertEquals( + Cons(Atom("a"), Cons(Cons(Atom("b"), Cons(Atom("c"), Empty)), Cons(Atom("d"), Empty))), + result, + "Expected nested lists" + ) + } + } } \ No newline at end of file diff --git a/tests/prolog/builtins/AnalysisOperatorsTests.kt b/tests/prolog/builtins/AnalysisOperatorsTests.kt index dbd3b03..a6866c3 100644 --- a/tests/prolog/builtins/AnalysisOperatorsTests.kt +++ b/tests/prolog/builtins/AnalysisOperatorsTests.kt @@ -7,13 +7,13 @@ import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import prolog.ast.Database.Program import prolog.ast.arithmetic.Integer +import prolog.ast.lists.List +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty import prolog.ast.logic.Fact import prolog.ast.logic.Rule -import prolog.ast.terms.CompoundTerm -import prolog.ast.terms.AnonymousVariable -import prolog.ast.terms.Atom -import prolog.ast.terms.Structure -import prolog.ast.terms.Variable +import prolog.ast.terms.* +import prolog.logic.equivalent class AnalysisOperatorsTests { @Test @@ -401,4 +401,246 @@ class AnalysisOperatorsTests { ) } } + + @Nested + class `Univ operator` { + @Test + fun `univ without variables`() { + val univ = Univ(Atom("foo"), Cons(Atom("foo"), Empty)) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertTrue(subs.isEmpty(), "Expected empty substitutions") + } + + @Test + fun `univ with variable list`() { + val list = Variable("List") + val univ = Univ( + CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b"))), + list + ) + val expected = Cons(Atom("foo"), Cons(Atom("a"), Cons(Atom("b"), Empty))) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(expected, subs[list], "Expected List to be $expected") + } + + @Test + fun `univ with variable in compound`() { + val list = Variable("List") + val univ = Univ( + CompoundTerm(Atom("foo"), listOf(Variable("X"))), + list + ) + val expected = Cons(Atom("foo"), Cons(Variable("X"), Empty)) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(expected, subs[list], "Expected List to be $expected") + } + + @Test + fun `univ with var foo`() { + val univ = Univ( + Variable("Term"), + Cons(Atom("foo"), Empty) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(Atom("foo"), subs[Variable("Term")], "Expected Term to be foo") + } + + @Test + fun `univ with var foo(a)`() { + val univ = Univ( + Variable("Term"), + Cons(Atom("foo"), Cons(Atom("a"), Empty)) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals( + Structure(Atom("foo"), listOf(Atom("a"))), + subs[Variable("Term")], + "Expected Term to be foo(a)" + ) + } + + @Test + fun `univ with var foo(X)`() { + val univ = Univ( + Variable("Term"), + Cons(Atom("foo"), Cons(Variable("X"), Empty)) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals( + Structure(Atom("foo"), listOf(Variable("X"))), + subs[Variable("Term")], + "Expected Term to be foo(X)" + ) + } + + @Test + fun `univ with var foo(a, b(X), c)`() { + val univ = Univ( + Variable("Term"), + Cons( + Atom("foo"), + Cons(Atom("a"), Cons(CompoundTerm(Atom("b"), listOf(Variable("X"))), Cons(Atom("c"), Empty))) + ) + ) + val expected = CompoundTerm( + Atom("foo"), + listOf(Atom("a"), CompoundTerm(Atom("b"), listOf(Variable("X"))), Atom("c")) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(expected, subs[Variable("Term")], "Expected Term to be $expected") + } + + @Test + fun `univ with unified var should return substitution`() { + val univ = Univ( + CompoundTerm( + Atom("foo"), listOf( + Atom("a"), + CompoundTerm(Atom("bar"), listOf(Variable("X"))), + Atom("c") + ) + ), + Cons( + Atom("foo"), + Cons(Atom("a"), Cons(CompoundTerm(Atom("bar"), listOf(Atom("b"))), Cons(Atom("c"), Empty))) + ) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(Atom("b"), subs[Variable("X")], "Expected X to be b") + } + + @Test + fun `univ of incompatible structures should fail`() { + val univ = Univ( + CompoundTerm(Atom("foo"), listOf(Atom("a"))), + Cons(Atom("bar"), Empty) + ) + + val result = univ.satisfy(emptyMap()).toList() + + assertEquals(0, result.size, "Expected 0 results") + } + + @Test + fun `univ with two vars should throw`() { + val univ = Univ(Variable("X"), Variable("Y")) + + val exception = assertThrows { + univ.satisfy(emptyMap()).toList() + } + assertEquals("Arguments are not sufficiently instantiated", exception.message) + } + + class OpenUniv(term: Term, list: Term) : Univ(term, list) { + public override fun listToTerm(list: List): Term = super.listToTerm(list) + public override fun termToList(term: Term): List = super.termToList(term) + } + + @Test + fun `a to term`() { + val univ = OpenUniv(Atom(""), Empty) + + val originalList = Cons(Atom("a"), Empty) + val originalTerm = Atom("a") + + val term = univ.listToTerm(originalList) + assertEquals(originalTerm, term, "Expected term to be a") + + val list = univ.termToList(term) + assertEquals(originalList, list, "Expected list to be [a]") + } + + @Test + fun `foo, bar to compound`() { + val univ = OpenUniv(Atom(""), Empty) + + val originalList = Cons(Atom("foo"), Cons(Atom("bar"), Empty)) + val originalTerm = CompoundTerm(Atom("foo"), listOf(Atom("bar"))) + + val term = univ.listToTerm(originalList) + assertEquals(originalTerm, term) + + val list = univ.termToList(term) + assertEquals(originalList, list) + } + + @Test + fun `foo, bar, baz to compound`() { + val univ = OpenUniv(Atom(""), Empty) + + val originalList = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Atom("baz"), Empty))) + val originalTerm = CompoundTerm(Atom("foo"), listOf(Atom("bar"), Atom("baz"))) + + val term = univ.listToTerm(originalList) + assertEquals(originalTerm, term) + + val list = univ.termToList(term) + assertEquals(originalList, list) + } + + @Test + fun `foo, bar, (baz, bar) to compound with list`() { + val univ = OpenUniv(Atom(""), Empty) + + val originalList = + Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("baz"), Cons(Atom("bar"), Empty)), Empty))) + val originalTerm = CompoundTerm( + Atom("foo"), + listOf(Atom("bar"), Cons(Atom("baz"), Cons(Atom("bar"), Empty))) + ) + + val term = univ.listToTerm(originalList) + assertTrue(equivalent(originalTerm, term, emptyMap()), "Expected term to be foo(bar, [baz, bar]))") + + val list = univ.termToList(term) + assertTrue(equivalent(originalList, list, emptyMap()), "Expected list to be [foo, bar, [baz, bar]]") + } + } } diff --git a/tests/prolog/builtins/ListOperatorsTests.kt b/tests/prolog/builtins/ListOperatorsTests.kt new file mode 100644 index 0000000..be1e521 --- /dev/null +++ b/tests/prolog/builtins/ListOperatorsTests.kt @@ -0,0 +1,60 @@ +package prolog.builtins + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty +import prolog.ast.terms.Atom + +class ListOperatorsTests { + @Test + fun `size empty list is 0`() { + assertEquals(0, Empty.size.value, "Expected size of empty list to be 0") + } + + @Test + fun `size of singleton is 1`() { + val list = Cons(Atom("a"), Empty) + + assertEquals(1, list.size.value, "Expected size of singleton list to be 1") + } + + @Test + fun `size of list with five elements is 5`() { + val list = Cons(Atom("a"), Cons(Atom("b"), Cons(Atom("c"), Cons(Atom("d"), Cons(Atom("e"), Empty))))) + + assertEquals(5, list.size.value, "Expected size of list with five elements to be 5") + } + + @Test + fun `member(a, list of a)`() { + val atom = Atom("a") + val list = Cons(atom, Empty) + + val member = Member(atom, list) + + val result = member.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected one solution") + assertTrue(result[0].isSuccess, "Expected success") + assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected empty substitution map") + } + + @Disabled("Not required functionality") + @Test + fun `member with variable in list`() { + val atom = Atom("a") + val variable = Atom("X") + val list = Cons(variable, Empty) + + val member = Member(atom, list) + + val result = member.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected one solution") + assertTrue(result[0].isSuccess, "Expected success") + assertEquals(atom, result[0].getOrNull()!![variable], "Expected variable to be unified with atom") + } +} \ No newline at end of file diff --git a/tests/prolog/logic/UnificationTests.kt b/tests/prolog/logic/UnificationTests.kt index 10a37e7..242ab85 100644 --- a/tests/prolog/logic/UnificationTests.kt +++ b/tests/prolog/logic/UnificationTests.kt @@ -10,13 +10,16 @@ import prolog.ast.terms.Variable import prolog.builtins.Add import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Nested +import prolog.ast.lists.List +import prolog.ast.lists.List.Empty +import prolog.ast.lists.List.Cons /* * Based on: https://en.wikipedia.org/wiki/Unification_%28computer_science%29#Examples_of_syntactic_unification_of_first-order_terms */ class UnificationTests { @Nested - class `unify` { + class `unify logic` { @Test fun identical_atoms_unify() { val atom1 = Atom("a") @@ -330,7 +333,7 @@ class UnificationTests { } @Nested - class `applySubstitution` { + class `applySubstitution logic` { @Test fun `apply substitution without sub`() { val term = Variable("X") @@ -367,4 +370,67 @@ class UnificationTests { assertEquals(Integer(35), result) } } + + @Nested + class `equivalent logic` { + @Test + fun `empty lists are equivalent`() { + val eq = equivalent(Empty, Empty, emptyMap()) + assertTrue(eq, "Empty lists should be equivalent") + } + + @Test + fun `singletons are equivalent`() { + val atom = Atom("a") + val list1 = Cons(atom, Empty) + val list2 = Cons(atom, Empty) + + val eq = equivalent(list1, list2, emptyMap()) + + assertTrue(eq, "Singleton lists should be equivalent") + } + + @Test + fun `singleton and empty list are not equivalent`() { + val atom = Atom("a") + val list1 = Cons(atom, Empty) + val list2 = Empty + + var eq = equivalent(list1, list2, emptyMap()) + assertFalse(eq, "Singleton and empty lists should not be equivalent") + + eq = equivalent(list2, list1, emptyMap()) + assertFalse(eq, "Empty and singleton lists should not be equivalent") + } + + @Test + fun `identical lists are equivalent`() { + val list1 = Cons(Atom("a"), Cons(Atom("b"), Empty)) + val list2 = Cons(Atom("a"), Cons(Atom("b"), Empty)) + + var eq = equivalent(list1, list2, emptyMap()) + assertTrue(eq, "Identical lists should be equivalent") + + eq = equivalent(list2, list1, emptyMap()) + assertTrue(eq, "Identical lists should be equivalent") + } + + @Test + fun `identical nested lists are equivalent`() { + val list1 = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("baz"), Cons(Atom("bar"), Empty)), Empty))) + val list2 = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("baz"), Cons(Atom("bar"), Empty)), Empty))) + + var eq = equivalent(list1, list2, emptyMap()) + assertTrue(eq, "Identical nested lists should be equivalent") + } + + @Test + fun `lists with different nests are not equivalent`() { + val list1 = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("bar"), Cons(Atom("baz"), Empty)), Empty))) + val list2 = Cons(Atom("foo"), Cons(Atom("bar"), Cons(Cons(Atom("baz"), Cons(Atom("bar"), Empty)), Empty))) + + var eq = equivalent(list1, list2, emptyMap()) + assertFalse(eq, "Lists with different nests should not be equivalent") + } + } } \ No newline at end of file