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