diff --git a/examples/meta/continuations.pl b/examples/meta/continuations.pl new file mode 100644 index 0000000..8661361 --- /dev/null +++ b/examples/meta/continuations.pl @@ -0,0 +1,22 @@ +% Based on +% https://occasionallycogent.com/continuations_in_prolog/ + +test(Cont, Term) :- + writeln("Inside test"), + reset(test_, Term, Cont), + writeln("After reset"). + +test_ :- + writeln("Entering reset"), + shift(Y), + X is 1 + (2 * Y), + write("In test X = "), write(X), writeln("; done"). + +main :- + test(Cont, Term), + \+ \+ ( writeln("Calling Cont(2)"), + Term = 2, call(Cont)), + \+ \+ ( writeln("Calling Cont(4)"), + Term = 4, call(Cont)). + +:- initialization(main). diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 226af60..7076dc8 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) } @@ -144,11 +145,23 @@ open class Preprocessor { } } + // Delimited Continuations + Functor.of("reset/3") -> Reset(args[0] as Goal, args[1], args[2]) + Functor.of("shift/1") -> Shift(args[0]) + // IO Functor.of("write/1") -> Write(args[0]) Functor.of("nl/0") -> Nl + Functor.of("writeln/1") -> WriteLn(args[0]) 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]) + 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/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 8db42e9..9051484 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -3,9 +3,10 @@ package parser.grammars import com.github.h0tk3y.betterParse.combinators.* import com.github.h0tk3y.betterParse.grammar.parser import com.github.h0tk3y.betterParse.parser.Parser +import com.github.h0tk3y.betterParse.utils.Tuple2 import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer -import prolog.ast.lists.List +import prolog.ast.lists.List as PList import prolog.ast.terms.* import prolog.builtins.Dynamic import prolog.ast.lists.List.Empty @@ -96,14 +97,19 @@ open class TermsGrammar : Tokens() { if (t2 == null) t1 else CompoundTerm(Atom(t2!!.t1), listOf(t1, t2!!.t2)) } + protected val not: Parser by (notOp * parser(::term900)) use { + CompoundTerm(Atom(t1.text), listOf(t2)) + } + protected val term900: Parser by (not or term700) + protected val op1000: Parser by (comma) use { text } - protected val term1000: Parser by (term700 * zeroOrMore(op1000 * term700)) use { - t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } + protected val term1000: Parser by (term900 * zeroOrMore(op1000 * term900)) use { + constructRightAssociative(t1, t2) } protected val op1100: Parser by (semicolon) use { text } protected val term1100: Parser by (term1000 * zeroOrMore(op1100 * term1000)) use { - t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } + constructRightAssociative(t1, t2) } protected val dynamic: Parser by (dynamicOp * functor) use { @@ -117,12 +123,12 @@ open class TermsGrammar : Tokens() { } // Lists - protected val list: Parser by (-leftBracket * separated( + protected val list: Parser by (-leftBracket * separated( parser(::termNoConjunction), comma, acceptZero = true ) * -rightBracket) use { - var list: List = Empty + var list: PList = Empty // Construct the list in reverse order for (term in this.terms.reversed()) { list = Cons(term, list) @@ -139,4 +145,12 @@ open class TermsGrammar : Tokens() { protected val body: Parser by term use { this as Body } override val rootParser: Parser by term + + fun constructRightAssociative(left: Term, pairs: List>): Term { + if (pairs.isEmpty()) return left + + val (name, next) = pairs.first() + val remainingPairs = pairs.drop(1) + return CompoundTerm(Atom(name), listOf(left, constructRightAssociative(next, remainingPairs))) + } } diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index b998fee..ccdbae9 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -21,6 +21,8 @@ abstract class Tokens : Grammar() { protected val semicolon: Token by literalToken(";") // 1000 protected val comma: Token by literalToken(",") + // 900 + protected val notOp: Token by literalToken("\\+") // 700 protected val univOp: Token by literalToken("=..") protected val notEquivalent: Token by literalToken("\\==") diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 63908d1..c1376f9 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -9,6 +9,7 @@ import prolog.ast.terms.Atom import prolog.ast.terms.Body import prolog.ast.terms.Goal import prolog.flags.AppliedCut +import prolog.flags.AppliedShift import prolog.logic.applySubstitution import prolog.logic.numbervars @@ -17,7 +18,7 @@ import prolog.logic.numbervars */ object Fail : Atom("fail"), Body { override fun satisfy(subs: Substitutions): Answers = emptySequence() - override fun applySubstitution(subs: Substitutions): Fail = Fail + override fun applySubstitution(subs: Substitutions): Fail = this } /** @@ -30,7 +31,7 @@ typealias False = Fail */ object True : Atom("true"), Body { override fun satisfy(subs: Substitutions): Answers = sequenceOf(Result.success(emptyMap())) - override fun applySubstitution(subs: Substitutions): True = True + override fun applySubstitution(subs: Substitutions): True = this } // TODO Repeat/0 @@ -40,62 +41,72 @@ class Cut() : Atom("!") { return sequenceOf(Result.failure(AppliedCut(emptyMap()))) } - override fun applySubstitution(subs: Substitutions): Cut = Cut() + override fun applySubstitution(subs: Substitutions): Cut = this } /** * Conjunction (and). True if both Goal1 and Goal2 are true. */ -class Conjunction(val left: LogicOperand, private val right: LogicOperand) : +open class Conjunction(val left: LogicOperand, private val right: LogicOperand) : LogicOperator(Atom(","), left, right) { override fun satisfy(subs: Substitutions): Answers = sequence { + fun satisfyRight(leftSubs: Substitutions): Answers = sequence { + right.satisfy(subs + leftSubs).forEach { right -> + right.fold( + // If the right part succeeds, yield the result with the left substitutions + onSuccess = { rightSubs -> + yield(Result.success(leftSubs + rightSubs)) + }, + onFailure = { exception -> + // If the right part fails, check if it's a cut + if (exception is AppliedCut) { + // If it's a cut, yield the result with the left substitutions + val newSubs = if (exception.subs != null) leftSubs + exception.subs else null + yield(Result.failure(AppliedCut(newSubs))) + return@sequence + } + + // If it's not a cut, yield the failure + yield(Result.failure(exception)) + } + ) + } + } + + fun findNextCutSolution(appliedCut: AppliedCut): Answers = sequence { + val leftSubs = appliedCut.subs + right.satisfy(subs + (appliedCut.subs!!)).firstOrNull()?.map { rightSubs -> + // If the right part succeeds, yield the result with the left substitutions + yield(Result.success(leftSubs + rightSubs)) + return@sequence + } ?: yield(Result.failure(AppliedCut())) + } + // Satisfy the left part first, which either succeeds or fails left.satisfy(subs).forEach { left -> left.fold( // If it succeeds, satisfy the right part with the updated substitutions and return all results - onSuccess = { leftSubs -> - right.satisfy(subs + leftSubs).forEach { right -> - right.fold( - // If the right part succeeds, yield the result with the left substitutions - onSuccess = { rightSubs -> - yield(Result.success(leftSubs + rightSubs)) - }, - // If the right part fails, check if it's a cut - onFailure = { exception -> - if (exception is AppliedCut) { - if (exception.subs != null) { - // If it's a cut, yield the result with the left substitutions - yield(Result.failure(AppliedCut(leftSubs + exception.subs))) - } else { - yield(Result.failure(AppliedCut())) - } - return@sequence - } - - // If it's not a cut, yield the failure - yield(Result.failure(exception)) - } - ) - } - }, + onSuccess = { leftSubs -> yieldAll(satisfyRight(leftSubs)) }, // If it fails, check these conditions: onFailure = { exception -> - // 1. If the left part is a cut, satisfy the right part ONCE, and stop searching for more solutions - if (exception is AppliedCut) { - right.satisfy(subs + (exception.subs!!)).firstOrNull()?.fold( - onSuccess = { - // If the right part succeeds, yield the result with the left substitutions - yield(Result.success(exception.subs + it)) - return@sequence - }, - onFailure = { - // If the right part fails, yield the failure - yield(Result.failure(it)) + when (exception) { + // 1. If the left part is a cut, satisfy the right part ONCE, and stop searching for more solutions + is AppliedCut -> yieldAll(findNextCutSolution(exception)) + + // 2. If the left part is a shift, we need to check if the right part can be satisfied + is AppliedShift -> { + if (exception.cont == null) { + // Pass the right of our disjunction as the continuation + val newShift = AppliedShift(exception.subs, exception.ball, right as Goal) + yield(Result.failure(newShift)) + } else { + // Satisfy the right part with the updated substitutions + yieldAll(satisfyRight(exception.subs)) } - ) ?: yield(Result.failure(AppliedCut())) - } else { - // 2. Any other failure should be returned as is - yield(Result.failure(exception)) + } + + // 3. Any other failure should be returned as is + else -> yield(Result.failure(exception)) } } ) @@ -114,7 +125,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/delimitedContinuationsOperators.kt b/src/prolog/builtins/delimitedContinuationsOperators.kt new file mode 100644 index 0000000..5a01eb4 --- /dev/null +++ b/src/prolog/builtins/delimitedContinuationsOperators.kt @@ -0,0 +1,75 @@ +package prolog.builtins + +import prolog.Answers +import prolog.Substitutions +import prolog.ast.arithmetic.Integer +import prolog.ast.terms.Atom +import prolog.ast.terms.Goal +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.flags.AppliedShift +import prolog.logic.applySubstitution +import prolog.logic.unifyLazy + +/** + * These classes provide support for delimited continuations in Prolog. + * More specifically, they implement the reset/3 and shift/1 operators. + * + * See also [SWI-Prolog 4.9 Delimited Continuations](https://www.swi-prolog.org/pldoc/man?section=delcont) + */ + +/** + * Call [Goal]. If Goal calls [Shift], and the arguments of Shift can be unified with Ball, Shift causes Reset to + * return, unifying Cont with a Goal that represents the continuation after shift. + */ +class Reset(private val goal: Goal, private val ball: Term, private val cont: Term) : + Structure(Atom("reset"), listOf(goal, ball, cont)) { + override fun satisfy(subs: Substitutions): Answers = sequence { + goal.satisfy(subs).forEach { goalResult -> + goalResult.fold( + // If Goal succeeds, then reset/3 also succeeds and binds Cont and Term1 to 0. + onSuccess = { goalSubs -> + unifyLazy(cont, Integer(0), goalSubs).forEach { contResult -> + contResult.map { contSubs -> + yield(Result.success(goalSubs + contSubs)) + } + } + }, + onFailure = { failure -> + // If at some point Goal calls shift(Term2), then its further execution is suspended. + if (failure is AppliedShift) { + require(failure.cont != null) { "Shift must have a continuation" } + // Reset/3 succeeds immediately, binding Term1 to Term2 and Cont to the remainder of Goal. + val shiftSubs = failure.subs + val appliedBall = applySubstitution(failure.ball, shiftSubs) + val appliedCont = applySubstitution(failure.cont, shiftSubs) + Conjunction(Unify(ball, appliedBall), Unify(appliedCont, cont)) + .satisfy(shiftSubs) + .forEach { conResult -> + conResult.map { conSubs -> + yield(Result.success(shiftSubs + conSubs)) + } + } + } else { + // If Goal fails, then reset also fails. + yield(Result.failure(failure)) + } + } + ) + } + } +} + +/** + * Variables that have been bound during the procedure called by reset/3 stay bound after a shift/1: + */ +class Shift(private val ball: Term) : Structure(Atom("shift"), listOf(ball)) { + override fun satisfy(subs: Substitutions): Answers = sequence { + val shift = AppliedShift( + subs = subs, + ball = ball, + cont = null + ) + yield(Result.failure(shift)) + } +} diff --git a/src/prolog/builtins/ioOperators.kt b/src/prolog/builtins/ioOperators.kt index 4c80d61..514690c 100644 --- a/src/prolog/builtins/ioOperators.kt +++ b/src/prolog/builtins/ioOperators.kt @@ -17,6 +17,8 @@ import prolog.logic.unifyLazy * Write [Term] to the current output, using brackets and operators where appropriate. */ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satisfiable { + constructor(message: String) : this(Atom(message)) + override fun satisfy(subs: Substitutions): Answers { val t = applySubstitution(term, subs) @@ -45,6 +47,12 @@ object Nl : Atom("nl"), Satisfiable { override fun applySubstitution(subs: Substitutions): Nl = this } +class WriteLn(private val term: Term) : Conjunction(Write(term), Nl) { + constructor(message: String) : this(Atom(message)) + override fun applySubstitution(subs: Substitutions): WriteLn = WriteLn(applySubstitution(term, subs)) + override fun toString(): String = "writeln($term)" +} + /** * Read the next Prolog term from the current input stream and unify it with [Term]. * diff --git a/src/prolog/builtins/listOperators.kt b/src/prolog/builtins/listOperators.kt index 4adbced..d1a1944 100644 --- a/src/prolog/builtins/listOperators.kt +++ b/src/prolog/builtins/listOperators.kt @@ -8,20 +8,23 @@ import prolog.ast.lists.List.Empty import prolog.ast.terms.Atom import prolog.ast.terms.Operator import prolog.ast.terms.Term +import prolog.logic.applySubstitution 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) + override fun applySubstitution(subs: Substitutions): Member = Member( + applySubstitution(element, subs), + applySubstitution(list, subs) as List + ) + override fun toString(): String = "$element ∈ $list" } diff --git a/src/prolog/builtins/metaOperators.kt b/src/prolog/builtins/metaOperators.kt new file mode 100644 index 0000000..047fa5f --- /dev/null +++ b/src/prolog/builtins/metaOperators.kt @@ -0,0 +1,44 @@ +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.ast.terms.Term +import prolog.flags.AppliedCut +import prolog.logic.applySubstitution + +class Call(private val goal: Term) : Operator(Atom("call"), null, goal) { + override fun satisfy(subs: Substitutions): Answers { + val appliedGoal = applySubstitution(goal, subs) as Goal + return appliedGoal.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/flags/AppliedShift.kt b/src/prolog/flags/AppliedShift.kt new file mode 100644 index 0000000..a96b55d --- /dev/null +++ b/src/prolog/flags/AppliedShift.kt @@ -0,0 +1,14 @@ +package prolog.flags + +import prolog.Substitutions +import prolog.ast.terms.Goal +import prolog.ast.terms.Term + +/** + * An exception that indicates that a shift has been applied in the Prolog engine. + */ +data class AppliedShift( + val subs: Substitutions, + val ball: Term, + val cont: Term? = null, +) : Throwable() \ No newline at end of file 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/src/tool/mapEach.kt b/src/tool/mapEach.kt new file mode 100644 index 0000000..1786867 --- /dev/null +++ b/src/tool/mapEach.kt @@ -0,0 +1,3 @@ +package tool + +fun Sequence.mapEach(transform: (T) -> R): Sequence = map(transform) diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt index 4c86e0d..250f804 100644 --- a/tests/e2e/Examples.kt +++ b/tests/e2e/Examples.kt @@ -27,7 +27,7 @@ class Examples { @Test fun debugHelper() { - loader.load("examples/basics/arithmetics.pl") + loader.load("examples/meta/continuations.pl") } @ParameterizedTest @@ -65,6 +65,7 @@ class Examples { ) fun meta() = listOf( + Arguments.of("continuations.pl", "Inside test\nEntering reset\nAfter reset\nCalling Cont(2)\nIn test X = 5; done\nCalling Cont(4)\nIn test X = 9; done\n"), Arguments.of("mib_voorbeelden.pl", "b\nf(b)\nf(g(a,a),h(c,d),i(e,f))\nf(g(a,a),h(c,c),i(e,f))\nf(g(a,a),h(c,c),i(e,e))\n") ) diff --git a/tests/parser/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index f5cde74..5ca662a 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -126,10 +126,11 @@ class LogicGrammarTests { val rule = result[0] as Rule assertEquals(Functor.of("guest/2"), rule.head.functor, "Expected functor 'guest/2'") assertEquals(Functor.of(",/2"), (rule.body as CompoundTerm).functor, "Expected functor ',/2'") - val l1 = (rule.body as CompoundTerm).arguments[0] as CompoundTerm - assertEquals(Functor.of(",/2"), l1.functor, "Expected functor ',/2'") - val l2 = l1.arguments[0] as CompoundTerm - assertEquals(Functor.of("invited/2"), l2.functor, "Expected functor 'invited/2'") + val l0 = (rule.body as CompoundTerm) + val l1 = l0.arguments[0] as CompoundTerm + assertEquals(Functor.of("invited/2"), l1.functor, "Expected functor 'invited/2'") + val l2 = l0.arguments[1] as CompoundTerm + assertEquals(Functor.of(",/2"), l2.functor, "Expected functor ',/2'") } @Test diff --git a/tests/prolog/builtins/DelimitedContinuationsOperatorsTests.kt b/tests/prolog/builtins/DelimitedContinuationsOperatorsTests.kt new file mode 100644 index 0000000..994a992 --- /dev/null +++ b/tests/prolog/builtins/DelimitedContinuationsOperatorsTests.kt @@ -0,0 +1,75 @@ +package prolog.builtins + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import prolog.ast.Database.Program +import prolog.ast.arithmetic.Integer +import prolog.ast.lists.List.Cons +import prolog.ast.lists.List.Empty +import prolog.ast.logic.Rule +import prolog.ast.terms.Atom +import prolog.ast.terms.Structure +import prolog.ast.terms.Variable +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class DelimitedContinuationsOperatorsTests { + @BeforeEach + fun setup() { + Program.reset() + } + + @Test + fun `example member`() { + val member = Member(Variable("X"), Cons(Integer(1), Cons(Integer(2), Cons(Integer(3), Empty)))) + val reset = Reset(member, Variable("Ball"), Variable("Cont")) + + val result = reset.satisfy(emptyMap()).toList() + + assertEquals(3, result.size, "Expected 3 results") + + assertTrue(result[0].isSuccess, "Expected success for first result") + val subs0 = result[0].getOrNull()!! + assertEquals(2, subs0.size, "Expected 2 substitutions for first result") + assertEquals(Integer(1), subs0[Variable("X")]) + assertEquals(Integer(0), subs0[Variable("Cont")]) + + assertTrue(result[1].isSuccess, "Expected success for second result") + val subs1 = result[1].getOrNull()!! + assertEquals(2, subs1.size, "Expected 2 substitutions for second result") + assertEquals(Integer(2), subs1[Variable("X")]) + assertEquals(Integer(0), subs1[Variable("Cont")]) + + assertTrue(result[2].isSuccess, "Expected success for third result") + val subs2 = result[2].getOrNull()!! + assertEquals(2, subs2.size, "Expected 2 substitutions for third result") + assertEquals(Integer(3), subs2[Variable("X")]) + assertEquals(Integer(0), subs2[Variable("Cont")]) + } + + @Test + fun `example with output`() { + val reset = Reset(Atom("test_"), Variable("Term"), Variable("Cont")) + val isOp = Is(Variable("X"), Add(Integer(1), Multiply(Integer(2), Variable("Y")))) + Program.load( + listOf( + Rule( + Structure(Atom("test"), listOf(Variable("Cont"), Variable("Term"))), + Conjunction(WriteLn("Inside test"), Conjunction(reset, WriteLn("After reset"))) + ), + Rule( + Atom("test_"), + Conjunction(WriteLn("Entering reset"), Conjunction(Shift(Variable("Y")), Conjunction(isOp, Conjunction(Write("In test X = "), Conjunction(Write(Variable("X")), WriteLn("; done")))))) + ) + ) + ) + + val notNot2 = Not(Not(Conjunction(WriteLn("calling Cont(2)"), Conjunction(Unify(Variable("Term"), Integer(2)), Call(Variable("Cont")))))) + val notNot4 = Not(Not(Conjunction(WriteLn("calling Cont(4)"), Conjunction(Unify(Variable("Term"), Integer(4)), Call(Variable("Cont")))))) + val query = Conjunction(Structure(Atom("test"), listOf(Variable("Cont"), Variable("Term"))), Conjunction(notNot2, notNot4)) + + val result = query.satisfy(emptyMap()).toList() + } +} 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