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