From 5bfeb9617683c829de32c54f544d19720db15e9c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 9 May 2025 08:36:11 +0200 Subject: [PATCH] Call & Ignore --- src/interpreter/Preprocessor.kt | 14 ++++++-- src/prolog/builtins/controlOperators.kt | 15 +++++++- src/prolog/builtins/listOperators.kt | 9 ++--- src/prolog/builtins/metaOperators.kt | 39 +++++++++++++++++++++ src/prolog/logic/unification.kt | 23 ++++-------- src/repl/Repl.kt | 12 +++++-- tests/prolog/builtins/MetaOperatorsTests.kt | 34 ++++++++++++++++++ 7 files changed, 118 insertions(+), 28 deletions(-) create mode 100644 src/prolog/builtins/metaOperators.kt create mode 100644 tests/prolog/builtins/MetaOperatorsTests.kt diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 226af60..79c2c5b 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -3,6 +3,7 @@ package interpreter import io.Logger import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Integer +import prolog.ast.lists.List as PList import prolog.ast.logic.Clause import prolog.ast.logic.Fact import prolog.ast.logic.LogicOperand @@ -50,9 +51,9 @@ open class Preprocessor { protected open fun preprocess(term: Term, nested: Boolean = false): Term { // TODO Remove hardcoding by storing the functors as constants in operators? - val prepped = when { - term == Variable("_") -> AnonymousVariable.create() - term is Atom || term is Structure -> { + val prepped = when (term) { + Variable("_") -> AnonymousVariable.create() + is Atom, is Structure -> { // Preprocess the arguments first to recognize builtins val args = if (term is Structure) { term.arguments.map { preprocess(it, nested = true) } @@ -149,6 +150,13 @@ open class Preprocessor { Functor.of("nl/0") -> Nl Functor.of("read/1") -> Read(args[0]) + // Lists + Functor.of("member/2") -> Member(args[0], args[1] as PList) + + // Meta + Functor.of("call/1") -> Call(args[0] as Goal) + Functor.of("ignore/1") -> Ignore(args[0] as Goal) + // Other Functor.of("initialization/1") -> Initialization(args[0] as Goal) Functor.of("forall/2") -> ForAll(args[0] as LogicOperand, args[1] as Goal) diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 63908d1..eb3d48c 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -114,7 +114,20 @@ class Conjunction(val left: LogicOperand, private val right: LogicOperand) : open class Disjunction(private val left: LogicOperand, private val right: LogicOperand) : LogicOperator(Atom(";"), left, right) { override fun satisfy(subs: Substitutions): Answers = sequence { - yieldAll(left.satisfy(subs)) + left.satisfy(subs).forEach { left -> + left.fold( + onSuccess = { leftSubs -> + yield(Result.success(leftSubs)) + }, + onFailure = { failure -> + if (failure is AppliedCut) { + val leftSubs = failure.subs + yield(Result.failure(AppliedCut(leftSubs))) + return@sequence + } + } + ) + } yieldAll(right.satisfy(subs)) } diff --git a/src/prolog/builtins/listOperators.kt b/src/prolog/builtins/listOperators.kt index 4adbced..5dedc11 100644 --- a/src/prolog/builtins/listOperators.kt +++ b/src/prolog/builtins/listOperators.kt @@ -10,15 +10,12 @@ 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( + private var solution: Operator = when (list) { + is Empty -> Disjunction(Fail, Fail) + 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) diff --git a/src/prolog/builtins/metaOperators.kt b/src/prolog/builtins/metaOperators.kt new file mode 100644 index 0000000..379fb9b --- /dev/null +++ b/src/prolog/builtins/metaOperators.kt @@ -0,0 +1,39 @@ +package prolog.builtins + +import prolog.Answers +import prolog.Substitutions +import prolog.ast.terms.Atom +import prolog.ast.terms.Goal +import prolog.ast.terms.Operator +import prolog.flags.AppliedCut + +class Call(private val goal: Goal) : Operator(Atom("call"), null, goal) { + override fun satisfy(subs: Substitutions): Answers = goal.satisfy(subs) +} + +/** + * Calls [Goal] once, but succeeds, regardless of whether Goal succeeded or not. + */ +class Ignore(goal: Goal) : Operator(Atom("ignore"), null, goal) { + private val disjunction = Disjunction( + Conjunction(Call(goal), Cut()), + True + ) + + override fun satisfy(subs: Substitutions): Answers = sequence { + disjunction.satisfy(subs).forEach { result -> + result.fold( + onSuccess = { newSubs -> + yield(Result.success(newSubs)) + }, + onFailure = { failure -> + if (failure is AppliedCut && failure.subs != null) { + yield(Result.success(failure.subs)) + } else { + yield(Result.failure(failure)) + } + } + ) + } + } +} diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 8b9457a..1ac842b 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -7,12 +7,11 @@ import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Number -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.terms.* +import prolog.ast.lists.List as PList // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { @@ -29,16 +28,8 @@ fun applySubstitution(term: Term, subs: Substitutions): Term = when { else -> term } -//TODO Combine with the other applySubstitution function -fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when { - variable(expr, emptyMap()) -> applySubstitution(expr as Term, subs) as Expression - atomic(expr, subs) -> expr - expr is LogicOperator -> { - expr.arguments = expr.arguments.map { applySubstitution(it, subs) } - expr - } - - else -> expr +fun applySubstitution(expr: Expression, subs: Substitutions): Expression { + return applySubstitution(expr as Term, subs) as Expression } // Check if a variable occurs in a term @@ -90,7 +81,7 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence } } - t1 is List && t2 is List -> { + t1 is PList && t2 is PList -> { if (equivalent(t1, t2, subs)) { yield(Result.success(emptyMap())) } else if (t1.size == t2.size) { @@ -120,8 +111,8 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence } private fun unifyArgs( - args1: kotlin.collections.List, - args2: kotlin.collections.List, + args1: List, + args2: List, subs: Substitutions ): Answers = sequence { // Using the current subs, unify the first argument of each list @@ -171,7 +162,7 @@ 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 -> { + term1 is PList && term2 is PList -> { if (term1.isEmpty() && term2.isEmpty()) { true } else if (term1.isEmpty() || term2.isEmpty()) { diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 32326ba..688cea4 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -6,6 +6,7 @@ import io.Terminal import parser.ReplParser import prolog.Answer import prolog.Answers +import prolog.flags.AppliedCut class Repl { private val io = Terminal() @@ -101,8 +102,15 @@ class Repl { } return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } }, - onFailure = { - return "ERROR: ${it.message}" + onFailure = { failure -> + if (failure is AppliedCut) { + if (failure.subs != null) { + return prettyPrint(Result.success(failure.subs)) + } + return "false." + } + + return "ERROR: ${failure.message}" } ) } diff --git a/tests/prolog/builtins/MetaOperatorsTests.kt b/tests/prolog/builtins/MetaOperatorsTests.kt new file mode 100644 index 0000000..aeb642c --- /dev/null +++ b/tests/prolog/builtins/MetaOperatorsTests.kt @@ -0,0 +1,34 @@ +package prolog.builtins + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import prolog.ast.arithmetic.Integer +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty +import prolog.ast.terms.Variable + +class MetaOperatorsTests { + @Test + fun `ignore of failing goal succeeds`() { + val goal = Member(Integer(4), Cons(Integer(1), Cons(Integer(2), Cons(Integer(3), Empty)))) + + val result = Ignore(goal).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Should return one result") + assertTrue(result[0].isSuccess, "Result should be successful") + assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected empty substitutions") + } + + @Test + fun `ignore of succeeding goal returns first solution`() { + val goal = Member(Variable("X"), Cons(Integer(1), Cons(Integer(2), Cons(Integer(3), Empty)))) + + val result = Ignore(goal).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Should return one result") + assertTrue(result[0].isSuccess, "Result should be successful") + val subs = result[0].getOrNull()!! + assertEquals(Integer(1), subs[Variable("X")], "Expected first solution to be 1") + } +} \ No newline at end of file