IO Operators

This commit is contained in:
Tibo De Peuter 2025-04-27 20:11:15 +02:00
parent b9f419a59d
commit 82a8fccf87
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
22 changed files with 450 additions and 199 deletions

View file

@ -1,3 +0,0 @@
data object Debug {
val on: Boolean = true
}

View file

@ -1,98 +1,15 @@
import better_parser.PrologParser import interpreter.FileLoader
import better_parser.SimpleReplParser import io.Logger
import interpreter.SourceFileReader
import prolog.Answer
import prolog.Program import prolog.Program
import prolog.ast.logic.Fact import prolog.ast.logic.Clause
import prolog.ast.logic.Rule import repl.Repl
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", ".")
fun main() { 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) FileLoader().load("tests/parser/resources/parent.pl")
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}")
}
}
Repl().start()
} }

View file

@ -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<Clause> = 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)
}
}
}

View file

@ -0,0 +1,4 @@
package interpreter
class Preprocessor {
}

View file

@ -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)
}
}
}

10
src/io/IoHandler.kt Normal file
View file

@ -0,0 +1,10 @@
package io
interface IoHandler {
fun prompt(
message: String,
hint: () -> String = { "Please enter a valid input." }
): String
fun say(message: String)
}

24
src/io/Logger.kt Normal file
View file

@ -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)
}

63
src/io/Terminal.kt Normal file
View file

@ -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)
}
}

View file

@ -1,9 +1,12 @@
package parser package parser
import com.github.h0tk3y.betterParse.grammar.Grammar
import com.github.h0tk3y.betterParse.grammar.parseToEnd
import parser.grammars.QueryGrammar
import prolog.builtins.Query import prolog.builtins.Query
class ReplParser: Parser { class ReplParser : Parser {
override fun parse(input: String): Query { private val grammar: Grammar<Query> = QueryGrammar() as Grammar<Query>
TODO("Not yet implemented")
} override fun parse(input: String): Query = grammar.parseToEnd(input)
} }

View file

@ -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<Query> by (body * -dot) use {
Query(this as LogicOperand)
}
override val rootParser: Parser<Any> by query
}

View file

@ -1,6 +1,6 @@
package prolog package prolog
import Debug import io.Logger
import prolog.ast.logic.Clause import prolog.ast.logic.Clause
import prolog.ast.logic.Predicate import prolog.ast.logic.Predicate
import prolog.ast.logic.Resolvent import prolog.ast.logic.Resolvent
@ -16,13 +16,14 @@ object Program: Resolvent {
var predicates: Map<Functor, Predicate> = emptyMap() var predicates: Map<Functor, Predicate> = emptyMap()
init { init {
Logger.debug("Initializing ${this::class.java.simpleName}")
setup() setup()
Logger.debug("Initialization of ${this::class.java.simpleName} complete")
} }
private fun setup() { private fun setup() {
if (Debug.on) { Logger.debug("Setting up ${this::class.java.simpleName}")
println("Setting up Prolog program...")
}
// Initialize the program with built-in predicates // Initialize the program with built-in predicates
load(listOf( load(listOf(
)) ))
@ -35,6 +36,7 @@ object Program: Resolvent {
fun query(goal: Goal): Answers = solve(goal, emptyMap()) fun query(goal: Goal): Answers = solve(goal, emptyMap())
override fun solve(goal: Goal, subs: Substitutions): Answers { override fun solve(goal: Goal, subs: Substitutions): Answers {
Logger.debug("Solving goal $goal")
val functor = goal.functor val functor = goal.functor
// If the predicate does not exist, return false // If the predicate does not exist, return false
val predicate = predicates[functor] ?: return emptySequence() val predicate = predicates[functor] ?: return emptySequence()
@ -57,6 +59,8 @@ object Program: Resolvent {
// If the predicate does not exist, create a new one // If the predicate does not exist, create a new one
predicates += Pair(functor, Predicate(listOf(clause))) predicates += Pair(functor, Predicate(listOf(clause)))
} }
Logger.debug("Loaded clause $clause into predicate $functor")
} }
} }
@ -74,6 +78,7 @@ object Program: Resolvent {
} }
fun clear() { fun clear() {
Logger.debug("Clearing ${this::class.java.simpleName}")
predicates = emptyMap() predicates = emptyMap()
setup() setup()
} }

View file

