diff --git a/.idea/2025LogProg-PrologInterpreter.iml b/.idea/2025LogProg-PrologInterpreter.iml
new file mode 100644
index 0000000..42d53f5
--- /dev/null
+++ b/.idea/2025LogProg-PrologInterpreter.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..5b434ac
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index a50fab0..cd901a3 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,6 +10,8 @@ repositories {
}
dependencies {
+ // CLI argument parsing
+ implementation("com.xenomachina:kotlin-argparser:2.0.7")
// Parser combinator library
implementation("com.github.h0tk3y.betterParse:better-parse:0.4.4")
@@ -26,20 +28,26 @@ sourceSets {
}
}
-tasks {
- withType {
- manifest {
- attributes["Main-Class"] = "MainKt"
- }
- from(configurations.runtimeClasspath.get().map {
- if (it.isDirectory) it else zipTree(it)
- })
- }
-
- test {
- useJUnitPlatform()
- testLogging {
- events("passed", "skipped", "failed")
- }
+tasks.named("test") {
+ useJUnitPlatform()
+ testLogging {
+ events("passed", "skipped", "failed")
+ }
+}
+
+tasks.register("fatJar") {
+ manifest {
+ attributes["Main-Class"] = "MainKt"
+ }
+ duplicatesStrategy = DuplicatesStrategy.EXCLUDE
+ from(configurations.runtimeClasspath.get().map {
+ if (it.isDirectory) it else zipTree(it)
+ })
+ with(tasks.jar.get() as CopySpec)
+}
+
+tasks {
+ build {
+ dependsOn("fatJar")
}
}
diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl
index 357e09b..0ed5d04 100644
--- a/examples/scratchpad.pl
+++ b/examples/scratchpad.pl
@@ -1 +1,32 @@
-choice(X) :- X = 1, !; X = 2.
+% choice(X) :- X = 1, !; X = 2.
+:- dynamic declaration/1.
+
+add_declaration_first(NewDecl) :-
+ asserta(declaration(NewDecl)).
+
+add_declaration_last(NewDecl) :-
+ assertz(declaration(NewDecl)).
+
+database :-
+ add_declaration_first('Man is born free, and everywhere he is in chains.'),
+ retract(declaration(_)),
+ add_declaration_last('The revolution devours its own children.'),
+ add_declaration_first('I disapprove of what you say, but I will defend to the death your right to say it.'),
+ add_declaration_first('Give me Liberty, or give me Death!'),
+ add_declaration_last('So this is how liberty dies, with thunderous applause.').
+
+show_declarations :-
+ declaration(Decl),
+ write(Decl), nl,
+ fail.
+
+show_declarations.
+
+:- initialization(main).
+
+main :-
+ database,
+ show_declarations,
+ retractall(declaration(_)),
+ show_declarations.
+
diff --git a/src/Debug.kt b/src/Debug.kt
deleted file mode 100644
index ba9e63e..0000000
--- a/src/Debug.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-data object Debug {
- val on: Boolean = true
-}
\ No newline at end of file
diff --git a/src/Main.kt b/src/Main.kt
index 38efc51..2416fdb 100644
--- a/src/Main.kt
+++ b/src/Main.kt
@@ -1,107 +1,29 @@
-import better_parser.SimpleReplParser
-import interpreter.SourceFileReader
-import prolog.Answer
-import kotlin.system.exitProcess
+import com.xenomachina.argparser.ArgParser
+import interpreter.FileLoader
+import io.GhentPrologArgParser
+import io.Logger
+import repl.Repl
-fun help(): String {
- println("Unknown command. Type 'h' for help.")
- println("Commands:")
- println(" ; - find next solution")
- println(" a - abort")
- println(" . - end query")
- println(" h - help")
- println(" exit - exit Prolog REPL")
- return ""
-}
+fun main(args: Array) {
+ // Parse command line arguments
+ val parsedArgs = ArgParser(args).parseInto(::GhentPrologArgParser)
-fun say(message: String) {
- println(message)
-}
+ parsedArgs.run {
+ val loader = FileLoader()
-fun prompt(message: String): String {
- print("$message ")
- var input = readlnOrNull()
+ // Set the verbosity level
+ Logger.level = verbosity
- while (input.isNullOrBlank()) {
- if (input == null) {
- println("Exiting Prolog REPL.")
- exitProcess(0)
+ // Check if script was provided
+ for (file in script) {
+ loader.load(file)
}
- if (input.isBlank()) {
- print("$message ")
- }
-
- input = readlnOrNull()
- }
-
- if (input == "exit") {
- println("Exiting Prolog REPL.")
- exitProcess(0)
- }
-
- return input
-}
-
-fun prettyResult(result: Answer): String {
- result.fold(
- onSuccess = {
- val subs = result.getOrNull()!!
- if (subs.isEmpty()) {
- return "true."
- }
- return subs.entries.joinToString(", ") { "${it.key} = ${it.value}" }
- },
- onFailure = {
- return "Failure: ${result.exceptionOrNull()!!}"
- }
- )
-}
-
-val knownCommands = setOf(";", "a", ".")
-
-fun main() {
- SourceFileReader().readFile("tests/better_parser/resources/parent.pl")
-
- val parser = SimpleReplParser(debug = false)
-
- say("Prolog REPL. Type 'exit' to quit.")
-
- while (true) {
- val queryString = prompt("?-")
-
- try {
- val query = parser.parse(queryString)
- val answers = query.satisfy(emptyMap())
-
- if (answers.none()) {
- say("false.")
- } else {
- val iterator = answers.iterator()
-
- var previous = iterator.next()
-
- while (iterator.hasNext()) {
- var command = prompt(prettyResult(previous))
-
- while (command !in knownCommands) {
- say("Unknown action: $command (h for help)")
- command = prompt("Action?")
- }
-
- when (command) {
- ";" -> previous = iterator.next()
- "a" -> break
- "." -> break
- }
- }
-
- say(prettyResult(previous))
- }
-
- } catch (e: Exception) {
- println("Error: ${e.message}")
+ // Check if REPL was requested
+ if (repl) {
+ Repl()
+ } else {
+ Logger.warn("REPL not started. Use -r or --repl to start the REPL.")
}
}
-
}
diff --git a/src/better_parser/PrologParser.kt b/src/better_parser/PrologParser.kt
deleted file mode 100644
index 73e63eb..0000000
--- a/src/better_parser/PrologParser.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.grammar.Grammar
-import com.github.h0tk3y.betterParse.grammar.parseToEnd
-import prolog.Program
-import prolog.ast.logic.Clause
-import prolog.ast.terms.Atom
-
-class PrologParser {
- private val parser: Grammar> = SimpleSourceParser() as Grammar>
-
- public fun parse(input: String) {
- val clauses: List = parser.parseToEnd(input)
-
- Program.load(clauses)
- }
-}
\ No newline at end of file
diff --git a/src/better_parser/PrologSourceParser.kt b/src/better_parser/PrologSourceParser.kt
deleted file mode 100644
index 0383507..0000000
--- a/src/better_parser/PrologSourceParser.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.combinators.*
-import com.github.h0tk3y.betterParse.grammar.Grammar
-import com.github.h0tk3y.betterParse.lexer.literalToken
-import com.github.h0tk3y.betterParse.lexer.regexToken
-import com.github.h0tk3y.betterParse.parser.Parser
-import prolog.ast.logic.Fact
-import prolog.ast.arithmetic.Integer
-import prolog.ast.arithmetic.Float
-import prolog.ast.logic.Clause
-import prolog.ast.logic.LogicOperand
-import prolog.ast.logic.Rule
-import prolog.ast.terms.*
-import prolog.builtins.Conjunction
-import prolog.builtins.Disjunction
-
-class PrologSourceParser : Grammar>() {
- // Define the tokens
- private val atom by regexToken("[a-z][a-zA-Z0-9_]*")
- private val variable by regexToken("[A-Z][a-zA-Z0-9_]*")
- private val number by regexToken("-?[0-9]+(\\.[0-9]+)?")
- private val whitespace by regexToken("\\s+", ignore = true)
-
- private val comma by literalToken(",")
- private val semicolon by literalToken(";")
- private val neck by literalToken(":-")
- private val lparen by literalToken("(")
- private val rparen by literalToken(")")
- private val dot by literalToken(".")
-
- private val atomParser by atom use { Atom(text) }
- private val variableParser by variable use { Variable(text) }
- private val intParser by number use { Integer(text.toInt()) }
- private val floatParser by number use { Float(text.toFloat()) }
- private val numberParser by (intParser or floatParser)
- private val compoundTermParser by (atomParser and skip(lparen) and separated(
- atomParser or variableParser,
- comma
- ) and skip(rparen)) use {
- CompoundTerm(t1, t2.terms)
- }
-
- private val termParser: Parser by (numberParser or variableParser or compoundTermParser or atomParser)
-
- private val logicOperandParser: Parser by (termParser or compoundTermParser or atomParser) map {
- it as LogicOperand
- }
-
- private val conjunctionParser: Parser by (logicOperandParser and comma and logicOperandParser) use {
- Conjunction(t1, t3)
- }
- private val disjunctionParser: Parser by (logicOperandParser and semicolon and logicOperandParser) use {
- Disjunction(t1, t3)
- }
-
- private val operatorParser: Parser by (conjunctionParser or disjunctionParser)
-
- private val headParser by (compoundTermParser or atomParser)
- private val bodyParser by (operatorParser or compoundTermParser or atomParser)
-
- private val factParser by (headParser and dot) use { Fact(t1 as Head) }
- private val ruleParser by (headParser and neck and bodyParser and dot) use {
- Rule(t1 as Head, t3 as Body)
- }
-
- private val clauseParser: Parser by (factParser or ruleParser)
-
- override val rootParser: Parser> by zeroOrMore(clauseParser)
-}
diff --git a/src/better_parser/SimplePrologParser.kt b/src/better_parser/SimplePrologParser.kt
deleted file mode 100644
index 1fb49c7..0000000
--- a/src/better_parser/SimplePrologParser.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.combinators.*
-import com.github.h0tk3y.betterParse.grammar.Grammar
-import com.github.h0tk3y.betterParse.grammar.parser
-import com.github.h0tk3y.betterParse.lexer.Token
-import com.github.h0tk3y.betterParse.lexer.literalToken
-import com.github.h0tk3y.betterParse.lexer.regexToken
-import com.github.h0tk3y.betterParse.lexer.token
-import com.github.h0tk3y.betterParse.parser.Parser
-import prolog.ast.arithmetic.Float
-import prolog.ast.arithmetic.Integer
-import prolog.ast.terms.Atom
-import prolog.ast.terms.Structure
-import prolog.ast.terms.Term
-import prolog.ast.terms.Variable
-
-open class SimplePrologParser : Grammar() {
- // Prolog tokens
- protected val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*")
- protected val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*")
-
- // Arithmetic tokens
- private val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+")
- private val integerToken: Token by regexToken("-?([1-9][0-9]*|0)")
-
- // Special tokens
- protected val neck by literalToken(":-")
- protected val comma: Token by literalToken(",")
- protected val leftParenthesis: Token by literalToken("(")
- protected val rightParenthesis: Token by literalToken(")")
- protected val dot by literalToken(".")
-
- // Ignored tokens
- protected val whitespace: Token by regexToken("\\s+", ignore = true)
- protected val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true)
- protected val multiLineComment: Token by regexToken("/\\*.*?\\*/", ignore = true)
-
- protected val dummy by token { _, _ -> -1 } use { throw IllegalStateException("This parser should not be used") }
-
- // Prolog parsers
- protected val variable: Parser by variableToken use { Variable(text) }
- protected val atom: Parser by nameToken use { Atom(text) }
- protected val compound: Parser by (atom and skip(leftParenthesis) and separated(
- parser(::term),
- comma,
- acceptZero = true
- ) and skip(rightParenthesis)) use {
- Structure(t1, t2.terms)
- }
-
- // Arithmetic parsers
- private val int: Parser by integerToken use { Integer(text.toInt()) }
- private val float: Parser by floatToken use {
- Float(text.toFloat())
- }
-
- protected val term: Parser by (dummy
- or float
- or int
- or variable
- or compound
- or atom
- ) map { it }
-
- override val rootParser: Parser = term
-}
\ No newline at end of file
diff --git a/src/better_parser/SimpleReplParser.kt b/src/better_parser/SimpleReplParser.kt
deleted file mode 100644
index 0c8a598..0000000
--- a/src/better_parser/SimpleReplParser.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.combinators.times
-import com.github.h0tk3y.betterParse.combinators.unaryMinus
-import com.github.h0tk3y.betterParse.combinators.use
-import com.github.h0tk3y.betterParse.grammar.parseToEnd
-import com.github.h0tk3y.betterParse.parser.Parser
-import prolog.ast.logic.LogicOperand
-import prolog.builtins.Query
-
-class SimpleReplParser(val debug: Boolean = false) : SimpleSourceParser() {
- override val rootParser: Parser by (body * -dot) use { Query(this as LogicOperand) }
-
- fun parse(input: String): Query {
- if (debug) {
- println("Parsing input: $input")
- }
-
- val query = parseToEnd(input) as Query
-
- if (debug) {
- println("Parsed query: $query")
- }
-
- return query
- }
-}
\ No newline at end of file
diff --git a/src/better_parser/SimpleSourceParser.kt b/src/better_parser/SimpleSourceParser.kt
deleted file mode 100644
index 437f9ed..0000000
--- a/src/better_parser/SimpleSourceParser.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.combinators.*
-import com.github.h0tk3y.betterParse.grammar.parser
-import com.github.h0tk3y.betterParse.lexer.literalToken
-import com.github.h0tk3y.betterParse.parser.Parser
-import prolog.ast.arithmetic.ArithmeticOperator
-import prolog.ast.logic.*
-import prolog.ast.terms.*
-import prolog.builtins.Conjunction
-import prolog.builtins.Disjunction
-
-open class SimpleSourceParser : SimplePrologParser() {
- protected val simpleLogicOperand: Parser by (dummy
- or compound
- or atom
- )
- protected val logicOperand: Parser by (dummy
- or parser(::operator)
- or simpleLogicOperand
- )
-
- protected val arithmeticOperator: Parser by dummy
- protected val logicOperator: Parser by (simpleLogicOperand * comma * logicOperand) use {
- Conjunction(t1, t3)
- }
-
- protected val operator: Parser by (arithmeticOperator or logicOperator) use { this as Operator }
-
- protected val head: Parser by (dummy
- or compound
- or atom
- )
- protected val body: Parser by (dummy
- or operator
- or head
- ) use { this as Body }
-
- // ----
-
- private val rule: Parser by (head * -neck * body) use { Rule(t1, t2) }
- private val fact: Parser by head use { Fact(this) }
-
- private val clause: Parser by ((rule or fact) * -dot)
- private val clauses: Parser> by zeroOrMore(clause)
-
- override val rootParser: Parser by clauses
-}
\ No newline at end of file
diff --git a/src/gpl b/src/gpl
index c1eecb1..714a5a2 100755
--- a/src/gpl
+++ b/src/gpl
@@ -20,7 +20,7 @@ fi
if [ ! -f "${JAR_PATH}" ]; then
printf 'Info: JAR file not found at "%s"\n' "${JAR_PATH}"
printf 'Info: Building the project...\n'
- ./gradlew build
+ ./gradlew fatJar
if [ "${?}" -ne 0 ]; then
printf 'Error: Build failed\n'
exit 1
diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt
new file mode 100644
index 0000000..7af15ab
--- /dev/null
+++ b/src/interpreter/FileLoader.kt
@@ -0,0 +1,55 @@
+package interpreter
+
+import io.Logger
+import parser.ScriptParser
+import prolog.ast.Database
+import prolog.ast.Database.Program
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Predicate
+
+class FileLoader {
+ private val parser = ScriptParser()
+
+ fun load(filePath: String) {
+ Logger.info("Loading file: $filePath")
+
+ val input = readFile(filePath)
+
+ Logger.debug("Parsing content of $filePath")
+ val clauses: List = parser.parse(input)
+
+ Logger.debug("Adding clauses to program")
+ addToProgram(clauses, filePath)
+
+ Logger.debug("Finished loading file: $filePath")
+ }
+
+ fun readFile(filePath: String): String {
+ try {
+ Logger.debug("Reading $filePath")
+ val file = java.io.File(filePath)
+
+ if (!file.exists()) {
+ Logger.error("File $filePath does not exist")
+ throw IllegalArgumentException("File not found: $filePath")
+ }
+
+ return file.readText()
+ } catch (e: Exception) {
+ Logger.error("Error reading file: $filePath")
+ throw RuntimeException("Error reading file: $filePath", e)
+ }
+ }
+
+ fun addToProgram(clauses: List, filePath: String) {
+ Logger.debug("Grouping clauses by functor")
+ val groupedClauses = clauses.groupBy { it.functor }
+ val predicates: List = groupedClauses.map { (_, clauses) ->
+ Predicate(clauses)
+ }
+
+ val database = Database(filePath)
+ predicates.forEach { database.load(it) }
+ Program.consult(database)
+ }
+}
\ No newline at end of file
diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt
new file mode 100644
index 0000000..6c17705
--- /dev/null
+++ b/src/interpreter/Preprocessor.kt
@@ -0,0 +1,141 @@
+package interpreter
+
+import io.Logger
+import prolog.ast.arithmetic.Expression
+import prolog.ast.arithmetic.Integer
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Fact
+import prolog.ast.logic.LogicOperand
+import prolog.ast.logic.Rule
+import prolog.ast.terms.*
+import prolog.builtins.*
+
+/**
+ * Preprocessor for Prolog
+ *
+ * This class preprocesses Prolog code and applies various transformations such as recognizing builtins.
+ */
+open class Preprocessor {
+ /**
+ * Preprocesses the input Prolog code.
+ *
+ * @param input The already parsed Prolog code as a list of clauses.
+ * @return The preprocessed Prolog code as a list of clauses.
+ */
+ fun preprocess(input: List): List {
+ return input.map { preprocess(it) }
+ }
+
+ fun preprocess(input: Query): Query {
+ return Query(preprocess(input.query) as Goal)
+ }
+
+ private fun preprocess(clause: Clause): Clause {
+ return when (clause) {
+ is Fact -> {
+ Fact(preprocess(clause.head) as Head)
+ }
+
+ is Rule -> {
+ Rule(
+ preprocess(clause.head) as Head,
+ preprocess(clause.body as Term) as Body
+ )
+ }
+
+ else -> clause
+ }
+ }
+
+ 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 -> {
+ // Preprocess the arguments first to recognize builtins
+ val args = term.arguments.map { preprocess(it, nested = true) }
+
+ when {
+ // TODO Remove hardcoding by storing the functors as constants in operators?
+
+ term.functor == ":-/2" -> Rule( args[0] as Head, args[1] as Body )
+
+ // Logic
+ term.functor == "=/2" -> Unify(args[0], args[1])
+ term.functor == "\\=/2" -> NotUnify(args[0], args[1])
+ term.functor == ",/2" -> Conjunction(args[0] as LogicOperand, args[1] as LogicOperand)
+ term.functor == ";/2" -> Disjunction(args[0] as LogicOperand, args[1] as LogicOperand)
+ term.functor == "\\+/1" -> Not(args[0] as Goal)
+ term.functor == "==/2" -> Equivalent(args[0], args[1])
+
+ term.functor == "=\\=/2" && args.all { it is Expression } -> EvaluatesToDifferent(args[0] as Expression, args[1] as Expression)
+ term.functor == "=:=/2" && args.all { it is Expression } -> EvaluatesTo(args[0] as Expression, args[1] as Expression)
+ term.functor == "is/2" && args.all { it is Expression } -> Is(args[0] as Expression, args[1] as Expression)
+
+ // Arithmetic
+
+ term.functor == "-/1" && args.all { it is Expression } -> Negate(args[0] as Expression)
+ term.functor == "-/2" && args.all { it is Expression } -> Subtract(args[0] as Expression, args[1] as Expression)
+ term.functor == "+/1" && args.all { it is Expression } -> Positive(args[0] as Expression)
+ term.functor == "+/2" && args.all { it is Expression } -> Add(args[0] as Expression, args[1] as Expression)
+ term.functor == "*/2" && args.all { it is Expression } -> Multiply(args[0] as Expression, args[1] as Expression)
+ term.functor == "//2" && args.all { it is Expression } -> Divide(args[0] as Expression, args[1] 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
+ term.functor == "dynamic/1" -> Dynamic((args[0] as Atom).name)
+ term.functor == "retract/1" -> Retract(args[0])
+ term.functor == "assert/1" -> {
+ if (args[0] is Rule) {
+ Assert(args[0] as Rule)
+ } else {
+ Assert(Fact(args[0] as Head))
+ }
+ }
+ 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
+ term.functor == "write/1" -> Write(args[0])
+ term.functor == "read/1" -> Read(args[0])
+ term.functor == "initialization/1" -> Initialization(args[0] as Goal)
+
+ else -> {
+ term.arguments = args
+ term
+ }
+ }
+ }
+
+ else -> term
+ }
+
+ Logger.debug(
+ "Preprocessed term $term into $prepped (kind ${prepped::class.simpleName})",
+ !nested && (prepped != term || prepped::class != term::class)
+ )
+
+ return prepped
+ }
+}
\ No newline at end of file
diff --git a/src/interpreter/SourceFileReader.kt b/src/interpreter/SourceFileReader.kt
deleted file mode 100644
index e3e58cc..0000000
--- a/src/interpreter/SourceFileReader.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package interpreter
-
-import better_parser.PrologParser
-
-class SourceFileReader {
- private val parser = PrologParser()
-
- fun readFile(filePath: String) {
- return try {
- val file = java.io.File(filePath)
- if (!file.exists()) {
- throw IllegalArgumentException("File not found: $filePath")
- }
-
- val content = file.readText()
-
- // Parse the content using SimpleSourceParser
- parser.parse(content)
- } catch (e: Exception) {
- throw RuntimeException("Error reading file: $filePath", e)
- }
- }
-}
\ No newline at end of file
diff --git a/src/io/GhentPrologArgParser.kt b/src/io/GhentPrologArgParser.kt
new file mode 100644
index 0000000..f27f579
--- /dev/null
+++ b/src/io/GhentPrologArgParser.kt
@@ -0,0 +1,21 @@
+package io
+
+import com.xenomachina.argparser.ArgParser
+import com.xenomachina.argparser.default
+
+class GhentPrologArgParser(parser: ArgParser) {
+ val script by parser.adding("-s", "--script", help = "Script to run")
+ val repl by parser.flagging("-r", "--repl", help = "Start the REPL")
+
+ val verbosity by parser.mapping(
+ "--vvv" to Logger.Level.DEBUG,
+ "--debug" to Logger.Level.DEBUG,
+ "--vv" to Logger.Level.INFO,
+ "--verbose" to Logger.Level.INFO,
+ "--info" to Logger.Level.INFO,
+ "-v" to Logger.Level.WARN,
+ "--warn" to Logger.Level.WARN,
+ "--error" to Logger.Level.ERROR,
+ help = "Set the verbosity level (default: WARN)",
+ ).default(Logger.defaultLevel)
+}
\ No newline at end of file
diff --git a/src/io/IoHandler.kt b/src/io/IoHandler.kt
new file mode 100644
index 0000000..c5706a1
--- /dev/null
+++ b/src/io/IoHandler.kt
@@ -0,0 +1,10 @@
+package io
+
+interface IoHandler {
+ fun prompt(
+ message: String,
+ hint: () -> String = { "Please enter a valid input." }
+ ): String
+
+ fun say(message: String)
+}
\ No newline at end of file
diff --git a/src/io/Logger.kt b/src/io/Logger.kt
new file mode 100644
index 0000000..ac9de5a
--- /dev/null
+++ b/src/io/Logger.kt
@@ -0,0 +1,25 @@
+package io
+
+object Logger {
+ enum class Level {
+ DEBUG, INFO, WARN, ERROR
+ }
+
+ val defaultLevel: Level = Level.WARN
+ var level: Level = defaultLevel
+
+ private val io = Terminal()
+
+ fun log(message: String, messageLevel: Level = defaultLevel, onlyIf: Boolean) {
+ if (level <= messageLevel && onlyIf) {
+ io.checkNewLine()
+ val text = "$messageLevel: $message\n"
+ io.say(text)
+ }
+ }
+
+ fun debug(message: String, onlyIf: Boolean = true) = log(message, Level.DEBUG, onlyIf)
+ fun info(message: String, onlyIf: Boolean = true) = log(message, Level.INFO, onlyIf)
+ fun warn(message: String, onlyIf: Boolean = true) = log(message, Level.WARN, onlyIf)
+ fun error(message: String, onlyIf: Boolean = true) = log(message, Level.ERROR, onlyIf)
+}
diff --git a/src/io/Terminal.kt b/src/io/Terminal.kt
new file mode 100644
index 0000000..1b9df94
--- /dev/null
+++ b/src/io/Terminal.kt
@@ -0,0 +1,71 @@
+package io
+
+import prolog.ast.Database.Program
+import java.io.BufferedReader
+import java.io.BufferedWriter
+import java.io.InputStream
+import java.io.OutputStream
+
+/**
+ * Handles input and output from a terminal.
+ */
+class Terminal(
+ inputStream: InputStream = System.`in`,
+ outputStream: OutputStream = System.`out`,
+ errorStream: OutputStream = System.err
+) : IoHandler {
+ val input: BufferedReader = inputStream.bufferedReader()
+ val output: BufferedWriter = outputStream.bufferedWriter()
+ val error: BufferedWriter = errorStream.bufferedWriter()
+
+ override fun prompt(
+ message: String,
+ hint: () -> String
+ ): String {
+ say("$message ")
+ var input: String = readLine()
+ while (input.isBlank()) {
+ say(hint(), error)
+ input = readLine()
+ }
+ return input
+ }
+
+ override fun say(message: String) {
+ output.write(message)
+ output.flush()
+ }
+
+ fun say(message: String, writer: BufferedWriter = output) {
+ writer.write(message)
+ writer.flush()
+ }
+
+ /**
+ * Reads a line from the input stream.
+ */
+ fun readLine(reader: BufferedReader = input): String {
+ val line = reader.readLine()
+ if (line == null) {
+ Logger.info("End of stream reached")
+ cleanup()
+ }
+ return line
+
+ }
+
+ fun cleanup() {
+ Logger.info("Closing terminal streams")
+ input.close()
+ output.close()
+ error.close()
+ System.exit(0)
+ }
+
+ fun checkNewLine() {
+ if (Program.storeNewLine) {
+ say("\n")
+ Program.storeNewLine = false
+ }
+ }
+}
diff --git a/src/lexer/Lexer.kt b/src/lexer/Lexer.kt
deleted file mode 100644
index c239fbd..0000000
--- a/src/lexer/Lexer.kt
+++ /dev/null
@@ -1,127 +0,0 @@
-package lexer
-
-import lexer.errors.LexingError
-import lexer.errors.LexingErrorType
-import lexer.state.LexerPosition
-import lexer.state.TokenPosition
-
-class Lexer(private val source: String) {
- private var tokens: List = emptyList()
- private val position = LexerPosition(0, 0, -1)
-
- /**
- * Scans the source code and returns a list of tokens.
- * @return List of [Token]s
- */
- fun scan(): List {
- while (hasNext()) {
- val char: Char = peek()
- tokens += when {
- char == '(' -> scanSymbol(TokenType.PARENTHESIS_LEFT)
- char == ')' -> scanSymbol(TokenType.PARENTHESIS_RIGHT)
- char == '.' -> scanSymbol(TokenType.DOT)
- char == '"' -> scanQuotedString()
- char == '%' -> { scanComment(); continue }
- char.isLetterOrDigit() -> scanAlphanumeric()
- char.isWhitespace() -> { scanWhitespace(); continue }
- else -> throw LexingError(LexingErrorType.UNKNOWN_TOKEN, "Did not recognize $char", position)
- }
- }
- tokens += Token(TokenType.EOF, "EOF", getPosition(0))
- return tokens
- }
-
- private fun hasNext(): Boolean {
- // Check if the position is within the source length
- return position.offset < source.length
- }
-
- private fun peek(): Char {
- // Peek should only be called if there is a next character
- require(hasNext()) {
- LexingError(LexingErrorType.UNEXPECTED_END_OF_INPUT, "Expected additional character", position)
- }
-
- return source[position.offset]
- }
-
- private fun next(): Char {
- // Advance the position and return the character
- val char = peek()
- position.offset++
- position.column++
- return char
- }
-
- private fun getPosition(length: Int = 1): TokenPosition {
- // Return a new TokenPosition based on the current LexerPosition
- return TokenPosition(position.line, position.column, length)
- }
-
- /* * * * * * *
- * Scanners *
- * * * * * * */
-
- /**
- * Scans a symbol token, given the expected [TokenType].
- * @param tokenType The expected [TokenType]
- * @return The scanned [Token]
- */
- private fun scanSymbol(tokenType: TokenType): Token {
- return Token(tokenType, next().toString(), getPosition(1))
- }
-
- private fun scanAlphanumeric(): Token {
- // Scan all alphanumeric characters
- var length = 0
- while (hasNext() && peek().isLetterOrDigit()) {
- next()
- length++
- }
- val value = source.substring(position.offset - length, position.offset)
- return Token(TokenType.ALPHANUMERIC, value, getPosition(length))
- }
-
- private fun scanQuotedString(): Token {
- // "Assert" that the next character is the start of a quoted string
- require(next() == '"') {
- LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected opening quote '('", position)
- }
-
- var length = 0
- while (hasNext() && peek() != '"') {
- next()
- length++
- }
-
- // "Assert" that the next character is the end of the quoted string
- require(next() == '"') {
- LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected closing quote ')'", position)
- }
-
- val value = source.substring(position.offset - length - 1, position.offset - 1)
- return Token(TokenType.ALPHANUMERIC, value, getPosition(length))
- }
-
- private fun scanComment() {
- // "Assert" that the next character is the start of a comment
- require(next() == '%') {
- LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected opening comment '%'", position)
- }
-
- // Skip all characters until the end of the line
- while (hasNext() && peek() != '\n') {
- next()
- }
- }
-
- private fun scanWhitespace() {
- // Skip all whitespace characters
- while (hasNext() && peek().isWhitespace()) {
- if (next() == '\n') {
- position.line++
- position.column = 0
- }
- }
- }
-}
diff --git a/src/lexer/Token.kt b/src/lexer/Token.kt
deleted file mode 100644
index c163bfd..0000000
--- a/src/lexer/Token.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package lexer
-
-import lexer.state.TokenPosition
-
-data class Token(
- val type: TokenType,
- val value: String,
- val position: TokenPosition
-)
diff --git a/src/lexer/TokenType.kt b/src/lexer/TokenType.kt
deleted file mode 100644
index 50d5141..0000000
--- a/src/lexer/TokenType.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package lexer
-
-enum class TokenType {
- ALPHANUMERIC,
- // TODO Replace with SMALL_LETTER, CAPITAL_LETTER, DIGIT, HEX_DIGIT, ... ?
-
- // Structure
- COMMA,
- DOT,
- PARENTHESIS_LEFT, PARENTHESIS_RIGHT,
-
- // Special
-
- EOF
-}
diff --git a/src/lexer/errors/LexingError.kt b/src/lexer/errors/LexingError.kt
deleted file mode 100644
index 7a5f4c9..0000000
--- a/src/lexer/errors/LexingError.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package lexer.errors
-
-import lexer.state.LexerPosition
-
-data class LexingError(
- val type: LexingErrorType,
- override val message: String,
- val position: LexerPosition
-) : Throwable(
- """
- ${position.line}:${position.column + 1} ${type}: $message
-""".trimIndent()
-)
diff --git a/src/lexer/errors/LexingErrorType.kt b/src/lexer/errors/LexingErrorType.kt
deleted file mode 100644
index bff243a..0000000
--- a/src/lexer/errors/LexingErrorType.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package lexer.errors
-
-enum class LexingErrorType {
- UNKNOWN_TOKEN,
- UNEXPECTED_TOKEN,
- UNEXPECTED_END_OF_INPUT,
-}
\ No newline at end of file
diff --git a/src/lexer/state/LexerPosition.kt b/src/lexer/state/LexerPosition.kt
deleted file mode 100644
index 583bf29..0000000
--- a/src/lexer/state/LexerPosition.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package lexer.state
-
-data class LexerPosition(var offset: Int, var line: Int, var column: Int)
diff --git a/src/lexer/state/TokenPosition.kt b/src/lexer/state/TokenPosition.kt
deleted file mode 100644
index 2f19f76..0000000
--- a/src/lexer/state/TokenPosition.kt
+++ /dev/null
@@ -1,3 +0,0 @@
-package lexer.state
-
-data class TokenPosition(val line: Int, val column: Int, val length: Int)
diff --git a/src/parser/Parser.kt b/src/parser/Parser.kt
index e2e63e8..e8c7383 100644
--- a/src/parser/Parser.kt
+++ b/src/parser/Parser.kt
@@ -1,137 +1,11 @@
package parser
-import lexer.Token
-import lexer.TokenType
-import parser.errors.ParsingError
-import parser.errors.ParsingErrorType
-import parser.state.ParserPosition
-import prolog.ast.logic.Clause
-import prolog.ast.logic.Fact
-import prolog.ast.logic.Rule
-import prolog.ast.terms.Atom
-import prolog.ast.terms.Structure
-import prolog.ast.terms.Term
-
-class Parser(private val tokens: List) {
- private val position: ParserPosition = ParserPosition(0)
-
- fun parse(): List {
- val terms = mutableListOf()
-
- while (hasNext()) {
- position.save()
-
- var term: Term? = null
-
- while (term == null) {
- // Try each parser rule in order
-
- }
-
- require(term != null) {
- ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Expected a term", position)
- }
-
- terms.add(term)
- }
-
- return terms
- }
-
+interface Parser {
/**
- * Matches the current token with any of the expected types.
- * If it matches, it consumes the token and returns true.
+ * Parses the input string and returns the parsed result.
*
- * @param types The list of expected token types.
- * @return True if the current token matches any of the expected types, false otherwise.
+ * @param input The input string to parse.
+ * @return The parsed result, which is the AST of the input.
*/
- private fun match(types: List): Boolean {
- for (type in types) {
- if (check(type)) {
- next()
- return true
- }
- }
-
- return false
- }
-
- /**
- * Checks if the current token matches the expected type.
- */
- private fun check(type: TokenType): Boolean {
- return hasNext() && peek().type == type
- }
-
- private fun hasNext(): Boolean {
- // Check if the position is within the tokens list
- // TODO Check for EOF instead?
- return position.offset < tokens.size
- }
-
- private fun peek(): Token {
- require(hasNext()) { "Unexpected end of input" }
-
- return tokens[position.offset]
- }
-
- private fun next(): Token {
- val token = peek()
- position.offset++
- return token
- }
-
- private fun previous(): Token {
- require(0 < position.offset) { "No previous token" }
- return tokens[position.offset - 1]
- }
-
- /* * * * * *
- * Parsers *
- * * * * * */
-
- private fun parseWithTry(parseRule: () -> Term): Term {
- try {
- return parseRule()
- } catch (e: Exception) {
- throw ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Unexpected token", position)
- }
- }
-
- private fun parseClause(): Clause {
- return try {
- Fact(parseStructure())
- } catch (e: Exception) {
- Fact(parseAtom())
- }
- }
-
- private fun parseStructure(): Structure {
- val name = parseAtom()
- val args = mutableListOf()
-
- require(match(listOf(TokenType.PARENTHESIS_LEFT))) {
- ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Expected '(' after structure name", position)
- }
-
- // TODO Handle arguments
-
- require(match(listOf(TokenType.PARENTHESIS_RIGHT))) {
- ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Expected ')' after structure arguments", position)
- }
-
- return Structure(name, args)
- }
-
- private fun parseAtom(): Atom {
- return Atom(parseLetterDigit())
- }
-
- private fun parseLetterDigit(): String {
- require(match(listOf(TokenType.ALPHANUMERIC)) && previous().value[0].isLowerCase()) {
- ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Expected lowercase letter", position)
- }
-
- return previous().value
- }
-}
+ fun parse(input: String): Any
+}
\ No newline at end of file
diff --git a/src/parser/ReplParser.kt b/src/parser/ReplParser.kt
new file mode 100644
index 0000000..a39fae8
--- /dev/null
+++ b/src/parser/ReplParser.kt
@@ -0,0 +1,12 @@
+package parser
+
+import com.github.h0tk3y.betterParse.grammar.Grammar
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import parser.grammars.QueryGrammar
+import prolog.builtins.Query
+
+class ReplParser : Parser {
+ private val grammar: Grammar = QueryGrammar() as Grammar
+
+ override fun parse(input: String): Query = grammar.parseToEnd(input)
+}
\ No newline at end of file
diff --git a/src/parser/ScriptParser.kt b/src/parser/ScriptParser.kt
new file mode 100644
index 0000000..f95731c
--- /dev/null
+++ b/src/parser/ScriptParser.kt
@@ -0,0 +1,18 @@
+package parser
+
+import com.github.h0tk3y.betterParse.grammar.Grammar
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import interpreter.Preprocessor
+import io.Logger
+import parser.grammars.LogicGrammar
+import prolog.ast.logic.Clause
+
+class ScriptParser: Parser {
+ private val grammar: Grammar> = LogicGrammar() as Grammar>
+ private val preprocessor = Preprocessor()
+
+ override fun parse(input: String): List {
+ val raw = grammar.parseToEnd(input)
+ return preprocessor.preprocess(raw)
+ }
+}
\ No newline at end of file
diff --git a/src/parser/errors/ParsingError.kt b/src/parser/errors/ParsingError.kt
deleted file mode 100644
index 7ddbfc2..0000000
--- a/src/parser/errors/ParsingError.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package parser.errors
-
-import parser.state.ParserPosition
-
-class ParsingError(private val type: ParsingErrorType, override val message: String, private val position: ParserPosition) :
- Throwable() {
- override fun toString(): String {
- return """
- ($position) ${type}: $message
- """.trimIndent()
- }
-}
\ No newline at end of file
diff --git a/src/parser/errors/ParsingErrorType.kt b/src/parser/errors/ParsingErrorType.kt
deleted file mode 100644
index 5e017d8..0000000
--- a/src/parser/errors/ParsingErrorType.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package parser.errors
-
-enum class ParsingErrorType {
- UNEXPECTED_TOKEN,
-
- INTERNAL_ERROR,
-}
\ No newline at end of file
diff --git a/src/parser/grammars/LogicGrammar.kt b/src/parser/grammars/LogicGrammar.kt
new file mode 100644
index 0000000..396aa50
--- /dev/null
+++ b/src/parser/grammars/LogicGrammar.kt
@@ -0,0 +1,21 @@
+package parser.grammars
+
+import com.github.h0tk3y.betterParse.combinators.*
+import com.github.h0tk3y.betterParse.parser.Parser
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Fact
+import prolog.ast.logic.Rule
+import prolog.ast.terms.Atom
+
+class LogicGrammar : TermsGrammar() {
+ protected val constraint: Parser by (-neck * body) use {
+ Rule(Atom(""), this)
+ }
+ protected val rule: Parser by (head * -neck * body) use { Rule(t1, t2) }
+ protected val fact: Parser by head use { Fact(this) }
+
+ protected val clause: Parser by ((rule or constraint or fact) * -dot)
+ protected val clauses: Parser> by oneOrMore(clause)
+
+ override val rootParser: Parser by clauses
+}
diff --git a/src/parser/grammars/QueryGrammar.kt b/src/parser/grammars/QueryGrammar.kt
new file mode 100644
index 0000000..971af85
--- /dev/null
+++ b/src/parser/grammars/QueryGrammar.kt
@@ -0,0 +1,16 @@
+package parser.grammars
+
+import com.github.h0tk3y.betterParse.combinators.times
+import com.github.h0tk3y.betterParse.combinators.unaryMinus
+import com.github.h0tk3y.betterParse.combinators.use
+import com.github.h0tk3y.betterParse.parser.Parser
+import prolog.ast.logic.LogicOperand
+import prolog.builtins.Query
+
+class QueryGrammar : TermsGrammar() {
+ protected val query: Parser by (body * -dot) use {
+ Query(this as LogicOperand)
+ }
+
+ override val rootParser: Parser by query
+}
\ No newline at end of file
diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt
new file mode 100644
index 0000000..3fb9e48
--- /dev/null
+++ b/src/parser/grammars/TermsGrammar.kt
@@ -0,0 +1,124 @@
+package parser.grammars
+
+import com.github.h0tk3y.betterParse.combinators.*
+import com.github.h0tk3y.betterParse.grammar.parser
+import com.github.h0tk3y.betterParse.parser.Parser
+import prolog.ast.arithmetic.Float
+import prolog.ast.arithmetic.Integer
+import prolog.ast.terms.*
+import prolog.builtins.Dynamic
+
+/**
+ * Precedence is based on the following table:
+ *
+ * | Precedence | Type | Operators |
+ * |------------|------|-----------------------------------------------------------------------------------------------|
+ * | 1200 | xfx | --\>, :-, =\>, ==\> |
+ * | 1200 | fx | :-, ?- |
+ * | 1150 | fx | dynamic |
+ * | 1105 | xfy | | |
+ * | 1100 | xfy | ; |
+ * | 1050 | xfy | ->, *-> |
+ * | 1000 | xfy | , |
+ * | 990 | xfx | := |
+ * | 900 | fy | \+ |
+ * | 700 | xfx | <, =, =.., =:=, =<, ==, =\=, >, >=, \=, \==, as, is, >:<, :< |
+ * | 600 | xfy | : |
+ * | 500 | yfx | +, -, /\, \/, xor |
+ * | 500 | fx | ? |
+ * | 400 | yfx | *, /, //, div, rdiv, <<, >>, mod, rem |
+ * | 200 | xfx | ** |
+ * | 200 | xfy | ^ |
+ * | 200 | fy | +, -, \ |
+ * | 100 | yfx | . |
+ * | 1 | fx | $ |
+ *
+ * It is very easy to extend this grammar to support more operators. Just add them at the appropriate rule or create a
+ * new rule and chain it to the existing ones.
+ *
+ * @see [SWI-Prolog Predicate op/3](https://www.swi-prolog.org/pldoc/man?predicate=op/3)
+ */
+open class TermsGrammar : Tokens() {
+ // Basic named terms
+ protected val variable: Parser by (variableToken or anonymousVariableToken) use { Variable(text) }
+ protected val simpleAtom: Parser by (nameToken or exclamation) use { Atom(text) }
+ protected val quotedAtom: Parser by (dummy
+ or ticked
+ or doubleTicked
+ or backTicked
+ ) use { Atom(text.substring(1, text.length - 1)) }
+ protected val atom: Parser by (quotedAtom or simpleAtom)
+ protected val compound: Parser by (atom * -leftParenthesis * separated(
+ parser(::termNoConjunction),
+ comma,
+ acceptZero = true
+ ) * -rightParenthesis) use {
+ Structure(t1, t2.terms)
+ }
+
+ // Basic arithmetic
+ protected val int: Parser by integerToken use { Integer(text.toInt()) }
+ protected val float: Parser by floatToken use { Float(text.toFloat()) }
+
+ protected val functor: Parser by (nameToken * divide * int) use { "${t1.text}${t2.text}$t3" }
+
+ // Base terms (atoms, compounds, variables, numbers)
+ protected val baseTerm: Parser by (dummy
+ or (-leftParenthesis * parser(::term) * -rightParenthesis)
+ or compound
+ or atom
+ or variable
+ or float
+ or int
+ )
+
+ protected val op200: Parser by ((plus or minus) * parser(::term200)) use {
+ CompoundTerm(Atom(t1.text), listOf(t2))
+ }
+ protected val term200: Parser by (op200 or baseTerm)
+
+ protected val op400: Parser by (multiply or divide) use { text }
+ protected val term400: Parser by (term200 * zeroOrMore(op400 * term200)) use {
+ t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
+ }
+
+ protected val op500: Parser by (plus or minus) use { text }
+ protected val term500: Parser by (term400 * zeroOrMore(op500 * term400)) use {
+ t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
+ }
+
+ protected val op700: Parser by (equivalent or equals or notEquals or isOp) use { text }
+ protected val term700: Parser by (term500 * optional(op700 * term500)) use {
+ if (t2 == null) t1 else CompoundTerm(Atom(t2!!.t1), listOf(t1, t2!!.t2))
+ }
+
+ 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 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)) }
+ }
+
+ protected val dynamic: Parser by (dynamicOp * functor) use {
+ CompoundTerm( Atom(t1.text), listOf(Atom(t2)) )
+ }
+ protected val term1150: Parser by (dynamic or term1100) use { this }
+
+ protected val op1200: Parser by (neck) use { text }
+ protected val term1200: Parser by (term1150 * zeroOrMore(op1200 * term1100)) use {
+ t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
+ }
+
+ // Term - highest level expression
+ protected val term: Parser by term1200
+ protected val termNoConjunction: Parser by term700
+
+ // Parts for clauses
+ protected val head: Parser by (compound or atom)
+ protected val body: Parser by term use { this as Body }
+
+ override val rootParser: Parser by term
+}
diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt
new file mode 100644
index 0000000..ac8c36f
--- /dev/null
+++ b/src/parser/grammars/Tokens.kt
@@ -0,0 +1,56 @@
+package parser.grammars
+
+import com.github.h0tk3y.betterParse.combinators.use
+import com.github.h0tk3y.betterParse.grammar.Grammar
+import com.github.h0tk3y.betterParse.lexer.Token
+import com.github.h0tk3y.betterParse.lexer.literalToken
+import com.github.h0tk3y.betterParse.lexer.regexToken
+import com.github.h0tk3y.betterParse.lexer.token
+
+abstract class Tokens : Grammar() {
+ protected val leftParenthesis: Token by literalToken("(")
+ protected val rightParenthesis: Token by literalToken(")")
+ protected val exclamation: Token by literalToken("!")
+ // 1200
+ protected val neck by literalToken(":-")
+ // 1150
+ protected val dynamicOp by literalToken("dynamic")
+ // 1100
+ protected val semicolon: Token by literalToken(";")
+ // 1000
+ protected val comma: Token by literalToken(",")
+ // 700
+ protected val equivalent: Token by literalToken("==")
+ protected val equals: Token by literalToken("=")
+ protected val isOp: Token by literalToken("is")
+ // 500
+ protected val plus: Token by literalToken("+")
+ protected val minus: Token by literalToken("-")
+ protected val notEquals: Token by literalToken("\\=")
+ // 400
+ protected val multiply: Token by literalToken("*")
+ protected val divide: Token by literalToken("/")
+ // 100
+ protected val dot by literalToken(".")
+
+ // Prolog tokens
+ protected val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*")
+ protected val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*")
+ protected val anonymousVariableToken: Token by literalToken("_")
+
+ // Arithmetic tokens
+ protected val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+")
+ protected val integerToken: Token by regexToken("-?([1-9][0-9]*|0)")
+
+ // Ignored tokens
+ protected val whitespace: Token by regexToken("\\s+", ignore = true)
+ protected val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true)
+ protected val multiLineComment: Token by regexToken("/\\*.*?\\*/", ignore = true)
+
+ protected val ticked: Token by regexToken("'[^']*'")
+ protected val doubleTicked: Token by regexToken("\"[^\"]*\"")
+ protected val backTicked: Token by regexToken("`[^`]*`")
+
+ // Helper
+ protected val dummy by token { _, _ -> -1 } use { throw IllegalStateException("This parser should not be used") }
+}
diff --git a/src/parser/state/ParserPosition.kt b/src/parser/state/ParserPosition.kt
deleted file mode 100644
index f3b5586..0000000
--- a/src/parser/state/ParserPosition.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package parser.state
-
-import parser.errors.ParsingError
-import parser.errors.ParsingErrorType
-
-data class ParserPosition(var offset: Int) {
- private val checkpoints: ArrayDeque = ArrayDeque()
-
- fun save() {
- checkpoints.addLast(this.copy())
- }
-
- fun reload() {
- require(checkpoints.isNotEmpty()) {
- ParsingError(ParsingErrorType.INTERNAL_ERROR, "No checkpoint to reload from", this)
- }
-
- val checkpoint = checkpoints.removeLast()
- offset = checkpoint.offset
- }
-
- override fun toString(): String {
- return "at $offset"
- }
-}
diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt
deleted file mode 100644
index 398b71a..0000000
--- a/src/prolog/Program.kt
+++ /dev/null
@@ -1,80 +0,0 @@
-package prolog
-
-import Debug
-import prolog.ast.logic.Clause
-import prolog.ast.logic.Predicate
-import prolog.ast.logic.Resolvent
-import prolog.ast.terms.Functor
-import prolog.ast.terms.Goal
-
-typealias Database = Program
-
-/**
- * Prolog Program or database.
- */
-object Program: Resolvent {
- var predicates: Map = emptyMap()
-
- init {
- setup()
- }
-
- private fun setup() {
- if (Debug.on) {
- println("Setting up Prolog program...")
- }
- // Initialize the program with built-in predicates
- load(listOf(
- ))
- }
-
- /**
- * Queries the program with a goal.
- * @return true if the goal can be proven, false otherwise.
- */
- fun query(goal: Goal): Answers = solve(goal, emptyMap())
-
- override fun solve(goal: Goal, subs: Substitutions): Answers {
- val functor = goal.functor
- // If the predicate does not exist, return false
- val predicate = predicates[functor] ?: return emptySequence()
- // If the predicate exists, evaluate the goal against it
- return predicate.solve(goal, subs)
- }
-
- /**
- * Loads a list of clauses into the program.
- */
- fun load(clauses: List) {
- for (clause in clauses) {
- val functor = clause.functor
- val predicate = predicates[functor]
-
- if (predicate != null) {
- // If the predicate already exists, add the clause to it
- predicate.add(clause)
- } else {
- // If the predicate does not exist, create a new one
- predicates += Pair(functor, Predicate(listOf(clause)))
- }
- }
- }
-
- fun load(predicate: Predicate) {
- val functor = predicate.functor
- val existingPredicate = predicates[functor]
-
- if (existingPredicate != null) {
- // If the predicate already exists, add the clauses to it
- existingPredicate.addAll(predicate.clauses)
- } else {
- // If the predicate does not exist, create a new one
- predicates += Pair(functor, predicate)
- }
- }
-
- fun clear() {
- predicates = emptyMap()
- setup()
- }
-}
\ No newline at end of file
diff --git a/src/prolog/Substitution.kt b/src/prolog/Substitution.kt
index e9fb28b..9058f8c 100644
--- a/src/prolog/Substitution.kt
+++ b/src/prolog/Substitution.kt
@@ -4,8 +4,8 @@ import prolog.ast.terms.Term
abstract class Substitution(val from: Term, val to: Term) {
val mapped: Pair? = if (from != to) from to to else null
- override fun toString(): String = "$from -> $to"
+ override fun toString(): String = "$from |-> $to"
}
typealias Substitutions = Map
typealias Answer = Result
-typealias Answers = Sequence
\ No newline at end of file
+typealias Answers = Sequence
diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt
new file mode 100644
index 0000000..be2da7a
--- /dev/null
+++ b/src/prolog/ast/Database.kt
@@ -0,0 +1,130 @@
+package prolog.ast
+
+import io.Logger
+import prolog.Answers
+import prolog.Substitutions
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Predicate
+import prolog.ast.logic.Resolvent
+import prolog.ast.terms.Functor
+import prolog.ast.terms.Goal
+
+/**
+ * Prolog Program or Database
+ */
+open class Database(val sourceFile: String) {
+ var predicates: Map = emptyMap()
+
+ /**
+ * Initializes the database by running the initialization clauses of that database.
+ */
+ fun initialize() {
+ databases.add(this)
+
+ if (sourceFile !== "") {
+ Logger.debug("Moving clauses from $sourceFile to main database")
+ predicates.filter { it.key != "/_" }
+ .forEach { (_, predicate) -> db.load(predicate, force = true) }
+ }
+
+ Logger.info("Initializing database from $sourceFile")
+ if (predicates.contains("/_")) {
+ Logger.debug("Loading clauses from /_ predicate")
+ predicates["/_"]?.clauses?.forEach {
+ Logger.debug("Loading clause $it")
+ val goal = it.body as Goal
+ goal.satisfy(emptyMap()).toList()
+ }
+ }
+ }
+
+ /**
+ * Loads a list of clauses into the program.
+ *
+ * @param clauses The list of clauses to load.
+ * @param index The index at which to insert the clause. If null, the clause is added to the end of the list.
+ * @param force If true, the clause is added even if the predicate is static.
+ */
+ fun load(clauses: List, index: Int? = null, force: Boolean = false) {
+ for (clause in clauses) {
+ val functor = clause.functor
+ val predicate = predicates[functor]
+
+ if (predicate != null) {
+ // If the predicate already exists, add the clause to it
+ predicate.add(clause, index, force)
+ } else {
+ // If the predicate does not exist, create a new one, usually during Program execution, so dynamic.
+ predicates += Pair(functor, Predicate(listOf(clause), dynamic = true))
+ }
+
+ Logger.debug("Loaded clause $clause into predicate $functor")
+ }
+ }
+
+ fun load(predicate: Predicate, force: Boolean = false) {
+ val functor = predicate.functor
+ val existingPredicate = predicates[functor]
+
+ if (existingPredicate != null) {
+ // If the predicate already exists, add the clauses to it
+ existingPredicate.addAll(predicate.clauses)
+ } else {
+ // If the predicate does not exist, create a new one
+ predicates += Pair(functor, predicate)
+ }
+ }
+
+ fun clear() {
+ if (sourceFile == "") {
+ Logger.debug("Clearing main database")
+ predicates = emptyMap()
+ return
+ }
+
+ Logger.debug("Clearing database $sourceFile")
+ // Remove our clauses from the database
+ predicates.forEach { (_, predicate) ->
+ val dbPredicate = db.predicates[predicate.functor]
+ predicate.clauses.forEach { clause -> dbPredicate?.clauses?.remove(clause) }
+ }
+ databases.remove(this)
+ }
+
+ /**
+ * Object to handle execution
+ *
+ * This object is a singleton that manages a list of databases.
+ */
+ companion object Program : Resolvent {
+ var db = Database("")
+ var databases: MutableList = mutableListOf(db)
+ var storeNewLine: Boolean = false
+ var variableRenamingStart: Int = 0
+
+ /**
+ * Queries the program with a goal.
+ * @return true if the goal can be proven, false otherwise.
+ */
+ fun query(goal: Goal): Answers = solve(goal, emptyMap())
+
+ override fun solve(goal: Goal, subs: Substitutions): Answers {
+ val functor = goal.functor
+ // If the predicate does not exist, return false
+ val predicate = db.predicates[functor] ?: return emptySequence()
+ // If the predicate exists, evaluate the goal against it
+ return predicate.solve(goal, subs)
+ }
+
+ fun consult(database: Database) = database.initialize()
+ fun disregard(database: Database) = database.clear()
+
+ fun load(clauses: List, index: Int? = null, force: Boolean = false) = db.load(clauses, index, force)
+
+ fun reset() {
+ databases.toList().map { it.clear() }
+ variableRenamingStart = 0
+ storeNewLine = false
+ }
+ }
+}
diff --git a/src/prolog/ast/arithmetic/Float.kt b/src/prolog/ast/arithmetic/Float.kt
index 3bbf694..80d0bde 100644
--- a/src/prolog/ast/arithmetic/Float.kt
+++ b/src/prolog/ast/arithmetic/Float.kt
@@ -1,6 +1,7 @@
package prolog.ast.arithmetic
import prolog.Substitutions
+import prolog.ast.terms.Term
class Float(override val value: kotlin.Float): Number {
// Floats are already evaluated
@@ -31,4 +32,18 @@ class Float(override val value: kotlin.Float): Number {
is Integer -> Float(value * other.value.toFloat())
else -> throw IllegalArgumentException("Cannot multiply $this and $other")
}
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Float) return false
+ if (value != other.value) return false
+ return true
+ }
+
+ override fun hashCode(): Int {
+ return super.hashCode()
+ }
+
+ override fun clone(): Float = Float(value)
+ override fun applySubstitution(subs: Substitutions): Float = this
}
\ No newline at end of file
diff --git a/src/prolog/ast/arithmetic/Integer.kt b/src/prolog/ast/arithmetic/Integer.kt
index 50028a9..d0a48cb 100644
--- a/src/prolog/ast/arithmetic/Integer.kt
+++ b/src/prolog/ast/arithmetic/Integer.kt
@@ -1,11 +1,16 @@
package prolog.ast.arithmetic
+import prolog.Answers
import prolog.Substitutions
+import prolog.ast.logic.LogicOperand
+import prolog.ast.terms.Term
-data class Integer(override val value: Int) : Number {
+data class Integer(override val value: Int) : Number, LogicOperand() {
// Integers are already evaluated
override fun simplify(subs: Substitutions): Simplification = Simplification(this, this)
+ override fun satisfy(subs: Substitutions): Answers = sequenceOf(Result.success(emptyMap()))
+
override fun toString(): String = value.toString()
override operator fun plus(other: Number): Number = when (other) {
@@ -37,4 +42,7 @@ data class Integer(override val value: Int) : Number {
is Integer -> Integer(value * other.value)
else -> throw IllegalArgumentException("Cannot multiply $this and $other")
}
+
+ override fun clone(): Integer = Integer(value)
+ override fun applySubstitution(subs: Substitutions): Integer = this
}
diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt
index 58b2eb4..ed8690f 100644
--- a/src/prolog/ast/logic/Clause.kt
+++ b/src/prolog/ast/logic/Clause.kt
@@ -1,13 +1,14 @@
package prolog.ast.logic
import prolog.Answers
+import prolog.ast.Database.Program
import prolog.Substitutions
-import prolog.ast.terms.Body
-import prolog.ast.terms.Functor
-import prolog.ast.terms.Goal
-import prolog.ast.terms.Head
+import prolog.ast.terms.*
import prolog.builtins.True
import prolog.flags.AppliedCut
+import prolog.logic.applySubstitution
+import prolog.logic.numbervars
+import prolog.logic.occurs
import prolog.logic.unifyLazy
/**
@@ -15,29 +16,45 @@ import prolog.logic.unifyLazy
*
* A clause consists of a [Head] and body separated by the neck operator, or it is a [Fact].
*
- * @see [prolog.ast.terms.Variable]
+ * @see [Variable]
* @see [Predicate]
*/
-abstract class Clause(val head: Head, val body: Body) : Resolvent {
+abstract class Clause(var head: Head, var body: Body) : Term, Resolvent {
val functor: Functor = head.functor
- override fun solve (goal: Goal, subs: Substitutions): Answers = sequence {
+ 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.
// Only if the body can be proven, the substitutions should be returned.
// Do this in a lazy way.
- unifyLazy(goal, head, subs).forEach { headAnswer ->
- headAnswer.map { newHeadSubs ->
+
+ val preHead = applySubstitution(head, subs)
+ val preGoal = applySubstitution(goal, subs)
+
+ val (headEnd, headRenaming) = numbervars(preHead, Program.variableRenamingStart, subs)
+ val headReverse = headRenaming.entries.associate { (a, b) -> b to a }
+ Program.variableRenamingStart = headEnd
+
+ val renamedHead = applySubstitution(head, headRenaming)
+ unifyLazy(preGoal, renamedHead, subs).forEach { headAnswer ->
+ headAnswer.map { headSubs ->
// If the body can be proven, yield the (combined) substitutions
- body.satisfy(subs + newHeadSubs).forEach { bodyAnswer ->
+ val preBody = applySubstitution(body, headRenaming + headSubs) as Body
+ preBody.satisfy(subs).forEach { bodyAnswer ->
bodyAnswer.fold(
- onSuccess = { newBodySubs ->
- yield(Result.success(newHeadSubs + newBodySubs))
+ onSuccess = { bodySubs ->
+ var result = (headSubs + bodySubs)
+ .mapKeys { applySubstitution(it.key, headReverse) }
+ .mapValues { applySubstitution(it.value, headReverse) }
+ result = result.map { it.key to applySubstitution(it.value, result) }
+ .toMap()
+ .filterNot { it.key in headRenaming.keys && !occurs(it.key as Variable, goal, emptyMap())}
+ yield(Result.success(result))
},
onFailure = { error ->
if (error is AppliedCut) {
// Find single solution and return immediately
if (error.subs != null) {
- yield(Result.failure(AppliedCut(newHeadSubs + error.subs)))
+ yield(Result.failure(AppliedCut(headSubs + error.subs)))
} else {
yield(Result.failure(AppliedCut()))
}
@@ -52,10 +69,19 @@ abstract class Clause(val head: Head, val body: Body) : Resolvent {
}
}
- override fun toString(): String {
- return when {
- body is True -> head.toString()
- else -> "$head :- $body"
- }
+ override fun toString(): String = if (body is True) head.toString() else "$head :- $body"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Clause) return false
+
+ if (head != other.head) return false
+ if (body != other.body) return false
+
+ return true
}
-}
\ No newline at end of file
+
+ override fun hashCode(): Int {
+ return super.hashCode()
+ }
+}
diff --git a/src/prolog/ast/logic/Fact.kt b/src/prolog/ast/logic/Fact.kt
index a52990e..f0cae19 100644
--- a/src/prolog/ast/logic/Fact.kt
+++ b/src/prolog/ast/logic/Fact.kt
@@ -1,6 +1,12 @@
package prolog.ast.logic
+import prolog.Substitutions
import prolog.ast.terms.Head
+import prolog.ast.terms.Term
import prolog.builtins.True
+import prolog.logic.applySubstitution
-class Fact(head: Head) : Clause(head, True)
\ No newline at end of file
+class Fact(head: Head) : Clause(head, True) {
+ override fun clone(): Fact = Fact(head)
+ override fun applySubstitution(subs: Substitutions): Fact = Fact(applySubstitution(head as Term, subs) as Head)
+}
\ No newline at end of file
diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt
index 1396cd1..25fb737 100644
--- a/src/prolog/ast/logic/Predicate.kt
+++ b/src/prolog/ast/logic/Predicate.kt
@@ -15,48 +15,54 @@ import prolog.flags.AppliedCut
class Predicate : Resolvent {
val functor: Functor
val clauses: MutableList
+ var dynamic = false
/**
* Creates a predicate with the given functor and an empty list of clauses.
*/
- constructor(functor: Functor) {
+ constructor(functor: Functor, dynamic: Boolean = false) {
this.functor = functor
this.clauses = mutableListOf()
+ this.dynamic = dynamic
}
/**
* Creates a predicate with the given clauses.
*/
- constructor(clauses: List) {
+ constructor(clauses: List, dynamic: Boolean = false) {
this.functor = clauses.first().functor
-
require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" }
this.clauses = clauses.toMutableList()
+ this.dynamic = dynamic
}
/**
* Adds a clause to the predicate.
+ *
+ * @param clause The clause to add.
+ * @param index The index at which to insert the clause. If null, the clause is added to the end of the list.
+ * @param force If true, the clause is added even if the predicate is static.
*/
- fun add(clause: Clause) {
+ fun add(clause: Clause, index: Int? = null, force: Boolean = false) {
require(clause.functor == functor) { "Clause functor does not match predicate functor" }
+ require(dynamic || force) { "No permission to modify static procedure '$functor'" }
- if (Debug.on) {
- println("Adding clause $clause to predicate $functor")
- }
-
- clauses.add(clause)
+ if (index != null) clauses.add(index, clause) else clauses.add(clause)
}
/**
* Adds a list of clauses to the predicate.
*/
- fun addAll(clauses: List) {
+ fun addAll(clauses: List, force: Boolean = false) {
require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" }
+ require(dynamic || force) { "No permission to modify static procedure '$functor'" }
+
this.clauses.addAll(clauses)
}
override fun solve(goal: Goal, subs: Substitutions): Answers = sequence {
require(goal.functor == functor) { "Goal functor does not match predicate functor" }
+
// Try to unify the goal with the clause
// If the unification is successful, yield the substitutions
clauses.forEach { clause ->
@@ -80,4 +86,4 @@ class Predicate : Resolvent {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/src/prolog/ast/logic/Rule.kt b/src/prolog/ast/logic/Rule.kt
index 9eba36e..add106d 100644
--- a/src/prolog/ast/logic/Rule.kt
+++ b/src/prolog/ast/logic/Rule.kt
@@ -1,6 +1,15 @@
package prolog.ast.logic
+import prolog.Substitutions
import prolog.ast.terms.Body
import prolog.ast.terms.Head
+import prolog.ast.terms.Term
+import prolog.logic.applySubstitution
-class Rule(head: Head, body: Body) : Clause(head, body)
\ No newline at end of file
+class Rule(head: Head, body: Body) : Clause(head, body) {
+ override fun clone(): Rule = Rule(head, body)
+ override fun applySubstitution(subs: Substitutions): Rule = Rule(
+ head = applySubstitution(head as Term, subs) as Head,
+ body = applySubstitution(body, subs) as Body
+ )
+}
diff --git a/src/prolog/ast/terms/AnonymousVariable.kt b/src/prolog/ast/terms/AnonymousVariable.kt
new file mode 100644
index 0000000..8211292
--- /dev/null
+++ b/src/prolog/ast/terms/AnonymousVariable.kt
@@ -0,0 +1,21 @@
+package prolog.ast.terms
+
+import io.Logger
+import prolog.Substitutions
+
+class AnonymousVariable(private val id: Int) : Variable("_$id") {
+ companion object {
+ private var counter = 0
+ fun create(): AnonymousVariable {
+ val id = counter
+ counter++
+ Logger.debug("Creating anonymous variable: _${id}")
+ return AnonymousVariable(id)
+ }
+ }
+
+ override fun toString(): String = "_"
+
+ override fun clone(): AnonymousVariable = AnonymousVariable(id)
+ override fun applySubstitution(subs: Substitutions): AnonymousVariable = this
+}
\ No newline at end of file
diff --git a/src/prolog/ast/terms/Atom.kt b/src/prolog/ast/terms/Atom.kt
index 3a6afad..dcd0a7d 100644
--- a/src/prolog/ast/terms/Atom.kt
+++ b/src/prolog/ast/terms/Atom.kt
@@ -21,4 +21,7 @@ open class Atom(val name: String) : Goal(), Head, Body, Resolvent {
override fun hashCode(): Int {
return javaClass.hashCode()
}
+
+ override fun clone(): Atom = Atom(name)
+ override fun applySubstitution(subs: Substitutions): Atom = Atom(name)
}
\ No newline at end of file
diff --git a/src/prolog/ast/terms/Body.kt b/src/prolog/ast/terms/Body.kt
index dc61c7d..4918d1e 100644
--- a/src/prolog/ast/terms/Body.kt
+++ b/src/prolog/ast/terms/Body.kt
@@ -2,4 +2,4 @@ package prolog.ast.terms
import prolog.ast.logic.Satisfiable
-interface Body : Satisfiable
\ No newline at end of file
+interface Body : Term, Satisfiable
\ No newline at end of file
diff --git a/src/prolog/ast/terms/Goal.kt b/src/prolog/ast/terms/Goal.kt
index e0a5c49..95f9016 100644
--- a/src/prolog/ast/terms/Goal.kt
+++ b/src/prolog/ast/terms/Goal.kt
@@ -1,7 +1,7 @@
package prolog.ast.terms
import prolog.Answers
-import prolog.Program
+import prolog.ast.Database.Program
import prolog.Substitutions
import prolog.ast.logic.LogicOperand
diff --git a/src/prolog/ast/terms/Operator.kt b/src/prolog/ast/terms/Operator.kt
index 9d0e067..cf43b24 100644
--- a/src/prolog/ast/terms/Operator.kt
+++ b/src/prolog/ast/terms/Operator.kt
@@ -6,7 +6,7 @@ abstract class Operator(
private val symbol: Atom,
private val leftOperand: Operand? = null,
private val rightOperand: Operand
-) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)) {
+) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)), Term {
override fun toString(): String {
return when (leftOperand) {
null -> "${symbol.name} $rightOperand"
diff --git a/src/prolog/ast/terms/Structure.kt b/src/prolog/ast/terms/Structure.kt
index 0585de9..bcd5e71 100644
--- a/src/prolog/ast/terms/Structure.kt
+++ b/src/prolog/ast/terms/Structure.kt
@@ -3,6 +3,7 @@ package prolog.ast.terms
import prolog.Answers
import prolog.Substitutions
import prolog.ast.logic.Resolvent
+import prolog.logic.applySubstitution
import prolog.logic.unifyLazy
typealias Argument = Term
@@ -22,4 +23,21 @@ open class Structure(val name: Atom, var arguments: List) : Goal(), He
else -> "${name.name}(${arguments.joinToString(", ")})"
}
}
-}
\ No newline at end of file
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Structure) return false
+ if (functor != other.functor) return false
+ return arguments.zip(other.arguments).all { (a, b) -> a == b }
+ }
+
+ override fun hashCode(): Int {
+ return javaClass.hashCode()
+ }
+
+ override fun clone(): Structure = Structure(name, arguments)
+ override fun applySubstitution(subs: Substitutions): Structure = Structure(
+ name,
+ arguments.map { applySubstitution(it, subs) }
+ )
+}
diff --git a/src/prolog/ast/terms/Term.kt b/src/prolog/ast/terms/Term.kt
index 0fdad49..8174310 100644
--- a/src/prolog/ast/terms/Term.kt
+++ b/src/prolog/ast/terms/Term.kt
@@ -1,14 +1,19 @@
package prolog.ast.terms
+import prolog.Substitutions
import prolog.logic.compare
+import prolog.ast.arithmetic.Integer
+import prolog.ast.arithmetic.Float
/**
* Value in Prolog.
*
- * A [Term] is either a [Variable], [Atom], [Integer][prolog.ast.arithmetic.Integer],
- * [Float][prolog.ast.arithmetic.Float] or [CompoundTerm].
+ * A [Term] is either a [Variable], [Atom], [Integer],
+ * [Float] or [CompoundTerm].
* In addition, SWI-Prolog also defines the type TODO string.
*/
-interface Term : Comparable {
+interface Term : Comparable, Cloneable {
override fun compareTo(other: Term): Int = compare(this, other, emptyMap())
+ fun applySubstitution(subs: Substitutions): Term
+ public override fun clone(): Term
}
diff --git a/src/prolog/ast/terms/Variable.kt b/src/prolog/ast/terms/Variable.kt
index 713c44d..8068864 100644
--- a/src/prolog/ast/terms/Variable.kt
+++ b/src/prolog/ast/terms/Variable.kt
@@ -1,10 +1,12 @@
package prolog.ast.terms
+import prolog.Answers
import prolog.Substitutions
import prolog.ast.arithmetic.Expression
import prolog.ast.arithmetic.Simplification
+import prolog.ast.logic.LogicOperand
-data class Variable(val name: String) : Term, Expression {
+open class Variable(val name: String) : Term, Body, Expression, LogicOperand() {
override fun simplify(subs: Substitutions): Simplification {
// If the variable is bound, return the value of the binding
// If the variable is not bound, return the variable itself
@@ -16,5 +18,28 @@ data class Variable(val name: String) : Term, Expression {
return Simplification(this, result)
}
+ override fun satisfy(subs: Substitutions): Answers {
+ // If the variable is bound, satisfy the bound term
+ if (this in subs) {
+ val boundTerm = subs[this]!! as Body
+ return boundTerm.satisfy(subs)
+ }
+
+ return sequenceOf(Result.failure(IllegalArgumentException("Unbound variable: $this")))
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other == null || other !is Variable) return false
+ return name == other.name
+ }
+
+ override fun hashCode(): Int {
+ return name.hashCode()
+ }
+
override fun toString(): String = name
+
+ override fun clone(): Variable = Variable(name)
+ override fun applySubstitution(subs: Substitutions): Variable = this
}
\ No newline at end of file
diff --git a/src/prolog/builtins/arithmeticOperators.kt b/src/prolog/builtins/arithmeticOperators.kt
index 2636bf6..306337e 100644
--- a/src/prolog/builtins/arithmeticOperators.kt
+++ b/src/prolog/builtins/arithmeticOperators.kt
@@ -11,23 +11,6 @@ import prolog.ast.terms.*
import prolog.logic.*
// TODO >
-class GreaterThan(private val left: Expression, private val right: Expression) :
- Operator(Atom(">"), left, right), Satisfiable {
- override fun satisfy(subs: Substitutions): Answers {
- val t1 = left.simplify(subs)
- val t2 = right.simplify(subs)
-
- if (!atomic(t1.to, subs) || !atomic(t2.to, subs)) {
- return sequenceOf(Result.failure(IllegalArgumentException("Both operands must be instantiated")))
- }
-
- return if (0 < compare(t1.to, t2.to, subs)) {
- sequenceOf(Result.success(emptyMap()))
- } else {
- emptySequence()
- }
- }
-}
// TODO <
@@ -79,11 +62,11 @@ class EvaluatesTo(private val left: Expression, private val right: Expression) :
/**
* True when Number is the value to which Expr evaluates.
*/
-class Is(private val left: Expression, private val right: Expression) :
- Operator(Atom("is"), left, right), Satisfiable {
+class Is(val number: Expression, val expr: Expression) :
+ Operator(Atom("is"), number, expr), Satisfiable {
override fun satisfy(subs: Substitutions): Answers {
- val t1 = left.simplify(subs)
- val t2 = right.simplify(subs)
+ val t1 = number.simplify(subs)
+ val t2 = expr.simplify(subs)
if (!atomic(t2.to, subs)) {
return sequenceOf(Result.failure(IllegalArgumentException("Right operand must be instantiated")))
@@ -136,7 +119,7 @@ open class Subtract(private val expr1: Expression, private val expr2: Expression
/**
* Result = Expr1 * Expr2
*/
-class Multiply(private val expr1: Expression, private val expr2: Expression) :
+class Multiply(val expr1: Expression, val expr2: Expression) :
ArithmeticOperator(Atom("*"), expr1, expr2) {
override fun simplify(subs: Substitutions): Simplification {
val result = Variable("Result")
@@ -161,7 +144,7 @@ class Divide(private val expr1: Expression, private val expr2: Expression) :
// TODO Expr rem Expr
class Between(private val expr1: Expression, private val expr2: Expression, private val expr3: Expression) :
- Operator(Atom("between"), expr1, expr2) {
+ CompoundTerm(Atom("between"), listOf(expr1, expr2, expr3)), Satisfiable {
override fun satisfy(subs: Substitutions): Answers {
val e1 = expr1.simplify(subs)
val e2 = expr2.simplify(subs)
@@ -169,8 +152,8 @@ class Between(private val expr1: Expression, private val expr2: Expression, priv
require(e1.to is Integer && e2.to is Integer) { "Arguments must be integers" }
- val v1 = e1.to as Integer
- val v2 = e2.to as Integer
+ val v1 = e1.to
+ val v2 = e2.to
return if (variable(e3.to, subs)) {
between(v1, v2, e3.to as Variable).map { answer ->
@@ -182,4 +165,6 @@ class Between(private val expr1: Expression, private val expr2: Expression, priv
}
}
}
+
+ override fun toString(): String = "$expr1..$expr3..$expr2"
}
diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt
index c1ebe63..b9eeef4 100644
--- a/src/prolog/builtins/controlOperators.kt
+++ b/src/prolog/builtins/controlOperators.kt
@@ -3,10 +3,11 @@ package prolog.builtins
import prolog.Answers
import prolog.Substitutions
import prolog.ast.logic.LogicOperand
+import prolog.ast.logic.LogicOperator
import prolog.ast.terms.Atom
import prolog.ast.terms.Body
import prolog.ast.terms.Goal
-import prolog.ast.logic.LogicOperator
+import prolog.ast.terms.Structure
import prolog.flags.AppliedCut
/**
@@ -34,6 +35,8 @@ class Cut() : Atom("!") {
override fun satisfy(subs: Substitutions): Answers {
return sequenceOf(Result.failure(AppliedCut(emptyMap())))
}
+
+ override fun applySubstitution(subs: Substitutions): Cut = Cut()
}
/**
@@ -94,6 +97,11 @@ class Conjunction(val left: LogicOperand, private val right: LogicOperand) :
)
}
}
+
+ override fun applySubstitution(subs: Substitutions): Conjunction = Conjunction(
+ left.applySubstitution(subs) as LogicOperand,
+ right.applySubstitution(subs) as LogicOperand
+ )
}
/**
@@ -105,6 +113,12 @@ open class Disjunction(private val left: LogicOperand, private val right: LogicO
yieldAll(left.satisfy(subs))
yieldAll(right.satisfy(subs))
}
+
+ override fun clone(): Disjunction = Disjunction(left.clone() as LogicOperand, right.clone() as LogicOperand)
+ override fun applySubstitution(subs: Substitutions): Disjunction = Disjunction(
+ left.applySubstitution(subs) as LogicOperand,
+ right.applySubstitution(subs) as LogicOperand
+ )
}
@Deprecated("Use Disjunction instead")
@@ -120,7 +134,8 @@ class Bar(leftOperand: LogicOperand, rightOperand: LogicOperand) : Disjunction(l
class Not(private val goal: Goal) : LogicOperator(Atom("\\+"), rightOperand = goal) {
override fun satisfy(subs: Substitutions): Answers {
// If the goal can be proven, return an empty sequence
- if (goal.satisfy(subs).toList().isNotEmpty()) {
+ val goalResults = goal.satisfy(subs).iterator()
+ if (goalResults.hasNext()) {
return emptySequence()
}
// If the goal cannot be proven, return a sequence with an empty map
diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt
new file mode 100644
index 0000000..54ebeb3
--- /dev/null
+++ b/src/prolog/builtins/databaseOperators.kt
@@ -0,0 +1,116 @@
+package prolog.builtins
+
+import prolog.Answers
+import prolog.Substitutions
+import prolog.ast.logic.Clause
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.logic.Predicate
+import prolog.ast.Database.Program
+import prolog.ast.terms.Functor
+import prolog.ast.terms.Term
+import prolog.ast.logic.Fact
+import prolog.ast.Database
+import prolog.ast.terms.Body
+import prolog.ast.terms.Goal
+import prolog.ast.terms.Operator
+import prolog.logic.applySubstitution
+import prolog.logic.unifyLazy
+
+/**
+ * (Make) the [Predicate] with the corresponding [Functor] dynamic.
+ */
+class Dynamic(private val dynamicFunctor: Functor): Goal(), Body {
+ override val functor: Functor = "dynamic/1"
+
+ override fun satisfy(subs: Substitutions): Answers {
+ val predicate = Program.db.predicates[dynamicFunctor]
+ if (predicate == null) {
+ return sequenceOf(Result.failure(Exception("Predicate $dynamicFunctor not found")))
+ }
+
+ predicate.dynamic = true
+ return sequenceOf(Result.success(emptyMap()))
+ }
+
+ override fun toString(): String = "dynamic $dynamicFunctor"
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is Dynamic) return false
+ return dynamicFunctor == other.dynamicFunctor
+ }
+
+ override fun hashCode(): Int = super.hashCode()
+
+ override fun clone(): Dynamic = Dynamic(dynamicFunctor)
+ override fun applySubstitution(subs: Substitutions): Dynamic = Dynamic(dynamicFunctor)
+}
+
+class Assert(clause: Clause) : AssertZ(clause) {
+ override val functor: Functor = "assert/1"
+}
+
+/**
+ * Assert a [Clause] as a first clause of the [Predicate] into the [Program].
+ */
+class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) {
+ override fun satisfy(subs: Substitutions): Answers {
+ // Add clause to the program
+ val evaluatedClause = applySubstitution(clause, subs) as Clause
+ Program.load(listOf(evaluatedClause), 0)
+
+ return sequenceOf(Result.success(emptyMap()))
+ }
+}
+
+/**
+ * Assert a [Clause] as a last clause of the [Predicate] into the [Program].
+ */
+open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) {
+ override fun satisfy(subs: Substitutions): Answers {
+ // Add clause to the program
+ val evaluatedClause = applySubstitution(clause, subs) as Clause
+ Program.load(listOf(evaluatedClause))
+
+ 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
+
+ val predicate = Program.db.predicates[functorName]
+ if (predicate == null) {
+ return@sequence
+ }
+
+ predicate.clauses.toList().forEach { clause ->
+ unifyLazy(term, clause.head, subs).forEach { unifyResult ->
+ unifyResult.fold(
+ onSuccess = { substitutions ->
+ // If unification is successful, remove the clause
+ predicate.clauses.remove(clause)
+ yield(Result.success(substitutions))
+ },
+ onFailure = {
+ // If unification fails, do nothing
+ }
+ )
+ }
+ }
+ }
+}
diff --git a/src/prolog/builtins/ioOperators.kt b/src/prolog/builtins/ioOperators.kt
new file mode 100644
index 0000000..1271296
--- /dev/null
+++ b/src/prolog/builtins/ioOperators.kt
@@ -0,0 +1,75 @@
+package prolog.builtins
+
+import io.Logger
+import io.Terminal
+import parser.ReplParser
+import prolog.Answers
+import prolog.ast.Database.Program
+import prolog.Substitutions
+import prolog.ast.logic.Satisfiable
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Operator
+import prolog.ast.terms.Term
+import prolog.logic.applySubstitution
+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 {
+ override fun satisfy(subs: Substitutions): Answers {
+ val t = applySubstitution(term, subs)
+
+ Terminal().say(t.toString())
+
+ Program.storeNewLine = true
+
+ return sequenceOf(Result.success(emptyMap()))
+ }
+
+ override fun toString(): String = "write($term)"
+}
+
+/**
+ * Write a newline character to the current output stream.
+ */
+object Nl : Atom("nl"), Satisfiable {
+ override fun satisfy(subs: Substitutions): Answers {
+ Terminal().say("\n")
+ Program.storeNewLine = false
+ return sequenceOf(Result.success(emptyMap()))
+ }
+}
+
+/**
+ * Read the next Prolog term from the current input stream and unify it with [Term].
+ *
+ * On reaching end-of-file, [Term] is unified with the [Atom] `end_of_file`.
+ */
+class Read(private val term: Term) : Operator(Atom("read"), null, term), Satisfiable {
+ private val io = Terminal()
+ private val parser = ReplParser()
+
+ private fun readTerm(): Term {
+ val input = io.readLine()
+ Logger.debug("Read input: $input")
+
+ return when (input) {
+ "end_of_file" -> Atom("end_of_file")
+ else -> {
+ val out = parser.parse(input).query
+ Logger.debug("Parsed input: $out")
+ out as? Term ?: throw IllegalArgumentException("Expected a term, but got: $out")
+ }
+ }
+ }
+
+ override fun satisfy(subs: Substitutions): Answers = sequence {
+ val t1 = applySubstitution(term, subs)
+
+ val t2 = readTerm()
+ Logger.debug("Read term: $t2")
+
+ yieldAll(unifyLazy(t1, t2, subs))
+ }
+}
diff --git a/src/prolog/builtins/other.kt b/src/prolog/builtins/other.kt
index b34d3e0..4cc582f 100644
--- a/src/prolog/builtins/other.kt
+++ b/src/prolog/builtins/other.kt
@@ -6,6 +6,11 @@ import prolog.ast.logic.LogicOperand
import prolog.ast.terms.Atom
import prolog.ast.logic.LogicOperator
-class Query(private val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) {
+class Initialization(val goal: LogicOperand) : LogicOperator(Atom(":-"), null, goal) {
+ override fun satisfy(subs: Substitutions): Answers = goal.satisfy(subs).take(1)
+ override fun toString(): String = goal.toString()
+}
+
+class Query(val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) {
override fun satisfy(subs: Substitutions): Answers = query.satisfy(subs)
}
diff --git a/src/prolog/builtins/unificationOperators.kt b/src/prolog/builtins/unificationOperators.kt
index fc40df7..892c616 100644
--- a/src/prolog/builtins/unificationOperators.kt
+++ b/src/prolog/builtins/unificationOperators.kt
@@ -26,6 +26,11 @@ class Unify(private val term1: Term, private val term2: Term): Operator(Atom("="
}
}
+class NotUnify(term1: Term, term2: Term) : Operator(Atom("\\="), term1, term2) {
+ private val not = Not(Unify(term1, term2))
+ override fun satisfy(subs: Substitutions): Answers = not.satisfy(subs)
+}
+
class Equivalent(private val term1: Term, private val term2: Term) : Operator(Atom("=="), term1, term2) {
override fun satisfy(subs: Substitutions): Answers = sequence {
val t1 = applySubstitution(term1, subs)
diff --git a/src/prolog/logic/arithmetic.kt b/src/prolog/logic/arithmetic.kt
index 0d4d341..36b903c 100644
--- a/src/prolog/logic/arithmetic.kt
+++ b/src/prolog/logic/arithmetic.kt
@@ -58,7 +58,7 @@ fun succ(term1: Expression, term2: Expression, subs: Substitutions): Answers {
it.fold(
onSuccess = { result ->
val t1 = applySubstitution(term1, result)
- if (t1 in result) {
+ if (t1 in result || t1 in result.values) {
val e1 = t1.simplify(result)
if (e1.to is Integer && e1.to.value < 0) {
return@sequence
diff --git a/src/prolog/logic/terms.kt b/src/prolog/logic/terms.kt
index 6bf2665..8c2b26f 100644
--- a/src/prolog/logic/terms.kt
+++ b/src/prolog/logic/terms.kt
@@ -1,7 +1,10 @@
package prolog.logic
+import prolog.Substitutions
import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
import prolog.ast.terms.Term
+import prolog.ast.terms.Variable
/**
* True when Term is a term with functor Name/Arity. If Term is a variable it is unified with a new term whose
@@ -20,3 +23,53 @@ fun functor(term: Term, name: Atom, arity: Int): Boolean {
// TODO Implement
return true
}
+
+/**
+ * Unify the free variables in Term with a term $VAR(N), where N is the number of the variable.
+ * Counting starts at Start.
+ * End is unified with the number that should be given to the next variable.
+ *
+ * Source: [SWI-Prolog Predicate numbervars/3](https://www.swi-prolog.org/pldoc/man?predicate=numbervars/3)
+ *
+ * @return Pair of the next number and only the new substitutions of variables to $VAR(N)
+ */
+fun numbervars(
+ term: Term,
+ start: Int = 0,
+ subs: Substitutions = emptyMap(),
+ sessionSubs: Substitutions = emptyMap()
+): Pair {
+ when {
+ variable(term, subs) -> {
+ // All instances of the same variable are unified with the same term
+ if (term in sessionSubs) {
+ return Pair(start, emptyMap())
+ }
+
+ val from = term as Variable
+ var suggestedName = "${from.name}@$start"
+ // If the suggested name is already in use, find a new one
+ while ((subs + sessionSubs).any { (it.key as Variable).name == suggestedName }) {
+ val randomInfix = ((0..9) + ('a'..'z') + ('A'..'Z')).random()
+ suggestedName = "${from.name}@${randomInfix}_($start)"
+ }
+ return Pair(start + 1, mapOf(from to Variable(suggestedName)))
+ }
+
+ compound(term, subs) -> {
+ val from = applySubstitution(term, subs) as Structure
+ var n = start
+ val s: MutableMap = sessionSubs.toMutableMap()
+ from.arguments.forEach { arg ->
+ val (newN, newSubs) = numbervars(arg, n, subs, s)
+ n = newN
+ s += newSubs
+ }
+ return Pair(n, s)
+ }
+
+ else -> {
+ return Pair(start, emptyMap())
+ }
+ }
+}
diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt
index ad24281..5b5d21e 100644
--- a/src/prolog/logic/unification.kt
+++ b/src/prolog/logic/unification.kt
@@ -4,42 +4,52 @@ import prolog.Answer
import prolog.Answers
import prolog.Substitutions
import prolog.ast.arithmetic.Expression
-import prolog.ast.logic.LogicOperator
-import prolog.ast.terms.*
-import kotlin.NoSuchElementException
-import prolog.ast.arithmetic.Number
-import prolog.ast.arithmetic.Integer
import prolog.ast.arithmetic.Float
+import prolog.ast.arithmetic.Integer
+import prolog.ast.arithmetic.Number
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Fact
+import prolog.ast.logic.LogicOperator
+import prolog.ast.logic.Rule
+import prolog.ast.terms.*
// Apply substitutions to a term
fun applySubstitution(term: Term, subs: Substitutions): Term = when {
- variable(term, emptyMap()) -> subs[(term as Variable)] ?: term
- atomic(term, subs) -> term
- compound(term, subs) -> {
- val structure = term as Structure
- Structure(structure.name, structure.arguments.map { applySubstitution(it, subs) })
+ term is Fact -> term.applySubstitution(subs)
+
+ variable(term, emptyMap()) -> {
+ val variable = term as Variable
+ subs[variable]?.let { applySubstitution(term = it, subs = subs) } ?: term
}
+ atomic(term, subs) -> term
+ compound(term, subs) -> {
+ term.applySubstitution(subs)
+ }
+
else -> term
}
+
//TODO Combine with the other applySubstitution function
fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when {
- variable(expr, subs) -> applySubstitution(expr as Term, subs) as Expression
- atomic(expr, subs) -> expr
+ 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
}
// Check if a variable occurs in a term
-private fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean = when {
+fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean = when {
variable(term, subs) -> term == variable
atomic(term, subs) -> false
compound(term, subs) -> {
val structure = term as Structure
structure.arguments.any { occurs(variable, it, subs) }
}
+
else -> false
}
@@ -49,19 +59,21 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence
val t2 = applySubstitution(term2, subs)
when {
- equivalent(t1, t2, subs) -> yield(Result.success(subs))
+ equivalent(t1, t2, subs) -> yield(Result.success(emptyMap()))
variable(t1, subs) -> {
val variable = t1 as Variable
if (!occurs(variable, t2, subs)) {
- yield(Result.success(subs + (variable to t2)))
+ yield(Result.success(mapOf(term1 to t2)))
}
}
+
variable(t2, subs) -> {
val variable = t2 as Variable
if (!occurs(variable, t1, subs)) {
- yield(Result.success(subs + (variable to t1)))
+ yield(Result.success(mapOf(term2 to t1)))
}
}
+
compound(t1, subs) && compound(t2, subs) -> {
val structure1 = t1 as Structure
val structure2 = t2 as Structure
@@ -75,22 +87,25 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence
}
// Combine the results of all unifications
val combinedResults = results.reduce { acc, result ->
- acc.flatMap { a -> result.map { b ->
- if (a.isSuccess && b.isSuccess) a.getOrThrow() + b.getOrThrow() else emptyMap()
- } }.map { Result.success(it) }
+ acc.flatMap { a ->
+ result.map { b ->
+ if (a.isSuccess && b.isSuccess) a.getOrThrow() + b.getOrThrow() else emptyMap()
+ }
+ }.map { Result.success(it) }
}
yieldAll(combinedResults)
}
}
}
+
else -> {}
}
}
fun unify(term1: Term, term2: Term): Answer {
- val substitutions = unifyLazy(term1, term2, emptyMap()).toList()
- return if (substitutions.isNotEmpty()) {
- substitutions.first()
+ val substitutions = unifyLazy(term1, term2, emptyMap()).iterator()
+ return if (substitutions.hasNext()) {
+ substitutions.next()
} else {
Result.failure(NoSuchElementException())
}
@@ -122,37 +137,40 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int {
return when (t1) {
is Variable -> {
when (t2) {
- is Variable -> t1.name.compareTo(t2.name)
+ is Variable -> t1.name.compareTo(t2.name)
is Number -> -1
- is Atom -> -1
+ is Atom -> -1
is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
}
}
+
is Number -> {
when (t2) {
- is Variable -> 1
- is Integer -> (t1.value as Int).compareTo(t2.value)
- is Float -> (t1.value as kotlin.Float).compareTo(t2.value)
- is Atom -> -1
+ is Variable -> 1
+ is Integer -> (t1.value as Int).compareTo(t2.value)
+ is Float -> (t1.value as kotlin.Float).compareTo(t2.value)
+ is Atom -> -1
is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
}
}
+
is Atom -> {
when (t2) {
- is Variable -> 1
+ is Variable -> 1
is Number -> 1
- is Atom -> t1.name.compareTo(t2.name)
+ is Atom -> t1.name.compareTo(t2.name)
is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
}
}
+
is Structure -> {
when (t2) {
is Variable -> 1
is Number -> 1
- is Atom -> 1
+ is Atom -> 1
is Structure -> {
val arityComparison = t1.arguments.size.compareTo(t2.arguments.size)
if (arityComparison != 0) return arityComparison
@@ -164,9 +182,11 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int {
}
return 0
}
+
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
}
}
+
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
}
}
diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt
index 34e999c..5e8b0d4 100644
--- a/src/repl/Repl.kt
+++ b/src/repl/Repl.kt
@@ -1,4 +1,99 @@
package repl
+import interpreter.Preprocessor
+import io.Logger
+import io.Terminal
+import parser.ReplParser
+import prolog.Answer
+import prolog.Answers
+
class Repl {
-}
\ No newline at end of file
+ private val io = Terminal()
+ private val parser = ReplParser()
+ private val preprocessor = Preprocessor()
+
+ init {
+ welcome()
+ while (true) {
+ try {
+ printAnswers(query())
+ } catch (e: Exception) {
+ Logger.error("Error parsing REPL: ${e.message}")
+ }
+ }
+ }
+
+ private fun welcome() {
+ io.checkNewLine()
+ io.say("Prolog REPL. Type '^D' to quit.\n")
+ }
+
+ private fun query(): Answers {
+ val queryString = io.prompt("?-", { "| " })
+ val simpleQuery = parser.parse(queryString)
+ val query = preprocessor.preprocess(simpleQuery)
+ return query.satisfy(emptyMap())
+ }
+
+ private fun printAnswers(answers: Answers) {
+ val knownCommands = setOf(";", "a", ".", "h")
+
+ val iterator = answers.iterator()
+
+ if (!iterator.hasNext()) {
+ io.say("false.\n")
+ } else {
+ io.say(prettyPrint(iterator.next()))
+
+ while (iterator.hasNext()) {
+ var command = io.prompt("")
+
+ while (command !in knownCommands) {
+ io.say("Unknown action: $command (h for help)\n")
+ command = io.prompt("Action?")
+ }
+
+ when (command) {
+ ";" -> {
+ io.say(prettyPrint(iterator.next()))
+ }
+ "a" -> return
+ "." -> return
+ "h" -> {
+ help()
+ io.say("Action?")
+ }
+ }
+ }
+ }
+
+ io.say("\n")
+ }
+
+ private fun help(): String {
+ io.say("Commands:\n")
+ io.say(" ; find next solution\n")
+ io.say(" a abort\n")
+ io.say(" . end query\n")
+ io.say(" h help\n")
+ return ""
+ }
+
+ private fun prettyPrint(result: Answer): String {
+ result.fold(
+ onSuccess = {
+ val subs = result.getOrNull()!!
+ if (subs.isEmpty()) {
+ io.checkNewLine()
+ return "true."
+ }
+ return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" }
+ },
+ onFailure = {
+ val text = "Failure: ${it.message}"
+ Logger.warn(text)
+ return text
+ }
+ )
+ }
+}
diff --git a/tests/better_parser/SimplePrologPrologParserTests.kt b/tests/better_parser/SimplePrologPrologParserTests.kt
deleted file mode 100644
index 9135c42..0000000
--- a/tests/better_parser/SimplePrologPrologParserTests.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-package better_parser
-
-import com.github.h0tk3y.betterParse.grammar.Grammar
-import com.github.h0tk3y.betterParse.grammar.parseToEnd
-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 org.junit.jupiter.params.ParameterizedTest
-import org.junit.jupiter.params.provider.ValueSource
-import prolog.ast.arithmetic.Float
-import prolog.ast.arithmetic.Integer
-import prolog.ast.terms.Atom
-import prolog.ast.terms.Structure
-import prolog.ast.terms.Term
-import prolog.ast.terms.Variable
-import prolog.logic.equivalent
-
-class SimplePrologPrologParserTests {
- private lateinit var parser: Grammar
-
- @BeforeEach
- fun setup() {
- parser = SimplePrologParser() as Grammar
- }
-
- @ParameterizedTest
- @ValueSource(strings = ["a", "foo", "foo1", "fooBar", "foo_bar"])
- fun `parse atom`(name: String) {
- val result = parser.parseToEnd(name)
-
- assertEquals(Atom(name), result, "Expected atom '$name'")
- }
-
- @ParameterizedTest
- @ValueSource(strings = ["X", "X1", "X_1"])
- fun `parse variable`(name: String) {
- val result = parser.parseToEnd(name)
-
- assertEquals(Variable(name), result, "Expected atom '$name'")
- }
-
- @Test
- fun `empty compound term`() {
- val input = "f()"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Structure(Atom("f"), emptyList()), result, emptyMap()),
- "Expected atom 'f'"
- )
- }
-
- @Test
- fun `parse compound term f(a)`() {
- val input = "f(a)"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Structure(Atom("f"), listOf(Atom("a"))), result, emptyMap()),
- "Expected atom 'f(a)'"
- )
- }
-
- @Test
- fun `parse compound term f(a, b)`() {
- val input = "f(a, b)"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), result, emptyMap()),
- "Expected atom 'f(a, b)'"
- )
- }
-
- @Test
- fun `parse compound term with variable f(a, X)`() {
- val input = "f(a, X)"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Structure(Atom("f"), listOf(Atom("a"), Variable("X"))), result, emptyMap()),
- "Expected atom 'f(a, X)'"
- )
- }
-
- @Test
- fun `parse nested compound term f(a, g(b))`() {
- val input = "f(a, g(b))"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(
- Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))),
- result,
- emptyMap()
- ),
- "Expected atom 'f(a, g(b))'"
- )
- }
-
- @Test
- fun `parse compound term with variable f(a, g(X))`() {
- val input = "f(a, g(X))"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(
- Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Variable("X"))))),
- result,
- emptyMap()
- ),
- "Expected atom 'f(a, g(X))'"
- )
- }
-
- @ParameterizedTest
- @ValueSource(ints = [-987654321, -543, -21, -1, 0, 1, 5, 12, 345, 123456789])
- fun `parse integer`(number: Int) {
- val input = number.toString()
-
- val result = parser.parseToEnd(input)
-
- assertEquals(Integer(number), result, "Expected integer '$number'")
- }
-
- @Test
- fun `parse float`() {
- val input = "42.0"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Float(42.0f), result, emptyMap()),
- "Expected float '42.0'"
- )
- }
-
- @Test
- fun `parse negative float`() {
- val input = "-42.0"
-
- val result = parser.parseToEnd(input)
-
- assertTrue(
- equivalent(Float(-42.0f), result, emptyMap()),
- "Expected float '-42.0'"
- )
- }
-}
\ No newline at end of file
diff --git a/tests/compare.sh b/tests/compare.sh
new file mode 100644
index 0000000..d876e13
--- /dev/null
+++ b/tests/compare.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+
+# This script is expected to be run from the root of the project.
+
+WATCH_ALONG=0
+
+# Paths to the two implementations
+GPL="src/gpl"
+SPL="swipl"
+
+GPL_FLAGS=("--error")
+SPL_FLAGS=("--quiet" "-t" "'true'")
+
+# Directory containing test files
+TEST_DIR="examples"
+
+# Temporary files for storing outputs
+GPL_OUT=$(mktemp)
+GPL_ERR=$(mktemp)
+SPL_OUT=$(mktemp)
+SPL_ERR=$(mktemp)
+
+touch "$GPL_OUT" "$GPL_ERR" "$SPL_OUT" "$SPL_ERR"
+
+# Flag to track if all tests pass
+PASSED=0
+FAILED=0
+
+# Iterate over all test files in the test directory
+#for TESTFILE in $(find ${TEST_DIR} -type f); do
+files=(
+"examples/program.pl"
+"examples/basics/disjunction.pl"
+"examples/basics/fraternity.pl"
+"examples/basics/unification.pl"
+"examples/basics/write.pl"
+)
+for TESTFILE in "${files[@]}"; do
+ # Run both programs with the test file
+ "${SPL}" "${SPL_FLAGS[@]}" "$TESTFILE" > "${SPL_OUT}" 2> "${SPL_ERR}"
+ "${GPL}" "${GPL_FLAGS[@]}" -s "$TESTFILE" > "${GPL_OUT}" 2> "${GPL_ERR}"
+
+ # Compare the outputs
+ was_different="$(
+ diff -q "$SPL_OUT" "$GPL_OUT" > /dev/null
+ echo $?
+ )"
+ if [[ "${was_different}" -ne 0 || "${WATCH_ALONG}" -eq 1 ]]; then
+ if [ "${was_different}" -ne 0 ]; then
+ message="TEST FAILED"
+ fi
+ printf "%s for %s\n" "${message:="Result"}" "${TESTFILE}"
+ printf "\nTest:\n%s\n" "$(cat "${TESTFILE}")"
+ printf "\nExpected:\n%s\n" "$(cat "${SPL_OUT}" && cat "${SPL_ERR}")"
+ printf "\nGot:\n%s\n" "$(cat "${GPL_OUT}" && cat "${GPL_ERR}")"
+ echo "-----------------------------------------"
+ fi
+
+ if [ "${was_different}" -ne 0 ]; then
+ FAILED=$((FAILED + 1))
+ else
+ PASSED=$((PASSED + 1))
+ fi
+done
+
+# Clean up temporary files
+rm "$SPL_OUT" "$GPL_OUT" "$SPL_ERR" "$GPL_ERR"
+
+# Final result, summary
+if [ $FAILED -eq 0 ]; then
+ echo "All tests passed!"
+else
+ printf "Tests passed: %d\nTests failed: %d\n" "$PASSED" "$FAILED"
+ exit 1
+fi
diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt
new file mode 100644
index 0000000..f93fc35
--- /dev/null
+++ b/tests/e2e/Examples.kt
@@ -0,0 +1,63 @@
+package e2e
+
+import interpreter.FileLoader
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.Arguments
+import org.junit.jupiter.params.provider.MethodSource
+import prolog.ast.Database.Program
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class Examples {
+ private val loader = FileLoader()
+ private lateinit var outStream: ByteArrayOutputStream
+
+ @BeforeEach
+ fun setup() {
+ Program.reset()
+
+ outStream = ByteArrayOutputStream()
+ System.setOut(PrintStream(outStream))
+ }
+
+ @Test
+ fun debugHelper() {
+ loader.load("examples/basics/backtracking.pl")
+ }
+
+ @ParameterizedTest
+ @MethodSource("basics")
+ fun `Identical output for basics`(inputFile: String, expected: String) {
+ loader.load("examples/basics/$inputFile")
+ assertEquals(expected, outStream.toString())
+ }
+
+ @ParameterizedTest
+ @MethodSource("other")
+ fun `Identical output for other`(inputFile: String, expected: String) {
+ loader.load("examples/$inputFile")
+ assertEquals(expected, outStream.toString())
+ }
+
+ fun basics() = listOf(
+ Arguments.of("arithmetics.pl", "gimli is a level 4 fighter with 35 hitpoints.\nlegolas is a level 5 ranger with 30 hitpoints.\ngandalf is a level 10 wizard with 25 hitpoints.\nfrodo is a level 2 rogue with 20 hitpoints.\nlegolas threw gimli, and gimli took 5 damage.\ngimli is a level 4 fighter with 30 hitpoints.\ngandalf casts aid.\ngimli is a level 4 fighter with 35 hitpoints.\nlegolas leveled up.\nlegolas is a level 6 ranger with 30 hitpoints"),
+ Arguments.of("backtracking.pl", "0\ns(0)\ns(s(0))\ns(s(s(0)))\n"),
+ Arguments.of("cut.pl", "0\n"),
+ Arguments.of("disjunction.pl", "Alice likes Italian food.\nBob likes Italian food.\n"),
+ Arguments.of("equality.pl", "X == Y failed\nX = Y succeeded\nX == Y succeeded\nX = Y succeeded\nX == Y succeeded\n"),
+ Arguments.of("forall.pl", "Only alice likes pizza.\n"),
+ Arguments.of("fraternity.pl", "Citizen robespierre is eligible for the event.\nCitizen danton is eligible for the event.\nCitizen camus is eligible for the event.\n"),
+ Arguments.of("liberty.pl", "Give me Liberty, or give me Death!\nI disapprove of what you say, but I will defend to the death your right to say it.\nThe revolution devours its own children.\nSo this is how liberty dies, with thunderous applause.\n"),
+ Arguments.of("unification.pl", "While alice got an A, carol got an A, but bob did not get an A, dave did not get an A, unfortunately.\n"),
+ Arguments.of("write.pl", "gpl zegt: dag(wereld)\n"),
+ )
+
+ fun other() = listOf(
+ Arguments.of("program.pl", "10\nhello(world)")
+ )
+}
diff --git a/tests/interpreter/OpenPreprocessor.kt b/tests/interpreter/OpenPreprocessor.kt
new file mode 100644
index 0000000..4e35b2a
--- /dev/null
+++ b/tests/interpreter/OpenPreprocessor.kt
@@ -0,0 +1,9 @@
+package interpreter
+
+import prolog.ast.terms.Term
+
+class OpenPreprocessor : Preprocessor() {
+ public override fun preprocess(term: Term, nested: Boolean): Term {
+ return super.preprocess(term, nested)
+ }
+}
\ No newline at end of file
diff --git a/tests/interpreter/ParserPreprocessorIntegrationTests.kt b/tests/interpreter/ParserPreprocessorIntegrationTests.kt
new file mode 100644
index 0000000..05df756
--- /dev/null
+++ b/tests/interpreter/ParserPreprocessorIntegrationTests.kt
@@ -0,0 +1,92 @@
+package interpreter
+
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import parser.grammars.TermsGrammar
+import prolog.ast.arithmetic.Float
+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.ast.terms.Variable
+import prolog.builtins.Is
+import prolog.builtins.Subtract
+
+class ParserPreprocessorIntegrationTests {
+ @Nested
+ class `Arithmetic`() {
+ val parser = TermsGrammar()
+ val preprocessor = OpenPreprocessor()
+
+ @ParameterizedTest
+ @ValueSource(strings = ["-1", "-1.0", "-1.5"])
+ fun `can parse negative numbers`(input: String) {
+ val number = if (input.contains('.')) {
+ Float(input.substring(1).toFloat())
+ } else {
+ Integer(input.substring(1).toInt())
+ }
+ val negativeNumber = if (input.contains('.')) {
+ Float(input.toFloat())
+ } else {
+ Integer(input.toInt())
+ }
+
+ // Check if parser returns the same result
+
+ val parsed = parser.parseToEnd("X is $input") as Term
+
+ assertEquals(
+ Structure(Atom("is"), listOf(
+ Variable("X"),
+ Structure(Atom("-"), listOf(number)),
+ )),
+ parsed
+ )
+
+ // Check if preprocessor returns the same result
+
+ val prepped = preprocessor.preprocess(parsed)
+
+ val expected = Is(
+ Variable("X"),
+ Subtract(Integer(0), number)
+ )
+
+ assertEquals(expected, prepped)
+ assertEquals(expected.toString(), prepped.toString())
+
+ // Check if evaluation is correct
+
+ val solutions = (prepped as Is).satisfy(emptyMap()).toList()
+
+ assertEquals(1, solutions.size)
+ assertEquals(negativeNumber, solutions[0].getOrNull()!![Variable("X")])
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["X is 1 - 2", "X is 1-2"])
+ fun `can add negative numbers`(input: String) {
+ val result = parser.parseToEnd(input) as Term
+
+ assertEquals(
+ Structure(Atom("is"), listOf(Variable("X"), Structure(Atom("-"), listOf(Integer(1), Integer(2))))),
+ result
+ )
+
+ val prepped = preprocessor.preprocess(result)
+
+ val expected = Is(
+ Variable("X"),
+ Subtract(Integer(1), Integer(2))
+ )
+
+ assertEquals(expected, prepped)
+ assertEquals(expected.toString(), prepped.toString())
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt
new file mode 100644
index 0000000..bf54869
--- /dev/null
+++ b/tests/interpreter/PreprocessorTests.kt
@@ -0,0 +1,622 @@
+package interpreter
+
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import org.junit.jupiter.api.Assertions.*
+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.*
+
+class PreprocessorTests {
+ val preprocessor = OpenPreprocessor()
+
+ companion object {
+ val preprocessor = OpenPreprocessor()
+
+ fun test(tests: Map) {
+ for ((input, expected) in tests) {
+ val result = preprocessor.preprocess(input)
+ assertEquals(expected, result, "Expected preprocessed")
+ assertEquals(expected::class, result::class, "Expected same class")
+ }
+ }
+ }
+
+ @Test
+ fun `can preprocess anonymous variable`() {
+ val input = Variable("_")
+
+ val result = preprocessor.preprocess(input)
+
+ assertInstanceOf(AnonymousVariable::class.java, result, "Expected anonymous variable")
+ assertTrue((result as Variable).name.matches("_\\d+".toRegex()), "Expected anonymous variable name")
+ }
+
+ @Test
+ fun `multiple anonymous variables should be unique`() {
+ val input = CompoundTerm(Atom("foo"), listOf(Variable("_"), Variable("_")))
+
+ val result = preprocessor.preprocess(input)
+
+ assertInstanceOf(CompoundTerm::class.java, result, "Expected compound term")
+ assertEquals(2, (result as CompoundTerm).arguments.size, "Expected two terms")
+ for (argument in result.arguments) {
+ assertTrue(
+ (argument as Variable).name.matches("_\\d+".toRegex()),
+ "Expected anonymous variable name, but got ${argument.name}"
+ )
+ }
+ val first = result.arguments[0] as Variable
+ val second = result.arguments[1] as Variable
+ assertNotEquals(first.name, second.name, "Expected different anonymous variable names")
+ }
+
+ @Test
+ fun `can preprocess nested anonymous variables`() {
+ val input = TermsGrammar().parseToEnd("name(character(Name, _, _, _))") as Term
+
+ val result = preprocessor.preprocess(input)
+
+ assertInstanceOf(CompoundTerm::class.java, result, "Expected compound term")
+ assertEquals(1, (result as CompoundTerm).arguments.size, "Expected one term")
+ assertInstanceOf(CompoundTerm::class.java, result.arguments[0], "Expected compound term")
+ val inner = result.arguments[0] as CompoundTerm
+ assertEquals(4, inner.arguments.size, "Expected four terms")
+ for (argument in inner.arguments) {
+ if ((argument as Variable).name != "Name") {
+ assertTrue(
+ (argument as Variable).name.matches("_\\d+".toRegex()),
+ "Expected anonymous variable name, but got ${argument.name}"
+ )
+ }
+ }
+
+ }
+
+ @Nested
+ class `Arithmetic operators` {
+ @Test
+ fun `evaluates to different`() {
+ test(
+ mapOf(
+ Atom("=\\=") to Atom("=\\="),
+ CompoundTerm(Atom("=\\="), emptyList()) to CompoundTerm(Atom("=\\="), emptyList()),
+ Atom("EvaluatesToDifferent") to Atom("EvaluatesToDifferent"),
+ CompoundTerm(Atom("EvaluatesToDifferent"), emptyList()) to CompoundTerm(
+ Atom("EvaluatesToDifferent"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("=\\="), listOf(Atom("a"))) to CompoundTerm(
+ Atom("=\\="),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("=\\="), listOf(Integer(1))) to CompoundTerm(
+ Atom("=\\="),
+ listOf(Integer(1))
+ ),
+ CompoundTerm(Atom("=\\="), listOf(Atom("=\\="))) to CompoundTerm(
+ Atom("=\\="),
+ listOf(Atom("=\\="))
+ ),
+ CompoundTerm(Atom("=\\="), listOf(Integer(1), Integer(2))) to EvaluatesToDifferent(
+ Integer(1), Integer(2)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `evaluates to`() {
+ test(
+ mapOf(
+ Atom("=:=") to Atom("=:="),
+ CompoundTerm(Atom("=:="), emptyList()) to CompoundTerm(Atom("=:="), emptyList()),
+ Atom("EvaluatesTo") to Atom("EvaluatesTo"),
+ CompoundTerm(Atom("EvaluatesTo"), emptyList()) to CompoundTerm(
+ Atom("EvaluatesTo"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("=:="), listOf(Atom("a"))) to CompoundTerm(
+ Atom("=:="),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("=:="), listOf(Atom("=:="))) to CompoundTerm(
+ Atom("=:="),
+ listOf(Atom("=:="))
+ ),
+ CompoundTerm(Atom("=:="), listOf(Integer(1), Integer(2))) to EvaluatesTo(
+ Integer(1), Integer(2)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `is`() {
+ test(
+ mapOf(
+ Atom("is") to Atom("is"),
+ CompoundTerm(Atom("is"), emptyList()) to CompoundTerm(Atom("is"), emptyList()),
+ Atom("Is") to Atom("Is"),
+ CompoundTerm(Atom("Is"), emptyList()) to CompoundTerm(Atom("Is"), emptyList()),
+ CompoundTerm(Atom("is"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("is"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("is"), listOf(Integer(1))) to CompoundTerm(
+ Atom("is"),
+ listOf(Integer(1))
+ ),
+ CompoundTerm(Atom("is"), listOf(Atom("is"))) to CompoundTerm(
+ Atom("is"),
+ listOf(Atom("is"))
+ ),
+ CompoundTerm(Atom("is"), listOf(Integer(1), Integer(2))) to Is(
+ Integer(1), Integer(2)
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `negate and subtract`() {
+ test(
+ mapOf(
+ Atom("-") to Atom("-"),
+ CompoundTerm(Atom("-"), emptyList()) to CompoundTerm(Atom("-"), emptyList()),
+ Atom("Negate") to Atom("Negate"),
+ CompoundTerm(Atom("Negate"), emptyList()) to CompoundTerm(
+ Atom("Negate"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("-"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("-"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("-"), listOf(Integer(1))) to Negate(Integer(1)),
+ CompoundTerm(Atom("-"), listOf(Atom("-"))) to CompoundTerm(
+ Atom("-"),
+ listOf(Atom("-"))
+ ),
+ CompoundTerm(Atom("-"), listOf(Integer(1), Integer(2))) to Subtract(
+ Integer(1), Integer(2)
+ ),
+ CompoundTerm(Atom("-"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
+ Atom("-"),
+ listOf(Atom("1"), Atom("2"))
+ ),
+ CompoundTerm(Atom("-"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
+ Atom("-"),
+ listOf(Integer(1), Integer(2), Integer(3))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun `positive and add`() {
+ test(
+ mapOf(
+ Atom("+") to Atom("+"),
+ CompoundTerm(Atom("+"), emptyList()) to CompoundTerm(Atom("+"), emptyList()),
+ Atom("Positive") to Atom("Positive"),
+ CompoundTerm(Atom("Positive"), emptyList()) to CompoundTerm(
+ Atom("Positive"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("+"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("+"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("+"), listOf(Integer(1))) to Positive(Integer(1)),
+ CompoundTerm(Atom("+"), listOf(Atom("+"))) to CompoundTerm(
+ Atom("+"),
+ listOf(Atom("+"))
+ ),
+ CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2))) to Add(
+ Integer(1), Integer(2)
+ ),
+ CompoundTerm(Atom("+"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
+ Atom("+"),
+ listOf(Atom("1"), Atom("2"))
+ ),
+ CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
+ Atom("+"),
+ listOf(Integer(1), Integer(2), Integer(3))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun multiply() {
+ test(
+ mapOf(
+ Atom("*") to Atom("*"),
+ CompoundTerm(Atom("*"), emptyList()) to CompoundTerm(Atom("*"), emptyList()),
+ Atom("Multiply") to Atom("Multiply"),
+ CompoundTerm(Atom("Multiply"), emptyList()) to CompoundTerm(
+ Atom("Multiply"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("*"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("*"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("*"), listOf(Integer(1))) to CompoundTerm(Atom("*"), listOf(Integer(1))),
+ CompoundTerm(Atom("*"), listOf(Atom("*"))) to CompoundTerm(
+ Atom("*"),
+ listOf(Atom("*"))
+ ),
+ CompoundTerm(Atom("*"), listOf(Integer(1), Integer(2))) to Multiply(
+ Integer(1), Integer(2)
+ ),
+ CompoundTerm(Atom("*"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
+ Atom("*"),
+ listOf(Atom("1"), Atom("2"))
+ ),
+ CompoundTerm(Atom("*"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
+ Atom("*"),
+ listOf(Integer(1), Integer(2), Integer(3))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun divide() {
+ test(
+ mapOf(
+ Atom("/") to Atom("/"),
+ CompoundTerm(Atom("/"), emptyList()) to CompoundTerm(Atom("/"), emptyList()),
+ Atom("Divide") to Atom("Divide"),
+ CompoundTerm(Atom("Divide"), emptyList()) to CompoundTerm(
+ Atom("Divide"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("/"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("/"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("/"), listOf(Integer(1))) to CompoundTerm(Atom("/"), listOf(Integer(1))),
+ CompoundTerm(Atom("/"), listOf(Atom("/"))) to CompoundTerm(
+ Atom("/"),
+ listOf(Atom("/"))
+ ),
+ CompoundTerm(Atom("/"), listOf(Integer(1), Integer(2))) to Divide(
+ Integer(1), Integer(2)
+ ),
+ CompoundTerm(Atom("/"), listOf(Atom("1"), Atom("2"))) to CompoundTerm(
+ Atom("/"),
+ listOf(Atom("1"), Atom("2"))
+ ),
+ CompoundTerm(Atom("/"), listOf(Integer(1), Integer(2), Integer(3))) to CompoundTerm(
+ Atom("/"),
+ listOf(Integer(1), Integer(2), Integer(3))
+ )
+ )
+ )
+ }
+
+ @Test
+ fun between() {
+ test(
+ mapOf(
+ Atom("between") to Atom("between"),
+ CompoundTerm(Atom("between"), emptyList()) to CompoundTerm(
+ Atom("between"),
+ emptyList()
+ ),
+ Atom("Between") to Atom("Between"),
+ CompoundTerm(Atom("Between"), emptyList()) to CompoundTerm(
+ Atom("Between"),
+ emptyList()
+ ),
+ CompoundTerm(Atom("between"), listOf(Atom("a"))) to CompoundTerm(
+ Atom("between"),
+ listOf(Atom("a"))
+ ),
+ CompoundTerm(Atom("between"), listOf(Integer(1))) to CompoundTerm(
+ Atom("between"),
+ listOf(Integer(1))
+ ),
+ CompoundTerm(Atom("between"), listOf(Atom("between"))) to CompoundTerm(
+ Atom("between"),
+ listOf(Atom("between"))
+ ),
+ CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2))) to CompoundTerm(
+ Atom("between"),
+ listOf(Integer(1), Integer(2))
+ ),
+ CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2), Integer(3))) to Between(
+ Integer(1), Integer(2), Integer(3)
+ ),
+ )
+ )
+ }
+
+ @Test
+ fun `fun combinations`() {
+ /*
+ * [X - 1] is [(1 + 2) * ((12 / 3) - 0)]
+ * should return
+ * X = 13
+ */
+ val sum_ = CompoundTerm(Atom("+"), listOf(Integer(1), Integer(2)))
+ val sum = Add(Integer(1), Integer(2))
+
+ val div_ = CompoundTerm(Atom("/"), listOf(Integer(12), Integer(3)))
+ val div = Divide(Integer(12), Integer(3))
+
+ val sub_ = CompoundTerm(Atom("-"), listOf(div_, Integer(0)))
+ val sub = Subtract(div, Integer(0))
+
+ val right_ = CompoundTerm(Atom("*"), listOf(sum_, sub_))
+ val right = Multiply(sum, sub)
+
+ val left_ = CompoundTerm(Atom("-"), listOf(Variable("X"), Integer(1)))
+ val left = Subtract(Variable("X"), Integer(1))
+
+ val expr_ = CompoundTerm(Atom("is"), listOf(left_, right_))
+ val expr = Is(left, right)
+
+ val result = OpenPreprocessor().preprocess(expr_)
+
+ assertEquals(expr, result)
+ assertEquals(Is::class, result::class)
+ val `is` = result as Is
+
+ assertEquals(left, `is`.number)
+ assertEquals(Subtract::class, `is`.number::class)
+
+ assertEquals(right, `is`.expr)
+ assertEquals(Multiply::class, `is`.expr::class)
+ val multiply = `is`.expr as Multiply
+
+ assertEquals(sum, multiply.expr1)
+ assertEquals(Add::class, multiply.expr1::class)
+ }
+ }
+
+ @Nested
+ class `Control operators` {
+ private var preprocessor = OpenPreprocessor()
+
+ @Test
+ fun fail() {
+ test(
+ mapOf(
+ Atom("fail") to Fail,
+ CompoundTerm(Atom("fail"), emptyList()) to Fail,
+ Atom("Fail") to Atom("Fail"),
+ CompoundTerm(Atom("Fail"), emptyList()) to CompoundTerm(Atom("Fail"), emptyList()),
+ CompoundTerm(Atom("fail"), listOf(Atom("a"))) to CompoundTerm(Atom("fail"), listOf(Atom("a"))),
+ CompoundTerm(Atom("fail"), listOf(Atom("fail"))) to CompoundTerm(Atom("fail"), listOf(Fail))
+ )
+ )
+ }
+
+ @Test
+ fun `true`() {
+ test(
+ mapOf(
+ Atom("true") to True,
+ CompoundTerm(Atom("true"), emptyList()) to True,
+ Atom("True") to Atom("True"),
+ CompoundTerm(Atom("True"), emptyList()) to CompoundTerm(Atom("True"), emptyList()),
+ CompoundTerm(Atom("true"), listOf(Atom("a"))) to CompoundTerm(Atom("true"), listOf(Atom("a"))),
+ CompoundTerm(Atom("true"), listOf(Atom("true"))) to CompoundTerm(Atom("true"), listOf(True))
+ )
+ )
+ }
+
+ @Test
+ fun cut() {
+ test(
+ mapOf(
+ Atom("!") to Cut(),
+ CompoundTerm(Atom("!"), emptyList()) to Cut(),
+ CompoundTerm(Atom("!"), listOf(Atom("a"))) to CompoundTerm(Atom("!"), listOf(Atom("a"))),
+ CompoundTerm(Atom("!"), listOf(Atom("!"))) to CompoundTerm(Atom("!"), listOf(Cut()))
+ )
+ )
+ }
+
+ @Test
+ fun conjunction() {
+ test(
+ mapOf(
+ CompoundTerm(Atom(","), listOf(Atom("a"), Atom("b"))) to Conjunction(Atom("a"), Atom("b")),
+ CompoundTerm(Atom(","), listOf(Atom("a"), Atom("b"), Atom("c"))) to CompoundTerm(
+ Atom(","),
+ listOf(Atom("a"), Atom("b"), Atom("c"))
+ ),
+ // Nested conjunctions
+ CompoundTerm(
+ Atom(","),
+ listOf(Atom("a"), CompoundTerm(Atom(","), listOf(Atom("b"), Atom("c"))))
+ ) to Conjunction(Atom("a"), Conjunction(Atom("b"), Atom("c"))),
+ )
+ )
+ }
+
+ @Test
+ fun disjunction() {
+ test(
+ mapOf(
+ CompoundTerm(Atom(";"), listOf(Atom("a"), Atom("b"))) to Disjunction(Atom("a"), Atom("b")),
+ CompoundTerm(Atom(";"), listOf(Atom("a"), Atom("b"), Atom("c"))) to CompoundTerm(
+ Atom(";"),
+ listOf(Atom("a"), Atom("b"), Atom("c"))
+ ),
+ // Nested disjunctions
+ CompoundTerm(
+ Atom(";"),
+ listOf(Atom("a"), CompoundTerm(Atom(";"), listOf(Atom("b"), Atom("c"))))
+ ) to Disjunction(Atom("a"), Disjunction(Atom("b"), Atom("c"))),
+ )
+ )
+ }
+
+ @Test
+ fun not() {
+ test(
+ mapOf(
+ CompoundTerm(Atom("\\+"), listOf(Atom("a"))) to Not(Atom("a")),
+ CompoundTerm(Atom("\\+"), listOf(Atom("a"), Atom("b"))) to CompoundTerm(
+ Atom("\\+"),
+ listOf(Atom("a"), Atom("b"))
+ ),
+ // Nested not
+ CompoundTerm(
+ Atom("foo"),
+ listOf(
+ Atom("bar"),
+ CompoundTerm(Atom("\\+"), listOf(CompoundTerm(Atom("\\+"), listOf(Atom("baz")))))
+ )
+ ) to CompoundTerm(Atom("foo"), listOf(Atom("bar"), Not(Not(Atom("baz"))))),
+ )
+ )
+ }
+
+ @Test
+ fun `is`() {
+ test(
+ mapOf(
+ CompoundTerm(Atom("is"), listOf(Variable("T"), Integer(1))) to Is(Variable("T"), Integer(1)),
+ CompoundTerm(Atom("is"), listOf(Variable("T"), Add(Variable("HP"), Integer(5)))) to Is(
+ Variable("T"),
+ Add(Variable("HP"), Integer(5))
+ ),
+ CompoundTerm(Atom("is"), listOf(Variable("T"), Subtract(Variable("HP"), Integer(5)))) to Is(
+ Variable("T"),
+ Subtract(Variable("HP"), Integer(5))
+ ),
+ )
+ )
+ }
+ }
+
+ @Nested
+ class `Database operators` {
+ private val preprocessor = OpenPreprocessor()
+
+ @Test
+ fun `assert(fact)`() {
+ val input = Structure(
+ Atom("assert"), listOf(
+ Structure(
+ Atom(":-"), listOf(
+ Atom("a"),
+ Atom("b")
+ )
+ )
+ )
+ )
+ val expected = Assert(
+ Rule(
+ Atom("a"),
+ Atom("b")
+ )
+ )
+
+ val result = preprocessor.preprocess(input)
+
+ 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)
+ }
+
+ @Test
+ fun `dynamic declaration`() {
+ val input = Structure(
+ Atom("dynamic"), listOf(
+ Atom("declaration/1")
+ )
+ )
+ val expected = Dynamic("declaration/1")
+
+ val result = preprocessor.preprocess(input)
+
+ assertEquals(expected, result)
+ }
+ }
+}
diff --git a/tests/interpreter/SourceFileReaderTests.kt b/tests/interpreter/SourceFileReaderTests.kt
index f999baf..654a6ca 100644
--- a/tests/interpreter/SourceFileReaderTests.kt
+++ b/tests/interpreter/SourceFileReaderTests.kt
@@ -2,31 +2,27 @@ package interpreter
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import prolog.Program
+import prolog.ast.Database.Program
class SourceFileReaderTests {
@BeforeEach
fun setup() {
- Program.clear()
+ Program.reset()
}
@Test
fun a() {
- val inputFile = "tests/better_parser/resources/a.pl"
- val reader = SourceFileReader()
+ val inputFile = "tests/parser/resources/a.pl"
+ val reader = FileLoader()
reader.readFile(inputFile)
-
- println(Program.predicates)
}
@Test
fun foo() {
- val inputFile = "tests/better_parser/resources/foo.pl"
- val reader = SourceFileReader()
+ val inputFile = "tests/parser/resources/foo.pl"
+ val reader = FileLoader()
reader.readFile(inputFile)
-
- println(Program.predicates)
}
}
\ No newline at end of file
diff --git a/tests/lexer/ScanPrologParserTests.kt b/tests/lexer/ScanPrologParserTests.kt
deleted file mode 100644
index 5e99b01..0000000
--- a/tests/lexer/ScanPrologParserTests.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package lexer
-
-import lexer.errors.LexingError
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
-import kotlin.test.assertEquals
-
-/**
- * Tests for the Prolog lexer.
- *
- * These tests are based on the Prolog syntax.
- */
-class ScanPrologParserTests {
- @Test
- fun scan_simple_atom() {
- val tokens = Lexer("atom.").scan()
-
- assertEquals(3, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}")
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_variable() {
- val tokens = Lexer("X.").scan()
-
- assertEquals(3, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}")
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_variable_with_number() {
- val tokens = Lexer("X1.").scan()
-
- assertEquals(3, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}")
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_variable_with_underscore() {
- val tokens = Lexer("X_1.").scan()
-
- assertEquals(3, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}")
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_variable_that_starts_with_a_number() {
- assertThrows { Lexer("1X.").scan() }
- }
-}
diff --git a/tests/lexer/ScanTests.kt b/tests/lexer/ScanTests.kt
deleted file mode 100644
index a21f571..0000000
--- a/tests/lexer/ScanTests.kt
+++ /dev/null
@@ -1,191 +0,0 @@
-package lexer
-
-import lexer.errors.LexingError
-import org.junit.jupiter.api.Test
-import org.junit.jupiter.api.assertThrows
-import org.junit.jupiter.api.Assertions.*
-
-class ScanTests {
- @Test
- fun scan_emptyString_returns_EOF() {
- val tokens = Lexer("").scan()
- assertEquals(1, tokens.size, "Expected 1 token, got ${tokens.size}")
- assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}")
- }
-
- @Test
- fun scan_unknownSymbol_returns_Error() {
- assertThrows { Lexer("€").scan() }
- }
-
- @Test
- fun scan_dot_returns_Dot() {
- val tokens = Lexer(".").scan()
- assertEquals(2, tokens.size)
- assertEquals(TokenType.DOT, tokens[0].type, "Expected DOT token, got ${tokens[0].type}")
- assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}")
- }
-
- @Test
- fun scan_two_dots_returns_two_dots() {
- val tokens = Lexer("..").scan()
- assertEquals(3, tokens.size)
- assertEquals(TokenType.DOT, tokens[0].type, "Expected DOT token, got ${tokens[0].type}")
- assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}")
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_letter_returns_letter() {
- val tokens = Lexer("a").scan()
-
- assertEquals(2, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}")
-
- assertEquals(0, tokens[0].position.line, "Expected line 0, got ${tokens[0].position.line}")
- assertEquals(0, tokens[0].position.column, "Expected column 0, got ${tokens[0].position.column}")
- assertEquals(1, tokens[0].position.length, "Expected length 1, got ${tokens[0].position.length}")
- }
-
- @Test
- fun scan_word_returns_alphanumerics() {
- val lexer = Lexer("word")
- val tokens = lexer.scan()
-
- assertEquals(2, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}")
-
- assertEquals(4, tokens[0].position.length, "Expected length 4, got ${tokens[0].position.length}")
-
- assertEquals("word", tokens[0].value, "Expected 'word', got ${tokens[0].value}")
- }
-
- @Test
- fun scan_space_returns_nothing() {
- val lexer = Lexer(" ")
- val tokens = lexer.scan()
-
- assertEquals(1, tokens.size)
-
- assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}")
- }
-
- @Test
- fun scan_whitespace_various_returns_nothing() {
- val lexer = Lexer(" \t\n\r")
- val tokens = lexer.scan()
-
- assertEquals(1, tokens.size)
-
- assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}")
- }
-
-
- @Test
- fun scan_separated_words() {
- val tokens = Lexer("word1 word2").scan()
-
- assertEquals(3, tokens.size, "Expected 3 tokens, got ${tokens.size}")
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals("word1", tokens[0].value, "Expected 'word1', got ${tokens[0].value}")
- assertEquals(5, tokens[0].position.length, "Expected length 5, got ${tokens[0].position.length}")
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[1].type, "Expected ALPHANUMERIC token, got ${tokens[1].type}")
- assertEquals("word2", tokens[1].value, "Expected 'word2', got ${tokens[1].value}")
- assertEquals(5, tokens[1].position.length, "Expected length 5, got ${tokens[1].position.length}")
- }
-
- @Test
- fun scan_multiline() {
- val tokens = Lexer(
- """
- word1
- word2
- """.trimIndent()
- ).scan()
-
- assertEquals(3, tokens.size, "Expected 3 tokens, got ${tokens.size}")
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals("word1", tokens[0].value, "Expected 'word1', got ${tokens[0].value}")
- assertEquals(5, tokens[0].position.length, "Expected length 5, got ${tokens[0].position.length}")
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[1].type, "Expected ALPHANUMERIC token, got ${tokens[1].type}")
- assertEquals("word2", tokens[1].value, "Expected 'word2', got ${tokens[1].value}")
- assertEquals(5, tokens[1].position.length, "Expected length 5, got ${tokens[1].position.length}")
- }
-
- @Test
- fun scan_parenthesis_returns_parenthesis() {
- val lexer = Lexer("()")
- val tokens = lexer.scan()
-
- assertEquals(3, tokens.size)
-
- assertEquals(
- TokenType.PARENTHESIS_LEFT,
- tokens[0].type,
- "Expected LEFT_PARENTHESES token, got ${tokens[0].type}"
- )
- assertEquals(
- TokenType.PARENTHESIS_RIGHT,
- tokens[1].type,
- "Expected RIGHT_PARENTHESES token, got ${tokens[1].type}"
- )
- assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}")
- }
-
- @Test
- fun scan_simple_quoted_string_returns_string() {
- val lexer = Lexer("\"string\"")
- val tokens = lexer.scan()
-
- assertEquals(2, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}")
-
- assertEquals("string", tokens[0].value, "Expected 'string', got ${tokens[0].value}")
- }
-
- @Test
- fun scan_quoted_string_with_space_returns_string() {
- val lexer = Lexer("\"string with space\"")
- val tokens = lexer.scan()
-
- assertEquals(2, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}")
-
- assertEquals("string with space", tokens[0].value, "Expected 'string with space', got ${tokens[0].value}")
- }
-
- @Test
- fun scan_comments_returns_nothing() {
- val lexer = Lexer("% comment")
- val tokens = lexer.scan()
-
- assertEquals(1, tokens.size)
-
- assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}")
- }
-
- @Test
- fun scan_comment_and_sentence_returns_sentence() {
- val tokens = Lexer("""
- % comment
- sentence
- """.trimIndent()).scan()
-
- assertEquals(2, tokens.size)
-
- assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}")
- assertEquals("sentence", tokens[0].value, "Expected 'sentence', got ${tokens[0].value}")
- }
-}
diff --git a/tests/parser/OperatorParserTests.kt b/tests/parser/OperatorParserTests.kt
new file mode 100644
index 0000000..9c19db2
--- /dev/null
+++ b/tests/parser/OperatorParserTests.kt
@@ -0,0 +1,43 @@
+package parser
+
+import com.github.h0tk3y.betterParse.combinators.use
+import com.github.h0tk3y.betterParse.grammar.Grammar
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import com.github.h0tk3y.betterParse.parser.Parser
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import parser.grammars.TermsGrammar
+import prolog.ast.terms.Atom
+import prolog.ast.terms.CompoundTerm
+import prolog.ast.terms.Operator
+import prolog.ast.terms.Structure
+
+class OperatorParserTests {
+ class OperatorParser: TermsGrammar() {
+ override val rootParser: Parser by body use { this as CompoundTerm }
+ }
+
+ private var parser = OperatorParser() as Grammar
+
+ @Test
+ fun `parse conjunction`() {
+ val input = "a, b"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Structure(Atom(","), listOf(Atom("a"), Atom("b"))), result, "Expected atom 'a, b'")
+ }
+
+ class BodyParser : TermsGrammar() {
+ override val rootParser: Parser by body
+ }
+
+ @Test
+ fun `parse equality`() {
+ val input = "a = b"
+
+ val result = BodyParser().parseToEnd(input)
+
+ assertEquals(Structure(Atom("="), listOf(Atom("a"), Atom("b"))), result, "Expected atom 'a = b'")
+ }
+}
\ No newline at end of file
diff --git a/tests/parser/ParseFromTextTests.kt b/tests/parser/ParseFromTextTests.kt
deleted file mode 100644
index c7de15e..0000000
--- a/tests/parser/ParseFromTextTests.kt
+++ /dev/null
@@ -1,4 +0,0 @@
-package parser
-
-class ParseFromTextTests {
-}
\ No newline at end of file
diff --git a/tests/parser/ParseTests.kt b/tests/parser/ParseTests.kt
deleted file mode 100644
index 4056820..0000000
--- a/tests/parser/ParseTests.kt
+++ /dev/null
@@ -1,91 +0,0 @@
-package parser
-
-import lexer.Token
-import lexer.state.TokenPosition
-import lexer.TokenType
-import org.junit.jupiter.api.Assertions.assertEquals
-import org.junit.jupiter.api.Assertions.assertTrue
-import org.junit.jupiter.api.Test
-import prolog.ast.terms.Atom
-import prolog.ast.terms.CompoundTerm
-
-class ParseTests {
- @Test
- fun `parse atom a`() {
- val input = Token(TokenType.ALPHANUMERIC, "a", TokenPosition(0, 0, 1))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom("a"), result[0], "Expected atom 'a'")
- }
-
- @Test
- fun `parse atom foo`() {
- val input = Token(TokenType.ALPHANUMERIC, "foo", TokenPosition(0, 0, 3))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom("foo"), result[0], "Expected atom 'foo'")
- }
-
- @Test
- fun `parse atom foo1`() {
- val input = Token(TokenType.ALPHANUMERIC, "foo1", TokenPosition(0, 0, 4))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom("foo1"), result[0], "Expected atom 'foo1'")
- }
-
- @Test
- fun `parse atom fooBar`() {
- val name = "fooBar"
- val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 6))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom(name), result[0], "Expected atom 'fooBar'")
- }
-
- @Test
- fun `parse atom foo_bar`() {
- val name = "foo_bar"
- val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 7))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom(name), result[0], "Expected atom 'foo_bar'")
- }
-
- @Test
- fun `parse atom my_FooBar1`() {
- val name = "my_FooBar1"
- val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 11))
-
- val result = Parser(listOf(input)).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertEquals(Atom(name), result[0], "Expected atom 'my_FooBar1'")
- }
-
- @Test
- fun `parse compound term f()`() {
- val input = listOf(
- Token(TokenType.ALPHANUMERIC, "f", TokenPosition(0, 0, 1)),
- Token(TokenType.PARENTHESIS_LEFT, "(", TokenPosition(0, 1, 2)),
- Token(TokenType.PARENTHESIS_RIGHT, ")", TokenPosition(0, 3, 4))
- )
-
- val result = Parser(input).parse()
-
- assertEquals(1, result.size, "Expected 1 term")
- assertTrue(result[0] is CompoundTerm)
- assertEquals("f", (result[0] as CompoundTerm).name)
- assertEquals(0, (result[0] as CompoundTerm).arguments.size)
- }
-}
\ No newline at end of file
diff --git a/tests/parser/builtins/DatabaseOperatorsParserTests.kt b/tests/parser/builtins/DatabaseOperatorsParserTests.kt
new file mode 100644
index 0000000..74aa53e
--- /dev/null
+++ b/tests/parser/builtins/DatabaseOperatorsParserTests.kt
@@ -0,0 +1,75 @@
+package parser.builtins
+
+import com.github.h0tk3y.betterParse.grammar.Grammar
+import com.github.h0tk3y.betterParse.grammar.parseToEnd
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import parser.grammars.TermsGrammar
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.terms.Term
+
+class DatabaseOperatorsParserTests {
+ private lateinit var parser: Grammar
+
+ @BeforeEach
+ fun setup() {
+ parser = TermsGrammar() as Grammar
+ }
+
+ @Test
+ fun `parse assert(rule)`() {
+ val input = "assert((a :- b))"
+ val expected = Structure(Atom("assert"), listOf(
+ Structure(Atom(":-"), listOf(
+ Atom("a"),
+ Atom("b")
+ ))
+ ))
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `parse assertA(rule)`() {
+ val input = "assertA((a :- b))"
+ val expected = Structure(Atom("assertA"), listOf(
+ Structure(Atom(":-"), listOf(
+ Atom("a"),
+ Atom("b")
+ ))
+ ))
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `parse assertZ(rule)`() {
+ val input = "assertZ((a :- b))"
+ val expected = Structure(Atom("assertZ"), listOf(
+ Structure(Atom(":-"), listOf(
+ Atom("a"),
+ Atom("b")
+ ))
+ ))
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(expected, result)
+ }
+
+ @Test
+ fun `parse dynamic declaration`() {
+ val input = "dynamic declaration/1"
+ val expected = Structure(Atom("dynamic"), listOf(Atom("declaration/1")))
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(expected, result)
+ }
+}
diff --git a/tests/better_parser/SimpleSourcePrologParserTests.kt b/tests/parser/grammars/LogicGrammarTests.kt
similarity index 72%
rename from tests/better_parser/SimpleSourcePrologParserTests.kt
rename to tests/parser/grammars/LogicGrammarTests.kt
index 1eac0f9..57a1f99 100644
--- a/tests/better_parser/SimpleSourcePrologParserTests.kt
+++ b/tests/parser/grammars/LogicGrammarTests.kt
@@ -1,7 +1,8 @@
-package better_parser
+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,14 +15,13 @@ import prolog.ast.terms.CompoundTerm
import prolog.ast.terms.Structure
import prolog.ast.terms.Variable
import prolog.builtins.Conjunction
-import prolog.builtins.Disjunction
-class SimpleSourcePrologParserTests {
+class LogicGrammarTests {
private lateinit var parser: Grammar>
@BeforeEach
fun setup() {
- parser = SimpleSourceParser() as Grammar>
+ parser = LogicGrammar() as Grammar>
}
@ParameterizedTest
@@ -40,7 +40,7 @@ class SimpleSourcePrologParserTests {
assertEquals(1, result.size, "Expected 1 fact")
assertTrue(result[0] is Fact, "Expected a fact")
- assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'")
+ assertEquals(input, "${result[0]}.", "Expected fact to be '$input'")
}
@ParameterizedTest
@@ -114,7 +114,7 @@ class SimpleSourcePrologParserTests {
assertEquals(1, result.size, "Expected 1 rule")
assertInstanceOf(Rule::class.java, result[0], "Expected a rule")
val rule = result[0] as Rule
- assertInstanceOf(Conjunction::class.java, rule.body, "Expected body to be a conjunction")
+ assertInstanceOf(CompoundTerm::class.java, rule.body, "Expected body to be a compound term")
}
@Test
@@ -125,8 +125,38 @@ class SimpleSourcePrologParserTests {
assertEquals(1, result.size, "Expected 1 rule")
val rule = result[0] as Rule
- assertTrue(rule.body is Conjunction, "Expected body to be a conjunction")
- val conjunction = rule.body as Conjunction
- assertEquals("invited/2", (conjunction.left as CompoundTerm).functor, "Expected functor 'invited/2'")
+ assertEquals("guest/2", rule.head.functor, "Expected functor 'guest/2'")
+ assertEquals(",/2", (rule.body as CompoundTerm).functor, "Expected functor ',/2'")
+ val l1 = (rule.body as CompoundTerm).arguments[0] as CompoundTerm
+ assertEquals(",/2", l1.functor, "Expected functor ',/2'")
+ val l2 = l1.arguments[0] as CompoundTerm
+ assertEquals("invited/2", l2.functor, "Expected functor 'invited/2'")
+ }
+
+ @Test
+ fun `parse check_identical(X, Y)`() {
+ var input = "check_identical(X, Y) :- X == Y."
+
+ assertDoesNotThrow {
+ val result = parser.parseToEnd(input)
+ }
+
+ input = "check_identical(X, Y) :- X = Y, !, write('X == Y succeeded'), nl."
+
+ assertDoesNotThrow {
+ val result = parser.parseToEnd(input)
+ }
+ }
+
+ @Test
+ fun `parse constraints`() {
+ val input = ":- a."
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(1, result.size, "Expected 1 rule")
+ assertTrue(result[0] is Rule, "Expected a rule")
+ val rule = result[0] as Rule
+ assertEquals("/_", rule.head.functor, "Expected a constraint")
}
}
\ No newline at end of file
diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt
new file mode 100644
index 0000000..d3b45e2
--- /dev/null
+++ b/tests/parser/grammars/TermsGrammarTests.kt
@@ -0,0 +1,355 @@
+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.assertTrue
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertDoesNotThrow
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import prolog.ast.arithmetic.Float
+import prolog.ast.arithmetic.Integer
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.terms.Term
+import prolog.ast.terms.Variable
+import prolog.builtins.Is
+import prolog.logic.equivalent
+
+class TermsGrammarTests {
+ private lateinit var parser: Grammar
+
+ @BeforeEach
+ fun setup() {
+ parser = TermsGrammar() as Grammar
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["a", "foo", "foo1", "fooBar", "foo_bar"])
+ fun `parse atom`(name: String) {
+ val result = parser.parseToEnd(name)
+
+ assertEquals(Atom(name), result, "Expected atom '$name'")
+ }
+
+ @ParameterizedTest
+ @ValueSource(
+ strings = [
+ "'tick'",
+ "\"doubleTick\"",
+ "`backTick`",
+ "'i have spaces'",
+ "`i have a 'quote' inside`",
+ "'I have Cases'",
+ "'I, h@v3 many (!!!) special characters?! {}'"
+ ]
+ )
+ fun `Parse a quoted atom`(input: String) {
+ val result = parser.parseToEnd(input)
+
+ val expected = input.substring(1, input.length - 1)
+
+ assertEquals(Atom(expected), result, "Expected atom")
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["X", "X1", "X_1"])
+ fun `parse variable`(name: String) {
+ val result = parser.parseToEnd(name)
+
+ assertEquals(Variable(name), result)
+ }
+
+ @Test
+ fun `parse anonymous variable`() {
+ val input = "_"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Variable("_"), result, "Expected anonymous variable")
+ }
+
+ @Test
+ fun `empty compound term`() {
+ val input = "f()"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Structure(Atom("f"), emptyList()), result, "Expected atom 'f'")
+ }
+
+ @Test
+ fun `parse compound term f(a)`() {
+ val input = "f(a)"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Structure(Atom("f"), listOf(Atom("a"))), result, "Expected atom 'f(a)'")
+ }
+
+ @Test
+ fun `parse compound term f(a, b)`() {
+ val input = "f(a, b)"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("f"), listOf(Atom("a"), Atom("b"))),
+ result,
+ "Expected atom 'f(a, b)'"
+ )
+ }
+
+ @Test
+ fun `parse compound term with variable f(a, X)`() {
+ val input = "f(a, X)"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("f"), listOf(Atom("a"), Variable("X"))),
+ result,
+ "Expected atom 'f(a, X)'"
+ )
+ }
+
+ @Test
+ fun `parse compound term with var and int`() {
+ val input = "check_identical(A, 13)"
+ val result = parser.parseToEnd(input)
+ assertEquals(
+ Structure(Atom("check_identical"), listOf(Variable("A"), Integer(13))),
+ result,
+ "Expected atom 'check_identical(A, 13)'"
+ )
+ }
+
+ @Test
+ fun `parse nested compound term f(a, g(b))`() {
+ val input = "f(a, g(b))"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))),
+ result,
+ "Expected atom 'f(a, g(b))'"
+ )
+ }
+
+ @Test
+ fun `parse compound term with variable f(a, g(X))`() {
+ val input = "f(a, g(X))"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Variable("X"))))),
+ result,
+ "Expected atom 'f(a, g(X))'"
+ )
+ }
+
+ @Test
+ fun `parse nested compound term with variables`() {
+ val input = "hit(character(Name, Class, Level, HP), character(Name, Class, Level, T))"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(
+ Atom("hit"),
+ listOf(
+ Structure(Atom("character"), listOf(Variable("Name"), Variable("Class"), Variable("Level"), Variable("HP"))),
+ Structure(Atom("character"), listOf(Variable("Name"), Variable("Class"), Variable("Level"), Variable("T")))
+ )
+ ),
+ result,
+ "Expected atom 'hit(character(Name, Class, Level, HP), character(Name, Class, Level, T))'"
+ )
+ }
+
+ @Test
+ fun `parse compound term with anonymous variables`() {
+ val input = "f(a, _, g(X))"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("f"), listOf(Atom("a"), Variable("_"), Structure(Atom("g"), listOf(Variable("X"))))),
+ result,
+ "Expected atom 'f(a, _, g(X))'"
+ )
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = [0, 1, 5, 12, 345, 123456789])
+ fun `parse positive integer`(number: Int) {
+ val input = number.toString()
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Integer(number), result, "Expected integer '$number'")
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = [-987654321, -543, -21, -1])
+ fun `parse negative integer`(number: Int) {
+ val input = number.toString()
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Structure(Atom("-"), listOf(Integer(0 - number))), result, "Expected integer '$number'")
+ }
+
+ @Test
+ fun `parse float`() {
+ val input = "42.0"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Float(42.0f), result, "Expected float '42.0'")
+ }
+
+ @Test
+ fun `parse negative float`() {
+ val input = "-42.0"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(Structure(Atom("-"), listOf(Float(42.0f))), result, "Expected float '-42.0'")
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["got_an_a(Student)", "grade(Student, Grade)"])
+ fun `parse unification`(input: String) {
+ assertDoesNotThrow { parser.parseToEnd(input) }
+ }
+
+ @Nested
+ class `Operators and precedence` {
+ private lateinit var parser: Grammar
+
+ @BeforeEach
+ fun setup() {
+ parser = TermsGrammar() as Grammar
+ }
+
+ @Test
+ fun `can parse equivalent`() {
+ val input = "X == Y"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("=="), listOf(Variable("X"), Variable("Y"))),
+ result,
+ "Expected equivalent operator"
+ )
+ }
+
+ @Test
+ fun `can parse cut`() {
+ val input = "!"
+ val result = parser.parseToEnd(input)
+ assertEquals(Atom("!"), result, "Expected cut operator")
+ }
+
+ @Test
+ fun `can parse 'is'`() {
+ val input = "T is 1"
+ val result = parser.parseToEnd(input)
+ assertEquals(
+ Structure(Atom("is"), listOf(Variable("T"), Integer(1))),
+ result
+ )
+ }
+
+ @Test
+ fun `can parse 'is' with addition`() {
+ val input = "T is 1 + 2"
+ val result = parser.parseToEnd(input)
+ assertEquals(
+ Structure(Atom("is"), listOf(Variable("T"), Structure(Atom("+"), listOf(Integer(1), Integer(2))))),
+ result
+ )
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["+", "-", "*", "/"])
+ fun `can parse with spaces`(operator: String) {
+ val input = "1 $operator 2"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom(operator), listOf(Integer(1), Integer(2))),
+ result,
+ "Expected operator '$operator'"
+ )
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = ["+", "-", "*", "/"])
+ fun `can parse without spaces`(operator: String) {
+ val input = "1${operator}2"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom(operator), listOf(Integer(1), Integer(2))),
+ result,
+ "Expected operator '$operator' without spaces"
+ )
+ }
+
+ @Test
+ fun `parse addition and multiplication`() {
+ val input = "1 + 2 * 3"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("+"), listOf(Integer(1), Structure(Atom("*"), listOf(Integer(2), Integer(3))))),
+ result,
+ "Expected addition and multiplication"
+ )
+ }
+
+ @Test
+ fun `parse multiplication and addition`() {
+ val input = "1 * 2 + 3"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(Atom("+"), listOf(Structure(Atom("*"), listOf(Integer(1), Integer(2))), Integer(3))),
+ result,
+ "Expected multiplication and addition"
+ )
+ }
+
+ @Test
+ fun `complex expression`() {
+ val input = "1 + 2 * 3 - 4 / 5"
+
+ val result = parser.parseToEnd(input)
+
+ assertEquals(
+ Structure(
+ Atom("-"),
+ listOf(
+ Structure(Atom("+"), listOf(Integer(1), Structure(Atom("*"), listOf(Integer(2), Integer(3))))),
+ Structure(Atom("/"), listOf(Integer(4), Integer(5)))
+ )
+ ),
+ result,
+ "Expected complex expression"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/better_parser/resources/a.pl b/tests/parser/resources/a.pl
similarity index 100%
rename from tests/better_parser/resources/a.pl
rename to tests/parser/resources/a.pl
diff --git a/tests/better_parser/resources/foo.pl b/tests/parser/resources/foo.pl
similarity index 100%
rename from tests/better_parser/resources/foo.pl
rename to tests/parser/resources/foo.pl
diff --git a/tests/better_parser/resources/parent.pl b/tests/parser/resources/parent.pl
similarity index 57%
rename from tests/better_parser/resources/parent.pl
rename to tests/parser/resources/parent.pl
index 8ac4196..15426de 100644
--- a/tests/better_parser/resources/parent.pl
+++ b/tests/parser/resources/parent.pl
@@ -4,7 +4,4 @@ female(mary).
parent(john, jimmy).
parent(mary, jimmy).
father(X, Y) :- parent(X, Y), male(X).
-mother(X, Y) :- parent(X, Y), female(X).
-
-foo(0).
-foo(X) :- X > 0, Y is X - 1, foo(Y).
+mother(X, Y) :- parent(X, Y), female(X).
\ No newline at end of file
diff --git a/tests/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt
index e8ce952..a1cbbfc 100644
--- a/tests/prolog/EvaluationTests.kt
+++ b/tests/prolog/EvaluationTests.kt
@@ -2,20 +2,23 @@ package prolog
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 prolog.ast.arithmetic.Integer
import prolog.ast.logic.Fact
import prolog.ast.logic.Rule
+import prolog.builtins.Conjunction
+import prolog.builtins.Disjunction
+import prolog.builtins.Query
import prolog.logic.equivalent
import prolog.ast.terms.Atom
import prolog.ast.terms.Structure
import prolog.ast.terms.Variable
-import prolog.builtins.*
+import prolog.ast.Database.Program
class EvaluationTests {
@BeforeEach
fun setUp() {
- Program.clear()
+ Program.reset()
}
@Test
@@ -105,20 +108,24 @@ class EvaluationTests {
val variable2 = Variable("Y")
val parent = Rule(
- Structure(Atom("parent"), listOf(variable1, variable2)),
- /* :- */ Disjunction(
- Structure(Atom("father"), listOf(variable1, variable2)),
- /* ; */
- Structure(Atom("mother"), listOf(variable1, variable2))
+ Structure(Atom("parent"), listOf(variable1, variable2)), /* :- */
+ Disjunction(
+ Structure(Atom("father"), listOf(variable1, variable2)),
+ /* ; */
+ Structure(Atom("mother"), listOf(variable1, variable2))
)
)
Program.load(listOf(father, mother, parent))
- val result1 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jimmy"))))
- assertTrue(result1.toList().isNotEmpty())
- val result2 = Program.query(Structure(Atom("parent"), listOf(Atom("jane"), Atom("jimmy"))))
- assertTrue(result2.toList().isNotEmpty())
+ val result1 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jimmy")))).toList()
+ assertEquals(1, result1.size, "Expected 1 result")
+ assertTrue(result1[0].isSuccess, "Expected success")
+ assertTrue(result1[0].getOrNull()!!.isEmpty(), "Expected no substitutions")
+ val result2 = Program.query(Structure(Atom("parent"), listOf(Atom("jane"), Atom("jimmy")))).toList()
+ assertEquals(1, result2.size, "Expected 1 result")
+ assertTrue(result2[0].isSuccess, "Expected success")
+ assertTrue(result2[0].getOrNull()!!.isEmpty(), "Expected no substitutions")
val result3 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jane"))))
assertFalse(result3.any())
@@ -211,36 +218,204 @@ class EvaluationTests {
assertEquals(expectedResults.size, actualResults.size, "Number of results should match")
for (i in expectedResults.indices) {
assertEquals(expectedResults[i].size, actualResults[i].getOrNull()!!.size, "Substitution size should match")
- assertTrue(expectedResults[i].all { actualResults[i].getOrNull()!![it.key]?.let { it1 -> equivalent(it.value, it1, emptyMap()) } ?: false }, "Substitution values should match")
+ assertTrue(expectedResults[i].all {
+ actualResults[i].getOrNull()!![it.key]?.let { it1 ->
+ equivalent(
+ it.value,
+ it1,
+ emptyMap()
+ )
+ } ?: false
+ }, "Substitution values should match")
}
}
- /**
- foo(0).
- foo(X) :- X > 0, Y is X - 1, foo(Y).
- */
@Test
- fun recursive_query() {
- val fact = Fact(Structure(Atom("foo"), listOf(Integer(0))))
+ fun `likes(alice, pizza)`() {
+ val fact = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))))
+ val goal = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza")))
+
+ Program.load(listOf(fact))
+
+ val result = Program.query(goal).toList()
+
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs = result[0].getOrNull()!!
+ assertEquals(0, subs.size, "Expected no substitutions")
+ }
+
+ @Test
+ fun `likes(Person, pizza)`() {
+ val fact = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))))
+ val goal = Structure(Atom("likes"), listOf(Variable("Person"), Atom("pizza")))
+
+ Program.load(listOf(fact))
+
+ val result = Program.query(goal).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("alice"), subs[Variable("Person")], "Expected Person to be alice")
+ }
+
+ @Test
+ fun `likes_food(alice)`() {
+ val fact = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))))
val rule = Rule(
- Structure(Atom("foo"), listOf(Variable("X"))),
- Conjunction(
- GreaterThan(Variable("X"), Integer(0)),
- Conjunction(
- Is(Variable("Y"), Subtract(Variable("X"), Integer(1))),
- Structure(Atom("foo"), listOf(Variable("Y")))
- )
- )
+ Structure(Atom("likes_food"), listOf(Variable("Person"))),
+ Structure(Atom("likes"), listOf(Variable("Person"), Atom("pizza")))
)
+ val goal = Structure(Atom("likes_food"), listOf(Atom("alice")))
+
Program.load(listOf(fact, rule))
- val result = Program.query(Structure(Atom("foo"), listOf(Integer(0)))).toList()
+ val result = Program.query(goal).toList()
- val result5 = Program.query(Structure(Atom("foo"), listOf(Integer(5)))).toList()
-
- assertTrue(Program.query(Structure(Atom("foo"), listOf(Atom("1")))).any())
- assertTrue(Program.query(Structure(Atom("foo"), listOf(Atom("2")))).any())
- assertFalse(Program.query(Structure(Atom("foo"), listOf(Atom("-1")))).any())
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs = result[0].getOrNull()!!
+ assertEquals(0, subs.size, "Expected no substitutions")
}
-}
\ No newline at end of file
+
+ @Test
+ fun `likes_food(Person)`() {
+ val fact = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))))
+ val rule = Rule(
+ Structure(Atom("likes_food"), listOf(Variable("Person"))),
+ Structure(Atom("likes"), listOf(Variable("Person"), Atom("pizza")))
+ )
+
+ val goal = Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza")))
+
+ Program.load(listOf(fact, rule))
+
+ val result = Program.query(goal).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("alice"), subs[Variable("X")], "Expected Person to be alice")
+ }
+
+ @Test
+ fun `requires querying exact`() {
+ val fact1 = Fact(Atom("a"))
+ val fact2 = Fact(Atom("b"))
+ val rule1 = Rule(
+ Atom("c"),
+ Conjunction(
+ Atom("a"),
+ Atom("b")
+ )
+ )
+
+ Program.load(listOf(fact1, fact2, rule1))
+
+ val result = Program.query(Atom("c")).toList()
+
+ assertEquals(1, result.size, "Expected 1 result")
+ }
+
+ @Test
+ fun `requires querying with variable`() {
+ val fact1 = Fact(Atom("a"))
+ val fact2 = Fact(Atom("b"))
+ val rule1 = Rule(
+ Structure(Atom("has fact"), listOf(Variable("X"))),
+ Variable("X")
+ )
+
+ Program.load(listOf(fact1, fact2, rule1))
+
+ val result = Program.query(Structure(Atom("has fact"), listOf(Atom("a")))).toList()
+
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs = result[0].getOrNull()!!
+ assertEquals(0, subs.size, "Expected no substitutions")
+ }
+
+ @Nested
+ class `requires querying with filled variable` {
+ @BeforeEach
+ fun setup() {
+ val fact1 = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))))
+ val fact2 = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pasta"))))
+ val fact3 = Fact(Structure(Atom("likes"), listOf(Atom("bob"), Atom("pasta"))))
+ val rule1 = Rule(
+ Structure(Atom("likes_italian_food"), listOf(Variable("Person"))),
+ Disjunction(
+ Structure(Atom("likes"), listOf(Variable("Person"), Atom("pizza"))),
+ Structure(Atom("likes"), listOf(Variable("Person"), Atom("pasta")))
+ )
+ )
+
+ Program.reset()
+ Program.load(listOf(fact1, fact2, fact3, rule1))
+ }
+
+ @Test
+ fun `likes_italian_food(alice)`() {
+ val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Atom("alice")))).toList()
+
+ assertEquals(2, result.size, "Expected 2 results")
+
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs1 = result[0].getOrNull()!!
+ assertEquals(0, subs1.size, "Expected no substitutions")
+
+ assertTrue(result[1].isSuccess, "Expected success")
+ val subs2 = result[1].getOrNull()!!
+ assertEquals(0, subs2.size, "Expected no substitutions")
+ }
+
+ @Test
+ fun `likes_italian_food(X)`() {
+ val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Variable("X")))).toList()
+
+ assertEquals(3, result.size, "Expected 3 results")
+
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs3 = result[0].getOrNull()!!
+ assertEquals(1, subs3.size, "Expected 1 substitution, especially without 'Person'")
+ assertEquals(Atom("alice"), subs3[Variable("X")], "Expected alice")
+
+ assertTrue(result[1].isSuccess, "Expected success")
+ val subs4 = result[1].getOrNull()!!
+ assertEquals(1, subs4.size, "Expected 1 substitution, especially without 'Person'")
+ assertEquals(Atom("alice"), subs4[Variable("X")], "Expected alice")
+
+ assertTrue(result[2].isSuccess, "Expected success")
+ val subs5 = result[2].getOrNull()!!
+ assertEquals(1, subs5.size, "Expected 1 substitution, especially without 'Person'")
+ assertEquals(Atom("bob"), subs5[Variable("X")], "Expected bob")
+ }
+
+ @Test
+ fun `likes_italian_food(Person)`() {
+ val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Variable("Person")))).toList()
+
+ assertEquals(3, result.size, "Expected 3 results")
+
+ assertTrue(result[0].isSuccess, "Expected success")
+ val subs3 = result[0].getOrNull()!!
+ assertEquals(1, subs3.size, "Expected 1 substitution")
+ assertEquals(Atom("alice"), subs3[Variable("Person")], "Expected alice")
+
+ assertTrue(result[1].isSuccess, "Expected success")
+ val subs4 = result[1].getOrNull()!!
+ assertEquals(1, subs4.size, "Expected 1 substitution")
+ assertEquals(Atom("alice"), subs4[Variable("Person")], "Expected alice")
+
+ assertTrue(result[2].isSuccess, "Expected success")
+ val subs5 = result[2].getOrNull()!!
+ assertEquals(1, subs5.size, "Expected 1 substitution")
+ assertEquals(Atom("bob"), subs5[Variable("Person")], "Expected bob")
+ }
+ }
+}
diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt
index 15ad926..bb6098f 100644
--- a/tests/prolog/builtins/ControlOperatorsTests.kt
+++ b/tests/prolog/builtins/ControlOperatorsTests.kt
@@ -3,7 +3,7 @@ package prolog.builtins
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import prolog.Program
+import prolog.ast.Database.Program
import prolog.ast.logic.Fact
import prolog.ast.logic.Rule
import prolog.ast.terms.Atom
@@ -14,7 +14,25 @@ import prolog.ast.terms.Variable
class ControlOperatorsTests {
@BeforeEach
fun setUp() {
- Program.clear()
+ Program.reset()
+ }
+
+ @Test
+ fun `rule with cut as body`() {
+ Program.load(
+ listOf(
+ Rule(Atom("foo"), Cut()),
+ Fact(Atom("foo"))
+ )
+ )
+
+ val goal = Atom("foo")
+
+ val result = Program.query(goal).toList()
+
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].isSuccess, "Expected success")
+ assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected empty substitutions")
}
// See also: https://stackoverflow.com/a/23292126
@@ -37,7 +55,7 @@ class ControlOperatorsTests {
// Now with cut
- Program.clear()
+ Program.reset()
Program.load(
listOf(
@@ -86,7 +104,7 @@ class ControlOperatorsTests {
// Now with cut in the middle
- Program.clear()
+ Program.reset()
Program.load(
listOf(
@@ -120,7 +138,7 @@ class ControlOperatorsTests {
// Now with cut at the end
- Program.clear()
+ Program.reset()
Program.load(
listOf(
diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt
new file mode 100644
index 0000000..8e96085
--- /dev/null
+++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt
@@ -0,0 +1,295 @@
+package prolog.builtins
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Nested
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import prolog.ast.Database
+import prolog.ast.Database.Program
+import prolog.ast.logic.Clause
+import prolog.ast.logic.Fact
+import prolog.ast.logic.Predicate
+import prolog.ast.logic.Rule
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.terms.Variable
+
+class DatabaseOperatorsTests {
+ @BeforeEach
+ fun setup() {
+ Program.reset()
+ }
+
+ abstract class AssertTestsBase {
+ protected abstract fun createAssert(clause: Clause): Structure
+
+ @BeforeEach
+ fun setup() {
+ Program.reset()
+ }
+
+ @ParameterizedTest
+ @ValueSource(classes = [AssertA::class, AssertZ::class, Assert::class])
+ fun `assert(fact atom)`(assertKind: Class<*>) {
+ val fact = Fact(Atom("a"))
+ createAssert(fact).satisfy(emptyMap())
+
+ assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
+ assertEquals(fact, Program.db.predicates["a/_"]!!.clauses[0])
+ }
+
+ @Test
+ fun `assert(fact structure)`() {
+ val fact = Fact(Structure(Atom("a"), listOf(Atom("b"))))
+ createAssert(fact).satisfy(emptyMap())
+
+ assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
+ assertEquals(fact, Program.db.predicates["a/1"]!!.clauses[0])
+ }
+
+ @Test
+ fun `assert(rule)`() {
+ val rule = Rule(
+ Structure(Atom("a"), listOf(Atom("b"))),
+ Atom("c")
+ )
+ createAssert(rule).satisfy(emptyMap())
+
+ assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
+ assertEquals(rule, Program.db.predicates["a/1"]!!.clauses[0])
+ }
+ }
+
+ @Nested
+ class AssertTests : AssertTestsBase() {
+ override fun createAssert(clause: Clause): Structure {
+ return Assert(clause)
+ }
+ }
+
+ @Nested
+ class AssertATests : AssertTestsBase() {
+ override fun createAssert(clause: Clause): Structure {
+ return AssertA(clause)
+ }
+
+ @Test
+ fun `asserta adds to the beginning`() {
+ val rule1 = Rule(
+ Structure(Atom("a"), listOf(Atom("b"))),
+ Atom("c")
+ )
+ val rule2 = Rule(
+ Structure(Atom("a"), listOf(Atom("d"))),
+ Atom("e")
+ )
+ AssertA(rule1).satisfy(emptyMap())
+ AssertA(rule2).satisfy(emptyMap())
+
+ assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
+ assertEquals(rule2, Program.db.predicates["a/1"]!!.clauses[0])
+ assertEquals(rule1, Program.db.predicates["a/1"]!!.clauses[1])
+ }
+ }
+
+ @Nested
+ class AssertZTests : AssertTestsBase() {
+ override fun createAssert(clause: Clause): Structure {
+ return AssertZ(clause)
+ }
+
+ @Test
+ fun `assertz adds to the end`() {
+ val rule1 = Rule(
+ Structure(Atom("a"), listOf(Atom("b"))),
+ Atom("c")
+ )
+ val rule2 = Rule(
+ Structure(Atom("a"), listOf(Atom("d"))),
+ Atom("e")
+ )
+ AssertZ(rule1).satisfy(emptyMap())
+ AssertZ(rule2).satisfy(emptyMap())
+
+ assertEquals(1, Program.db.predicates.size, "Expected 1 predicate")
+ assertEquals(rule1, Program.db.predicates["a/1"]!!.clauses[0])
+ assertEquals(rule2, Program.db.predicates["a/1"]!!.clauses[1])
+ }
+ }
+
+ @Test
+ 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.db.load(predicate)
+
+ assertEquals(1, Program.query(Atom("a")).count())
+
+ val retract = Retract(Atom("a"))
+
+ assertEquals(1, retract.satisfy(emptyMap()).toList().size, "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.db.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()).iterator()
+
+ assertEquals(3, predicate.clauses.size, "Expected 3 clauses")
+
+ assertTrue(result.hasNext(), "Expected more results")
+
+ val answer = result.next()
+
+ assertTrue(answer.isSuccess, "Expected success")
+ assertTrue(answer.getOrNull()!!.isEmpty(), "Expected no substitutions")
+
+ assertTrue(result.hasNext(), "Expected more results")
+ assertEquals(2, predicate.clauses.size, "Expected 2 clauses")
+
+ assertTrue(result.next().isSuccess)
+ assertTrue(result.hasNext(), "Expected more results")
+ assertTrue(result.next().isSuccess)
+
+ assertFalse(result.hasNext(), "Expected more results")
+ 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.db.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()).iterator()
+
+ assertEquals(3, predicate.clauses.size, "Expected 3 clauses")
+
+ assertTrue(result.hasNext(), "Expected more results")
+ var answer = result.next()
+
+ 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")
+ assertTrue(result.hasNext(), "Expected more results")
+ assertEquals(2, predicate.clauses.size, "Expected 2 clauses")
+
+ answer = result.next()
+
+ 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")
+ assertTrue(result.hasNext(), "Expected more results")
+ assertEquals(1, predicate.clauses.size, "Expected 1 clause")
+
+ answer = result.next()
+
+ 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")
+ assertFalse(result.hasNext(), "Expected no more results")
+ assertEquals(0, predicate.clauses.size, "Expected no clauses")
+ }
+
+ @Test
+ fun `custom assert example`() {
+ var query = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza")))
+
+ var result = Program.query(query).toList()
+ assertEquals(0, result.size, "Expected 0 results")
+
+ var assert: Structure = Assert(Fact(query))
+ assert.satisfy(emptyMap())
+
+ result = Program.query(query).toList()
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].getOrNull()!!.isEmpty())
+
+ assert = AssertZ(Fact(Structure(Atom("likes"), listOf(Atom("bob"), Atom("sushi")))))
+ assert.satisfy(emptyMap())
+
+ query = Structure(Atom("likes"), listOf(Atom("bob"), Variable("X")))
+
+ result = Program.query(query).toList()
+ assertEquals(1, result.size, "Expected 1 result")
+ assertTrue(result[0].isSuccess, "Expected success")
+ assertEquals(Atom("sushi"), result[0].getOrNull()!![Variable("X")], "Expected sushi")
+
+ query = Structure(Atom("likes"), listOf(Variable("X"), Variable("Y")))
+
+ result = Program.query(query).toList()
+ assertEquals(2, result.size, "Expected 2 results")
+ assertTrue(result[0].isSuccess, "Expected success")
+ var result0 = result[0].getOrNull()!!
+ assertEquals(Atom("alice"), result0[Variable("X")], "Expected alice")
+ assertEquals(Atom("pizza"), result0[Variable("Y")], "Expected pizza")
+ assertTrue(result[1].isSuccess, "Expected success")
+ var result1 = result[1].getOrNull()!!
+ assertEquals(Atom("bob"), result1[Variable("X")], "Expected bob")
+ assertEquals(Atom("sushi"), result1[Variable("Y")], "Expected sushi")
+
+ assert = AssertA(
+ Rule(
+ Structure(Atom("likes"), listOf(Variable("X"), Atom("italian"))),
+ Structure(Atom("likes"), listOf(Variable("X"), Atom("pizza")))
+ )
+ )
+ assert.satisfy(emptyMap())
+
+ result = Program.query(query).toList()
+ assertEquals(3, result.size, "Expected 3 results")
+ assertTrue(result[0].isSuccess, "Expected success")
+ result0 = result[0].getOrNull()!!
+ assertEquals(Atom("alice"), result0[Variable("X")], "Expected alice")
+ assertEquals(Atom("italian"), result0[Variable("Y")], "Expected italian")
+ assertTrue(result[1].isSuccess, "Expected success")
+ result1 = result[1].getOrNull()!!
+ assertEquals(Atom("alice"), result1[Variable("X")], "Expected alice")
+ assertEquals(Atom("pizza"), result1[Variable("Y")], "Expected pizza")
+ assertTrue(result[2].isSuccess, "Expected success")
+ val result2 = result[2].getOrNull()!!
+ assertEquals(Atom("bob"), result2[Variable("X")], "Expected bob")
+ assertEquals(Atom("sushi"), result2[Variable("Y")], "Expected sushi")
+ }
+}
\ No newline at end of file
diff --git a/tests/prolog/builtins/IoOperatorsTests.kt b/tests/prolog/builtins/IoOperatorsTests.kt
new file mode 100644
index 0000000..05607ec
--- /dev/null
+++ b/tests/prolog/builtins/IoOperatorsTests.kt
@@ -0,0 +1,164 @@
+package prolog.builtins
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+import prolog.ast.arithmetic.Float
+import prolog.ast.arithmetic.Integer
+import prolog.ast.terms.Atom
+import prolog.ast.terms.CompoundTerm
+import prolog.ast.terms.Variable
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.io.PrintStream
+
+class IoOperatorsTests {
+ private var outStream = ByteArrayOutputStream()
+
+ @BeforeEach
+ fun setup() {
+ outStream = ByteArrayOutputStream()
+ System.setOut(PrintStream(outStream))
+ }
+
+ @Test
+ fun dummyTest() {
+ val message = "Hello, World!"
+ print(message)
+ assertEquals(message, outStream.toString(), "Output should match the message")
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = [
+ "a",
+ "hello",
+ "a very special christmas",
+ "1 2 3 piano"
+ ])
+ fun `write atoms`(name: String) {
+ val write = Write(Atom(name))
+
+ val result = write.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+ assertEquals(name, outStream.toString(), "Output should match the atom")
+ }
+
+ @Test
+ fun `write structure`() {
+ val write = Write(CompoundTerm(Atom("person"), listOf(Atom("john"), Atom("doe"))))
+
+ val result = write.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+ assertEquals("person(john, doe)", outStream.toString().trim(), "Output should match the structure")
+ }
+
+ @Test
+ fun `write arithmetic`() {
+ val a = Integer(1)
+ val b = Variable("B")
+ val c = Float(2.0f)
+ val d = Variable("D")
+
+ val mul = Multiply(c, d)
+ val sub = Subtract(b, mul)
+ val expr = EvaluatesTo(a, sub)
+
+ val expected1 = "(1 =:= (B - (2.0 * D)))"
+ val expected2 = "=:=(1, -(B, *(2.0, D)))"
+
+ val write = Write(expr)
+
+ val result = write.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+
+ val output = outStream.toString()
+ assertTrue(output == expected1 || output == expected2, "Output should match the arithmetic expression")
+ }
+
+ @Test
+ fun `write nl`() {
+ val nl = Nl
+
+ val result = nl.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+ assertTrue(outStream.toString().contains("\n"), "Output should contain a newline")
+ }
+
+ @Test
+ fun `read term`() {
+ val inputStream = ByteArrayInputStream("hello.".toByteArray())
+ System.setIn(inputStream)
+
+ val read = Read(Variable("X"))
+
+ val result = read.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+ val answer = result[0].getOrNull()!!
+ assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
+ assertInstanceOf(Atom::class.java, answer[Variable("X")], "Output should be an atom")
+ assertEquals(Atom("hello"), answer[Variable("X")], "Output should match the read term")
+ }
+
+ @Test
+ fun `read between(1, 2, 3)`() {
+ val inputStream = ByteArrayInputStream("between(1, 2, 3).".toByteArray())
+ System.setIn(inputStream)
+
+ val read = Read(Variable("X"))
+
+ val result = read.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+ val answer = result[0].getOrNull()!!
+ assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
+ assertInstanceOf(CompoundTerm::class.java, answer[Variable("X")], "Output should be a compound term")
+ assertEquals(
+ CompoundTerm(Atom("between"), listOf(Integer(1), Integer(2), Integer(3))),
+ answer[Variable("X")],
+ "Output should match the read term"
+ )
+ }
+
+ @Test
+ fun `read foo(a, X, b, Y, c, Z)`() {
+ val inputStream = ByteArrayInputStream("foo(A, x, B, y, C, z).".toByteArray())
+ System.setIn(inputStream)
+
+ val read = Read(CompoundTerm(Atom("foo"), listOf(
+ Atom("a"),
+ Variable("X"),
+ Atom("b"),
+ Variable("Y"),
+ Atom("c"),
+ Variable("Z")
+ )))
+
+ val result = read.satisfy(emptyMap()).toList()
+
+ assertEquals(1, result.size, "Should return one result")
+ assertTrue(result[0].isSuccess, "Result should be successful")
+
+ val answer = result[0].getOrNull()!!
+
+ assertTrue(answer.containsKey(Variable("X")), "Result should be successful")
+ assertTrue(answer.containsKey(Variable("Y")), "Result should be successful")
+ assertTrue(answer.containsKey(Variable("Z")), "Result should be successful")
+
+ assertEquals(Atom("x"), answer[Variable("X")], "Output should match the read term")
+ assertEquals(Atom("y"), answer[Variable("Y")], "Output should match the read term")
+ assertEquals(Atom("z"), answer[Variable("Z")], "Output should match the read term")
+ }
+}
\ No newline at end of file
diff --git a/tests/prolog/logic/ArithmeticTests.kt b/tests/prolog/logic/ArithmeticTests.kt
index 83fa8c7..ac532f3 100644
--- a/tests/prolog/logic/ArithmeticTests.kt
+++ b/tests/prolog/logic/ArithmeticTests.kt
@@ -583,10 +583,23 @@ class ArithmeticTests {
assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Float(6.0f), result[0].getOrNull()!!), "X should be equal to 6.0")
}
+ @Test
+ fun `addition with negative`() {
+ val t1 = Integer(1)
+ val t2 = Integer(-1)
+ val t3 = Integer(0)
+
+ val result = plus(t1, t2, t3, emptyMap()).toList()
+
+ assertEquals(1, result.size, "There should be one solution")
+ assertTrue(result[0].isSuccess, "Expected success")
+ assertTrue(result[0].getOrNull()!!.isEmpty(), "1 + -1 should already be equal to 0")
+ }
+
@RepeatedTest(100)
fun `random test for mul`() {
- val t1 = Integer((0..1000).random())
- val t2 = Integer((0..1000).random())
+ val t1 = Integer((-1000..1000).random())
+ val t2 = Integer((-1000..1000).random())
val t3 = Variable("X")
val result = mul(t1, t2, t3, emptyMap()).toList()
diff --git a/tests/prolog/logic/TermsTests.kt b/tests/prolog/logic/TermsTests.kt
new file mode 100644
index 0000000..dde66e6
--- /dev/null
+++ b/tests/prolog/logic/TermsTests.kt
@@ -0,0 +1,110 @@
+package prolog.logic
+
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertTrue
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.terms.Variable
+
+class TermsTests {
+ @Test
+ fun `rename vars in atom`() {
+ val term = Atom("a")
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start, end, "Expected end to still be at start")
+ assertTrue(subs.isEmpty(), "Expected no substitutions")
+ }
+
+ @Test
+ fun `rename vars in var`() {
+ val term = Variable("X")
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start + 1, end, "Expected end to be incremented by 1")
+ assertEquals(1, subs.size, "Expected one substitution")
+ assertTrue(subs.containsKey(term), "Expected subs to contain the original term")
+ assertEquals(Variable("X@$start"), subs[term], "Expected subs to contain the new term")
+ }
+
+ @Test
+ fun `rename vars in compound term without vars`() {
+ val term = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start, end, "Expected end to still be at start")
+ assertTrue(subs.isEmpty(), "Expected no substitutions")
+ }
+
+ @Test
+ fun `rename vars in compound term`() {
+ val term = Structure(Atom("f"), listOf(Variable("X"), Variable("Y")))
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start + 2, end, "Expected end to be incremented by 2")
+ assertEquals(2, subs.size, "Expected two substitutions")
+ assertTrue(subs.containsKey(term.arguments[0]), "Expected subs to contain the first original term")
+ assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term")
+ assertTrue(subs.containsKey(term.arguments[1]), "Expected subs to contain the second original term")
+ assertEquals(Variable("Y@${start + 1}"), subs[term.arguments[1]], "Expected subs to contain the new term")
+ }
+
+ @Test
+ fun `renaming identical vars should keep the same name`() {
+ val term = Structure(Atom("f"), listOf(Variable("X"), Variable("X")))
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start + 1, end, "Expected end to be incremented by 1")
+ assertEquals(1, subs.size, "Expected one substitution")
+ assertTrue(subs.containsKey(term.arguments[0]), "Expected subs to contain the first original term")
+ assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term")
+ assertTrue(subs.containsKey(term.arguments[1]), "Expected subs to contain the second original term")
+ assertEquals(Variable("X@$start"), subs[term.arguments[1]], "Expected subs to contain the new term")
+ }
+
+ @Test
+ fun `renaming identical vars should keep the same name in nested terms`() {
+ val term = Structure(Atom("f"), listOf(Variable("X"), Structure(Atom("g"), listOf(Variable("X")))))
+ val start = 0
+
+ val (end, subs) = numbervars(term, start)
+
+ assertEquals(start + 1, end, "Expected end to be incremented by 1")
+ assertEquals(1, subs.size, "Expected one substitution")
+ assertTrue(subs.containsKey(Variable("X")), "Expected subs to contain the variable")
+ assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term")
+ }
+
+ @Test
+ fun `renaming multiple times`() {
+ val variable = Variable("X")
+ val term = Structure(Atom("f"), listOf(variable))
+ val start = 0
+
+ val (end1, subs1) = numbervars(term, start, emptyMap())
+
+ assertEquals(start + 1, end1, "Expected end to be incremented by 1")
+ assertEquals(1, subs1.size, "Expected one substitution")
+ assertTrue(subs1.containsKey(variable), "Expected subs to contain the variable")
+ assertEquals(Variable("X@$start"), subs1[variable], "Expected subs to contain the new term")
+ val variable1 = subs1[variable] as Variable
+
+ val (end2, subs2) = numbervars(term, end1, subs1)
+
+ assertEquals(start + 2, end2, "Expected end to be incremented by 2")
+ assertEquals(1, subs2.size, "Expected one substitution")
+ assertTrue(subs2.containsKey(variable1), "Expected subs to contain the variable")
+ assertEquals(Variable("X@$start@$end1"), subs2[variable1], "Expected subs to contain the new term")
+ }
+}
\ No newline at end of file
diff --git a/tests/prolog/logic/UnificationTests.kt b/tests/prolog/logic/UnificationTests.kt
new file mode 100644
index 0000000..10a37e7
--- /dev/null
+++ b/tests/prolog/logic/UnificationTests.kt
@@ -0,0 +1,370 @@
+package prolog.logic
+
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import prolog.Substitutions
+import prolog.ast.arithmetic.Integer
+import prolog.ast.terms.Atom
+import prolog.ast.terms.Structure
+import prolog.ast.terms.Variable
+import prolog.builtins.Add
+import org.junit.jupiter.api.Disabled
+import org.junit.jupiter.api.Nested
+
+/*
+ * Based on: https://en.wikipedia.org/wiki/Unification_%28computer_science%29#Examples_of_syntactic_unification_of_first-order_terms
+ */
+class UnificationTests {
+ @Nested
+ class `unify` {
+ @Test
+ fun identical_atoms_unify() {
+ val atom1 = Atom("a")
+ val atom2 = Atom("a")
+
+ val result = unify(atom1, atom2)
+
+ assertTrue(result.isSuccess, "Identical atoms should unify")
+ assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
+ }
+
+ @Test
+ fun different_atoms_do_not_unify() {
+ val atom1 = Atom("a")
+ val atom2 = Atom("b")
+
+ val result = unify(atom1, atom2)
+
+ assertFalse(result.isSuccess, "Different atoms should not unify")
+ }
+
+ /**
+ * ?- X = X.
+ * true.
+ */
+ @Test
+ fun identical_variables_unify() {
+ val variable1 = Variable("X")
+ val variable2 = Variable("X")
+
+ val result = unify(variable1, variable2)
+
+ assertTrue(result.isSuccess, "Identical variables should unify")
+ assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
+ }
+
+ @Test
+ fun variable_unifies_with_atom() {
+ val variable = Variable("X")
+ val atom = Atom("a")
+
+ val result = unify(atom, variable)
+
+ assertTrue(result.isSuccess, "Variable should unify with atom")
+ assertEquals(1, result.getOrNull()!!.size, "There should be one substitution")
+ assertEquals(atom, result.getOrNull()!![variable], "Variable should be substituted with atom")
+ }
+
+ @Test
+ fun variables_alias_when_unified() {
+ val variable1 = Variable("X")
+ val variable2 = Variable("Y")
+
+ val result = unify(variable1, variable2)
+
+ assertTrue(result.isSuccess)
+ assertEquals(1, result.getOrNull()!!.size)
+ assertEquals(variable2, result.getOrNull()!![variable1], "Variable 1 should alias to variable 2")
+ }
+
+ @Test
+ fun identical_compound_terms_unify() {
+ val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+ val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+
+ val result = unify(structure1, structure2)
+
+ assertTrue(result.isSuccess, "Identical compound terms should unify")
+ assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
+ }
+
+ @Test
+ fun compound_terms_with_different_arguments_do_not_unify() {
+ val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+ val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("c")))
+
+ val result = unify(structure1, structure2)
+
+ assertFalse(result.isSuccess, "Different compound terms should not unify")
+ }
+
+ @Test
+ fun compound_terms_with_different_functors_do_not_unify() {
+ val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+ val structure2 = Structure(Atom("g"), listOf(Atom("a"), Atom("b")))
+
+ val result = unify(structure1, structure2)
+
+ assertFalse(result.isSuccess, "Compound terms with different functors should not unify")
+ }
+
+ /**
+ * ?- X = f(a, b).
+ * X = f(a, b).
+ */
+ @Test
+ fun variable_unifies_with_compound_term() {
+ val variable = Variable("X")
+ val structure = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+
+ val result = unify(variable, structure)
+
+ assertTrue(result.isSuccess, "Variable should unify with compound term")
+
+ val subs = result.getOrNull()!!
+
+ assertEquals(1, subs.size, "There should be one substitution")
+ assertTrue(subs.containsKey(variable), "Variable should be in the substitution map")
+ assertTrue(
+ equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), subs[variable]!!, subs),
+ "Variable should be substituted with compound term"
+ )
+ }
+
+ @Test
+ fun compound_term_with_variable_unifies_with_part() {
+ val variable = Variable("X")
+ val structure1 = Structure(Atom("f"), listOf(Atom("a"), variable))
+ val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
+
+ val result = unify(structure1, structure2)
+
+ assertTrue(result.isSuccess, "Compound term with variable should unify with part")
+
+ val subs = result.getOrNull()!!
+
+ assertEquals(1, subs.size, "There should be one substitution")
+ assertTrue(subs.containsKey(variable), "Variable should be in the substitution map")
+ val equivalence = equivalent(Atom("b"), subs[variable]!!, subs)
+ assertTrue(equivalence, "Variable should be substituted with atom")
+ }
+
+ @Test
+ fun compound_terms_with_variable_arguments_lists_alias_variables() {
+ val variable1 = Variable("X")
+ val variable2 = Variable("Y")
+
+ val structure1 = Structure(Atom("f"), listOf(variable1))
+ val structure2 = Structure(Atom("f"), listOf(variable2))
+
+ val result = unify(structure1, structure2)
+
+ assertTrue(result.isSuccess, "Compound terms with variable arguments should unify")
+
+ val subs = result.getOrNull()!!
+
+ assertEquals(1, subs.size, "There should be one substitution")
+ assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map")
+ assertEquals(variable2, subs[variable1], "Variable 1 should alias to variable 2")
+ }
+
+ /**
+ * f(X) = f(Y, Z)
+ */
+ @Test
+ fun compound_terms_with_different_arity_do_not_unify() {
+ val structure1 = Structure(Atom("f"), listOf(Variable("X")))
+ val structure2 = Structure(Atom("f"), listOf(Variable("Y"), Variable("Z")))
+
+ val result = unify(structure1, structure2)
+
+ assertFalse(result.isSuccess, "Compound terms with different arity should not unify")
+ }
+
+ /**
+ * ?- f(g(X)) = f(Y).
+ * Y = g(X).
+ */
+ @Test
+ fun nested_compound_terms_with_variables_unify() {
+ val variable2 = Variable("Y")
+
+ val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(Variable("X")))))
+ val structure2 = Structure(Atom("f"), listOf(variable2))
+
+ val result = unify(structure1, structure2)
+
+ assertTrue(result.isSuccess, "Nested compound terms with variables should unify")
+
+ val subs = result.getOrNull()!!
+
+ assertEquals(1, subs.size, "There should be one substitution")
+ assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map")
+ assertTrue(
+ equivalent(Structure(Atom("g"), listOf(Variable("X"))), subs[variable2]!!, subs),
+ "Variable should be substituted with compound term"
+ )
+ }
+
+ /**
+ * ?- f(g(X), X) = f(Y, a).
+ * X = a,
+ * Y = g(a).
+ */
+ @Test
+ fun compound_terms_with_more_variables() {
+ val variable1 = Variable("X")
+ val variable2 = Variable("Y")
+
+ val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(variable1)), variable1))
+ val structure2 = Structure(Atom("f"), listOf(variable2, Atom("a")))
+
+ val result = unify(structure1, structure2)
+
+ assertTrue(result.isSuccess, "Compound terms with more variables should unify")
+
+ val subs = result.getOrNull()!!
+
+ assertEquals(2, subs.size, "There should be two substitutions")
+ assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map")
+ assertTrue(
+ equivalent(Atom("a"), subs[variable1]!!, subs),
+ "Variable 1 should be substituted with atom"
+ )
+ assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map")
+ assertTrue(
+ equivalent(Structure(Atom("g"), listOf(Atom("a"))), subs[variable2]!!, subs),
+ "Variable 2 should be substituted with compound term"
+
+ )
+ }
+
+ /**
+ * ?- X = f(X).
+ * X = f(f(X)).
+ */
+ @Test
+ @Disabled("If the occurs check is applied, this should fail")
+ fun recursive_unification() {
+ val variable1 = Variable("X")
+ val structure2 = Structure(Atom("f"), listOf(Variable("X")))
+
+ val result = unifyLazy(variable1, structure2, emptyMap()).toList()
+
+ assertEquals(1, result.size, "There should be one result")
+ assertTrue(result[0].isSuccess, "Recursive unification should succeed")
+
+ val subs = result[0].getOrNull()!!
+
+ assertEquals(1, subs.size, "There should be one substitution")
+ assertTrue(subs.containsKey(variable1), "Variable should be in the substitution map")
+ assertEquals(structure2, subs[variable1], "Variable should be substituted with compound term")
+ }
+
+ /**
+ * ?- X = bar, Y = bar, X = Y.
+ * X = Y, Y = bar.
+ */
+ @Test
+ fun multiple_unification() {
+ val variable1 = Variable("X")
+ val variable2 = Variable("Y")
+ val atom = Atom("bar")
+
+ val map: Substitutions = mapOf(
+ variable1 to atom,
+ variable2 to atom
+ )
+ val result = unifyLazy(variable1, variable2, map).toList()
+
+ assertEquals(1, result.size, "There should be one substitution")
+ assertTrue(result[0].isSuccess, "Multiple unification should succeed")
+ assertEquals(0, result[0].getOrNull()!!.size, "No (additional) substitutions should be made")
+ }
+
+ /**
+ * ?- a = a().
+ * false.
+ */
+ @Test
+ fun atom_with_different_arity() {
+ val atom1 = Atom("a")
+ val structure2 = Structure(Atom("a"), emptyList())
+
+ val result = unify(atom1, structure2)
+
+ assertFalse(result.isSuccess, "Atom with different arity should not unify")
+ }
+
+ @Test
+ fun identical_integers_unify() {
+ val int1 = Integer(1)
+ val int2 = Integer(1)
+
+ val result = unify(int1, int2)
+
+ assertTrue(result.isSuccess, "Identical integers should unify")
+ assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
+ }
+
+ @Test
+ fun different_integers_do_not_unify() {
+ val int1 = Integer(1)
+ val int2 = Integer(2)
+
+ val result = unify(int1, int2)
+
+ assertFalse(result.isSuccess, "Different integers should not unify")
+ }
+
+
+ @Test
+ fun `1 + 2 does not unify with 3`() {
+ val expr1 = Add(Integer(1), Integer(2))
+ val expr2 = Integer(3)
+
+ val result = unify(expr1, expr2)
+
+ assertFalse(result.isSuccess, "1 + 2 should not unify with 3")
+ }
+ }
+
+ @Nested
+ class `applySubstitution` {
+ @Test
+ fun `apply substitution without sub`() {
+ val term = Variable("X")
+ val subs: Substitutions = emptyMap()
+
+ val result = applySubstitution(term, subs)
+
+ assertEquals(term, result)
+ }
+
+ @Test
+ fun `apply single substitution`() {
+ val sub = Variable("X") to Integer(5)
+ val subs: Substitutions = mapOf(sub)
+
+ val term = Variable("X")
+
+ val result = applySubstitution(term, subs)
+
+ assertEquals(Integer(5), result)
+ }
+
+ @Test
+ fun `apply chained substitution`() {
+ val sub1 = Variable("HP") to Variable("HP(19)")
+ val sub2 = Variable("HP(19)") to Integer(35)
+
+ val subs: Substitutions = mapOf(sub1, sub2)
+
+ val term = Variable("HP")
+
+ val result = applySubstitution(term, subs)
+
+ assertEquals(Integer(35), result)
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/prolog/logic/UnifyTests.kt b/tests/prolog/logic/UnifyTests.kt
deleted file mode 100644
index f2b3c57..0000000
--- a/tests/prolog/logic/UnifyTests.kt
+++ /dev/null
@@ -1,327 +0,0 @@
-package prolog.logic
-
-import org.junit.jupiter.api.Assertions.*
-import org.junit.jupiter.api.Disabled
-import org.junit.jupiter.api.Test
-import prolog.Substitutions
-import prolog.ast.arithmetic.Integer
-import prolog.ast.terms.Atom
-import prolog.ast.terms.Structure
-import prolog.ast.terms.Variable
-import prolog.builtins.Add
-
-/*
- * Based on: https://en.wikipedia.org/wiki/Unification_%28computer_science%29#Examples_of_syntactic_unification_of_first-order_terms
- */
-class UnifyTests {
- @Test
- fun identical_atoms_unify() {
- val atom1 = Atom("a")
- val atom2 = Atom("a")
-
- val result = unify(atom1, atom2)
-
- assertTrue(result.isSuccess, "Identical atoms should unify")
- assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
- }
-
- @Test
- fun different_atoms_do_not_unify() {
- val atom1 = Atom("a")
- val atom2 = Atom("b")
-
- val result = unify(atom1, atom2)
-
- assertFalse(result.isSuccess, "Different atoms should not unify")
- }
-
- /**
- * ?- X = X.
- * true.
- */
- @Test
- fun identical_variables_unify() {
- val variable1 = Variable("X")
- val variable2 = Variable("X")
-
- val result = unify(variable1, variable2)
-
- assertTrue(result.isSuccess, "Identical variables should unify")
- assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
- }
-
- @Test
- fun variable_unifies_with_atom() {
- val variable = Variable("X")
- val atom = Atom("a")
-
- val result = unify(atom, variable)
-
- assertTrue(result.isSuccess, "Variable should unify with atom")
- assertEquals(1, result.getOrNull()!!.size, "There should be one substitution")
- assertEquals(atom, result.getOrNull()!![variable], "Variable should be substituted with atom")
- }
-
- @Test
- fun variables_alias_when_unified() {
- val variable1 = Variable("X")
- val variable2 = Variable("Y")
-
- val result = unify(variable1, variable2)
-
- assertTrue(result.isSuccess)
- assertEquals(1, result.getOrNull()!!.size)
- assertEquals(variable2, result.getOrNull()!![variable1], "Variable 1 should alias to variable 2")
- }
-
- @Test
- fun identical_compound_terms_unify() {
- val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
- val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
-
- val result = unify(structure1, structure2)
-
- assertTrue(result.isSuccess, "Identical compound terms should unify")
- assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
- }
-
- @Test
- fun compound_terms_with_different_arguments_do_not_unify() {
- val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
- val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("c")))
-
- val result = unify(structure1, structure2)
-
- assertFalse(result.isSuccess, "Different compound terms should not unify")
- }
-
- @Test
- fun compound_terms_with_different_functors_do_not_unify() {
- val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
- val structure2 = Structure(Atom("g"), listOf(Atom("a"), Atom("b")))
-
- val result = unify(structure1, structure2)
-
- assertFalse(result.isSuccess, "Compound terms with different functors should not unify")
- }
-
- /**
- * ?- X = f(a, b).
- * X = f(a, b).
- */
- @Test
- fun variable_unifies_with_compound_term() {
- val variable = Variable("X")
- val structure = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
-
- val result = unify(variable, structure)
-
- assertTrue(result.isSuccess, "Variable should unify with compound term")
-
- val subs = result.getOrNull()!!
-
- assertEquals(1, subs.size, "There should be one substitution")
- assertTrue(subs.containsKey(variable), "Variable should be in the substitution map")
- assertTrue(
- equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), subs[variable]!!, subs),
- "Variable should be substituted with compound term"
- )
- }
-
- @Test
- fun compound_term_with_variable_unifies_with_part() {
- val variable = Variable("X")
- val structure1 = Structure(Atom("f"), listOf(Atom("a"), variable))
- val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b")))
-
- val result = unify(structure1, structure2)
-
- assertTrue(result.isSuccess, "Compound term with variable should unify with part")
-
- val subs = result.getOrNull()!!
-
- assertEquals(1, subs.size, "There should be one substitution")
- assertTrue(subs.containsKey(variable), "Variable should be in the substitution map")
- val equivalence = equivalent(Atom("b"), subs[variable]!!, subs)
- assertTrue(equivalence, "Variable should be substituted with atom")
- }
-
- @Test
- fun compound_terms_with_variable_arguments_lists_alias_variables() {
- val variable1 = Variable("X")
- val variable2 = Variable("Y")
-
- val structure1 = Structure(Atom("f"), listOf(variable1))
- val structure2 = Structure(Atom("f"), listOf(variable2))
-
- val result = unify(structure1, structure2)
-
- assertTrue(result.isSuccess, "Compound terms with variable arguments should unify")
-
- val subs = result.getOrNull()!!
-
- assertEquals(1, subs.size, "There should be one substitution")
- assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map")
- assertEquals(variable2, subs[variable1], "Variable 1 should alias to variable 2")
- }
-
- /**
- * f(X) = f(Y, Z)
- */
- @Test
- fun compound_terms_with_different_arity_do_not_unify() {
- val structure1 = Structure(Atom("f"), listOf(Variable("X")))
- val structure2 = Structure(Atom("f"), listOf(Variable("Y"), Variable("Z")))
-
- val result = unify(structure1, structure2)
-
- assertFalse(result.isSuccess, "Compound terms with different arity should not unify")
- }
-
- /**
- * ?- f(g(X)) = f(Y).
- * Y = g(X).
- */
- @Test
- fun nested_compound_terms_with_variables_unify() {
- val variable2 = Variable("Y")
-
- val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(Variable("X")))))
- val structure2 = Structure(Atom("f"), listOf(variable2))
-
- val result = unify(structure1, structure2)
-
- assertTrue(result.isSuccess, "Nested compound terms with variables should unify")
-
- val subs = result.getOrNull()!!
-
- assertEquals(1, subs.size, "There should be one substitution")
- assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map")
- assertTrue(
- equivalent(Structure(Atom("g"), listOf(Variable("X"))), subs[variable2]!!, subs),
- "Variable should be substituted with compound term"
- )
- }
-
- /**
- * ?- f(g(X), X) = f(Y, a).
- * X = a,
- * Y = g(a).
- */
- @Test
- fun compound_terms_with_more_variables() {
- val variable1 = Variable("X")
- val variable2 = Variable("Y")
-
- val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(variable1)), variable1))
- val structure2 = Structure(Atom("f"), listOf(variable2, Atom("a")))
-
- val result = unify(structure1, structure2)
-
- assertTrue(result.isSuccess, "Compound terms with more variables should unify")
-
- val subs = result.getOrNull()!!
-
- assertEquals(2, subs.size, "There should be two substitutions")
- assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map")
- assertTrue(
- equivalent(Atom("a"), subs[variable1]!!, subs),
- "Variable 1 should be substituted with atom"
- )
- assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map")
- assertTrue(
- equivalent(Structure(Atom("g"), listOf(Atom("a"))), subs[variable2]!!, subs),
- "Variable 2 should be substituted with compound term"
-
- )
- }
-
- /**
- * ?- X = f(X).
- * X = f(f(X)).
- */
- @Test
- fun recursive_unification() {
- val variable1 = Variable("X")
- val structure2 = Structure(Atom("f"), listOf(Variable("X")))
-
- val result = unifyLazy(variable1, structure2, emptyMap()).toList()
-
- assertEquals(1, result.size, "There should be one result")
- assertTrue(result[0].isSuccess, "Recursive unification should succeed")
-
- val subs = result[0].getOrNull()!!
-
- assertEquals(1, subs.size, "There should be one substitution")
- assertTrue(subs.containsKey(variable1), "Variable should be in the substitution map")
- assertEquals(structure2, subs[variable1], "Variable should be substituted with compound term")
- }
-
- /**
- * ?- X = bar, Y = bar, X = Y.
- * X = Y, Y = bar.
- */
- @Disabled
- @Test
- fun multiple_unification() {
- val variable1 = Variable("X")
- val variable2 = Variable("Y")
- val atom = Atom("bar")
-
- val map: Substitutions = mapOf(
- variable1 to atom,
- variable2 to atom
- )
- val result = unifyLazy(variable1, variable2, map).toList()
-
- assertEquals(1, result.size, "There should be one substitution")
- assertTrue(result[0].isSuccess, "Multiple unification should succeed")
- assertEquals(0, result[0].getOrNull()!!.size, "No (additional) substitutions should be made")
- }
-
- /**
- * ?- a = a().
- * false.
- */
- @Test
- fun atom_with_different_arity() {
- val atom1 = Atom("a")
- val structure2 = Structure(Atom("a"), emptyList())
-
- val result = unify(atom1, structure2)
-
- assertFalse(result.isSuccess, "Atom with different arity should not unify")
- }
-
- @Test
- fun identical_integers_unify() {
- val int1 = Integer(1)
- val int2 = Integer(1)
-
- val result = unify(int1, int2)
-
- assertTrue(result.isSuccess, "Identical integers should unify")
- assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made")
- }
-
- @Test
- fun different_integers_do_not_unify() {
- val int1 = Integer(1)
- val int2 = Integer(2)
-
- val result = unify(int1, int2)
-
- assertFalse(result.isSuccess, "Different integers should not unify")
- }
-
-
- @Test
- fun `1 + 2 does not unify with 3`() {
- val expr1 = Add(Integer(1), Integer(2))
- val expr2 = Integer(3)
-
- val result = unify(expr1, expr2)
-
- assertFalse(result.isSuccess, "1 + 2 should not unify with 3")
- }
-}
\ No newline at end of file