diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 6c17705..242a6ac 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -94,6 +94,7 @@ open class Preprocessor { // Database term.functor == "dynamic/1" -> Dynamic((args[0] as Atom).name) term.functor == "retract/1" -> Retract(args[0]) + term.functor == "retractall/1" -> RetractAll(args[0]) term.functor == "assert/1" -> { if (args[0] is Rule) { Assert(args[0] as Rule) @@ -138,4 +139,4 @@ open class Preprocessor { return prepped } -} \ No newline at end of file +} diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index 81e31aa..ea36376 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -1,5 +1,6 @@ package prolog.builtins +import io.Logger import prolog.Answers import prolog.Substitutions import prolog.ast.logic.Clause @@ -86,7 +87,7 @@ open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) * * @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) { +open 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) { @@ -101,6 +102,12 @@ class Retract(val term: Term) : Operator(Atom("retract"), null, term) { return@sequence } + // Check if the predicate is dynamic + if (!predicate.dynamic) { + yield(Result.failure(Exception("No permission to modify static procedure '$functorName'"))) + return@sequence + } + predicate.clauses.toList().forEach { clause -> unifyLazy(term, clause.head, subs).forEach { unifyResult -> unifyResult.fold( @@ -119,3 +126,29 @@ class Retract(val term: Term) : Operator(Atom("retract"), null, term) { override fun applySubstitution(subs: Substitutions): Retract = Retract(applySubstitution(term, subs)) } + +class RetractAll(term: Term) : Retract(term) { + override fun satisfy(subs: Substitutions): Answers { + // Check that term is a structure or atom + if (term !is Structure && term !is Atom) { + return sequenceOf(Result.failure(Exception("Cannot retract a non-structure or non-atom"))) + } + + // If the predicate does not exist, implicitly create it + val functor = term.functor + val predicate = Program.db.predicates[functor] + if (predicate == null) { + Logger.debug("Predicate $functor not found, creating it") + Program.db.predicates += functor to Predicate(functor, dynamic = true) + } + + // Propagate errors from the super class + super.satisfy(subs).forEach { + if (it.isFailure) { + return sequenceOf(it) + } + } + + return sequenceOf(Result.success(emptyMap())) + } +} diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 073d76e..4db4fbc 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -95,9 +95,7 @@ class Repl { return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } }, onFailure = { - val text = "Failure: ${it.message}" - Logger.warn(text) - return text + return "ERROR: ${it.message}" } ) } diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index aa5b7b1..73cfef0 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -15,6 +15,7 @@ import prolog.ast.logic.Fact import prolog.ast.logic.Predicate import prolog.ast.logic.Rule import prolog.ast.terms.Atom +import prolog.ast.terms.Functor import prolog.ast.terms.Structure import prolog.ast.terms.Variable @@ -131,7 +132,7 @@ class DatabaseOperatorsTests { @Test fun `simple retract`() { - val predicate = Predicate(listOf(Fact(Atom("a")))) + val predicate = Predicate(listOf(Fact(Atom("a"))), dynamic = true) Program.db.load(predicate) assertEquals(1, Program.query(Atom("a")).count()) @@ -146,11 +147,13 @@ class DatabaseOperatorsTests { @Test fun `retract atom`() { - val predicate = Predicate(listOf( - Fact(Atom("a")), - Fact(Atom("a")), - Fact(Atom("a")) - )) + val predicate = Predicate( + listOf( + Fact(Atom("a")), + Fact(Atom("a")), + Fact(Atom("a")) + ), dynamic = true + ) Program.db.load(predicate) val control = Program.query(Atom("a")).toList() @@ -181,11 +184,13 @@ class DatabaseOperatorsTests { @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")))) - )) + 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")))) + ), dynamic = true + ) Program.db.load(predicate) val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() @@ -289,4 +294,45 @@ class DatabaseOperatorsTests { assertEquals(Atom("bob"), result2[Variable("X")], "Expected bob") assertEquals(Atom("sushi"), result2[Variable("Y")], "Expected sushi") } + + @Test + fun `retract all`() { + 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")))) + ), dynamic = true + ) + + Program.db.load(predicate) + + val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() + assertEquals(3, control.size, "Expected 3 results") + assertEquals(3, Program.db.predicates["a/1"]!!.clauses.size, "Expected 3 clauses") + + val retract = RetractAll(Structure(Atom("a"), listOf(Variable("X")))) + val result = retract.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + assertEquals(0, Program.db.predicates["a/1"]!!.clauses.size, "Expected 0 clauses") + } + + @Test + fun `If Head refers to a predicate that is not defined, it is implicitly created as a dynamic predicate`() { + val predicateName = "idonotyetexist" + val predicateFunctor = "$predicateName/1" + + assertFalse(predicateFunctor in Program.db.predicates, "Expected predicate to not exist before") + + val retractAll = RetractAll(Structure(Atom(predicateName), listOf(Variable("X")))) + val result = retractAll.satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + + assertTrue(predicateFunctor in Program.db.predicates, "Expected predicate to exist after") + assertTrue(Program.db.predicates[predicateFunctor]!!.dynamic, "Expected predicate to be dynamic") + } } \ No newline at end of file