diff --git a/src/Debug.kt b/src/Debug.kt deleted file mode 100644 index ba9e63e..0000000 --- a/src/Debug.kt +++ /dev/null @@ -1,3 +0,0 @@ -data object Debug { - val on: Boolean = true -} \ No newline at end of file diff --git a/src/Main.kt b/src/Main.kt index 88278c2..1c9e0d6 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,98 +1,15 @@ -import better_parser.PrologParser -import better_parser.SimpleReplParser -import interpreter.SourceFileReader -import prolog.Answer +import interpreter.FileLoader +import io.Logger import prolog.Program -import prolog.ast.logic.Fact -import prolog.ast.logic.Rule -import prolog.ast.terms.Atom -import prolog.ast.terms.CompoundTerm -import prolog.ast.terms.Variable -import prolog.builtins.Conjunction - -fun help(): String { - println("Unknown command. Type 'h' for help.") - println("Commands:") - println(" ; - find next solution") - println(" a - abort") - println(" . - end query") - println(" h - help") - println(" exit - exit Prolog REPL") - return "" -} - -fun say(message: String) { - println(message) -} - -fun prompt(message: String): String { - print("$message ") - var input: String = readlnOrNull() ?: help() - while (input.isBlank()) { - input = readlnOrNull() ?: help() - } - 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", ".") +import prolog.ast.logic.Clause +import repl.Repl fun main() { - SourceFileReader().readFile("tests/better_parser/resources/parent.pl") + // TODO Make this a command line argument + // Turn on debug mode + Logger.level = Logger.Level.DEBUG - 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}") - } - } + FileLoader().load("tests/parser/resources/parent.pl") + Repl().start() } diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt new file mode 100644 index 0000000..2cd7022 --- /dev/null +++ b/src/interpreter/FileLoader.kt @@ -0,0 +1,39 @@ +package interpreter + +import io.Logger +import parser.ScriptParser +import prolog.Program +import prolog.ast.logic.Clause + +class FileLoader { + private val parser = ScriptParser() + + fun load(filePath: String): () -> Unit { + val input = readFile(filePath) + + Logger.debug("Parsing content of $filePath") + val clauses: List = parser.parse(input) + + Program.load(clauses) + + // TODO Pass next commands to execute + return {} + } + + 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) + } + } +} \ No newline at end of file diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt new file mode 100644 index 0000000..b6e7656 --- /dev/null +++ b/src/interpreter/Preprocessor.kt @@ -0,0 +1,4 @@ +package interpreter + +class Preprocessor { +} \ No newline at end of file diff --git a/src/interpreter/SourceFileReader.kt b/src/interpreter/SourceFileReader.kt deleted file mode 100644 index e3e58cc..0000000 --- a/src/interpreter/SourceFileReader.kt +++ /dev/null @@ -1,23 +0,0 @@ -package interpreter - -import better_parser.PrologParser - -class SourceFileReader { - private val parser = PrologParser() - - fun readFile(filePath: String) { - return try { - val file = java.io.File(filePath) - if (!file.exists()) { - throw IllegalArgumentException("File not found: $filePath") - } - - val content = file.readText() - - // Parse the content using SimpleSourceParser - parser.parse(content) - } catch (e: Exception) { - throw RuntimeException("Error reading file: $filePath", e) - } - } -} \ No newline at end of file diff --git a/src/io/IoHandler.kt b/src/io/IoHandler.kt new file mode 100644 index 0000000..c5706a1 --- /dev/null +++ b/src/io/IoHandler.kt @@ -0,0 +1,10 @@ +package io + +interface IoHandler { + fun prompt( + message: String, + hint: () -> String = { "Please enter a valid input." } + ): String + + fun say(message: String) +} \ No newline at end of file diff --git a/src/io/Logger.kt b/src/io/Logger.kt new file mode 100644 index 0000000..a0db3cc --- /dev/null +++ b/src/io/Logger.kt @@ -0,0 +1,24 @@ +package io + +object Logger { + enum class Level { + DEBUG, INFO, WARN, ERROR + } + + val defaultLevel: Level = Level.WARN + var level: Level = defaultLevel + + private val io: IoHandler = Terminal() + + fun log(message: String, messageLevel: Level = defaultLevel) { + if (level <= messageLevel) { + val text = "$messageLevel: $message\n" + io.say(text) + } + } + + fun debug(message: String) = log(message, Level.DEBUG) + fun info(message: String) = log(message, Level.INFO) + fun warn(message: String) = log(message, Level.WARN) + fun error(message: String) = log(message, Level.ERROR) +} diff --git a/src/io/Terminal.kt b/src/io/Terminal.kt new file mode 100644 index 0000000..ba12468 --- /dev/null +++ b/src/io/Terminal.kt @@ -0,0 +1,63 @@ +package io + +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) + } +} diff --git a/src/parser/ReplParser.kt b/src/parser/ReplParser.kt index 3bff927..a39fae8 100644 --- a/src/parser/ReplParser.kt +++ b/src/parser/ReplParser.kt @@ -1,9 +1,12 @@ package parser +import com.github.h0tk3y.betterParse.grammar.Grammar +import com.github.h0tk3y.betterParse.grammar.parseToEnd +import parser.grammars.QueryGrammar import prolog.builtins.Query -class ReplParser: Parser { - override fun parse(input: String): Query { - TODO("Not yet implemented") - } +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/grammars/QueryGrammar.kt b/src/parser/grammars/QueryGrammar.kt new file mode 100644 index 0000000..971af85 --- /dev/null +++ b/src/parser/grammars/QueryGrammar.kt @@ -0,0 +1,16 @@ +package parser.grammars + +import com.github.h0tk3y.betterParse.combinators.times +import com.github.h0tk3y.betterParse.combinators.unaryMinus +import com.github.h0tk3y.betterParse.combinators.use +import com.github.h0tk3y.betterParse.parser.Parser +import prolog.ast.logic.LogicOperand +import prolog.builtins.Query + +class QueryGrammar : TermsGrammar() { + protected val query: Parser by (body * -dot) use { + Query(this as LogicOperand) + } + + override val rootParser: Parser by query +} \ No newline at end of file diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index 398b71a..7bf9492 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -1,6 +1,6 @@ package prolog -import Debug +import io.Logger import prolog.ast.logic.Clause import prolog.ast.logic.Predicate import prolog.ast.logic.Resolvent @@ -16,13 +16,14 @@ object Program: Resolvent { var predicates: Map = emptyMap() init { + Logger.debug("Initializing ${this::class.java.simpleName}") setup() + Logger.debug("Initialization of ${this::class.java.simpleName} complete") } private fun setup() { - if (Debug.on) { - println("Setting up Prolog program...") - } + Logger.debug("Setting up ${this::class.java.simpleName}") + // Initialize the program with built-in predicates load(listOf( )) @@ -35,6 +36,7 @@ object Program: Resolvent { fun query(goal: Goal): Answers = solve(goal, emptyMap()) override fun solve(goal: Goal, subs: Substitutions): Answers { + Logger.debug("Solving goal $goal") val functor = goal.functor // If the predicate does not exist, return false val predicate = predicates[functor] ?: return emptySequence() @@ -57,6 +59,8 @@ object Program: Resolvent { // If the predicate does not exist, create a new one predicates += Pair(functor, Predicate(listOf(clause))) } + + Logger.debug("Loaded clause $clause into predicate $functor") } } @@ -74,6 +78,7 @@ object Program: Resolvent { } fun clear() { + Logger.debug("Clearing ${this::class.java.simpleName}") predicates = emptyMap() setup() } diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 1396cd1..5bd9c17 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -29,7 +29,6 @@ class Predicate : Resolvent { */ 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() } @@ -39,11 +38,6 @@ class Predicate : Resolvent { */ fun add(clause: Clause) { require(clause.functor == functor) { "Clause functor does not match predicate functor" } - - if (Debug.on) { - println("Adding clause $clause to predicate $functor") - } - clauses.add(clause) } diff --git a/src/prolog/ast/terms/Operator.kt b/src/prolog/ast/terms/Operator.kt index 9d0e067..cf43b24 100644 --- a/src/prolog/ast/terms/Operator.kt +++ b/src/prolog/ast/terms/Operator.kt @@ -6,7 +6,7 @@ abstract class Operator( private val symbol: Atom, private val leftOperand: Operand? = null, private val rightOperand: Operand -) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)) { +) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)), Term { override fun toString(): String { return when (leftOperand) { null -> "${symbol.name} $rightOperand" diff --git a/src/prolog/ast/terms/Structure.kt b/src/prolog/ast/terms/Structure.kt index 0585de9..385da2c 100644 --- a/src/prolog/ast/terms/Structure.kt +++ b/src/prolog/ast/terms/Structure.kt @@ -22,4 +22,15 @@ open class Structure(val name: Atom, var arguments: List) : Goal(), He else -> "${name.name}(${arguments.joinToString(", ")})" } } -} \ No newline at end of file + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Structure) return false + if (functor != other.functor) return false + return arguments.zip(other.arguments).all { (a, b) -> a == b } + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } +} diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/io.kt index aba1835..d04f74d 100644 --- a/src/prolog/builtins/io.kt +++ b/src/prolog/builtins/io.kt @@ -1,5 +1,8 @@ package prolog.builtins +import io.Logger +import io.Terminal +import parser.ReplParser import prolog.Answers import prolog.Substitutions import prolog.ast.logic.Satisfiable @@ -7,13 +10,60 @@ 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) - println(t.toString()) + Terminal().say(t.toString()) return sequenceOf(Result.success(emptyMap())) } -} \ No newline at end of file +} + +/** + * Write a newline character to the current output stream. + */ +object Nl : Atom("nl"), Satisfiable { + override fun satisfy(subs: Substitutions): Answers { + Terminal().say("\n") + return sequenceOf(Result.success(emptyMap())) + } +} + +/** + * Read the next Prolog term from the current input stream and unify it with [Term]. + * + * On reaching end-of-file, [Term] is unified with the [Atom] `end_of_file`. + */ +class Read(private val term: Term) : Operator(Atom("read"), null, term), Satisfiable { + private val io = Terminal() + private val parser = ReplParser() + + private fun readTerm(): Term { + val input = io.readLine() + Logger.debug("Read input: $input") + + return when (input) { + "end_of_file" -> Atom("end_of_file") + else -> { + val out = parser.parse(input).query + Logger.debug("Parsed input: $out") + out as? Term ?: throw IllegalArgumentException("Expected a term, but got: $out") + } + } + } + + override fun satisfy(subs: Substitutions): Answers = sequence { + val t1 = applySubstitution(term, subs) + + val t2 = readTerm() + Logger.debug("Read term: $t2") + + yieldAll(unifyLazy(t1, t2, subs)) + } +} diff --git a/src/prolog/builtins/other.kt b/src/prolog/builtins/other.kt index b34d3e0..9321880 100644 --- a/src/prolog/builtins/other.kt +++ b/src/prolog/builtins/other.kt @@ -6,6 +6,6 @@ import prolog.ast.logic.LogicOperand import prolog.ast.terms.Atom import prolog.ast.logic.LogicOperator -class Query(private val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) { +class Query(val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) { override fun satisfy(subs: Substitutions): Answers = query.satisfy(subs) } diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index ad24281..08b4d1c 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -14,21 +14,24 @@ import prolog.ast.arithmetic.Float // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { variable(term, emptyMap()) -> subs[(term as Variable)] ?: term - atomic(term, subs) -> term + atomic(term, subs) -> term compound(term, 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, subs) -> applySubstitution(expr as Term, subs) as Expression - atomic(expr, subs) -> expr + atomic(expr, subs) -> expr expr is LogicOperator -> { expr.arguments = expr.arguments.map { applySubstitution(it, subs) } expr } + else -> expr } @@ -40,6 +43,7 @@ private fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean val structure = term as Structure structure.arguments.any { occurs(variable, it, subs) } } + else -> false } @@ -56,12 +60,14 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence yield(Result.success(subs + (variable to t2))) } } + variable(t2, subs) -> { val variable = t2 as Variable if (!occurs(variable, t1, subs)) { yield(Result.success(subs + (variable to t1))) } } + compound(t1, subs) && compound(t2, subs) -> { val structure1 = t1 as Structure val structure2 = t2 as Structure @@ -75,14 +81,17 @@ 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 -> {} } } @@ -122,37 +131,40 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int { return when (t1) { is Variable -> { when (t2) { - is Variable -> t1.name.compareTo(t2.name) + is Variable -> t1.name.compareTo(t2.name) is Number -> -1 - is Atom -> -1 + is Atom -> -1 is Structure -> -1 else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") } } + is Number -> { when (t2) { - is Variable -> 1 - is Integer -> (t1.value as Int).compareTo(t2.value) - is Float -> (t1.value as kotlin.Float).compareTo(t2.value) - is Atom -> -1 + is Variable -> 1 + is Integer -> (t1.value as Int).compareTo(t2.value) + is Float -> (t1.value as kotlin.Float).compareTo(t2.value) + is Atom -> -1 is Structure -> -1 else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") } } + is Atom -> { when (t2) { - is Variable -> 1 + is Variable -> 1 is Number -> 1 - is Atom -> t1.name.compareTo(t2.name) + is Atom -> t1.name.compareTo(t2.name) is Structure -> -1 else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") } } + is Structure -> { when (t2) { is Variable -> 1 is Number -> 1 - is Atom -> 1 + is Atom -> 1 is Structure -> { val arityComparison = t1.arguments.size.compareTo(t2.arguments.size) if (arityComparison != 0) return arityComparison @@ -164,9 +176,11 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int { } return 0 } + else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") } } + else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") } } diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 34e999c..699ab86 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -1,4 +1,90 @@ package repl +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() + + fun start() { + io.say("Prolog REPL. Type '^D' to quit.\n") + while (true) { + try { + printAnswers(query()) + } catch (e: Exception) { + Logger.error("Error parsing REPL: ${e.message}") + } + } + } + + fun query(): Answers { + val queryString = io.prompt("?-", { "" }) + val query = parser.parse(queryString) + return query.satisfy(emptyMap()) + } + + fun printAnswers(answers: Answers) { + val knownCommands = setOf(";", "a", ".", "h") + + if (answers.none()) { + io.say("false.") + } else { + val iterator = answers.iterator() + var previous = iterator.next() + io.say(prettyPrint(previous)) + + 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) { + ";" -> previous = iterator.next() + "a" -> return + "." -> return + "h" -> { + help() + io.say("Action?") + } + } + } + + io.say(prettyPrint(previous)) + } + + io.say("\n") + } + + 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 "" + } + + fun prettyPrint(result: Answer): String { + result.fold( + onSuccess = { + val subs = result.getOrNull()!! + if (subs.isEmpty()) { + 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/interpreter/SourceFileReaderTests.kt b/tests/interpreter/SourceFileReaderTests.kt index f999baf..55fc73a 100644 --- a/tests/interpreter/SourceFileReaderTests.kt +++ b/tests/interpreter/SourceFileReaderTests.kt @@ -12,8 +12,8 @@ class SourceFileReaderTests { @Test fun a() { - val inputFile = "tests/better_parser/resources/a.pl" - val reader = SourceFileReader() + val inputFile = "tests/parser/resources/a.pl" + val reader = FileLoader() reader.readFile(inputFile) @@ -22,8 +22,8 @@ class SourceFileReaderTests { @Test fun foo() { - val inputFile = "tests/better_parser/resources/foo.pl" - val reader = SourceFileReader() + val inputFile = "tests/parser/resources/foo.pl" + val reader = FileLoader() reader.readFile(inputFile) diff --git a/tests/parser/ScriptParserTests.kt b/tests/parser/ScriptParserTests.kt deleted file mode 100644 index 2f6e2dd..0000000 --- a/tests/parser/ScriptParserTests.kt +++ /dev/null @@ -1,36 +0,0 @@ -package parser - -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertInstanceOf -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import prolog.ast.logic.Fact -import prolog.ast.terms.Atom -import prolog.logic.equivalent -import kotlin.test.assertTrue - -class ScriptParserTests { - private lateinit var parser: ScriptParser - - @BeforeEach - fun setup() { - parser = ScriptParser() - } - - @Test - fun `parse single atom`() { - val input = """ - a. - """.trimIndent() - - val result = parser.parse(input) - val expected = Fact(Atom("a")) - - assertEquals(1, result.size, "Should return one result") - assertInstanceOf(Fact::class.java, result[0], "Result should be a fact") - assertTrue( - equivalent(expected.head, result[0].head, emptyMap()), - "Expected fact 'a'" - ) - } -} \ No newline at end of file diff --git a/tests/parser/resources/parent.pl b/tests/parser/resources/parent.pl index 71c048c..15426de 100644 --- a/tests/parser/resources/parent.pl +++ b/tests/parser/resources/parent.pl @@ -4,9 +4,4 @@ female(mary). parent(john, jimmy). parent(mary, jimmy). father(X, Y) :- parent(X, Y), male(X). -mother(X, Y) :- parent(X, Y), female(X). - -:- write(hello), - nl. - -:- write(hello2). \ No newline at end of file +mother(X, Y) :- parent(X, Y), female(X). \ No newline at end of file diff --git a/tests/prolog/builtins/IoOperatorsTests.kt b/tests/prolog/builtins/IoOperatorsTests.kt index f51c313..8f7d5fe 100644 --- a/tests/prolog/builtins/IoOperatorsTests.kt +++ b/tests/prolog/builtins/IoOperatorsTests.kt @@ -1,8 +1,10 @@ package prolog.builtins import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Disabled import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource @@ -13,6 +15,7 @@ import java.io.ByteArrayOutputStream import java.io.PrintStream import prolog.ast.arithmetic.Integer import prolog.ast.terms.Variable +import java.io.ByteArrayInputStream class IoOperatorsTests { private var outStream = ByteArrayOutputStream() @@ -44,7 +47,7 @@ class IoOperatorsTests { assertEquals(1, result.size, "Should return one result") assertTrue(result[0].isSuccess, "Result should be successful") - assertEquals(name, outStream.toString().trim(), "Output should match the atom") + assertEquals(name, outStream.toString(), "Output should match the atom") } @Test @@ -79,7 +82,86 @@ class IoOperatorsTests { assertEquals(1, result.size, "Should return one result") assertTrue(result[0].isSuccess, "Result should be successful") - val output = outStream.toString().trim() + 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