diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 5bd2ed7..c65efd5 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -48,70 +48,93 @@ open class Preprocessor { } protected open fun preprocess(term: Term, nested: Boolean = false): Term { - val prepped = when (term) { - Atom("true") -> True - Structure(Atom("true"), emptyList()) -> True - Atom("false") -> False - Structure(Atom("false"), emptyList()) -> False - Atom("fail") -> Fail - Structure(Atom("fail"), emptyList()) -> Fail - Atom("!") -> Cut() - Structure(Atom("!"), emptyList()) -> Cut() - Atom("inf") -> Integer(Int.MAX_VALUE) - Atom("nl") -> Nl - Variable("_") -> AnonymousVariable.create() - is Structure -> { + // TODO Remove hardcoding by storing the functors as constants in operators? + + val prepped = when { + term == Variable("_") -> AnonymousVariable.create() + term is Atom || term is Structure -> { // Preprocess the arguments first to recognize builtins - val args = term.arguments.map { preprocess(it, nested = true) } + val args = if (term is Structure) { + term.arguments.map { preprocess(it, nested = true) } + } else emptyList() - when { - // TODO Remove hardcoding by storing the functors as constants in operators? - - term.functor == FunctorInfo.of(":-/2") -> Rule( args[0] as Head, args[1] as Body ) - - // Logic - term.functor == FunctorInfo.of("=/2") -> Unify(args[0], args[1]) - term.functor == FunctorInfo.of("\\=/2") -> NotUnify(args[0], args[1]) - term.functor == FunctorInfo.of(",/2") -> Conjunction(args[0] as LogicOperand, args[1] as LogicOperand) - term.functor == FunctorInfo.of(";/2") -> Disjunction(args[0] as LogicOperand, args[1] as LogicOperand) - term.functor == FunctorInfo.of("\\+/1") -> Not(args[0] as Goal) - term.functor == FunctorInfo.of("\\==/2") -> NotEquivalent(args[0], args[1]) - term.functor == FunctorInfo.of("==/2") -> Equivalent(args[0], args[1]) - - term.functor == FunctorInfo.of("=\\=/2") && args.all { it is Expression } -> EvaluatesToDifferent(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("=:=/2") && args.all { it is Expression } -> EvaluatesTo(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("is/2") && args.all { it is Expression } -> Is(args[0] as Expression, args[1] as Expression) + when (term.functor) { + // Analysis + Functor.of("functor/3") -> FunctorOp(args[0], args[1], args[2]) + Functor.of("arg/3") -> Arg(args[0], args[1], args[2]) + Functor.of("clause/2") -> ClauseOp(args[0] as Head, args[1] as Body) // Arithmetic + Functor.of("inf/0") -> Integer(Int.MAX_VALUE) + Functor.of("=\\=/2") -> if (args.all { it is Expression }) { + EvaluatesToDifferent(args[0] as Expression, args[1] as Expression) + } else term - term.functor == FunctorInfo.of("-/1") && args.all { it is Expression } -> Negate(args[0] as Expression) - term.functor == FunctorInfo.of("-/2") && args.all { it is Expression } -> Subtract(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("+/1") && args.all { it is Expression } -> Positive(args[0] as Expression) - term.functor == FunctorInfo.of("+/2") && args.all { it is Expression } -> Add(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("*/2") && args.all { it is Expression } -> Multiply(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("//2") && args.all { it is Expression } -> Divide(args[0] as Expression, args[1] as Expression) - term.functor == FunctorInfo.of("between/3") && args.all { it is Expression } -> Between(args[0] as Expression, args[1] as Expression, args[2] as Expression) - term.functor == FunctorInfo.of("succ/2") && args.all { it is Expression } -> Successor(args[0] as Expression, args[1] as Expression) + Functor.of("=:=/2") -> if (args.all { it is Expression }) { + EvaluatesTo(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("is/2") -> if (args.all { it is Expression }) { + Is(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("-/1") -> if (args.all { it is Expression }) Negate(args[0] as Expression) else term + Functor.of("+/1") -> if (args.all { it is Expression }) Positive(args[0] as Expression) else term + Functor.of("+/2") -> if (args.all { it is Expression }) { + Add(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("-/2") -> if (args.all { it is Expression }) { + Subtract(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("*/2") -> if (args.all { it is Expression }) { + Multiply(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("//2") -> if (args.all { it is Expression }) { + Divide(args[0] as Expression, args[1] as Expression) + } else term + + Functor.of("between/3") -> if (args.all { it is Expression }) { + Between(args[0] as Expression, args[1] as Expression, args[2] as Expression) + } else term + + Functor.of("succ/2") -> if (args.all { it is Expression }) { + Successor(args[0] as Expression, args[1] as Expression) + } else term + + // Control + Functor.of("fail/0") -> Fail + Functor.of("false/0") -> False + Functor.of("true/0") -> True + Functor.of("!/0") -> Cut() + Functor.of(",/2") -> Conjunction(args[0] as LogicOperand, args[1] as LogicOperand) + Functor.of(";/2") -> Disjunction(args[0] as LogicOperand, args[1] as LogicOperand) + Functor.of("|/2") -> Bar(args[0] as LogicOperand, args[1] as LogicOperand) + Functor.of("\\+/1") -> Not(args[0] as Goal) // Database - term.functor == FunctorInfo.of("dynamic/1") -> Dynamic(FunctorInfo.of((args[0] as Atom).name)) - term.functor == FunctorInfo.of("retract/1") -> Retract(args[0]) - term.functor == FunctorInfo.of("retractall/1") -> RetractAll(args[0]) - term.functor == FunctorInfo.of("assert/1") -> { + Functor.of("dynamic/1") -> Dynamic(Functor.of((args[0] as Atom).name)) + Functor.of("retract/1") -> Retract(args[0]) + Functor.of("retractall/1") -> RetractAll(args[0]) + Functor.of("assert/1") -> { if (args[0] is Rule) { Assert(args[0] as Rule) } else { Assert(Fact(args[0] as Head)) } } - term.functor == FunctorInfo.of("asserta/1") -> { + + Functor.of("asserta/1") -> { if (args[0] is Rule) { AssertA(args[0] as Rule) } else { AssertA(Fact(args[0] as Head)) } } - term.functor == FunctorInfo.of("assertz/1") -> { + + Functor.of("assertz/1") -> { if (args[0] is Rule) { AssertZ(args[0] as Rule) } else { @@ -119,14 +142,25 @@ open class Preprocessor { } } + // IO + Functor.of("write/1") -> Write(args[0]) + Functor.of("nl/0") -> Nl + Functor.of("read/1") -> Read(args[0]) + // Other - term.functor == FunctorInfo.of("write/1") -> Write(args[0]) - term.functor == FunctorInfo.of("read/1") -> Read(args[0]) - term.functor == FunctorInfo.of("initialization/1") -> Initialization(args[0] as Goal) - term.functor == FunctorInfo.of("forall/2") -> ForAll(args[0] as LogicOperand, args[1] as Goal) + Functor.of("initialization/1") -> Initialization(args[0] as Goal) + Functor.of("forall/2") -> ForAll(args[0] as LogicOperand, args[1] as Goal) + + // Unification + Functor.of("=/2") -> Unify(args[0], args[1]) + Functor.of("\\=/2") -> NotUnify(args[0], args[1]) + Functor.of("==/2") -> Equivalent(args[0], args[1]) + Functor.of("\\==/2") -> NotEquivalent(args[0], args[1]) + + Functor.of(":-/2") -> Rule(args[0] as Head, args[1] as Body) else -> { - term.arguments = args + if (term is Structure) term.arguments = args term } } diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt index 28589ef..b309883 100644 --- a/src/prolog/ast/Database.kt +++ b/src/prolog/ast/Database.kt @@ -6,14 +6,14 @@ import prolog.Substitutions import prolog.ast.logic.Clause import prolog.ast.logic.Predicate import prolog.ast.logic.Resolvent -import prolog.ast.terms.FunctorInfo +import prolog.ast.terms.Functor import prolog.ast.terms.Goal /** * Prolog Program or Database */ open class Database(val sourceFile: String) { - var predicates: Map = emptyMap() + var predicates: Map = emptyMap() /** * Initializes the database by running the initialization clauses of that database. @@ -23,14 +23,14 @@ open class Database(val sourceFile: String) { if (sourceFile !== "") { Logger.debug("Moving clauses from $sourceFile to main database") - predicates.filter { it.key != FunctorInfo.of("/_") } + predicates.filter { it.key != Functor.of("/_") } .forEach { (_, predicate) -> db.load(predicate, force = true) } } Logger.info("Initializing database from $sourceFile") - if (predicates.contains(FunctorInfo.of("/_"))) { + if (predicates.contains(Functor.of("/_"))) { Logger.debug("Loading clauses from /_ predicate") - predicates[FunctorInfo.of("/_")]?.clauses?.forEach { + predicates[Functor.of("/_")]?.clauses?.forEach { Logger.debug("Loading clause $it") val goal = it.body as Goal goal.satisfy(emptyMap()).toList() diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index be3cc62..a2af1f3 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -20,7 +20,7 @@ import prolog.logic.unifyLazy * @see [Predicate] */ abstract class Clause(var head: Head, var body: Body) : Term, Resolvent { - val functor: FunctorInfo = head.functor + val functor: Functor = head.functor override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { // If the clause is a rule, unify the goal with the head and then try to prove the body. diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index e935468..25fb737 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -2,25 +2,25 @@ package prolog.ast.logic import prolog.Answers import prolog.Substitutions -import prolog.ast.terms.FunctorInfo +import prolog.ast.terms.Functor import prolog.ast.terms.Goal import prolog.flags.AppliedCut /** - * Collection of [Clause]s with the same [FunctorInfo]. + * Collection of [Clause]s with the same [Functor]. * * If a goal is proved, the system looks for a predicate with the same functor, then uses indexing * to select candidate clauses and then tries these clauses one-by-one. */ class Predicate : Resolvent { - val functor: FunctorInfo + val functor: Functor val clauses: MutableList var dynamic = false /** * Creates a predicate with the given functor and an empty list of clauses. */ - constructor(functor: FunctorInfo, dynamic: Boolean = false) { + constructor(functor: Functor, dynamic: Boolean = false) { this.functor = functor this.clauses = mutableListOf() this.dynamic = dynamic diff --git a/src/prolog/ast/terms/Atom.kt b/src/prolog/ast/terms/Atom.kt index 2dae78d..fccf9fc 100644 --- a/src/prolog/ast/terms/Atom.kt +++ b/src/prolog/ast/terms/Atom.kt @@ -7,7 +7,7 @@ import prolog.logic.unifyLazy import prolog.ast.arithmetic.Integer open class Atom(val name: String) : Goal(), Head, Body, Resolvent { - override val functor: FunctorInfo = FunctorInfo(this, Integer(0)) + override val functor: Functor = Functor(this, Integer(0)) override fun solve(goal: Goal, subs: Substitutions): Answers = unifyLazy(goal, this, subs) diff --git a/src/prolog/ast/terms/FunctorInfo.kt b/src/prolog/ast/terms/Functor.kt similarity index 67% rename from src/prolog/ast/terms/FunctorInfo.kt rename to src/prolog/ast/terms/Functor.kt index eb61ef7..98d764f 100644 --- a/src/prolog/ast/terms/FunctorInfo.kt +++ b/src/prolog/ast/terms/Functor.kt @@ -3,27 +3,33 @@ package prolog.ast.terms import prolog.Substitutions import prolog.ast.arithmetic.Integer -data class FunctorInfo(val name: Atom, val arity: Integer) : Term { +data class Functor(val name: Atom, val arity: Integer) : Term { companion object { - fun of(functor: String): FunctorInfo { + fun of(functor: String): Functor { // Split the functor string into name and arity, by splitting the last "/" val lastSlash = functor.lastIndexOf('/') val name = Atom(functor.substring(0, lastSlash)) val arity = Integer(functor.substring(lastSlash + 1).toIntOrNull() ?: 0) - return FunctorInfo(name, arity) + return Functor(name, arity) } } override fun toString(): String = "${name.name}/$arity" - override fun applySubstitution(subs: Substitutions) : FunctorInfo = this + override fun applySubstitution(subs: Substitutions) : Functor = this override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null) return false - if (other !is FunctorInfo && other !is String) return false - if (other is FunctorInfo) { + if (other !is Functor && other !is String) return false + if (other is Functor) { return this.name.toString() == other.name.toString() && this.arity == other.arity } return this.toString() == other } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + arity.hashCode() + return result + } } diff --git a/src/prolog/ast/terms/Goal.kt b/src/prolog/ast/terms/Goal.kt index 0ba62f0..4f98585 100644 --- a/src/prolog/ast/terms/Goal.kt +++ b/src/prolog/ast/terms/Goal.kt @@ -13,7 +13,7 @@ import prolog.ast.logic.LogicOperand * or it fails if Prolog fails to prove it. */ abstract class Goal : LogicOperand(), Term { - abstract val functor: FunctorInfo + abstract val functor: Functor override fun satisfy(subs: Substitutions): Answers = Program.solve(this, subs) } \ No newline at end of file diff --git a/src/prolog/ast/terms/Head.kt b/src/prolog/ast/terms/Head.kt index fa5bd8d..fe5048d 100644 --- a/src/prolog/ast/terms/Head.kt +++ b/src/prolog/ast/terms/Head.kt @@ -4,5 +4,5 @@ package prolog.ast.terms * Part of a [Clause][prolog.ast.logic.Clause] before the neck operator. */ interface Head : Term { - val functor: FunctorInfo + val functor: Functor } diff --git a/src/prolog/ast/terms/Structure.kt b/src/prolog/ast/terms/Structure.kt index 4aadf01..e17b987 100644 --- a/src/prolog/ast/terms/Structure.kt +++ b/src/prolog/ast/terms/Structure.kt @@ -12,7 +12,7 @@ typealias Argument = Term typealias CompoundTerm = Structure open class Structure(val name: Atom, var arguments: List) : Goal(), Head, Body, Resolvent { - override val functor: FunctorInfo = FunctorInfo(name, Integer(arguments.size)) + override val functor: Functor = Functor(name, Integer(arguments.size)) override fun solve(goal: Goal, subs: Substitutions): Answers { return unifyLazy(goal, this, subs) diff --git a/src/prolog/builtins/analysingAndConstructionOperators.kt b/src/prolog/builtins/analysisOperators.kt similarity index 62% rename from src/prolog/builtins/analysingAndConstructionOperators.kt rename to src/prolog/builtins/analysisOperators.kt index f2a9079..8982313 100644 --- a/src/prolog/builtins/analysingAndConstructionOperators.kt +++ b/src/prolog/builtins/analysisOperators.kt @@ -2,19 +2,19 @@ package prolog.builtins import prolog.Answers import prolog.Substitutions +import prolog.ast.Database.Program import prolog.ast.arithmetic.Integer import prolog.ast.terms.* +import prolog.ast.logic.Clause import prolog.logic.* -import java.util.Locale -import java.util.Locale.getDefault /** - * [True] when [Term] is a term with [FunctorInfo] Name/Arity. + * [True] when [Term] is a term with [Functor] Name/Arity. * * If Term is a [Variable] it is unified with a new term whose arguments are all different variables. * If Term is [atomic], Arity will be unified with the integer 0, and Name will be unified with Term. */ -class Functor(private val term: Term, private val functorName: Term, private val functorArity: Term) : +class FunctorOp(private val term: Term, private val functorName: Term, private val functorArity: Term) : Structure(Atom("functor"), listOf(term, functorName, functorArity)) { override fun satisfy(subs: Substitutions): Answers { return when { @@ -42,7 +42,7 @@ class Functor(private val term: Term, private val functorName: Term, private val } } - override fun applySubstitution(subs: Substitutions): Functor = Functor( + override fun applySubstitution(subs: Substitutions): FunctorOp = FunctorOp( term.applySubstitution(subs), functorName.applySubstitution(subs), functorArity.applySubstitution(subs) @@ -95,3 +95,45 @@ class Arg(private val arg: Term, private val term: Term, private val value: Term } } } + +/** + * [True] if [Head] can be unified with a [Clause] and [Body] with the corresponding Clause Body. + * + * Gives alternative clauses on backtracking. For facts, Body is unified with the atom [True]. + * + * When accessing builtins (static predicates, private procedures), the program will act as if the builtins do not + * exist. Head can only match with dynamic or imported predicates. + * + * [SWI-Prolog Operator clause/2](https://www.swi-prolog.org/pldoc/doc_for?object=clause/2) + */ +class ClauseOp(private val head: Head, private val body: Body) : + Structure(Atom("clause"), listOf(head, body)) { + override fun satisfy(subs: Substitutions): Answers = sequence { + require(nonvariable(head, subs)) { "Arguments are not sufficiently instantiated" } + + val predicate = Program.db.predicates[head.functor] + + if (predicate != null) { + for (clause in predicate.clauses) { + val clauseHead = clause.head + val clauseBody = clause.body + + // Unify the head of the clause with the head of the goal + unifyLazy(clauseHead, head, subs).forEach { result -> + result.map { headSubs -> + // Unify the body of the clause with the body of the goal + unifyLazy(clauseBody, body, headSubs).forEach { bodyResult -> + bodyResult.map { bodySubs -> + // Combine the substitutions from the head and body + val combinedSubs = headSubs + bodySubs + yield(Result.success(combinedSubs)) + } + } + } + } + } + } else { + yield(Result.success(emptyMap())) + } + } +} diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index 05f228c..ddfa471 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -14,10 +14,10 @@ import prolog.logic.applySubstitution import prolog.logic.unifyLazy /** - * (Make) the [Predicate] with the corresponding [FunctorInfo] dynamic. + * (Make) the [Predicate] with the corresponding [Functor] dynamic. */ -class Dynamic(private val dynamicFunctor: FunctorInfo) : Goal(), Body { - override val functor = FunctorInfo(Atom("dynamic"), Integer(1)) +class Dynamic(private val dynamicFunctor: Functor) : Goal(), Body { + override val functor = Functor(Atom("dynamic"), Integer(1)) override fun satisfy(subs: Substitutions): Answers { val predicate = Program.db.predicates[dynamicFunctor] @@ -43,7 +43,7 @@ class Dynamic(private val dynamicFunctor: FunctorInfo) : Goal(), Body { } class Assert(clause: Clause) : AssertZ(clause) { - override val functor = FunctorInfo(Atom("assert"), Integer(1)) + override val functor = Functor(Atom("assert"), Integer(1)) } /** diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index 4d9ed7f..ace7862 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import parser.grammars.TermsGrammar import prolog.ast.arithmetic.Integer -import prolog.ast.logic.Fact import prolog.ast.logic.Rule import prolog.ast.terms.* import prolog.builtins.* @@ -612,7 +611,7 @@ class PreprocessorTests { Atom("declaration/1") ) ) - val expected = Dynamic(FunctorInfo.of("declaration/1")) + val expected = Dynamic(Functor.of("declaration/1")) val result = preprocessor.preprocess(input) diff --git a/tests/parser/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index 7ee3fcc..cbcb949 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -2,7 +2,6 @@ package parser.grammars import com.github.h0tk3y.betterParse.grammar.Grammar import com.github.h0tk3y.betterParse.grammar.parseToEnd -import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -14,8 +13,7 @@ import prolog.ast.logic.Rule import prolog.ast.terms.CompoundTerm import prolog.ast.terms.Structure import prolog.ast.terms.Variable -import prolog.builtins.Conjunction -import prolog.ast.terms.FunctorInfo +import prolog.ast.terms.Functor class LogicGrammarTests { private lateinit var parser: Grammar> @@ -95,13 +93,13 @@ class LogicGrammarTests { assertTrue(rule.head is Structure, "Expected head to be a structure") val head = rule.head as Structure - assertEquals(FunctorInfo.of("parent/2"), head.functor, "Expected functor 'parent/2'") + assertEquals(Functor.of("parent/2"), head.functor, "Expected functor 'parent/2'") assertEquals(Variable("X"), head.arguments[0], "Expected first argument 'X'") assertEquals(Variable("Y"), head.arguments[1], "Expected second argument 'Y'") assertTrue(rule.body is Structure, "Expected body to be a structure") val body = rule.body as Structure - assertEquals(FunctorInfo.of("father/2"), body.functor, "Expected functor 'father/2'") + assertEquals(Functor.of("father/2"), body.functor, "Expected functor 'father/2'") assertEquals(Variable("X"), body.arguments[0], "Expected first argument 'X'") assertEquals(Variable("Y"), body.arguments[1], "Expected second argument 'Y'") } @@ -126,12 +124,12 @@ class LogicGrammarTests { assertEquals(1, result.size, "Expected 1 rule") val rule = result[0] as Rule - assertEquals(FunctorInfo.of("guest/2"), rule.head.functor, "Expected functor 'guest/2'") - assertEquals(FunctorInfo.of(",/2"), (rule.body as CompoundTerm).functor, "Expected functor ',/2'") + 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(FunctorInfo.of(",/2"), l1.functor, "Expected functor ',/2'") + assertEquals(Functor.of(",/2"), l1.functor, "Expected functor ',/2'") val l2 = l1.arguments[0] as CompoundTerm - assertEquals(FunctorInfo.of("invited/2"), l2.functor, "Expected functor 'invited/2'") + assertEquals(Functor.of("invited/2"), l2.functor, "Expected functor 'invited/2'") } @Test @@ -158,6 +156,6 @@ class LogicGrammarTests { assertEquals(1, result.size, "Expected 1 rule") assertTrue(result[0] is Rule, "Expected a rule") val rule = result[0] as Rule - assertEquals(FunctorInfo.of("/0"), rule.head.functor, "Expected a constraint") + assertEquals(Functor.of("/0"), rule.head.functor, "Expected a constraint") } } \ No newline at end of file diff --git a/tests/prolog/builtins/AnalysingAndConstructionOperatorsTests.kt b/tests/prolog/builtins/AnalysisOperatorsTests.kt similarity index 51% rename from tests/prolog/builtins/AnalysingAndConstructionOperatorsTests.kt rename to tests/prolog/builtins/AnalysisOperatorsTests.kt index 8c2e28d..dbd3b03 100644 --- a/tests/prolog/builtins/AnalysingAndConstructionOperatorsTests.kt +++ b/tests/prolog/builtins/AnalysisOperatorsTests.kt @@ -1,20 +1,24 @@ package prolog.builtins -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertInstanceOf -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import prolog.ast.Database.Program import prolog.ast.arithmetic.Integer +import prolog.ast.logic.Fact +import prolog.ast.logic.Rule +import prolog.ast.terms.CompoundTerm import prolog.ast.terms.AnonymousVariable import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Variable -class AnalysingAndConstructionOperatorsTests { +class AnalysisOperatorsTests { @Test fun `functor(foo, foo, 0)`() { - val functor = Functor(Atom("foo"), Atom("foo"), Integer(0)) + val functor = FunctorOp(Atom("foo"), Atom("foo"), Integer(0)) val result = functor.satisfy(emptyMap()).toList() @@ -25,7 +29,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(foo(X), foo, Y)`() { - val functor = Functor( + val functor = FunctorOp( Structure(Atom("foo"), listOf(Variable("X"))), Atom("foo"), Variable("Y") @@ -43,7 +47,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(foo, X, Y)`() { val atom = Atom("foo") - val functor = Functor(atom, Variable("X"), Variable("Y")) + val functor = FunctorOp(atom, Variable("X"), Variable("Y")) val result = functor.satisfy(emptyMap()).toList() @@ -57,7 +61,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(X, foo, 1)`() { - val functor = Functor(Variable("X"), Atom("foo"), Integer(1)) + val functor = FunctorOp(Variable("X"), Atom("foo"), Integer(1)) val result = functor.satisfy(emptyMap()).toList() @@ -74,7 +78,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(foo(a), foo, 0)`() { - val functor = Functor(Structure(Atom("foo"), listOf(Atom("a"))), Atom("foo"), Integer(0)) + val functor = FunctorOp(Structure(Atom("foo"), listOf(Atom("a"))), Atom("foo"), Integer(0)) val result = functor.satisfy(emptyMap()).toList() @@ -83,7 +87,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(foo(X), foo, 0)`() { - val functor = Functor(Structure(Atom("foo"), listOf(Variable("X"))), Atom("foo"), Integer(0)) + val functor = FunctorOp(Structure(Atom("foo"), listOf(Variable("X"))), Atom("foo"), Integer(0)) val result = functor.satisfy(emptyMap()).toList() @@ -92,7 +96,7 @@ class AnalysingAndConstructionOperatorsTests { @Test fun `functor(X, Y, 1)`() { - val functor = Functor(Variable("X"), Variable("Y"), Integer(1)) + val functor = FunctorOp(Variable("X"), Variable("Y"), Integer(1)) val exception = assertThrows { functor.satisfy(emptyMap()).toList() @@ -233,4 +237,168 @@ class AnalysingAndConstructionOperatorsTests { } assertEquals("Domain error: not_less_than_zero", exception.message) } -} \ No newline at end of file + + @Nested + class `Clause operator` { + @BeforeEach + fun setup() { + Program.reset() + } + + @Test + fun `clause fact atom without variables`() { + Program.load(listOf(Fact(Atom("foo")))) + + val result = ClauseOp( + Atom("foo"), + True + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertTrue(subs.isEmpty(), "Expected empty substitutions") + } + + @Test + fun `clause fact compound without variables`() { + Program.load( + listOf( + Fact(CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b")))) + ) + ) + + val result = ClauseOp( + CompoundTerm(Atom("foo"), listOf(Atom("a"), Atom("b"))), + True + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertTrue(subs.isEmpty(), "Expected empty substitutions") + } + + @Test + fun `clause rule without variables`() { + Program.load(listOf(Rule(Atom("foo"), Atom("bar")))) + + val result = ClauseOp( + Atom("foo"), + Atom("bar") + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertTrue(subs.isEmpty(), "Expected empty substitutions") + } + + @Test + fun `clause fact variable body`() { + Program.load(listOf(Fact(Atom("foo")))) + + val variable = Variable("Term") + val result = ClauseOp( + Atom("foo"), + variable + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(True, subs[variable]) + } + + @Test + fun `clause fact with variable head`() { + Program.load( + listOf( + Fact(CompoundTerm(Atom("foo"), listOf(Atom("a")))) + ) + ) + + val result = ClauseOp( + CompoundTerm(Atom("foo"), listOf(Variable("X"))), + True + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(Atom("a"), subs[Variable("X")]) + } + + @Test + fun `clause rule with variable body`() { + Program.load(listOf(Rule(Atom("foo"), Atom("bar")))) + + val result = ClauseOp( + Atom("foo"), + Variable("Term") + ).satisfy(emptyMap()).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(1, subs.size, "Expected 1 substitution") + assertEquals(Atom("bar"), subs[Variable("Term")]) + } + + @Test + fun `clause fact variable value with backtracking`() { + Program.load( + listOf( + Fact(CompoundTerm(Atom("bar"), listOf(Atom("d")))), + Fact(CompoundTerm(Atom("bar"), listOf(Atom("e")))), + Fact(CompoundTerm(Atom("foo"), listOf(Atom("a")))), + Fact(CompoundTerm(Atom("foo"), listOf(Atom("b")))), + Fact(CompoundTerm(Atom("foo"), listOf(Atom("c")))), + Rule( + CompoundTerm(Atom("foo"), listOf(Variable("X"))), + CompoundTerm(Atom("bar"), listOf(Variable("X"))) + ) + ) + ) + + val x = Variable("X") + val term = Variable("Term") + + val result = ClauseOp( + CompoundTerm(Atom("foo"), listOf(x)), + term + ).satisfy(emptyMap()).toList() + + assertEquals(4, result.size, "Expected 4 results") + + assertTrue(result[0].isSuccess, "Expected success") + val subs1 = result[0].getOrNull()!! + assertEquals(2, subs1.size, "Expected 2 substitutions") + assertEquals(Atom("a"), subs1[x], "Expected a") + assertEquals(True, subs1[term], "Expected True") + + assertTrue(result[1].isSuccess, "Expected success") + val subs2 = result[1].getOrNull()!! + assertEquals(2, subs2.size, "Expected 2 substitutions") + assertEquals(Atom("b"), subs2[x], "Expected b") + assertEquals(True, subs2[term], "Expected True") + + assertTrue(result[2].isSuccess, "Expected success") + val subs3 = result[2].getOrNull()!! + assertEquals(2, subs3.size, "Expected 2 substitutions") + assertEquals(Atom("c"), subs3[x], "Expected c") + assertEquals(True, subs3[term], "Expected True") + + assertTrue(result[3].isSuccess, "Expected success") + val subs4 = result[3].getOrNull()!! + assertEquals(1, subs4.size, "Expected 2 substitutions") + assertEquals( + CompoundTerm(Atom("bar"), listOf(Variable("X"))), + subs4[term], + "Expected bar(X)" + ) + } + } +} diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index cbb5bb6..51a27c3 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -14,7 +14,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.FunctorInfo +import prolog.ast.terms.Functor import prolog.ast.terms.Structure import prolog.ast.terms.Variable @@ -39,7 +39,7 @@ class DatabaseOperatorsTests { createAssert(fact).satisfy(emptyMap()) assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.db.predicates[FunctorInfo.of("a/_")]!!.clauses[0]) + assertEquals(fact, Program.db.predicates[Functor.of("a/_")]!!.clauses[0]) } @Test @@ -48,7 +48,7 @@ class DatabaseOperatorsTests { createAssert(fact).satisfy(emptyMap()) assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[0]) + assertEquals(fact, Program.db.predicates[Functor.of("a/1")]!!.clauses[0]) } @Test @@ -60,7 +60,7 @@ class DatabaseOperatorsTests { createAssert(rule).satisfy(emptyMap()) assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") - assertEquals(rule, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[0]) + assertEquals(rule, Program.db.predicates[Functor.of("a/1")]!!.clauses[0]) } } @@ -91,8 +91,8 @@ class DatabaseOperatorsTests { AssertA(rule2).satisfy(emptyMap()) assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") - assertEquals(rule2, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[0]) - assertEquals(rule1, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[1]) + assertEquals(rule2, Program.db.predicates[Functor.of("a/1")]!!.clauses[0]) + assertEquals(rule1, Program.db.predicates[Functor.of("a/1")]!!.clauses[1]) } } @@ -116,8 +116,8 @@ class DatabaseOperatorsTests { AssertZ(rule2).satisfy(emptyMap()) assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") - assertEquals(rule1, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[0]) - assertEquals(rule2, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses[1]) + assertEquals(rule1, Program.db.predicates[Functor.of("a/1")]!!.clauses[0]) + assertEquals(rule2, Program.db.predicates[Functor.of("a/1")]!!.clauses[1]) } } @@ -308,20 +308,20 @@ class DatabaseOperatorsTests { val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() assertEquals(3, control.size, "Expected 3 results") - assertEquals(3, Program.db.predicates[FunctorInfo.of("a/1")]!!.clauses.size, "Expected 3 clauses") + assertEquals(3, Program.db.predicates[Functor.of("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[FunctorInfo.of("a/1")]!!.clauses.size, "Expected 0 clauses") + assertEquals(0, Program.db.predicates[Functor.of("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 = FunctorInfo.of("$predicateName/1") + val predicateFunctor = Functor.of("$predicateName/1") assertFalse(predicateFunctor in Program.db.predicates, "Expected predicate to not exist before")