This commit is contained in:
Tibo De Peuter 2025-05-02 23:50:29 +02:00
parent 80fb3d1e60
commit 5bfa1691dd
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
10 changed files with 276 additions and 17 deletions

8
.idea/2025LogProg-PrologInterpreter.iml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/examples" type="java-resource" />
</content>
</component>
</module>

8
.idea/modules.xml generated Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/2025LogProg-PrologInterpreter.iml" filepath="$PROJECT_DIR$/.idea/2025LogProg-PrologInterpreter.iml" />
</modules>
</component>
</project>

View file

@ -1,5 +1,4 @@
import com.xenomachina.argparser.ArgParser import com.xenomachina.argparser.ArgParser
import com.xenomachina.argparser.mainBody
import interpreter.FileLoader import interpreter.FileLoader
import io.GhentPrologArgParser import io.GhentPrologArgParser
import io.Logger import io.Logger

View file

@ -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) term.functor == "between/3" && args.all { it is Expression } -> Between(args[0] as Expression, args[1] as Expression, args[2] as Expression)
// Database // Database
term.functor == "retract/1" -> Retract(args[0])
term.functor == "assert/1" -> { term.functor == "assert/1" -> {
if (args[0] is Rule) { if (args[0] is Rule) {
Assert(args[0] as Rule) Assert(args[0] as Rule)
@ -99,7 +100,20 @@ open class Preprocessor {
Assert(Fact(args[0] as Head)) 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 // Other
term.functor == "write/1" -> Write(args[0]) term.functor == "write/1" -> Write(args[0])

View file

@ -13,7 +13,7 @@ import prolog.ast.terms.Goal
*/ */
object Program : Resolvent { object Program : Resolvent {
val internalDb = Database("") val internalDb = Database("")
private val databases: MutableList<Database> = mutableListOf(internalDb) val databases: MutableList<Database> = mutableListOf(internalDb)
var storeNewLine: Boolean = false var storeNewLine: Boolean = false
var variableRenamingStart: Int = 0 var variableRenamingStart: Int = 0

View file

@ -8,4 +8,4 @@ abstract class Substitution(val from: Term, val to: Term) {
} }
typealias Substitutions = Map<Term, Term> typealias Substitutions = Map<Term, Term>
typealias Answer = Result<Substitutions> typealias Answer = Result<Substitutions>
typealias Answers = Sequence<Answer> typealias Answers = Sequence<Answer>

View file

@ -8,6 +8,11 @@ import prolog.ast.terms.Structure
import prolog.ast.logic.Predicate import prolog.ast.logic.Predicate
import prolog.Program import prolog.Program
import prolog.ast.terms.Functor 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) { class Assert(clause: Clause) : AssertZ(clause) {
override val functor: Functor = "assert/1" 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]. * 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 { override fun satisfy(subs: Substitutions): Answers {
// Add clause to the program // Add clause to the program
Program.load(listOf(clause), 0) 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]. * 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 { override fun satisfy(subs: Substitutions): Answers {
// Add clause to the program // Add clause to the program
Program.load(listOf(clause)) Program.load(listOf(clause))
@ -36,3 +41,43 @@ open class AssertZ(val clause: Clause) : Structure(Atom("assertz"), listOf(claus
return sequenceOf(Result.success(emptyMap())) 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
}
)
}
}
}
}
}

View file

@ -42,10 +42,9 @@ class Repl {
val iterator = answers.iterator() val iterator = answers.iterator()
if (!iterator.hasNext()) { if (!iterator.hasNext()) {
io.say("false.") io.say("false.\n")
} else { } else {
var previous = iterator.next() io.say(prettyPrint(iterator.next()))
io.say(prettyPrint(previous))
while (iterator.hasNext()) { while (iterator.hasNext()) {
var command = io.prompt("") var command = io.prompt("")
@ -57,8 +56,7 @@ class Repl {
when (command) { when (command) {
";" -> { ";" -> {
previous = iterator.next() io.say(prettyPrint(iterator.next()))
io.say(prettyPrint(previous))
} }
"a" -> return "a" -> return
"." -> return "." -> return
@ -88,7 +86,7 @@ class Repl {
val subs = result.getOrNull()!! val subs = result.getOrNull()!!
if (subs.isEmpty()) { if (subs.isEmpty()) {
io.checkNewLine() io.checkNewLine()
return "true.\n" return "true."
} }
return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" }
}, },

View file

@ -529,5 +529,80 @@ class PreprocessorTests {
assertEquals(expected, result) 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)
}
} }
} }

View file

@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource
import prolog.Program import prolog.Program
import prolog.ast.logic.Clause import prolog.ast.logic.Clause
import prolog.ast.logic.Fact import prolog.ast.logic.Fact
import prolog.ast.logic.Predicate
import prolog.ast.logic.Rule import prolog.ast.logic.Rule
import prolog.ast.terms.Atom import prolog.ast.terms.Atom
import prolog.ast.terms.Structure import prolog.ast.terms.Structure
@ -16,6 +17,11 @@ import prolog.ast.terms.Variable
import kotlin.test.assertTrue import kotlin.test.assertTrue
class DatabaseOperatorsTests { class DatabaseOperatorsTests {
@BeforeEach
fun setup() {
Program.clear()
}
abstract class AssertTestsBase<T : Structure> { abstract class AssertTestsBase<T : Structure> {
protected abstract fun createAssert(clause: Clause): Structure protected abstract fun createAssert(clause: Clause): Structure
@ -114,7 +120,111 @@ class DatabaseOperatorsTests {
} }
@Test @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 query = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza")))
var result = Program.query(query).toList() var result = Program.query(query).toList()
@ -150,10 +260,12 @@ class DatabaseOperatorsTests {
assertEquals(Atom("bob"), result1[Variable("X")], "Expected bob") assertEquals(Atom("bob"), result1[Variable("X")], "Expected bob")
assertEquals(Atom("sushi"), result1[Variable("Y")], "Expected sushi") assertEquals(Atom("sushi"), result1[Variable("Y")], "Expected sushi")
assert = AssertA(Rule( assert = AssertA(
Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))), Rule(
Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza"))) Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))),
)) Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza")))
)
)
assert.satisfy(emptyMap()) assert.satisfy(emptyMap())
result = Program.query(query).toList() result = Program.query(query).toList()