diff --git a/.idea/2025LogProg-PrologInterpreter.iml b/.idea/2025LogProg-PrologInterpreter.iml new file mode 100644 index 0000000..42d53f5 --- /dev/null +++ b/.idea/2025LogProg-PrologInterpreter.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5b434ac --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Main.kt b/src/Main.kt index 55a12bf..2416fdb 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,5 +1,4 @@ import com.xenomachina.argparser.ArgParser -import com.xenomachina.argparser.mainBody import interpreter.FileLoader import io.GhentPrologArgParser import io.Logger diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 17e5dc9..7477702 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -92,6 +92,7 @@ open class Preprocessor { term.functor == "between/3" && args.all { it is Expression } -> Between(args[0] as Expression, args[1] as Expression, args[2] as Expression) // Database + term.functor == "retract/1" -> Retract(args[0]) term.functor == "assert/1" -> { if (args[0] is Rule) { Assert(args[0] as Rule) @@ -99,7 +100,20 @@ open class Preprocessor { Assert(Fact(args[0] as Head)) } } - term.functor == "asserta/1" -> AssertA(args[0] as Clause) + term.functor == "asserta/1" -> { + if (args[0] is Rule) { + AssertA(args[0] as Rule) + } else { + AssertA(Fact(args[0] as Head)) + } + } + term.functor == "assertz/1" -> { + if (args[0] is Rule) { + AssertZ(args[0] as Rule) + } else { + AssertZ(Fact(args[0] as Head)) + } + } // Other term.functor == "write/1" -> Write(args[0]) diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index c7f679a..e82b0bf 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -13,7 +13,7 @@ import prolog.ast.terms.Goal */ object Program : Resolvent { val internalDb = Database("") - private val databases: MutableList = mutableListOf(internalDb) + val databases: MutableList = mutableListOf(internalDb) var storeNewLine: Boolean = false var variableRenamingStart: Int = 0 diff --git a/src/prolog/Substitution.kt b/src/prolog/Substitution.kt index 062d63e..9058f8c 100644 --- a/src/prolog/Substitution.kt +++ b/src/prolog/Substitution.kt @@ -8,4 +8,4 @@ abstract class Substitution(val from: Term, val to: Term) { } typealias Substitutions = Map typealias Answer = Result -typealias Answers = Sequence \ No newline at end of file +typealias Answers = Sequence diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index ec13779..a002bfa 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -8,6 +8,11 @@ import prolog.ast.terms.Structure import prolog.ast.logic.Predicate import prolog.Program import prolog.ast.terms.Functor +import prolog.ast.terms.Term +import prolog.ast.logic.Fact +import prolog.ast.Database +import prolog.ast.terms.Operator +import prolog.logic.unifyLazy class Assert(clause: Clause) : AssertZ(clause) { override val functor: Functor = "assert/1" @@ -16,7 +21,7 @@ class Assert(clause: Clause) : AssertZ(clause) { /** * Assert a [Clause] as a first clause of the [Predicate] into the [Program]. */ -class AssertA(val clause: Clause) : Structure(Atom("asserta"), listOf(clause)) { +class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program Program.load(listOf(clause), 0) @@ -28,7 +33,7 @@ class AssertA(val clause: Clause) : Structure(Atom("asserta"), listOf(clause)) { /** * Assert a [Clause] as a last clause of the [Predicate] into the [Program]. */ -open class AssertZ(val clause: Clause) : Structure(Atom("assertz"), listOf(clause)) { +open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program Program.load(listOf(clause)) @@ -36,3 +41,43 @@ open class AssertZ(val clause: Clause) : Structure(Atom("assertz"), listOf(claus return sequenceOf(Result.success(emptyMap())) } } + +/** + * When [Term] is an [Atom] or a term, it is unified with the first unifying [Clause] in the [Database]. + * The [Fact] or Clause is removed from the Database. It respects the logical update view. + * + * @see [SWI-Prolog Predicate retract/1](https://www.swi-prolog.org/pldoc/doc_for?object=retract/1) + */ +class Retract(val term: Term) : Operator(Atom("retract"), null, term) { + override fun satisfy(subs: Substitutions): Answers = sequence { + // Check that term is a structure or atom + if (term !is Structure && term !is Atom) { + yield(Result.failure(Exception("Cannot retract a non-structure or non-atom"))) + return@sequence + } + + val functorName = term.functor + + Program.databases + .filter { it.predicates.containsKey(functorName) } + .mapNotNull { it.predicates[functorName] } + .map { predicate -> + val clausesIterator = predicate.clauses.iterator() + while (clausesIterator.hasNext()) { + val clause = clausesIterator.next() + unifyLazy(term, clause.head, subs).forEach { unifyResult -> + unifyResult.fold( + onSuccess = { substitutions -> + // If unification is successful, remove the clause + yield(Result.success(substitutions)) + clausesIterator.remove() + }, + onFailure = { + // If unification fails, do nothing + } + ) + } + } + } + } +} diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index a7c684a..aae1390 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -42,10 +42,9 @@ class Repl { val iterator = answers.iterator() if (!iterator.hasNext()) { - io.say("false.") + io.say("false.\n") } else { - var previous = iterator.next() - io.say(prettyPrint(previous)) + io.say(prettyPrint(iterator.next())) while (iterator.hasNext()) { var command = io.prompt("") @@ -57,8 +56,7 @@ class Repl { when (command) { ";" -> { - previous = iterator.next() - io.say(prettyPrint(previous)) + io.say(prettyPrint(iterator.next())) } "a" -> return "." -> return @@ -88,7 +86,7 @@ class Repl { val subs = result.getOrNull()!! if (subs.isEmpty()) { io.checkNewLine() - return "true.\n" + return "true." } return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } }, diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index d0467c6..0b98ff4 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -529,5 +529,80 @@ class PreprocessorTests { assertEquals(expected, result) } + @Test + fun `asserta(fact)`() { + val input = Structure( + Atom("asserta"), listOf( + Structure( + Atom(":-"), listOf( + Atom("a"), + Atom("b") + ) + ) + ) + ) + val expected = AssertA( + Rule( + Atom("a"), + Atom("b") + ) + ) + + val result = preprocessor.preprocess(input) + + assertEquals(expected, result) + } + + @Test + fun `assertz(fact)`() { + val input = Structure( + Atom("assertz"), listOf( + Structure( + Atom(":-"), listOf( + Atom("a"), + Atom("b") + ) + ) + ) + ) + val expected = AssertZ( + Rule( + Atom("a"), + Atom("b") + ) + ) + + val result = preprocessor.preprocess(input) + + assertEquals(expected, result) + } + + @Test + fun `retract(atom)`() { + val input = Structure( + Atom("retract"), listOf( + Atom("a") + ) + ) + val expected = Retract(Atom("a")) + + val result = preprocessor.preprocess(input) + + assertEquals(expected, result) + } + + @Test + fun `retract(compund with variable)`() { + val input = Structure( + Atom("retract"), listOf( + CompoundTerm(Atom("a"), listOf(Variable("X"))) + ) + ) + val expected = Retract(CompoundTerm(Atom("a"), listOf(Variable("X")))) + + val result = preprocessor.preprocess(input) + + assertEquals(expected, result) + } } } diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index f3d7c9a..a14453d 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource import prolog.Program import prolog.ast.logic.Clause import prolog.ast.logic.Fact +import prolog.ast.logic.Predicate import prolog.ast.logic.Rule import prolog.ast.terms.Atom import prolog.ast.terms.Structure @@ -16,6 +17,11 @@ import prolog.ast.terms.Variable import kotlin.test.assertTrue class DatabaseOperatorsTests { + @BeforeEach + fun setup() { + Program.clear() + } + abstract class AssertTestsBase { protected abstract fun createAssert(clause: Clause): Structure @@ -114,7 +120,111 @@ class DatabaseOperatorsTests { } @Test - fun `custom example`() { + fun `retract fails silently for unknown predicates`() { + val retract = Retract(Atom("unknown")) + val result = retract.satisfy(emptyMap()) + + assertTrue(result.none(), "Expected no results") + } + + @Test + fun `simple retract`() { + val predicate = Predicate(listOf(Fact(Atom("a")))) + Program.internalDb.load(predicate) + + assertEquals(1, Program.query(Atom("a")).count()) + + val retract = Retract(Atom("a")) + + assertTrue(retract.satisfy(emptyMap()).any(), "Expected 1 result") + assertEquals(0, predicate.clauses.size, "Expected 0 clauses") + + assertTrue(retract.satisfy(emptyMap()).none()) + } + + @Test + fun `retract atom`() { + val predicate = Predicate(listOf( + Fact(Atom("a")), + Fact(Atom("a")), + Fact(Atom("a")) + )) + Program.internalDb.load(predicate) + + val control = Program.query(Atom("a")).toList() + + assertEquals(3, control.size, "Expected 3 results") + + val retract = Retract(Atom("a")) + + val result = retract.satisfy(emptyMap()) + + assertEquals(3, predicate.clauses.size, "Expected 3 clauses") + + var answer = result.first() + + assertTrue(answer.isSuccess, "Expected success") + var subs = answer.getOrNull()!! + assertTrue(subs.isEmpty(), "Expected no substitutions") + assertEquals(2, predicate.clauses.size, "Expected 2 clauses") + + assertTrue(result.first().isSuccess) + assertTrue(result.first().isSuccess) + + assertEquals(0, predicate.clauses.size, "Expected no remaining clauses") + } + + @Test + fun `retract compound with variable`() { + val predicate = Predicate(listOf( + Fact(Structure(Atom("a"), listOf(Atom("b")))), + Fact(Structure(Atom("a"), listOf(Atom("c")))), + Fact(Structure(Atom("a"), listOf(Atom("d")))) + )) + Program.internalDb.load(predicate) + + val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() + + assertEquals(3, control.size, "Expected 3 results") + + val retract = Retract(Structure(Atom("a"), listOf(Variable("X")))) + + val result = retract.satisfy(emptyMap()) + + assertEquals(3, predicate.clauses.size, "Expected 3 clauses") + + var answer = result.first() + + assertTrue(answer.isSuccess, "Expected success") + var subs = answer.getOrNull()!! + assertTrue(subs.isNotEmpty(), "Expected substitutions") + assertTrue(Variable("X") in subs, "Expected variable X") + assertEquals(Atom("b"), subs[Variable("X")], "Expected b") + assertEquals(2, predicate.clauses.size, "Expected 2 clauses") + + answer = result.first() + + assertTrue(answer.isSuccess, "Expected success") + subs = answer.getOrNull()!! + assertTrue(subs.isNotEmpty(), "Expected substitutions") + assertTrue(Variable("X") in subs, "Expected variable X") + assertEquals(Atom("c"), subs[Variable("X")], "Expected c") + assertEquals(1, predicate.clauses.size, "Expected 1 clause") + + answer = result.first() + + assertTrue(answer.isSuccess, "Expected success") + subs = answer.getOrNull()!! + assertTrue(subs.isNotEmpty(), "Expected substitutions") + assertTrue(Variable("X") in subs, "Expected variable X") + assertEquals(Atom("d"), subs[Variable("X")], "Expected d") + assertEquals(0, predicate.clauses.size, "Expected no clauses") + + assertEquals(0, result.count(), "Expected no remaining results") + } + + @Test + fun `custom assert example`() { var query = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))) var result = Program.query(query).toList() @@ -150,10 +260,12 @@ class DatabaseOperatorsTests { assertEquals(Atom("bob"), result1[Variable("X")], "Expected bob") assertEquals(Atom("sushi"), result1[Variable("Y")], "Expected sushi") - assert = AssertA(Rule( - Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))), - Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza"))) - )) + assert = AssertA( + Rule( + Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))), + Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza"))) + ) + ) assert.satisfy(emptyMap()) result = Program.query(query).toList()