@ -29,7 +29,6 @@ class Predicate : Resolvent {
*/ */
constructor(clauses: List<Clause>) { constructor(clauses: List<Clause>) {
this.functor = clauses.first().functor this.functor = clauses.first().functor
require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" } require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" }
this.clauses = clauses.toMutableList() this.clauses = clauses.toMutableList()
} }
@ -39,11 +38,6 @@ class Predicate : Resolvent {
*/ */
fun add(clause: Clause) { fun add(clause: Clause) {
require(clause.functor == functor) { "Clause functor does not match predicate functor" } require(clause.functor == functor) { "Clause functor does not match predicate functor" }
if (Debug.on) {
println("Adding clause $clause to predicate $functor")
}
clauses.add(clause) clauses.add(clause)
} }

View file

@ -6,7 +6,7 @@ abstract class Operator(
private val symbol: Atom, private val symbol: Atom,
private val leftOperand: Operand? = null, private val leftOperand: Operand? = null,
private val rightOperand: Operand private val rightOperand: Operand
) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)) { ) : CompoundTerm(symbol, listOfNotNull(leftOperand, rightOperand)), Term {
override fun toString(): String { override fun toString(): String {
return when (leftOperand) { return when (leftOperand) {
null -> "${symbol.name} $rightOperand" null -> "${symbol.name} $rightOperand"

View file

@ -22,4 +22,15 @@ open class Structure(val name: Atom, var arguments: List<Argument>) : Goal(), He
else -> "${name.name}(${arguments.joinToString(", ")})" 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()
}
} }

View file

@ -1,5 +1,8 @@
package prolog.builtins package prolog.builtins
import io.Logger
import io.Terminal
import parser.ReplParser
import prolog.Answers import prolog.Answers
import prolog.Substitutions import prolog.Substitutions
import prolog.ast.logic.Satisfiable import prolog.ast.logic.Satisfiable
@ -7,13 +10,60 @@ import prolog.ast.terms.Atom
import prolog.ast.terms.Operator import prolog.ast.terms.Operator
import prolog.ast.terms.Term import prolog.ast.terms.Term
import prolog.logic.applySubstitution 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 { class Write(private val term: Term) : Operator(Atom("write"), null, term), Satisfiable {
override fun satisfy(subs: Substitutions): Answers { override fun satisfy(subs: Substitutions): Answers {
val t = applySubstitution(term, subs) val t = applySubstitution(term, subs)
println(t.toString()) Terminal().say(t.toString())
return sequenceOf(Result.success(emptyMap())) return sequenceOf(Result.success(emptyMap()))
} }
} }
/**
* 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))
}
}

View file

@ -6,6 +6,6 @@ import prolog.ast.logic.LogicOperand
import prolog.ast.terms.Atom import prolog.ast.terms.Atom
import prolog.ast.logic.LogicOperator 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) override fun satisfy(subs: Substitutions): Answers = query.satisfy(subs)
} }

View file

@ -14,21 +14,24 @@ import prolog.ast.arithmetic.Float
// Apply substitutions to a term // Apply substitutions to a term
fun applySubstitution(term: Term, subs: Substitutions): Term = when { fun applySubstitution(term: Term, subs: Substitutions): Term = when {
variable(term, emptyMap()) -> subs[(term as Variable)] ?: term variable(term, emptyMap()) -> subs[(term as Variable)] ?: term
atomic(term, subs) -> term atomic(term, subs) -> term
compound(term, subs) -> { compound(term, subs) -> {
val structure = term as Structure val structure = term as Structure
Structure(structure.name, structure.arguments.map { applySubstitution(it, subs) }) Structure(structure.name, structure.arguments.map { applySubstitution(it, subs) })
} }
else -> term else -> term
} }
//TODO Combine with the other applySubstitution function //TODO Combine with the other applySubstitution function
fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when { fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when {
variable(expr, subs) -> applySubstitution(expr as Term, subs) as Expression variable(expr, subs) -> applySubstitution(expr as Term, subs) as Expression
atomic(expr, subs) -> expr atomic(expr, subs) -> expr
expr is LogicOperator -> { expr is LogicOperator -> {
expr.arguments = expr.arguments.map { applySubstitution(it, subs) } expr.arguments = expr.arguments.map { applySubstitution(it, subs) }
expr expr
} }
else -> expr else -> expr
} }
@ -40,6 +43,7 @@ private fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean
val structure = term as Structure val structure = term as Structure
structure.arguments.any { occurs(variable, it, subs) } structure.arguments.any { occurs(variable, it, subs) }
} }
else -> false else -> false
} }
@ -56,12 +60,14 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence
yield(Result.success(subs + (variable to t2))) yield(Result.success(subs + (variable to t2)))
} }
} }
variable(t2, subs) -> { variable(t2, subs) -> {
val variable = t2 as Variable val variable = t2 as Variable
if (!occurs(variable, t1, subs)) { if (!occurs(variable, t1, subs)) {
yield(Result.success(subs + (variable to t1))) yield(Result.success(subs + (variable to t1)))
} }
} }
compound(t1, subs) && compound(t2, subs) -> { compound(t1, subs) && compound(t2, subs) -> {
val structure1 = t1 as Structure val structure1 = t1 as Structure
val structure2 = t2 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 // Combine the results of all unifications
val combinedResults = results.reduce { acc, result -> val combinedResults = results.reduce { acc, result ->
acc.flatMap { a -> result.map { b -> acc.flatMap { a ->
if (a.isSuccess && b.isSuccess) a.getOrThrow() + b.getOrThrow() else emptyMap() result.map { b ->
} }.map { Result.success(it) } if (a.isSuccess && b.isSuccess) a.getOrThrow() + b.getOrThrow() else emptyMap()
}
}.map { Result.success(it) }
} }
yieldAll(combinedResults) yieldAll(combinedResults)
} }
} }
} }
else -> {} else -> {}
} }
} }
@ -122,37 +131,40 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int {
return when (t1) { return when (t1) {
is Variable -> { is Variable -> {
when (t2) { when (t2) {
is Variable -> t1.name.compareTo(t2.name) is Variable -> t1.name.compareTo(t2.name)
is Number -> -1 is Number -> -1
is Atom -> -1 is Atom -> -1
is Structure -> -1 is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
} }
} }
is Number -> { is Number -> {
when (t2) { when (t2) {
is Variable -> 1 is Variable -> 1
is Integer -> (t1.value as Int).compareTo(t2.value) is Integer -> (t1.value as Int).compareTo(t2.value)
is Float -> (t1.value as kotlin.Float).compareTo(t2.value) is Float -> (t1.value as kotlin.Float).compareTo(t2.value)
is Atom -> -1 is Atom -> -1
is Structure -> -1 is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
} }
} }
is Atom -> { is Atom -> {
when (t2) { when (t2) {
is Variable -> 1 is Variable -> 1
is Number -> 1 is Number -> 1
is Atom -> t1.name.compareTo(t2.name) is Atom -> t1.name.compareTo(t2.name)
is Structure -> -1 is Structure -> -1
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
} }
} }
is Structure -> { is Structure -> {
when (t2) { when (t2) {
is Variable -> 1 is Variable -> 1
is Number -> 1 is Number -> 1
is Atom -> 1 is Atom -> 1
is Structure -> { is Structure -> {
val arityComparison = t1.arguments.size.compareTo(t2.arguments.size) val arityComparison = t1.arguments.size.compareTo(t2.arguments.size)
if (arityComparison != 0) return arityComparison if (arityComparison != 0) return arityComparison
@ -164,9 +176,11 @@ fun compare(term1: Term, term2: Term, subs: Substitutions): Int {
} }
return 0 return 0
} }
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
} }
} }
else -> throw IllegalArgumentException("Cannot compare $t1 with $t2") else -> throw IllegalArgumentException("Cannot compare $t1 with $t2")
} }
} }

View file

@ -1,4 +1,90 @@
package repl package repl
import io.Logger
import io.Terminal
import parser.ReplParser
import prolog.Answer
import prolog.Answers
class Repl { 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
}
)
}
} }

View file

@ -12,8 +12,8 @@ class SourceFileReaderTests {
@Test @Test
fun a() { fun a() {
val inputFile = "tests/better_parser/resources/a.pl" val inputFile = "tests/parser/resources/a.pl"
val reader = SourceFileReader() val reader = FileLoader()
reader.readFile(inputFile) reader.readFile(inputFile)
@ -22,8 +22,8 @@ class SourceFileReaderTests {
@Test @Test
fun foo() { fun foo() {
val inputFile = "tests/better_parser/resources/foo.pl" val inputFile = "tests/parser/resources/foo.pl"
val reader = SourceFileReader() val reader = FileLoader()
reader.readFile(inputFile) reader.readFile(inputFile)

View file

@ -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'"
)
}
}

View file

@ -5,8 +5,3 @@ parent(john, jimmy).
parent(mary, jimmy). parent(mary, jimmy).
father(X, Y) :- parent(X, Y), male(X). father(X, Y) :- parent(X, Y), male(X).
mother(X, Y) :- parent(X, Y), female(X). mother(X, Y) :- parent(X, Y), female(X).
:- write(hello),
nl.
:- write(hello2).

View file

@ -1,8 +1,10 @@
package prolog.builtins package prolog.builtins
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertInstanceOf
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource import org.junit.jupiter.params.provider.ValueSource
@ -13,6 +15,7 @@ import java.io.ByteArrayOutputStream
import java.io.PrintStream import java.io.PrintStream
import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Integer
import prolog.ast.terms.Variable import prolog.ast.terms.Variable
import java.io.ByteArrayInputStream
class IoOperatorsTests { class IoOperatorsTests {
private var outStream = ByteArrayOutputStream() private var outStream = ByteArrayOutputStream()
@ -44,7 +47,7 @@ class IoOperatorsTests {
assertEquals(1, result.size, "Should return one result") assertEquals(1, result.size, "Should return one result")
assertTrue(result[0].isSuccess, "Result should be successful") 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 @Test
@ -79,7 +82,86 @@ class IoOperatorsTests {
assertEquals(1, result.size, "Should return one result") assertEquals(1, result.size, "Should return one result")
assertTrue(result[0].isSuccess, "Result should be successful") 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") 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")
}
} }