diff --git a/.idea/2025LogProg-PrologInterpreter.iml b/.idea/2025LogProg-PrologInterpreter.iml
deleted file mode 100644
index 42d53f5..0000000
--- a/.idea/2025LogProg-PrologInterpreter.iml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index 5b434ac..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
index cd901a3..a50fab0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -10,8 +10,6 @@ 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")
@@ -28,26 +26,20 @@ sourceSets {
}
}
-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")
+ 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")
+ }
}
}
diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl
index 0ed5d04..357e09b 100644
--- a/examples/scratchpad.pl
+++ b/examples/scratchpad.pl
@@ -1,32 +1 @@
-% 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.
-
+choice(X) :- X = 1, !; X = 2.
diff --git a/src/Debug.kt b/src/Debug.kt
new file mode 100644
index 0000000..ba9e63e
--- /dev/null
+++ b/src/Debug.kt
@@ -0,0 +1,3 @@
+data object Debug {
+ val on: Boolean = true
+}
\ No newline at end of file
diff --git a/src/Main.kt b/src/Main.kt
index 2416fdb..38efc51 100644
--- a/src/Main.kt
+++ b/src/Main.kt
@@ -1,29 +1,107 @@
-import com.xenomachina.argparser.ArgParser
-import interpreter.FileLoader
-import io.GhentPrologArgParser
-import io.Logger
-import repl.Repl
+import better_parser.SimpleReplParser
+import interpreter.SourceFileReader
+import prolog.Answer
+import kotlin.system.exitProcess
-fun main(args: Array) {
- // Parse command line arguments
- val parsedArgs = ArgParser(args).parseInto(::GhentPrologArgParser)
+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 ""
+}
- parsedArgs.run {
- val loader = FileLoader()
+fun say(message: String) {
+ println(message)
+}
- // Set the verbosity level
- Logger.level = verbosity
+fun prompt(message: String): String {
+ print("$message ")
+ var input = readlnOrNull()
- // Check if script was provided
- for (file in script) {
- loader.load(file)
+ while (input.isNullOrBlank()) {
+ if (input == null) {
+ println("Exiting Prolog REPL.")
+ exitProcess(0)
}
- // Check if REPL was requested
- if (repl) {
- Repl()
- } else {
- Logger.warn("REPL not started. Use -r or --repl to start the REPL.")
+ 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}")
}
}
+
}
diff --git a/src/better_parser/PrologParser.kt b/src/better_parser/PrologParser.kt
new file mode 100644
index 0000000..73e63eb
--- /dev/null
+++ b/src/better_parser/PrologParser.kt
@@ -0,0 +1,17 @@
+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
new file mode 100644
index 0000000..0383507
--- /dev/null
+++ b/src/better_parser/PrologSourceParser.kt
@@ -0,0 +1,70 @@
+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
new file mode 100644
index 0000000..1fb49c7
--- /dev/null
+++ b/src/better_parser/SimplePrologParser.kt
@@ -0,0 +1,67 @@
+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
new file mode 100644
index 0000000..0c8a598
--- /dev/null
+++ b/src/better_parser/SimpleReplParser.kt
@@ -0,0 +1,27 @@
+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
new file mode 100644
index 0000000..437f9ed
--- /dev/null
+++ b/src/better_parser/SimpleSourceParser.kt
@@ -0,0 +1,48 @@
+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 714a5a2..c1eecb1 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 fatJar
+ ./gradlew build
if [ "${?}" -ne 0 ]; then
printf 'Error: Build failed\n'
exit 1
diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt
deleted file mode 100644
index 7af15ab..0000000
--- a/src/interpreter/FileLoader.kt
+++ /dev/null
@@ -1,55 +0,0 @@
-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
deleted file mode 100644
index 6c17705..0000000
--- a/src/interpreter/Preprocessor.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-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
new file mode 100644
index 0000000..e3e58cc
--- /dev/null
+++ b/src/interpreter/SourceFileReader.kt
@@ -0,0 +1,23 @@
+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
deleted file mode 100644
index f27f579..0000000
--- a/src/io/GhentPrologArgParser.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index c5706a1..0000000
--- a/src/io/IoHandler.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-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
deleted file mode 100644
index ac9de5a..0000000
--- a/src/io/Logger.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-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
deleted file mode 100644
index 1b9df94..0000000
--- a/src/io/Terminal.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-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
new file mode 100644
index 0000000..c239fbd
--- /dev/null
+++ b/src/lexer/Lexer.kt
@@ -0,0 +1,127 @@
+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
new file mode 100644
index 0000000..c163bfd
--- /dev/null
+++ b/src/lexer/Token.kt
@@ -0,0 +1,9 @@
+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
new file mode 100644
index 0000000..50d5141
--- /dev/null
+++ b/src/lexer/TokenType.kt
@@ -0,0 +1,15 @@
+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
new file mode 100644
index 0000000..7a5f4c9
--- /dev/null
+++ b/src/lexer/errors/LexingError.kt
@@ -0,0 +1,13 @@
+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
new file mode 100644
index 0000000..bff243a
--- /dev/null
+++ b/src/lexer/errors/LexingErrorType.kt
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 0000000..583bf29
--- /dev/null
+++ b/src/lexer/state/LexerPosition.kt
@@ -0,0 +1,3 @@
+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
new file mode 100644
index 0000000..2f19f76
--- /dev/null
+++ b/src/lexer/state/TokenPosition.kt
@@ -0,0 +1,3 @@
+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 e8c7383..e2e63e8 100644
--- a/src/parser/Parser.kt
+++ b/src/parser/Parser.kt
@@ -1,11 +1,137 @@
package parser
-interface 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
+ }
+
/**
- * Parses the input string and returns the parsed result.
+ * Matches the current token with any of the expected types.
+ * If it matches, it consumes the token and returns true.
*
- * @param input The input string to parse.
- * @return The parsed result, which is the AST of the input.
+ * @param types The list of expected token types.
+ * @return True if the current token matches any of the expected types, false otherwise.
*/
- fun parse(input: String): Any
-}
\ No newline at end of file
+ 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
+ }
+}
diff --git a/src/parser/ReplParser.kt b/src/parser/ReplParser.kt
deleted file mode 100644
index a39fae8..0000000
--- a/src/parser/ReplParser.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-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
deleted file mode 100644
index f95731c..0000000
--- a/src/parser/ScriptParser.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-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
new file mode 100644
index 0000000..7ddbfc2
--- /dev/null
+++ b/src/parser/errors/ParsingError.kt
@@ -0,0 +1,12 @@
+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
new file mode 100644
index 0000000..5e017d8
--- /dev/null
+++ b/src/parser/errors/ParsingErrorType.kt
@@ -0,0 +1,7 @@
+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
deleted file mode 100644
index 396aa50..0000000
--- a/src/parser/grammars/LogicGrammar.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-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
deleted file mode 100644
index 971af85..0000000
--- a/src/parser/grammars/QueryGrammar.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index 3fb9e48..0000000
--- a/src/parser/grammars/TermsGrammar.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-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
deleted file mode 100644
index ac8c36f..0000000
--- a/src/parser/grammars/Tokens.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-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
new file mode 100644
index 0000000..f3b5586
--- /dev/null
+++ b/src/parser/state/ParserPosition.kt
@@ -0,0 +1,25 @@
+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
new file mode 100644
index 0000000..398b71a
--- /dev/null
+++ b/src/prolog/Program.kt
@@ -0,0 +1,80 @@
+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 9058f8c..e9fb28b 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
+typealias Answers = Sequence
\ No newline at end of file
diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt
deleted file mode 100644
index be2da7a..0000000
--- a/src/prolog/ast/Database.kt
+++ /dev/null
@@ -1,130 +0,0 @@
-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 80d0bde..3bbf694 100644
--- a/src/prolog/ast/arithmetic/Float.kt
+++ b/src/prolog/ast/arithmetic/Float.kt
@@ -1,7 +1,6 @@
package prolog.ast.arithmetic
import prolog.Substitutions
-import prolog.ast.terms.Term
class Float(override val value: kotlin.Float): Number {
// Floats are already evaluated
@@ -32,18 +31,4 @@ 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 d0a48cb..50028a9 100644
--- a/src/prolog/ast/arithmetic/Integer.kt
+++ b/src/prolog/ast/arithmetic/Integer.kt
@@ -1,16 +1,11 @@
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, LogicOperand() {
+data class Integer(override val value: Int) : Number {
// 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) {
@@ -42,7 +37,4 @@ data class Integer(override val value: Int) : Number, LogicOperand() {
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 ed8690f..58b2eb4 100644
--- a/src/prolog/ast/logic/Clause.kt
+++ b/src/prolog/ast/logic/Clause.kt
@@ -1,14 +1,13 @@
package prolog.ast.logic
import prolog.Answers
-import prolog.ast.Database.Program
import prolog.Substitutions
-import prolog.ast.terms.*
+import prolog.ast.terms.Body
+import prolog.ast.terms.Functor
+import prolog.ast.terms.Goal
+import prolog.ast.terms.Head
import prolog.builtins.True
import prolog.flags.AppliedCut
-import prolog.logic.applySubstitution
-import prolog.logic.numbervars
-import prolog.logic.occurs
import prolog.logic.unifyLazy
/**
@@ -16,45 +15,29 @@ import prolog.logic.unifyLazy
*
* A clause consists of a [Head] and body separated by the neck operator, or it is a [Fact].
*
- * @see [Variable]
+ * @see [prolog.ast.terms.Variable]
* @see [Predicate]
*/
-abstract class Clause(var head: Head, var body: Body) : Term, Resolvent {
+abstract class Clause(val head: Head, val body: Body) : 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.
-
- 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 ->
+ unifyLazy(goal, head, subs).forEach { headAnswer ->
+ headAnswer.map { newHeadSubs ->
// If the body can be proven, yield the (combined) substitutions
- val preBody = applySubstitution(body, headRenaming + headSubs) as Body
- preBody.satisfy(subs).forEach { bodyAnswer ->
+ body.satisfy(subs + newHeadSubs).forEach { bodyAnswer ->
bodyAnswer.fold(
- 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))
+ onSuccess = { newBodySubs ->
+ yield(Result.success(newHeadSubs + newBodySubs))
},
onFailure = { error ->
if (error is AppliedCut) {
// Find single solution and return immediately
if (error.subs != null) {
- yield(Result.failure(AppliedCut(headSubs + error.subs)))
+ yield(Result.failure(AppliedCut(newHeadSubs + error.subs)))
} else {
yield(Result.failure(AppliedCut()))
}
@@ -69,19 +52,10 @@ abstract class Clause(var head: Head, var body: Body) : Term, Resolvent {
}
}
- 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
+ override fun toString(): String {
+ return when {
+ body is True -> head.toString()
+ else -> "$head :- $body"
+ }
}
-
- override fun hashCode(): Int {
- return super.hashCode()
- }
-}
+}
\ No newline at end of file
diff --git a/src/prolog/ast/logic/Fact.kt b/src/prolog/ast/logic/Fact.kt
index f0cae19..a52990e 100644
--- a/src/prolog/ast/logic/Fact.kt
+++ b/src/prolog/ast/logic/Fact.kt
@@ -1,12 +1,6 @@
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) {
- 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
+class Fact(head: Head) : Clause(head, True)
\ No newline at end of file
diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt
index 25fb737..1396cd1 100644
--- a/src/prolog/ast/logic/Predicate.kt
+++ b/src/prolog/ast/logic/Predicate.kt
@@ -15,54 +15,48 @@ 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, dynamic: Boolean = false) {
+ constructor(functor: Functor) {
this.functor = functor
this.clauses = mutableListOf()
- this.dynamic = dynamic
}
/**
* Creates a predicate with the given clauses.
*/
- constructor(clauses: List, dynamic: Boolean = false) {
+ constructor(clauses: List) {
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, index: Int? = null, force: Boolean = false) {
+ fun add(clause: Clause) {
require(clause.functor == functor) { "Clause functor does not match predicate functor" }
- require(dynamic || force) { "No permission to modify static procedure '$functor'" }
- if (index != null) clauses.add(index, clause) else clauses.add(clause)
+ if (Debug.on) {
+ println("Adding clause $clause to predicate $functor")
+ }
+
+ clauses.add(clause)
}
/**
* Adds a list of clauses to the predicate.
*/
- fun addAll(clauses: List, force: Boolean = false) {
+ fun addAll(clauses: List) {
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 ->
@@ -86,4 +80,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 add106d..9eba36e 100644
--- a/src/prolog/ast/logic/Rule.kt
+++ b/src/prolog/ast/logic/Rule.kt
@@ -1,15 +1,6 @@
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) {
- 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
- )
-}
+class Rule(head: Head, body: Body) : Clause(head, body)
\ No newline at end of file
diff --git a/src/prolog/ast/terms/AnonymousVariable.kt b/src/prolog/ast/terms/AnonymousVariable.kt
deleted file mode 100644
index 8211292..0000000
--- a/src/prolog/ast/terms/AnonymousVariable.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-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 dcd0a7d..3a6afad 100644
--- a/src/prolog/ast/terms/Atom.kt
+++ b/src/prolog/ast/terms/Atom.kt
@@ -21,7 +21,4 @@ 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 4918d1e..dc61c7d 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 : Term, Satisfiable
\ No newline at end of file
+interface Body : Satisfiable
\ No newline at end of file
diff --git a/src/prolog/ast/terms/Goal.kt b/src/prolog/ast/terms/Goal.kt
index 95f9016..e0a5c49 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.ast.Database.Program
+import prolog.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 cf43b24..9d0e067 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)), Term {
+) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)) {
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 bcd5e71..0585de9 100644
--- a/src/prolog/ast/terms/Structure.kt
+++ b/src/prolog/ast/terms/Structure.kt
@@ -3,7 +3,6 @@ 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
@@ -23,21 +22,4 @@ open class Structure(val name: Atom, var arguments: List) : Goal(), He
else -> "${name.name}(${arguments.joinToString(", ")})"
}
}
-
- 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) }
- )
-}
+}
\ No newline at end of file
diff --git a/src/prolog/ast/terms/Term.kt b/src/prolog/ast/terms/Term.kt
index 8174310..0fdad49 100644
--- a/src/prolog/ast/terms/Term.kt
+++ b/src/prolog/ast/terms/Term.kt
@@ -1,19 +1,14 @@
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],
- * [Float] or [CompoundTerm].
+ * A [Term] is either a [Variable], [Atom], [Integer][prolog.ast.arithmetic.Integer],
+ * [Float][prolog.ast.arithmetic.Float] or [CompoundTerm].
* In addition, SWI-Prolog also defines the type TODO string.
*/
-interface Term : Comparable, Cloneable {
+interface Term : Comparable {
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 8068864..713c44d 100644
--- a/src/prolog/ast/terms/Variable.kt
+++ b/src/prolog/ast/terms/Variable.kt
@@ -1,12 +1,10 @@
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
-open class Variable(val name: String) : Term, Body, Expression, LogicOperand() {
+data class Variable(val name: String) : Term, Expression {
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
@@ -18,28 +16,5 @@ open class Variable(val name: String) : Term, Body, Expression, LogicOperand() {
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 306337e..2636bf6 100644
--- a/src/prolog/builtins/arithmeticOperators.kt
+++ b/src/prolog/builtins/arithmeticOperators.kt
@@ -11,6 +11,23 @@ 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 <
@@ -62,11 +79,11 @@ class EvaluatesTo(private val left: Expression, private val right: Expression) :
/**
* True when Number is the value to which Expr evaluates.
*/
-class Is(val number: Expression, val expr: Expression) :
- Operator(Atom("is"), number, expr), Satisfiable {
+class Is(private val left: Expression, private val right: Expression) :
+ Operator(Atom("is"), left, right), Satisfiable {
override fun satisfy(subs: Substitutions): Answers {
- val t1 = number.simplify(subs)
- val t2 = expr.simplify(subs)
+ val t1 = left.simplify(subs)
+ val t2 = right.simplify(subs)
if (!atomic(t2.to, subs)) {
return sequenceOf(Result.failure(IllegalArgumentException("Right operand must be instantiated")))
@@ -119,7 +136,7 @@ open class Subtract(private val expr1: Expression, private val expr2: Expression
/**
* Result = Expr1 * Expr2
*/
-class Multiply(val expr1: Expression, val expr2: Expression) :
+class Multiply(private val expr1: Expression, private val expr2: Expression) :
ArithmeticOperator(Atom("*"), expr1, expr2) {
override fun simplify(subs: Substitutions): Simplification {
val result = Variable("Result")
@@ -144,7 +161,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) :
- CompoundTerm(Atom("between"), listOf(expr1, expr2, expr3)), Satisfiable {
+ Operator(Atom("between"), expr1, expr2) {
override fun satisfy(subs: Substitutions): Answers {
val e1 = expr1.simplify(subs)
val e2 = expr2.simplify(subs)
@@ -152,8 +169,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
- val v2 = e2.to
+ val v1 = e1.to as Integer
+ val v2 = e2.to as Integer
return if (variable(e3.to, subs)) {
between(v1, v2, e3.to as Variable).map { answer ->
@@ -165,6 +182,4 @@ 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 b9eeef4..c1ebe63 100644
--- a/src/prolog/builtins/controlOperators.kt
+++ b/src/prolog/builtins/controlOperators.kt
@@ -3,11 +3,10 @@ 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.terms.Structure
+import prolog.ast.logic.LogicOperator
import prolog.flags.AppliedCut
/**
@@ -35,8 +34,6 @@ class Cut() : Atom("!") {
override fun satisfy(subs: Substitutions): Answers {
return sequenceOf(Result.failure(AppliedCut(emptyMap())))
}
-
- override fun applySubstitution(subs: Substitutions): Cut = Cut()
}
/**
@@ -97,11 +94,6 @@ 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
- )
}
/**
@@ -113,12 +105,6 @@ 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")
@@ -134,8 +120,7 @@ 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
- val goalResults = goal.satisfy(subs).iterator()
- if (goalResults.hasNext()) {
+ if (goal.satisfy(subs).toList().isNotEmpty()) {
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
deleted file mode 100644
index 54ebeb3..0000000
--- a/src/prolog/builtins/databaseOperators.kt
+++ /dev/null
@@ -1,116 +0,0 @@
-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
deleted file mode 100644
index 1271296..0000000
--- a/src/prolog/builtins/ioOperators.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-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 4cc582f..b34d3e0 100644
--- a/src/prolog/builtins/other.kt
+++ b/src/prolog/builtins/other.kt
@@ -6,11 +6,6 @@ import prolog.ast.logic.LogicOperand
import prolog.ast.terms.Atom
import prolog.ast.logic.LogicOperator
-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) {
+class Query(private 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 892c616..fc40df7 100644
--- a/src/prolog/builtins/unificationOperators.kt
+++ b/src/prolog/builtins/unificationOperators.kt
@@ -26,11 +26,6 @@ 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 36b903c..0d4d341 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 || t1 in result.values) {
+ if (t1 in result) {
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 8c2b26f..6bf2665 100644
--- a/src/prolog/logic/terms.kt
+++ b/src/prolog/logic/terms.kt
@@ -1,10 +1,7 @@
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
@@ -23,53 +20,3 @@ 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 5b5d21e..ad24281 100644
--- a/src/prolog/logic/unification.kt
+++ b/src/prolog/logic/unification.kt
@@ -4,52 +4,42 @@ import prolog.Answer
import prolog.Answers
import prolog.Substitutions
import prolog.ast.arithmetic.Expression
-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.*
+import kotlin.NoSuchElementException
+import prolog.ast.arithmetic.Number
+import prolog.ast.arithmetic.Integer
+import prolog.ast.arithmetic.Float
// Apply substitutions to a term
fun applySubstitution(term: Term, subs: Substitutions): Term = when {
- 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
+ variable(term, emptyMap()) -> subs[(term as Variable)] ?: term
+ atomic(term, subs) -> term
compound(term, subs) -> {
- term.applySubstitution(subs)
+ val structure = term as Structure
+ Structure(structure.name, structure.arguments.map { applySubstitution(it, subs) })
}
-
else -> term
}
-
//TODO Combine with the other applySubstitution function
fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when {
- variable(expr, emptyMap()) -> applySubstitution(expr as Term, subs) as Expression
- atomic(expr, subs) -> expr
+ variable(expr, subs) -> 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
-fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean = when {
+private 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
}
@@ -59,21 +49,19 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence
val t2 = applySubstitution(term2, subs)
when {
- equivalent(t1, t2, subs) -> yield(Result.success(emptyMap()))
+ equivalent(t1, t2, subs) -> yield(Result.success(subs))
variable(t1, subs) -> {
val variable = t1 as Variable
if (!occurs(variable, t2, subs)) {
- yield(Result.success(mapOf(term1 to t2)))
+ yield(Result.success(subs + (variable to t2)))
}
}
-
variable(t2, subs) -> {
val variable = t2 as Variable
if (!occurs(variable, t1, subs)) {
- yield(Result.success(mapOf(term2 to t1)))
+ yield(Result.success(subs + (variable to t1)))
}
}
-
compound(t1, subs) && compound(t2, subs) -> {
val structure1 = t1 as Structure
val structure2 = t2 as Structure
@@ -87,25 +75,22 @@ 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()).iterator()
- return if (substitutions.hasNext()) {
- substitutions.next()
+ val substitutions = unifyLazy(term1, term2, emptyMap()).toList()
+ return if (substitutions.isNotEmpty()) {
+ substitutions.first()
} else {
Result.failure(NoSuchElementException())
}
@@ -137,40 +122,37 @@ 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
@@ -182,11 +164,9 @@ 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 5e8b0d4..34e999c 100644
--- a/src/repl/Repl.kt
+++ b/src/repl/Repl.kt
@@ -1,99 +1,4 @@
package repl
-import interpreter.Preprocessor
-import io.Logger
-import io.Terminal
-import parser.ReplParser
-import prolog.Answer
-import prolog.Answers
-
class Repl {
- 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
- }
- )
- }
-}
+}
\ No newline at end of file
diff --git a/tests/better_parser/SimplePrologPrologParserTests.kt b/tests/better_parser/SimplePrologPrologParserTests.kt
new file mode 100644
index 0000000..9135c42
--- /dev/null
+++ b/tests/better_parser/SimplePrologPrologParserTests.kt
@@ -0,0 +1,156 @@
+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/parser/grammars/LogicGrammarTests.kt b/tests/better_parser/SimpleSourcePrologParserTests.kt
similarity index 72%
rename from tests/parser/grammars/LogicGrammarTests.kt
rename to tests/better_parser/SimpleSourcePrologParserTests.kt
index 57a1f99..1eac0f9 100644
--- a/tests/parser/grammars/LogicGrammarTests.kt
+++ b/tests/better_parser/SimpleSourcePrologParserTests.kt
@@ -1,8 +1,7 @@
-package parser.grammars
+package better_parser
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
@@ -15,13 +14,14 @@ import prolog.ast.terms.CompoundTerm
import prolog.ast.terms.Structure
import prolog.ast.terms.Variable
import prolog.builtins.Conjunction
+import prolog.builtins.Disjunction
-class LogicGrammarTests {
+class SimpleSourcePrologParserTests {
private lateinit var parser: Grammar>
@BeforeEach
fun setup() {
- parser = LogicGrammar() as Grammar>
+ parser = SimpleSourceParser() as Grammar>
}
@ParameterizedTest
@@ -40,7 +40,7 @@ class LogicGrammarTests {
assertEquals(1, result.size, "Expected 1 fact")
assertTrue(result[0] is Fact, "Expected a fact")
- assertEquals(input, "${result[0]}.", "Expected fact to be '$input'")
+ assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'")
}
@ParameterizedTest
@@ -114,7 +114,7 @@ class LogicGrammarTests {
assertEquals(1, result.size, "Expected 1 rule")
assertInstanceOf(Rule::class.java, result[0], "Expected a rule")
val rule = result[0] as Rule
- assertInstanceOf(CompoundTerm::class.java, rule.body, "Expected body to be a compound term")
+ assertInstanceOf(Conjunction::class.java, rule.body, "Expected body to be a conjunction")
}
@Test
@@ -125,38 +125,8 @@ class LogicGrammarTests {
assertEquals(1, result.size, "Expected 1 rule")
val rule = result[0] as Rule
- 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")
+ 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'")
}
}
\ No newline at end of file
diff --git a/tests/parser/resources/a.pl b/tests/better_parser/resources/a.pl
similarity index 100%
rename from tests/parser/resources/a.pl
rename to tests/better_parser/resources/a.pl
diff --git a/tests/parser/resources/foo.pl b/tests/better_parser/resources/foo.pl
similarity index 100%
rename from tests/parser/resources/foo.pl
rename to tests/better_parser/resources/foo.pl
diff --git a/tests/parser/resources/parent.pl b/tests/better_parser/resources/parent.pl
similarity index 57%
rename from tests/parser/resources/parent.pl
rename to tests/better_parser/resources/parent.pl
index 15426de..8ac4196 100644
--- a/tests/parser/resources/parent.pl
+++ b/tests/better_parser/resources/parent.pl
@@ -4,4 +4,7 @@ female(mary).
parent(john, jimmy).
parent(mary, jimmy).
father(X, Y) :- parent(X, Y), male(X).
-mother(X, Y) :- parent(X, Y), female(X).
\ No newline at end of file
+mother(X, Y) :- parent(X, Y), female(X).
+
+foo(0).
+foo(X) :- X > 0, Y is X - 1, foo(Y).
diff --git a/tests/compare.sh b/tests/compare.sh
deleted file mode 100644
index d876e13..0000000
--- a/tests/compare.sh
+++ /dev/null
@@ -1,75 +0,0 @@
-#!/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
deleted file mode 100644
index f93fc35..0000000
--- a/tests/e2e/Examples.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-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
deleted file mode 100644
index 4e35b2a..0000000
--- a/tests/interpreter/OpenPreprocessor.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-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
deleted file mode 100644
index 05df756..0000000
--- a/tests/interpreter/ParserPreprocessorIntegrationTests.kt
+++ /dev/null
@@ -1,92 +0,0 @@
-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
deleted file mode 100644
index bf54869..0000000
--- a/tests/interpreter/PreprocessorTests.kt
+++ /dev/null
@@ -1,622 +0,0 @@
-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 654a6ca..f999baf 100644
--- a/tests/interpreter/SourceFileReaderTests.kt
+++ b/tests/interpreter/SourceFileReaderTests.kt
@@ -2,27 +2,31 @@ package interpreter
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import prolog.ast.Database.Program
+import prolog.Program
class SourceFileReaderTests {
@BeforeEach
fun setup() {
- Program.reset()
+ Program.clear()
}
@Test
fun a() {
- val inputFile = "tests/parser/resources/a.pl"
- val reader = FileLoader()
+ val inputFile = "tests/better_parser/resources/a.pl"
+ val reader = SourceFileReader()
reader.readFile(inputFile)
+
+ println(Program.predicates)
}
@Test
fun foo() {
- val inputFile = "tests/parser/resources/foo.pl"
- val reader = FileLoader()
+ val inputFile = "tests/better_parser/resources/foo.pl"
+ val reader = SourceFileReader()
reader.readFile(inputFile)
+
+ println(Program.predicates)
}
}
\ No newline at end of file
diff --git a/tests/lexer/ScanPrologParserTests.kt b/tests/lexer/ScanPrologParserTests.kt
new file mode 100644
index 0000000..5e99b01
--- /dev/null
+++ b/tests/lexer/ScanPrologParserTests.kt
@@ -0,0 +1,62 @@
+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
new file mode 100644
index 0000000..a21f571
--- /dev/null
+++ b/tests/lexer/ScanTests.kt
@@ -0,0 +1,191 @@
+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
deleted file mode 100644
index 9c19db2..0000000
--- a/tests/parser/OperatorParserTests.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-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
new file mode 100644
index 0000000..c7de15e
--- /dev/null
+++ b/tests/parser/ParseFromTextTests.kt
@@ -0,0 +1,4 @@
+package parser
+
+class ParseFromTextTests {
+}
\ No newline at end of file
diff --git a/tests/parser/ParseTests.kt b/tests/parser/ParseTests.kt
new file mode 100644
index 0000000..4056820
--- /dev/null
+++ b/tests/parser/ParseTests.kt
@@ -0,0 +1,91 @@
+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
deleted file mode 100644
index 74aa53e..0000000
--- a/tests/parser/builtins/DatabaseOperatorsParserTests.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-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/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt
deleted file mode 100644
index d3b45e2..0000000
--- a/tests/parser/grammars/TermsGrammarTests.kt
+++ /dev/null
@@ -1,355 +0,0 @@
-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/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt
index a1cbbfc..e8ce952 100644
--- a/tests/prolog/EvaluationTests.kt
+++ b/tests/prolog/EvaluationTests.kt
@@ -2,23 +2,20 @@ 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.ast.Database.Program
+import prolog.builtins.*
class EvaluationTests {
@BeforeEach
fun setUp() {
- Program.reset()
+ Program.clear()
}
@Test
@@ -108,24 +105,20 @@ 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")))).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 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 result3 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jane"))))
assertFalse(result3.any())
@@ -218,204 +211,36 @@ 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 `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"))))
+ fun recursive_query() {
+ val fact = Fact(Structure(Atom("foo"), listOf(Integer(0))))
val rule = Rule(
- 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(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_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"),
+ Structure(Atom("foo"), listOf(Variable("X"))),
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")))
+ GreaterThan(Variable("X"), Integer(0)),
+ Conjunction(
+ Is(Variable("Y"), Subtract(Variable("X"), Integer(1))),
+ Structure(Atom("foo"), listOf(Variable("Y")))
)
)
+ )
- Program.reset()
- Program.load(listOf(fact1, fact2, fact3, rule1))
- }
+ Program.load(listOf(fact, rule))
- @Test
- fun `likes_italian_food(alice)`() {
- val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Atom("alice")))).toList()
+ val result = Program.query(Structure(Atom("foo"), listOf(Integer(0)))).toList()
- assertEquals(2, result.size, "Expected 2 results")
+ val result5 = Program.query(Structure(Atom("foo"), listOf(Integer(5)))).toList()
- 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")
- }
+ 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())
}
-}
+}
\ No newline at end of file
diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt
index bb6098f..15ad926 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.ast.Database.Program
+import prolog.Program
import prolog.ast.logic.Fact
import prolog.ast.logic.Rule
import prolog.ast.terms.Atom
@@ -14,25 +14,7 @@ import prolog.ast.terms.Variable
class ControlOperatorsTests {
@BeforeEach
fun setUp() {
- 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")
+ Program.clear()
}
// See also: https://stackoverflow.com/a/23292126
@@ -55,7 +37,7 @@ class ControlOperatorsTests {
// Now with cut
- Program.reset()
+ Program.clear()
Program.load(
listOf(
@@ -104,7 +86,7 @@ class ControlOperatorsTests {
// Now with cut in the middle
- Program.reset()
+ Program.clear()
Program.load(
listOf(
@@ -138,7 +120,7 @@ class ControlOperatorsTests {
// Now with cut at the end
- Program.reset()
+ Program.clear()
Program.load(
listOf(
diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt
deleted file mode 100644
index 8e96085..0000000
--- a/tests/prolog/builtins/DatabaseOperatorsTests.kt
+++ /dev/null
@@ -1,295 +0,0 @@
-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
deleted file mode 100644
index 05607ec..0000000
--- a/tests/prolog/builtins/IoOperatorsTests.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-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 ac532f3..83fa8c7 100644
--- a/tests/prolog/logic/ArithmeticTests.kt
+++ b/tests/prolog/logic/ArithmeticTests.kt
@@ -583,23 +583,10 @@ 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((-1000..1000).random())
- val t2 = Integer((-1000..1000).random())
+ val t1 = Integer((0..1000).random())
+ val t2 = Integer((0..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
deleted file mode 100644
index dde66e6..0000000
--- a/tests/prolog/logic/TermsTests.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-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
deleted file mode 100644
index 10a37e7..0000000
--- a/tests/prolog/logic/UnificationTests.kt
+++ /dev/null
@@ -1,370 +0,0 @@
-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
new file mode 100644
index 0000000..f2b3c57
--- /dev/null
+++ b/tests/prolog/logic/UnifyTests.kt
@@ -0,0 +1,327 @@
+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