From d5632e92173abf443251d2445de31f03b696b1d0 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 27 Apr 2025 14:47:56 +0200 Subject: [PATCH 01/19] feat: Write --- src/prolog/builtins/io.kt | 19 +++++ tests/prolog/builtins/IoOperatorsTests.kt | 85 +++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/prolog/builtins/io.kt create mode 100644 tests/prolog/builtins/IoOperatorsTests.kt diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/io.kt new file mode 100644 index 0000000..aba1835 --- /dev/null +++ b/src/prolog/builtins/io.kt @@ -0,0 +1,19 @@ +package prolog.builtins + +import prolog.Answers +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 + +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()) + + return sequenceOf(Result.success(emptyMap())) + } +} \ No newline at end of file diff --git a/tests/prolog/builtins/IoOperatorsTests.kt b/tests/prolog/builtins/IoOperatorsTests.kt new file mode 100644 index 0000000..f51c313 --- /dev/null +++ b/tests/prolog/builtins/IoOperatorsTests.kt @@ -0,0 +1,85 @@ +package prolog.builtins + +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.terms.Atom +import prolog.ast.terms.CompoundTerm +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import prolog.ast.arithmetic.Integer +import prolog.ast.terms.Variable + +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().trim(), "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().trim() + assertTrue(output == expected1 || output == expected2, "Output should match the arithmetic expression") + } +} \ No newline at end of file From a4ec29f084785ccc254a761db2a889f200c12e71 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 27 Apr 2025 18:22:03 +0200 Subject: [PATCH 02/19] Remove old lexer+parser implementation --- src/lexer/Lexer.kt | 127 ----------------- src/lexer/Token.kt | 9 -- src/lexer/TokenType.kt | 15 -- src/lexer/errors/LexingError.kt | 13 -- src/lexer/errors/LexingErrorType.kt | 7 - src/lexer/state/LexerPosition.kt | 3 - src/lexer/state/TokenPosition.kt | 3 - src/parser/Parser.kt | 137 ------------------ src/parser/errors/ParsingError.kt | 12 -- src/parser/errors/ParsingErrorType.kt | 7 - src/parser/state/ParserPosition.kt | 25 ---- tests/lexer/ScanPrologParserTests.kt | 62 --------- tests/lexer/ScanTests.kt | 191 -------------------------- tests/parser/ParseFromTextTests.kt | 4 - tests/parser/ParseTests.kt | 91 ------------ 15 files changed, 706 deletions(-) delete mode 100644 src/lexer/Lexer.kt delete mode 100644 src/lexer/Token.kt delete mode 100644 src/lexer/TokenType.kt delete mode 100644 src/lexer/errors/LexingError.kt delete mode 100644 src/lexer/errors/LexingErrorType.kt delete mode 100644 src/lexer/state/LexerPosition.kt delete mode 100644 src/lexer/state/TokenPosition.kt delete mode 100644 src/parser/Parser.kt delete mode 100644 src/parser/errors/ParsingError.kt delete mode 100644 src/parser/errors/ParsingErrorType.kt delete mode 100644 src/parser/state/ParserPosition.kt delete mode 100644 tests/lexer/ScanPrologParserTests.kt delete mode 100644 tests/lexer/ScanTests.kt delete mode 100644 tests/parser/ParseFromTextTests.kt delete mode 100644 tests/parser/ParseTests.kt diff --git a/src/lexer/Lexer.kt b/src/lexer/Lexer.kt deleted file mode 100644 index c239fbd..0000000 --- a/src/lexer/Lexer.kt +++ /dev/null @@ -1,127 +0,0 @@ -package lexer - -import lexer.errors.LexingError -import lexer.errors.LexingErrorType -import lexer.state.LexerPosition -import lexer.state.TokenPosition - -class Lexer(private val source: String) { - private var tokens: List = emptyList() - private val position = LexerPosition(0, 0, -1) - - /** - * Scans the source code and returns a list of tokens. - * @return List of [Token]s - */ - fun scan(): List { - while (hasNext()) { - val char: Char = peek() - tokens += when { - char == '(' -> scanSymbol(TokenType.PARENTHESIS_LEFT) - char == ')' -> scanSymbol(TokenType.PARENTHESIS_RIGHT) - char == '.' -> scanSymbol(TokenType.DOT) - char == '"' -> scanQuotedString() - char == '%' -> { scanComment(); continue } - char.isLetterOrDigit() -> scanAlphanumeric() - char.isWhitespace() -> { scanWhitespace(); continue } - else -> throw LexingError(LexingErrorType.UNKNOWN_TOKEN, "Did not recognize $char", position) - } - } - tokens += Token(TokenType.EOF, "EOF", getPosition(0)) - return tokens - } - - private fun hasNext(): Boolean { - // Check if the position is within the source length - return position.offset < source.length - } - - private fun peek(): Char { - // Peek should only be called if there is a next character - require(hasNext()) { - LexingError(LexingErrorType.UNEXPECTED_END_OF_INPUT, "Expected additional character", position) - } - - return source[position.offset] - } - - private fun next(): Char { - // Advance the position and return the character - val char = peek() - position.offset++ - position.column++ - return char - } - - private fun getPosition(length: Int = 1): TokenPosition { - // Return a new TokenPosition based on the current LexerPosition - return TokenPosition(position.line, position.column, length) - } - - /* * * * * * * - * Scanners * - * * * * * * */ - - /** - * Scans a symbol token, given the expected [TokenType]. - * @param tokenType The expected [TokenType] - * @return The scanned [Token] - */ - private fun scanSymbol(tokenType: TokenType): Token { - return Token(tokenType, next().toString(), getPosition(1)) - } - - private fun scanAlphanumeric(): Token { - // Scan all alphanumeric characters - var length = 0 - while (hasNext() && peek().isLetterOrDigit()) { - next() - length++ - } - val value = source.substring(position.offset - length, position.offset) - return Token(TokenType.ALPHANUMERIC, value, getPosition(length)) - } - - private fun scanQuotedString(): Token { - // "Assert" that the next character is the start of a quoted string - require(next() == '"') { - LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected opening quote '('", position) - } - - var length = 0 - while (hasNext() && peek() != '"') { - next() - length++ - } - - // "Assert" that the next character is the end of the quoted string - require(next() == '"') { - LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected closing quote ')'", position) - } - - val value = source.substring(position.offset - length - 1, position.offset - 1) - return Token(TokenType.ALPHANUMERIC, value, getPosition(length)) - } - - private fun scanComment() { - // "Assert" that the next character is the start of a comment - require(next() == '%') { - LexingError(LexingErrorType.UNEXPECTED_TOKEN, "Expected opening comment '%'", position) - } - - // Skip all characters until the end of the line - while (hasNext() && peek() != '\n') { - next() - } - } - - private fun scanWhitespace() { - // Skip all whitespace characters - while (hasNext() && peek().isWhitespace()) { - if (next() == '\n') { - position.line++ - position.column = 0 - } - } - } -} diff --git a/src/lexer/Token.kt b/src/lexer/Token.kt deleted file mode 100644 index c163bfd..0000000 --- a/src/lexer/Token.kt +++ /dev/null @@ -1,9 +0,0 @@ -package lexer - -import lexer.state.TokenPosition - -data class Token( - val type: TokenType, - val value: String, - val position: TokenPosition -) diff --git a/src/lexer/TokenType.kt b/src/lexer/TokenType.kt deleted file mode 100644 index 50d5141..0000000 --- a/src/lexer/TokenType.kt +++ /dev/null @@ -1,15 +0,0 @@ -package lexer - -enum class TokenType { - ALPHANUMERIC, - // TODO Replace with SMALL_LETTER, CAPITAL_LETTER, DIGIT, HEX_DIGIT, ... ? - - // Structure - COMMA, - DOT, - PARENTHESIS_LEFT, PARENTHESIS_RIGHT, - - // Special - - EOF -} diff --git a/src/lexer/errors/LexingError.kt b/src/lexer/errors/LexingError.kt deleted file mode 100644 index 7a5f4c9..0000000 --- a/src/lexer/errors/LexingError.kt +++ /dev/null @@ -1,13 +0,0 @@ -package lexer.errors - -import lexer.state.LexerPosition - -data class LexingError( - val type: LexingErrorType, - override val message: String, - val position: LexerPosition -) : Throwable( - """ - ${position.line}:${position.column + 1} ${type}: $message -""".trimIndent() -) diff --git a/src/lexer/errors/LexingErrorType.kt b/src/lexer/errors/LexingErrorType.kt deleted file mode 100644 index bff243a..0000000 --- a/src/lexer/errors/LexingErrorType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package lexer.errors - -enum class LexingErrorType { - UNKNOWN_TOKEN, - UNEXPECTED_TOKEN, - UNEXPECTED_END_OF_INPUT, -} \ No newline at end of file diff --git a/src/lexer/state/LexerPosition.kt b/src/lexer/state/LexerPosition.kt deleted file mode 100644 index 583bf29..0000000 --- a/src/lexer/state/LexerPosition.kt +++ /dev/null @@ -1,3 +0,0 @@ -package lexer.state - -data class LexerPosition(var offset: Int, var line: Int, var column: Int) diff --git a/src/lexer/state/TokenPosition.kt b/src/lexer/state/TokenPosition.kt deleted file mode 100644 index 2f19f76..0000000 --- a/src/lexer/state/TokenPosition.kt +++ /dev/null @@ -1,3 +0,0 @@ -package lexer.state - -data class TokenPosition(val line: Int, val column: Int, val length: Int) diff --git a/src/parser/Parser.kt b/src/parser/Parser.kt deleted file mode 100644 index e2e63e8..0000000 --- a/src/parser/Parser.kt +++ /dev/null @@ -1,137 +0,0 @@ -package parser - -import lexer.Token -import lexer.TokenType -import parser.errors.ParsingError -import parser.errors.ParsingErrorType -import parser.state.ParserPosition -import prolog.ast.logic.Clause -import prolog.ast.logic.Fact -import prolog.ast.logic.Rule -import prolog.ast.terms.Atom -import prolog.ast.terms.Structure -import prolog.ast.terms.Term - -class Parser(private val tokens: List) { - private val position: ParserPosition = ParserPosition(0) - - fun parse(): List { - val terms = mutableListOf() - - while (hasNext()) { - position.save() - - var term: Term? = null - - while (term == null) { - // Try each parser rule in order - - } - - require(term != null) { - ParsingError(ParsingErrorType.UNEXPECTED_TOKEN, "Expected a term", position) - } - - terms.add(term) - } - - return terms - } - - /** - * Matches the current token with any of the expected types. - * If it matches, it consumes the token and returns true. - * - * @param types The list of expected token types. - * @return True if the current token matches any of the expected types, false otherwise. - */ - 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/errors/ParsingError.kt b/src/parser/errors/ParsingError.kt deleted file mode 100644 index 7ddbfc2..0000000 --- a/src/parser/errors/ParsingError.kt +++ /dev/null @@ -1,12 +0,0 @@ -package parser.errors - -import parser.state.ParserPosition - -class ParsingError(private val type: ParsingErrorType, override val message: String, private val position: ParserPosition) : - Throwable() { - override fun toString(): String { - return """ - ($position) ${type}: $message - """.trimIndent() - } -} \ No newline at end of file diff --git a/src/parser/errors/ParsingErrorType.kt b/src/parser/errors/ParsingErrorType.kt deleted file mode 100644 index 5e017d8..0000000 --- a/src/parser/errors/ParsingErrorType.kt +++ /dev/null @@ -1,7 +0,0 @@ -package parser.errors - -enum class ParsingErrorType { - UNEXPECTED_TOKEN, - - INTERNAL_ERROR, -} \ No newline at end of file diff --git a/src/parser/state/ParserPosition.kt b/src/parser/state/ParserPosition.kt deleted file mode 100644 index f3b5586..0000000 --- a/src/parser/state/ParserPosition.kt +++ /dev/null @@ -1,25 +0,0 @@ -package parser.state - -import parser.errors.ParsingError -import parser.errors.ParsingErrorType - -data class ParserPosition(var offset: Int) { - private val checkpoints: ArrayDeque = ArrayDeque() - - fun save() { - checkpoints.addLast(this.copy()) - } - - fun reload() { - require(checkpoints.isNotEmpty()) { - ParsingError(ParsingErrorType.INTERNAL_ERROR, "No checkpoint to reload from", this) - } - - val checkpoint = checkpoints.removeLast() - offset = checkpoint.offset - } - - override fun toString(): String { - return "at $offset" - } -} diff --git a/tests/lexer/ScanPrologParserTests.kt b/tests/lexer/ScanPrologParserTests.kt deleted file mode 100644 index 5e99b01..0000000 --- a/tests/lexer/ScanPrologParserTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package lexer - -import lexer.errors.LexingError -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import kotlin.test.assertEquals - -/** - * Tests for the Prolog lexer. - * - * These tests are based on the Prolog syntax. - */ -class ScanPrologParserTests { - @Test - fun scan_simple_atom() { - val tokens = Lexer("atom.").scan() - - assertEquals(3, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}") - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_variable() { - val tokens = Lexer("X.").scan() - - assertEquals(3, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}") - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_variable_with_number() { - val tokens = Lexer("X1.").scan() - - assertEquals(3, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}") - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_variable_with_underscore() { - val tokens = Lexer("X_1.").scan() - - assertEquals(3, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}") - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_variable_that_starts_with_a_number() { - assertThrows { Lexer("1X.").scan() } - } -} diff --git a/tests/lexer/ScanTests.kt b/tests/lexer/ScanTests.kt deleted file mode 100644 index a21f571..0000000 --- a/tests/lexer/ScanTests.kt +++ /dev/null @@ -1,191 +0,0 @@ -package lexer - -import lexer.errors.LexingError -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertThrows -import org.junit.jupiter.api.Assertions.* - -class ScanTests { - @Test - fun scan_emptyString_returns_EOF() { - val tokens = Lexer("").scan() - assertEquals(1, tokens.size, "Expected 1 token, got ${tokens.size}") - assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}") - } - - @Test - fun scan_unknownSymbol_returns_Error() { - assertThrows { Lexer("€").scan() } - } - - @Test - fun scan_dot_returns_Dot() { - val tokens = Lexer(".").scan() - assertEquals(2, tokens.size) - assertEquals(TokenType.DOT, tokens[0].type, "Expected DOT token, got ${tokens[0].type}") - assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}") - } - - @Test - fun scan_two_dots_returns_two_dots() { - val tokens = Lexer("..").scan() - assertEquals(3, tokens.size) - assertEquals(TokenType.DOT, tokens[0].type, "Expected DOT token, got ${tokens[0].type}") - assertEquals(TokenType.DOT, tokens[1].type, "Expected DOT token, got ${tokens[1].type}") - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_letter_returns_letter() { - val tokens = Lexer("a").scan() - - assertEquals(2, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}") - - assertEquals(0, tokens[0].position.line, "Expected line 0, got ${tokens[0].position.line}") - assertEquals(0, tokens[0].position.column, "Expected column 0, got ${tokens[0].position.column}") - assertEquals(1, tokens[0].position.length, "Expected length 1, got ${tokens[0].position.length}") - } - - @Test - fun scan_word_returns_alphanumerics() { - val lexer = Lexer("word") - val tokens = lexer.scan() - - assertEquals(2, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}") - - assertEquals(4, tokens[0].position.length, "Expected length 4, got ${tokens[0].position.length}") - - assertEquals("word", tokens[0].value, "Expected 'word', got ${tokens[0].value}") - } - - @Test - fun scan_space_returns_nothing() { - val lexer = Lexer(" ") - val tokens = lexer.scan() - - assertEquals(1, tokens.size) - - assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}") - } - - @Test - fun scan_whitespace_various_returns_nothing() { - val lexer = Lexer(" \t\n\r") - val tokens = lexer.scan() - - assertEquals(1, tokens.size) - - assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}") - } - - - @Test - fun scan_separated_words() { - val tokens = Lexer("word1 word2").scan() - - assertEquals(3, tokens.size, "Expected 3 tokens, got ${tokens.size}") - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals("word1", tokens[0].value, "Expected 'word1', got ${tokens[0].value}") - assertEquals(5, tokens[0].position.length, "Expected length 5, got ${tokens[0].position.length}") - - assertEquals(TokenType.ALPHANUMERIC, tokens[1].type, "Expected ALPHANUMERIC token, got ${tokens[1].type}") - assertEquals("word2", tokens[1].value, "Expected 'word2', got ${tokens[1].value}") - assertEquals(5, tokens[1].position.length, "Expected length 5, got ${tokens[1].position.length}") - } - - @Test - fun scan_multiline() { - val tokens = Lexer( - """ - word1 - word2 - """.trimIndent() - ).scan() - - assertEquals(3, tokens.size, "Expected 3 tokens, got ${tokens.size}") - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals("word1", tokens[0].value, "Expected 'word1', got ${tokens[0].value}") - assertEquals(5, tokens[0].position.length, "Expected length 5, got ${tokens[0].position.length}") - - assertEquals(TokenType.ALPHANUMERIC, tokens[1].type, "Expected ALPHANUMERIC token, got ${tokens[1].type}") - assertEquals("word2", tokens[1].value, "Expected 'word2', got ${tokens[1].value}") - assertEquals(5, tokens[1].position.length, "Expected length 5, got ${tokens[1].position.length}") - } - - @Test - fun scan_parenthesis_returns_parenthesis() { - val lexer = Lexer("()") - val tokens = lexer.scan() - - assertEquals(3, tokens.size) - - assertEquals( - TokenType.PARENTHESIS_LEFT, - tokens[0].type, - "Expected LEFT_PARENTHESES token, got ${tokens[0].type}" - ) - assertEquals( - TokenType.PARENTHESIS_RIGHT, - tokens[1].type, - "Expected RIGHT_PARENTHESES token, got ${tokens[1].type}" - ) - assertEquals(TokenType.EOF, tokens[2].type, "Expected EOF token, got ${tokens[2].type}") - } - - @Test - fun scan_simple_quoted_string_returns_string() { - val lexer = Lexer("\"string\"") - val tokens = lexer.scan() - - assertEquals(2, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}") - - assertEquals("string", tokens[0].value, "Expected 'string', got ${tokens[0].value}") - } - - @Test - fun scan_quoted_string_with_space_returns_string() { - val lexer = Lexer("\"string with space\"") - val tokens = lexer.scan() - - assertEquals(2, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals(TokenType.EOF, tokens[1].type, "Expected EOF token, got ${tokens[1].type}") - - assertEquals("string with space", tokens[0].value, "Expected 'string with space', got ${tokens[0].value}") - } - - @Test - fun scan_comments_returns_nothing() { - val lexer = Lexer("% comment") - val tokens = lexer.scan() - - assertEquals(1, tokens.size) - - assertEquals(TokenType.EOF, tokens[0].type, "Expected EOF token, got ${tokens[0].type}") - } - - @Test - fun scan_comment_and_sentence_returns_sentence() { - val tokens = Lexer(""" - % comment - sentence - """.trimIndent()).scan() - - assertEquals(2, tokens.size) - - assertEquals(TokenType.ALPHANUMERIC, tokens[0].type, "Expected ALPHANUMERIC token, got ${tokens[0].type}") - assertEquals("sentence", tokens[0].value, "Expected 'sentence', got ${tokens[0].value}") - } -} diff --git a/tests/parser/ParseFromTextTests.kt b/tests/parser/ParseFromTextTests.kt deleted file mode 100644 index c7de15e..0000000 --- a/tests/parser/ParseFromTextTests.kt +++ /dev/null @@ -1,4 +0,0 @@ -package parser - -class ParseFromTextTests { -} \ No newline at end of file diff --git a/tests/parser/ParseTests.kt b/tests/parser/ParseTests.kt deleted file mode 100644 index 4056820..0000000 --- a/tests/parser/ParseTests.kt +++ /dev/null @@ -1,91 +0,0 @@ -package parser - -import lexer.Token -import lexer.state.TokenPosition -import lexer.TokenType -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test -import prolog.ast.terms.Atom -import prolog.ast.terms.CompoundTerm - -class ParseTests { - @Test - fun `parse atom a`() { - val input = Token(TokenType.ALPHANUMERIC, "a", TokenPosition(0, 0, 1)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom("a"), result[0], "Expected atom 'a'") - } - - @Test - fun `parse atom foo`() { - val input = Token(TokenType.ALPHANUMERIC, "foo", TokenPosition(0, 0, 3)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom("foo"), result[0], "Expected atom 'foo'") - } - - @Test - fun `parse atom foo1`() { - val input = Token(TokenType.ALPHANUMERIC, "foo1", TokenPosition(0, 0, 4)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom("foo1"), result[0], "Expected atom 'foo1'") - } - - @Test - fun `parse atom fooBar`() { - val name = "fooBar" - val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 6)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom(name), result[0], "Expected atom 'fooBar'") - } - - @Test - fun `parse atom foo_bar`() { - val name = "foo_bar" - val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 7)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom(name), result[0], "Expected atom 'foo_bar'") - } - - @Test - fun `parse atom my_FooBar1`() { - val name = "my_FooBar1" - val input = Token(TokenType.ALPHANUMERIC, name, TokenPosition(0, 0, 11)) - - val result = Parser(listOf(input)).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertEquals(Atom(name), result[0], "Expected atom 'my_FooBar1'") - } - - @Test - fun `parse compound term f()`() { - val input = listOf( - Token(TokenType.ALPHANUMERIC, "f", TokenPosition(0, 0, 1)), - Token(TokenType.PARENTHESIS_LEFT, "(", TokenPosition(0, 1, 2)), - Token(TokenType.PARENTHESIS_RIGHT, ")", TokenPosition(0, 3, 4)) - ) - - val result = Parser(input).parse() - - assertEquals(1, result.size, "Expected 1 term") - assertTrue(result[0] is CompoundTerm) - assertEquals("f", (result[0] as CompoundTerm).name) - assertEquals(0, (result[0] as CompoundTerm).arguments.size) - } -} \ No newline at end of file From b9f419a59ddd7c7954386523e8684e3c2b2762df Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 27 Apr 2025 19:31:29 +0200 Subject: [PATCH 03/19] Rework parsing structure --- src/better_parser/PrologParser.kt | 17 ----- src/better_parser/PrologSourceParser.kt | 70 ------------------ src/better_parser/SimplePrologParser.kt | 67 ----------------- src/better_parser/SimpleReplParser.kt | 27 ------- src/better_parser/SimpleSourceParser.kt | 48 ------------ src/parser/Parser.kt | 11 +++ src/parser/ReplParser.kt | 9 +++ src/parser/ScriptParser.kt | 12 +++ src/parser/grammars/LogicGrammar.kt | 22 ++++++ src/parser/grammars/TermsGrammar.kt | 74 +++++++++++++++++++ src/parser/grammars/Tokens.kt | 32 ++++++++ tests/parser/ScriptParserTests.kt | 36 +++++++++ .../grammars/LogicGrammarTests.kt} | 63 ++++++++-------- .../grammars/TermsGrammarTests.kt} | 31 ++++---- .../{better_parser => parser}/resources/a.pl | 0 .../resources/foo.pl | 0 .../resources/parent.pl | 5 +- 17 files changed, 246 insertions(+), 278 deletions(-) delete mode 100644 src/better_parser/PrologParser.kt delete mode 100644 src/better_parser/PrologSourceParser.kt delete mode 100644 src/better_parser/SimplePrologParser.kt delete mode 100644 src/better_parser/SimpleReplParser.kt delete mode 100644 src/better_parser/SimpleSourceParser.kt create mode 100644 src/parser/Parser.kt create mode 100644 src/parser/ReplParser.kt create mode 100644 src/parser/ScriptParser.kt create mode 100644 src/parser/grammars/LogicGrammar.kt create mode 100644 src/parser/grammars/TermsGrammar.kt create mode 100644 src/parser/grammars/Tokens.kt create mode 100644 tests/parser/ScriptParserTests.kt rename tests/{better_parser/SimpleSourcePrologParserTests.kt => parser/grammars/LogicGrammarTests.kt} (50%) rename tests/{better_parser/SimplePrologPrologParserTests.kt => parser/grammars/TermsGrammarTests.kt} (84%) rename tests/{better_parser => parser}/resources/a.pl (100%) rename tests/{better_parser => parser}/resources/foo.pl (100%) rename tests/{better_parser => parser}/resources/parent.pl (79%) diff --git a/src/better_parser/PrologParser.kt b/src/better_parser/PrologParser.kt deleted file mode 100644 index 73e63eb..0000000 --- a/src/better_parser/PrologParser.kt +++ /dev/null @@ -1,17 +0,0 @@ -package better_parser - -import com.github.h0tk3y.betterParse.grammar.Grammar -import com.github.h0tk3y.betterParse.grammar.parseToEnd -import prolog.Program -import prolog.ast.logic.Clause -import prolog.ast.terms.Atom - -class PrologParser { - private val parser: Grammar> = SimpleSourceParser() as Grammar> - - public fun parse(input: String) { - val clauses: List = parser.parseToEnd(input) - - Program.load(clauses) - } -} \ No newline at end of file diff --git a/src/better_parser/PrologSourceParser.kt b/src/better_parser/PrologSourceParser.kt deleted file mode 100644 index 0383507..0000000 --- a/src/better_parser/PrologSourceParser.kt +++ /dev/null @@ -1,70 +0,0 @@ -package better_parser - -import com.github.h0tk3y.betterParse.combinators.* -import com.github.h0tk3y.betterParse.grammar.Grammar -import com.github.h0tk3y.betterParse.lexer.literalToken -import com.github.h0tk3y.betterParse.lexer.regexToken -import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.logic.Fact -import prolog.ast.arithmetic.Integer -import prolog.ast.arithmetic.Float -import prolog.ast.logic.Clause -import prolog.ast.logic.LogicOperand -import prolog.ast.logic.Rule -import prolog.ast.terms.* -import prolog.builtins.Conjunction -import prolog.builtins.Disjunction - -class PrologSourceParser : Grammar>() { - // Define the tokens - private val atom by regexToken("[a-z][a-zA-Z0-9_]*") - private val variable by regexToken("[A-Z][a-zA-Z0-9_]*") - private val number by regexToken("-?[0-9]+(\\.[0-9]+)?") - private val whitespace by regexToken("\\s+", ignore = true) - - private val comma by literalToken(",") - private val semicolon by literalToken(";") - private val neck by literalToken(":-") - private val lparen by literalToken("(") - private val rparen by literalToken(")") - private val dot by literalToken(".") - - private val atomParser by atom use { Atom(text) } - private val variableParser by variable use { Variable(text) } - private val intParser by number use { Integer(text.toInt()) } - private val floatParser by number use { Float(text.toFloat()) } - private val numberParser by (intParser or floatParser) - private val compoundTermParser by (atomParser and skip(lparen) and separated( - atomParser or variableParser, - comma - ) and skip(rparen)) use { - CompoundTerm(t1, t2.terms) - } - - private val termParser: Parser by (numberParser or variableParser or compoundTermParser or atomParser) - - private val logicOperandParser: Parser by (termParser or compoundTermParser or atomParser) map { - it as LogicOperand - } - - private val conjunctionParser: Parser by (logicOperandParser and comma and logicOperandParser) use { - Conjunction(t1, t3) - } - private val disjunctionParser: Parser by (logicOperandParser and semicolon and logicOperandParser) use { - Disjunction(t1, t3) - } - - private val operatorParser: Parser by (conjunctionParser or disjunctionParser) - - private val headParser by (compoundTermParser or atomParser) - private val bodyParser by (operatorParser or compoundTermParser or atomParser) - - private val factParser by (headParser and dot) use { Fact(t1 as Head) } - private val ruleParser by (headParser and neck and bodyParser and dot) use { - Rule(t1 as Head, t3 as Body) - } - - private val clauseParser: Parser by (factParser or ruleParser) - - override val rootParser: Parser> by zeroOrMore(clauseParser) -} diff --git a/src/better_parser/SimplePrologParser.kt b/src/better_parser/SimplePrologParser.kt deleted file mode 100644 index 1fb49c7..0000000 --- a/src/better_parser/SimplePrologParser.kt +++ /dev/null @@ -1,67 +0,0 @@ -package better_parser - -import com.github.h0tk3y.betterParse.combinators.* -import com.github.h0tk3y.betterParse.grammar.Grammar -import com.github.h0tk3y.betterParse.grammar.parser -import com.github.h0tk3y.betterParse.lexer.Token -import com.github.h0tk3y.betterParse.lexer.literalToken -import com.github.h0tk3y.betterParse.lexer.regexToken -import com.github.h0tk3y.betterParse.lexer.token -import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.arithmetic.Float -import prolog.ast.arithmetic.Integer -import prolog.ast.terms.Atom -import prolog.ast.terms.Structure -import prolog.ast.terms.Term -import prolog.ast.terms.Variable - -open class SimplePrologParser : Grammar() { - // Prolog tokens - protected val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*") - protected val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*") - - // Arithmetic tokens - private val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+") - private val integerToken: Token by regexToken("-?([1-9][0-9]*|0)") - - // Special tokens - protected val neck by literalToken(":-") - protected val comma: Token by literalToken(",") - protected val leftParenthesis: Token by literalToken("(") - protected val rightParenthesis: Token by literalToken(")") - protected val dot by literalToken(".") - - // Ignored tokens - protected val whitespace: Token by regexToken("\\s+", ignore = true) - protected val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true) - protected val multiLineComment: Token by regexToken("/\\*.*?\\*/", ignore = true) - - protected val dummy by token { _, _ -> -1 } use { throw IllegalStateException("This parser should not be used") } - - // Prolog parsers - protected val variable: Parser by variableToken use { Variable(text) } - protected val atom: Parser by nameToken use { Atom(text) } - protected val compound: Parser by (atom and skip(leftParenthesis) and separated( - parser(::term), - comma, - acceptZero = true - ) and skip(rightParenthesis)) use { - Structure(t1, t2.terms) - } - - // Arithmetic parsers - private val int: Parser by integerToken use { Integer(text.toInt()) } - private val float: Parser by floatToken use { - Float(text.toFloat()) - } - - protected val term: Parser by (dummy - or float - or int - or variable - or compound - or atom - ) map { it } - - override val rootParser: Parser = term -} \ No newline at end of file diff --git a/src/better_parser/SimpleReplParser.kt b/src/better_parser/SimpleReplParser.kt deleted file mode 100644 index 0c8a598..0000000 --- a/src/better_parser/SimpleReplParser.kt +++ /dev/null @@ -1,27 +0,0 @@ -package better_parser - -import com.github.h0tk3y.betterParse.combinators.times -import com.github.h0tk3y.betterParse.combinators.unaryMinus -import com.github.h0tk3y.betterParse.combinators.use -import com.github.h0tk3y.betterParse.grammar.parseToEnd -import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.logic.LogicOperand -import prolog.builtins.Query - -class SimpleReplParser(val debug: Boolean = false) : SimpleSourceParser() { - override val rootParser: Parser by (body * -dot) use { Query(this as LogicOperand) } - - fun parse(input: String): Query { - if (debug) { - println("Parsing input: $input") - } - - val query = parseToEnd(input) as Query - - if (debug) { - println("Parsed query: $query") - } - - return query - } -} \ No newline at end of file diff --git a/src/better_parser/SimpleSourceParser.kt b/src/better_parser/SimpleSourceParser.kt deleted file mode 100644 index 437f9ed..0000000 --- a/src/better_parser/SimpleSourceParser.kt +++ /dev/null @@ -1,48 +0,0 @@ -package better_parser - -import com.github.h0tk3y.betterParse.combinators.* -import com.github.h0tk3y.betterParse.grammar.parser -import com.github.h0tk3y.betterParse.lexer.literalToken -import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.arithmetic.ArithmeticOperator -import prolog.ast.logic.* -import prolog.ast.terms.* -import prolog.builtins.Conjunction -import prolog.builtins.Disjunction - -open class SimpleSourceParser : SimplePrologParser() { - protected val simpleLogicOperand: Parser by (dummy - or compound - or atom - ) - protected val logicOperand: Parser by (dummy - or parser(::operator) - or simpleLogicOperand - ) - - protected val arithmeticOperator: Parser by dummy - protected val logicOperator: Parser by (simpleLogicOperand * comma * logicOperand) use { - Conjunction(t1, t3) - } - - protected val operator: Parser by (arithmeticOperator or logicOperator) use { this as Operator } - - protected val head: Parser by (dummy - or compound - or atom - ) - protected val body: Parser by (dummy - or operator - or head - ) use { this as Body } - - // ---- - - private val rule: Parser by (head * -neck * body) use { Rule(t1, t2) } - private val fact: Parser by head use { Fact(this) } - - private val clause: Parser by ((rule or fact) * -dot) - private val clauses: Parser> by zeroOrMore(clause) - - override val rootParser: Parser by clauses -} \ No newline at end of file diff --git a/src/parser/Parser.kt b/src/parser/Parser.kt new file mode 100644 index 0000000..e8c7383 --- /dev/null +++ b/src/parser/Parser.kt @@ -0,0 +1,11 @@ +package parser + +interface Parser { + /** + * Parses the input string and returns the parsed result. + * + * @param input The input string to parse. + * @return The parsed result, which is the AST of the input. + */ + fun parse(input: String): Any +} \ No newline at end of file diff --git a/src/parser/ReplParser.kt b/src/parser/ReplParser.kt new file mode 100644 index 0000000..3bff927 --- /dev/null +++ b/src/parser/ReplParser.kt @@ -0,0 +1,9 @@ +package parser + +import prolog.builtins.Query + +class ReplParser: Parser { + override fun parse(input: String): Query { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/src/parser/ScriptParser.kt b/src/parser/ScriptParser.kt new file mode 100644 index 0000000..a61a28c --- /dev/null +++ b/src/parser/ScriptParser.kt @@ -0,0 +1,12 @@ +package parser + +import com.github.h0tk3y.betterParse.grammar.Grammar +import com.github.h0tk3y.betterParse.grammar.parseToEnd +import parser.grammars.LogicGrammar +import prolog.ast.logic.Clause + +class ScriptParser: Parser { + private val grammar: Grammar> = LogicGrammar() as Grammar> + + override fun parse(input: String): List = grammar.parseToEnd(input) +} \ No newline at end of file diff --git a/src/parser/grammars/LogicGrammar.kt b/src/parser/grammars/LogicGrammar.kt new file mode 100644 index 0000000..10c3b82 --- /dev/null +++ b/src/parser/grammars/LogicGrammar.kt @@ -0,0 +1,22 @@ +package parser.grammars + +import com.github.h0tk3y.betterParse.combinators.oneOrMore +import com.github.h0tk3y.betterParse.combinators.or +import com.github.h0tk3y.betterParse.combinators.separated +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.Clause +import prolog.ast.logic.Fact +import prolog.ast.logic.Rule + +class LogicGrammar : TermsGrammar() { + 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 fact) * -dot) + protected val clauses: Parser> by oneOrMore(clause) + + override val rootParser: Parser by clauses +} \ No newline at end of file diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt new file mode 100644 index 0000000..9a26f77 --- /dev/null +++ b/src/parser/grammars/TermsGrammar.kt @@ -0,0 +1,74 @@ +package parser.grammars + +import com.github.h0tk3y.betterParse.combinators.or +import com.github.h0tk3y.betterParse.combinators.separated +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.parser +import com.github.h0tk3y.betterParse.parser.Parser +import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer +import prolog.ast.logic.LogicOperand +import prolog.ast.logic.LogicOperator +import prolog.ast.terms.Atom +import prolog.ast.terms.Body +import prolog.ast.terms.Head +import prolog.ast.terms.Operator +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.ast.terms.Variable +import prolog.builtins.Conjunction + +open class TermsGrammar : Tokens() { + // Basic named terms + protected val variable: Parser by variableToken use { Variable(text) } + protected val atom: Parser by nameToken use { Atom(text) } + protected val compound: Parser by (atom * -leftParenthesis * separated( + parser(::term), + 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()) } + + // Operators + protected val simpleLogicOperand: Parser by (dummy + or compound + or atom + ) + protected val logicOperand: Parser by (dummy + or parser(::operator) + or simpleLogicOperand + ) + protected val logicOperator: Parser by (simpleLogicOperand * -comma * logicOperand) use { + Conjunction(t1, t2) + } + protected val operator: Parser by (dummy + or logicOperator + ) + + // Parts + protected val head: Parser by (dummy + or compound + or atom + ) + protected val body: Parser by (dummy + or operator + or head + ) use { this as Body } + + protected val term: Parser by (dummy + or float + or int + or variable + or compound + or atom + ) + + override val rootParser: Parser by term +} \ No newline at end of file diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt new file mode 100644 index 0000000..b2e8fd7 --- /dev/null +++ b/src/parser/grammars/Tokens.kt @@ -0,0 +1,32 @@ +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() { + // 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 + protected val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+") + protected 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") } +} \ No newline at end of file diff --git a/tests/parser/ScriptParserTests.kt b/tests/parser/ScriptParserTests.kt new file mode 100644 index 0000000..2f6e2dd --- /dev/null +++ b/tests/parser/ScriptParserTests.kt @@ -0,0 +1,36 @@ +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/better_parser/SimpleSourcePrologParserTests.kt b/tests/parser/grammars/LogicGrammarTests.kt similarity index 50% rename from tests/better_parser/SimpleSourcePrologParserTests.kt rename to tests/parser/grammars/LogicGrammarTests.kt index 1eac0f9..5365e9f 100644 --- a/tests/better_parser/SimpleSourcePrologParserTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -1,8 +1,8 @@ -package better_parser +package parser.grammars import com.github.h0tk3y.betterParse.grammar.Grammar import com.github.h0tk3y.betterParse.grammar.parseToEnd -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -14,14 +14,13 @@ import prolog.ast.terms.CompoundTerm import prolog.ast.terms.Structure import prolog.ast.terms.Variable import prolog.builtins.Conjunction -import prolog.builtins.Disjunction -class SimpleSourcePrologParserTests { +class LogicGrammarTests { private lateinit var parser: Grammar> @BeforeEach fun setup() { - parser = SimpleSourceParser() as Grammar> + parser = LogicGrammar() as Grammar> } @ParameterizedTest @@ -38,9 +37,9 @@ class SimpleSourcePrologParserTests { fun `parse simple fact`(input: String) { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 fact") - assertTrue(result[0] is Fact, "Expected a fact") - assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'") + Assertions.assertEquals(1, result.size, "Expected 1 fact") + Assertions.assertTrue(result[0] is Fact, "Expected a fact") + Assertions.assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'") } @ParameterizedTest @@ -53,9 +52,9 @@ class SimpleSourcePrologParserTests { fun `parse multiple facts`(input: String) { val result = parser.parseToEnd(input) - assertEquals(2, result.size, "Expected 2 facts") - assertTrue(result[0] is Fact, "Expected a fact") - assertTrue(result[1] is Fact, "Expected a fact") + Assertions.assertEquals(2, result.size, "Expected 2 facts") + Assertions.assertTrue(result[0] is Fact, "Expected a fact") + Assertions.assertTrue(result[1] is Fact, "Expected a fact") } @Test @@ -64,9 +63,9 @@ class SimpleSourcePrologParserTests { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 rule") - assertTrue(result[0] is Rule, "Expected a rule") - assertEquals("a :- b", result[0].toString()) + Assertions.assertEquals(1, result.size, "Expected 1 rule") + Assertions.assertTrue(result[0] is Rule, "Expected a rule") + Assertions.assertEquals("a :- b", result[0].toString()) } @ParameterizedTest @@ -77,8 +76,8 @@ class SimpleSourcePrologParserTests { fun `parse simple rule`(input: String) { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 rule") - assertTrue(result[0] is Rule, "Expected a rule") + Assertions.assertEquals(1, result.size, "Expected 1 rule") + Assertions.assertTrue(result[0] is Rule, "Expected a rule") } @Test @@ -87,22 +86,22 @@ class SimpleSourcePrologParserTests { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 rule") - assertTrue(result[0] is Rule, "Expected a rule") + Assertions.assertEquals(1, result.size, "Expected 1 rule") + Assertions.assertTrue(result[0] is Rule, "Expected a rule") val rule = result[0] as Rule - assertTrue(rule.head is Structure, "Expected head to be a structure") + Assertions.assertTrue(rule.head is Structure, "Expected head to be a structure") val head = rule.head as Structure - assertEquals("parent/2", head.functor, "Expected functor 'parent/2'") - assertEquals(Variable("X"), head.arguments[0], "Expected first argument 'X'") - assertEquals(Variable("Y"), head.arguments[1], "Expected second argument 'Y'") + Assertions.assertEquals("parent/2", head.functor, "Expected functor 'parent/2'") + Assertions.assertEquals(Variable("X"), head.arguments[0], "Expected first argument 'X'") + Assertions.assertEquals(Variable("Y"), head.arguments[1], "Expected second argument 'Y'") - assertTrue(rule.body is Structure, "Expected body to be a structure") + Assertions.assertTrue(rule.body is Structure, "Expected body to be a structure") val body = rule.body as Structure - assertEquals("father/2", body.functor, "Expected functor 'father/2'") - assertEquals(Variable("X"), body.arguments[0], "Expected first argument 'X'") - assertEquals(Variable("Y"), body.arguments[1], "Expected second argument 'Y'") + Assertions.assertEquals("father/2", body.functor, "Expected functor 'father/2'") + Assertions.assertEquals(Variable("X"), body.arguments[0], "Expected first argument 'X'") + Assertions.assertEquals(Variable("Y"), body.arguments[1], "Expected second argument 'Y'") } @Test @@ -111,10 +110,10 @@ class SimpleSourcePrologParserTests { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 rule") - assertInstanceOf(Rule::class.java, result[0], "Expected a rule") + Assertions.assertEquals(1, result.size, "Expected 1 rule") + Assertions.assertInstanceOf(Rule::class.java, result[0], "Expected a rule") val rule = result[0] as Rule - assertInstanceOf(Conjunction::class.java, rule.body, "Expected body to be a conjunction") + Assertions.assertInstanceOf(Conjunction::class.java, rule.body, "Expected body to be a conjunction") } @Test @@ -123,10 +122,10 @@ class SimpleSourcePrologParserTests { val result = parser.parseToEnd(input) - assertEquals(1, result.size, "Expected 1 rule") + Assertions.assertEquals(1, result.size, "Expected 1 rule") val rule = result[0] as Rule - assertTrue(rule.body is Conjunction, "Expected body to be a conjunction") + Assertions.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'") + Assertions.assertEquals("invited/2", (conjunction.left as CompoundTerm).functor, "Expected functor 'invited/2'") } } \ No newline at end of file diff --git a/tests/better_parser/SimplePrologPrologParserTests.kt b/tests/parser/grammars/TermsGrammarTests.kt similarity index 84% rename from tests/better_parser/SimplePrologPrologParserTests.kt rename to tests/parser/grammars/TermsGrammarTests.kt index 9135c42..339c7c2 100644 --- a/tests/better_parser/SimplePrologPrologParserTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -1,9 +1,8 @@ -package better_parser +package parser.grammars import com.github.h0tk3y.betterParse.grammar.Grammar import com.github.h0tk3y.betterParse.grammar.parseToEnd -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -16,12 +15,12 @@ import prolog.ast.terms.Term import prolog.ast.terms.Variable import prolog.logic.equivalent -class SimplePrologPrologParserTests { +class TermsGrammarTests { private lateinit var parser: Grammar @BeforeEach fun setup() { - parser = SimplePrologParser() as Grammar + parser = TermsGrammar() as Grammar } @ParameterizedTest @@ -29,7 +28,7 @@ class SimplePrologPrologParserTests { fun `parse atom`(name: String) { val result = parser.parseToEnd(name) - assertEquals(Atom(name), result, "Expected atom '$name'") + Assertions.assertEquals(Atom(name), result, "Expected atom '$name'") } @ParameterizedTest @@ -37,7 +36,7 @@ class SimplePrologPrologParserTests { fun `parse variable`(name: String) { val result = parser.parseToEnd(name) - assertEquals(Variable(name), result, "Expected atom '$name'") + Assertions.assertEquals(Variable(name), result, "Expected atom '$name'") } @Test @@ -46,7 +45,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Structure(Atom("f"), emptyList()), result, emptyMap()), "Expected atom 'f'" ) @@ -58,7 +57,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Structure(Atom("f"), listOf(Atom("a"))), result, emptyMap()), "Expected atom 'f(a)'" ) @@ -70,7 +69,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), result, emptyMap()), "Expected atom 'f(a, b)'" ) @@ -82,7 +81,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Structure(Atom("f"), listOf(Atom("a"), Variable("X"))), result, emptyMap()), "Expected atom 'f(a, X)'" ) @@ -94,7 +93,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent( Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))), result, @@ -110,7 +109,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent( Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Variable("X"))))), result, @@ -127,7 +126,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertEquals(Integer(number), result, "Expected integer '$number'") + Assertions.assertEquals(Integer(number), result, "Expected integer '$number'") } @Test @@ -136,7 +135,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Float(42.0f), result, emptyMap()), "Expected float '42.0'" ) @@ -148,7 +147,7 @@ class SimplePrologPrologParserTests { val result = parser.parseToEnd(input) - assertTrue( + Assertions.assertTrue( equivalent(Float(-42.0f), result, emptyMap()), "Expected float '-42.0'" ) diff --git a/tests/better_parser/resources/a.pl b/tests/parser/resources/a.pl similarity index 100% rename from tests/better_parser/resources/a.pl rename to tests/parser/resources/a.pl diff --git a/tests/better_parser/resources/foo.pl b/tests/parser/resources/foo.pl similarity index 100% rename from tests/better_parser/resources/foo.pl rename to tests/parser/resources/foo.pl diff --git a/tests/better_parser/resources/parent.pl b/tests/parser/resources/parent.pl similarity index 79% rename from tests/better_parser/resources/parent.pl rename to tests/parser/resources/parent.pl index 51178c4..71c048c 100644 --- a/tests/better_parser/resources/parent.pl +++ b/tests/parser/resources/parent.pl @@ -6,4 +6,7 @@ parent(mary, jimmy). father(X, Y) :- parent(X, Y), male(X). mother(X, Y) :- parent(X, Y), female(X). -kan_goed_koken(miriam). \ No newline at end of file +:- write(hello), + nl. + +:- write(hello2). \ No newline at end of file From 82a8fccf8770f66e45c6e137e8761cb52e59222c Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 27 Apr 2025 20:11:15 +0200 Subject: [PATCH 04/19] IO Operators --- src/Debug.kt | 3 - src/Main.kt | 101 ++------------------- src/interpreter/FileLoader.kt | 39 ++++++++ src/interpreter/Preprocessor.kt | 4 + src/interpreter/SourceFileReader.kt | 23 ----- src/io/IoHandler.kt | 10 ++ src/io/Logger.kt | 24 +++++ src/io/Terminal.kt | 63 +++++++++++++ src/parser/ReplParser.kt | 11 ++- src/parser/grammars/QueryGrammar.kt | 16 ++++ src/prolog/Program.kt | 13 ++- src/prolog/ast/logic/Predicate.kt | 6 -- src/prolog/ast/terms/Operator.kt | 2 +- src/prolog/ast/terms/Structure.kt | 13 ++- src/prolog/builtins/io.kt | 54 ++++++++++- src/prolog/builtins/other.kt | 2 +- src/prolog/logic/unification.kt | 42 ++++++--- src/repl/Repl.kt | 86 ++++++++++++++++++ tests/interpreter/SourceFileReaderTests.kt | 8 +- tests/parser/ScriptParserTests.kt | 36 -------- tests/parser/resources/parent.pl | 7 +- tests/prolog/builtins/IoOperatorsTests.kt | 86 +++++++++++++++++- 22 files changed, 450 insertions(+), 199 deletions(-) delete mode 100644 src/Debug.kt create mode 100644 src/interpreter/FileLoader.kt create mode 100644 src/interpreter/Preprocessor.kt delete mode 100644 src/interpreter/SourceFileReader.kt create mode 100644 src/io/IoHandler.kt create mode 100644 src/io/Logger.kt create mode 100644 src/io/Terminal.kt create mode 100644 src/parser/grammars/QueryGrammar.kt delete mode 100644 tests/parser/ScriptParserTests.kt 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 From 174855d7a38f4baa5a46bab55b8dae9d1b068299 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 27 Apr 2025 22:02:50 +0200 Subject: [PATCH 05/19] Preprocessing --- src/interpreter/Preprocessor.kt | 91 +++++++++++- src/prolog/builtins/arithmeticOperators.kt | 4 +- src/repl/Repl.kt | 12 +- tests/interpreter/PreprocessorTests.kt | 156 +++++++++++++++++++++ tests/parser/OperatorParserTests.kt | 28 ++++ 5 files changed, 284 insertions(+), 7 deletions(-) create mode 100644 tests/interpreter/PreprocessorTests.kt create mode 100644 tests/parser/OperatorParserTests.kt diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index b6e7656..d10b605 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -1,4 +1,93 @@ package interpreter -class Preprocessor { +import io.Logger +import prolog.ast.logic.Clause +import prolog.ast.logic.Fact +import prolog.ast.logic.LogicOperand +import prolog.ast.logic.Rule +import prolog.ast.terms.Atom +import prolog.ast.terms.Body +import prolog.ast.terms.Goal +import prolog.ast.terms.Head +import prolog.ast.terms.Structure +import prolog.ast.terms.Term +import prolog.builtins.Conjunction +import prolog.builtins.Cut +import prolog.builtins.Disjunction +import prolog.builtins.Fail +import prolog.builtins.False +import prolog.builtins.Not +import prolog.builtins.Query +import prolog.builtins.True + +/** + * 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): 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() + else -> { + when { + term is Structure && term.functor == ",/2" -> { + val args = term.arguments.map { preprocess(it) } + Conjunction(args[0] as LogicOperand, args[1] as LogicOperand) + } + term is Structure && term.functor == ";/2" -> { + val args = term.arguments.map { preprocess(it) } + Disjunction(args[0] as LogicOperand, args[1] as LogicOperand) + } + term is Structure && term.functor == "\\+/1" -> { + val args = term.arguments.map { preprocess(it) } + Not(args[0] as Goal) + } + else -> term + } + } + } + + if (prepped != term || prepped::class != term::class) { + Logger.debug("Preprocessed term: $term -> $prepped (${prepped::class})") + } + + return prepped + } } \ No newline at end of file diff --git a/src/prolog/builtins/arithmeticOperators.kt b/src/prolog/builtins/arithmeticOperators.kt index 2463313..7931e7d 100644 --- a/src/prolog/builtins/arithmeticOperators.kt +++ b/src/prolog/builtins/arithmeticOperators.kt @@ -152,8 +152,8 @@ class Between(private val expr1: Expression, private val expr2: Expression, priv require(e1.to is Integer && e2.to is Integer) { "Arguments must be integers" } - val v1 = e1.to as Integer - val v2 = e2.to as Integer + val v1 = e1.to + val v2 = e2.to return if (variable(e3.to, subs)) { between(v1, v2, e3.to as Variable).map { answer -> diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 699ab86..52cce29 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -1,5 +1,6 @@ package repl +import interpreter.Preprocessor import io.Logger import io.Terminal import parser.ReplParser @@ -9,6 +10,7 @@ import prolog.Answers class Repl { private val io = Terminal() private val parser = ReplParser() + private val preprocessor = Preprocessor() fun start() { io.say("Prolog REPL. Type '^D' to quit.\n") @@ -23,7 +25,8 @@ class Repl { fun query(): Answers { val queryString = io.prompt("?-", { "" }) - val query = parser.parse(queryString) + val simpleQuery = parser.parse(queryString) + val query = preprocessor.preprocess(simpleQuery) return query.satisfy(emptyMap()) } @@ -46,7 +49,10 @@ class Repl { } when (command) { - ";" -> previous = iterator.next() + ";" -> { + previous = iterator.next() + io.say(prettyPrint(previous)) + } "a" -> return "." -> return "h" -> { @@ -55,8 +61,6 @@ class Repl { } } } - - io.say(prettyPrint(previous)) } io.say("\n") diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt new file mode 100644 index 0000000..a97755a --- /dev/null +++ b/tests/interpreter/PreprocessorTests.kt @@ -0,0 +1,156 @@ +package interpreter + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import prolog.ast.terms.Atom +import prolog.ast.terms.CompoundTerm +import prolog.ast.terms.Term +import prolog.builtins.Conjunction +import prolog.builtins.Disjunction +import prolog.builtins.Cut +import prolog.builtins.Fail +import prolog.builtins.True +import prolog.builtins.Not + +class PreprocessorTests { + class OpenPreprocessor : Preprocessor() { + public override fun preprocess(input: Term): Term { + return super.preprocess(input) + } + } + + @Nested + class `Arithmetic operators` { + @Test + fun `evaluates to different`() { + assertEquals(1, 2) + } + } + + @Nested + class `Control operators` { + private var preprocessor = OpenPreprocessor() + + @Test + fun fail() { + val tests = 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)) + ) + + 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 `true`() { + val tests = 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)) + ) + + 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 cut() { + val tests = 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())) + ) + + 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 conjunction() { + val tests = 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"))), + ) + + 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 disjunction() { + val tests = 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"))), + ) + + 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 not() { + val tests = 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"))))), + ) + + for ((input, expected) in tests) { + val result = preprocessor.preprocess(input) + assertEquals(expected, result, "Expected preprocessed") + assertEquals(expected::class, result::class, "Expected same class") + } + } + } +} diff --git a/tests/parser/OperatorParserTests.kt b/tests/parser/OperatorParserTests.kt new file mode 100644 index 0000000..e15e89d --- /dev/null +++ b/tests/parser/OperatorParserTests.kt @@ -0,0 +1,28 @@ +package parser + +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.Operator +import prolog.ast.terms.Structure + +class OperatorParserTests { + class OperatorParser: TermsGrammar() { + override val rootParser: Parser by operator + } + + 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'") + } +} \ No newline at end of file From 32165a90f55a047c0ee06dffcf4edf36bf1303fd Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 28 Apr 2025 12:20:03 +0200 Subject: [PATCH 06/19] Arithmetic preprocessing --- src/interpreter/Preprocessor.kt | 80 +++- src/prolog/builtins/arithmeticOperators.kt | 10 +- tests/interpreter/PreprocessorTests.kt | 476 ++++++++++++++++----- 3 files changed, 442 insertions(+), 124 deletions(-) diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index d10b605..b05c99d 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -1,24 +1,13 @@ package interpreter import io.Logger +import prolog.ast.arithmetic.Expression import prolog.ast.logic.Clause import prolog.ast.logic.Fact import prolog.ast.logic.LogicOperand import prolog.ast.logic.Rule -import prolog.ast.terms.Atom -import prolog.ast.terms.Body -import prolog.ast.terms.Goal -import prolog.ast.terms.Head -import prolog.ast.terms.Structure -import prolog.ast.terms.Term -import prolog.builtins.Conjunction -import prolog.builtins.Cut -import prolog.builtins.Disjunction -import prolog.builtins.Fail -import prolog.builtins.False -import prolog.builtins.Not -import prolog.builtins.Query -import prolog.builtins.True +import prolog.ast.terms.* +import prolog.builtins.* /** * Preprocessor for Prolog @@ -45,12 +34,14 @@ open class Preprocessor { 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 } } @@ -65,23 +56,70 @@ open class Preprocessor { Structure(Atom("fail"), emptyList()) -> Fail Atom("!") -> Cut() Structure(Atom("!"), emptyList()) -> Cut() - else -> { + is Structure -> { + // Preprocess the arguments first to recognize builtins + val args = term.arguments.map { preprocess(it) } + when { - term is Structure && term.functor == ",/2" -> { - val args = term.arguments.map { preprocess(it) } + // TODO Remove hardcoding by storing the functors as constants in operators? + // Logic + term.functor == ",/2" -> { Conjunction(args[0] as LogicOperand, args[1] as LogicOperand) } - term is Structure && term.functor == ";/2" -> { - val args = term.arguments.map { preprocess(it) } + + term.functor == ";/2" -> { Disjunction(args[0] as LogicOperand, args[1] as LogicOperand) } - term is Structure && term.functor == "\\+/1" -> { - val args = term.arguments.map { preprocess(it) } + + term.functor == "\\+/1" -> { Not(args[0] as Goal) } + // Arithmetic + 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) + } + + 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) + } + else -> term } } + + else -> term } if (prepped != term || prepped::class != term::class) { diff --git a/src/prolog/builtins/arithmeticOperators.kt b/src/prolog/builtins/arithmeticOperators.kt index 7931e7d..ca86a2e 100644 --- a/src/prolog/builtins/arithmeticOperators.kt +++ b/src/prolog/builtins/arithmeticOperators.kt @@ -62,11 +62,11 @@ class EvaluatesTo(private val left: Expression, private val right: Expression) : /** * True when Number is the value to which Expr evaluates. */ -class Is(private val left: Expression, private val right: Expression) : - Operator(Atom("is"), left, right), Satisfiable { +class Is(val number: Expression, val expr: Expression) : + Operator(Atom("is"), number, expr), Satisfiable { override fun satisfy(subs: Substitutions): Answers { - val t1 = left.simplify(subs) - val t2 = right.simplify(subs) + val t1 = number.simplify(subs) + val t2 = expr.simplify(subs) if (!atomic(t2.to, subs)) { return sequenceOf(Result.failure(IllegalArgumentException("Right operand must be instantiated"))) @@ -119,7 +119,7 @@ open class Subtract(private val expr1: Expression, private val expr2: Expression /** * Result = Expr1 * Expr2 */ -class Multiply(private val expr1: Expression, private val expr2: Expression) : +class Multiply(val expr1: Expression, val expr2: Expression) : ArithmeticOperator(Atom("*"), expr1, expr2) { override fun simplify(subs: Substitutions): Simplification { val result = Variable("Result") diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index a97755a..ed2862b 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -3,15 +3,12 @@ package interpreter import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test +import prolog.ast.arithmetic.Integer import prolog.ast.terms.Atom import prolog.ast.terms.CompoundTerm import prolog.ast.terms.Term -import prolog.builtins.Conjunction -import prolog.builtins.Disjunction -import prolog.builtins.Cut -import prolog.builtins.Fail -import prolog.builtins.True -import prolog.builtins.Not +import prolog.ast.terms.Variable +import prolog.builtins.* class PreprocessorTests { class OpenPreprocessor : Preprocessor() { @@ -20,11 +17,318 @@ class PreprocessorTests { } } + companion object { + fun test(tests: Map) { + for ((input, expected) in tests) { + val result = OpenPreprocessor().preprocess(input) + assertEquals(expected, result, "Expected preprocessed") + assertEquals(expected::class, result::class, "Expected same class") + } + } + } + @Nested class `Arithmetic operators` { @Test fun `evaluates to different`() { - assertEquals(1, 2) + 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) } } @@ -34,123 +338,99 @@ class PreprocessorTests { @Test fun fail() { - val tests = 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( + 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)) + ) ) - - 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 `true`() { - val tests = 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( + 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)) + ) ) - - 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 cut() { - val tests = 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( + 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())) + ) ) - - 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 conjunction() { - val tests = 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( + 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"))), + ) ) - - 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 disjunction() { - val tests = 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( + 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"))), + ) ) - - 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 not() { - val tests = 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( + 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"))))), + ) ) - - for ((input, expected) in tests) { - val result = preprocessor.preprocess(input) - assertEquals(expected, result, "Expected preprocessed") - assertEquals(expected::class, result::class, "Expected same class") - } } } } From 8e6a34a2317cb29e796f5228ada6058a2dba66e3 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 28 Apr 2025 13:36:24 +0200 Subject: [PATCH 07/19] Argument parsing --- build.gradle.kts | 2 ++ src/Main.kt | 42 ++++++++++++++++++---- src/interpreter/Preprocessor.kt | 2 +- src/io/GhentPrologArgParser.kt | 21 +++++++++++ src/prolog/builtins/arithmeticOperators.kt | 4 ++- 5 files changed, 63 insertions(+), 8 deletions(-) create mode 100644 src/io/GhentPrologArgParser.kt diff --git a/build.gradle.kts b/build.gradle.kts index a50fab0..ea7f816 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,8 @@ repositories { } dependencies { + // CLI argument parsing + implementation("com.xenomachina:kotlin-argparser:2.0.7") // Parser combinator library implementation("com.github.h0tk3y.betterParse:better-parse:0.4.4") diff --git a/src/Main.kt b/src/Main.kt index 1c9e0d6..a9ea4f8 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,15 +1,45 @@ +import com.sun.org.apache.bcel.internal.util.Args +import com.xenomachina.argparser.ArgParser +import com.xenomachina.argparser.mainBody import interpreter.FileLoader +import io.GhentPrologArgParser import io.Logger import prolog.Program import prolog.ast.logic.Clause import repl.Repl -fun main() { - // TODO Make this a command line argument - // Turn on debug mode - Logger.level = Logger.Level.DEBUG +fun main(args: Array) = mainBody { + // Parse command line arguments + val parsedArgs = ArgParser(args).parseInto(::GhentPrologArgParser) - FileLoader().load("tests/parser/resources/parent.pl") + parsedArgs.run { + val loader = FileLoader() - Repl().start() + // Set the verbosity level + Logger.level = verbosity + + // Check if script was provided + for (file in script) { + loader.load(file) + } + + // Check if REPL was requested + if (repl) { + Repl().start() + } else { + Logger.warn("REPL not started. Use -r or --repl to start the REPL.") + } + } +} + +fun help() { + println(""" + Ghent Prolog: A Prolog interpreter in Kotlin + + Options: + -s, --source Specify the source file to load + -r, --repl Start the REPL (default) + -v, --verb + -h, --help Show this help message + """.trimIndent()) } diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index b05c99d..a20535d 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -123,7 +123,7 @@ open class Preprocessor { } if (prepped != term || prepped::class != term::class) { - Logger.debug("Preprocessed term: $term -> $prepped (${prepped::class})") + Logger.debug("Preprocessed term: $term -> $prepped (is ${prepped::class.simpleName})") } return prepped diff --git a/src/io/GhentPrologArgParser.kt b/src/io/GhentPrologArgParser.kt new file mode 100644 index 0000000..f27f579 --- /dev/null +++ b/src/io/GhentPrologArgParser.kt @@ -0,0 +1,21 @@ +package io + +import com.xenomachina.argparser.ArgParser +import com.xenomachina.argparser.default + +class GhentPrologArgParser(parser: ArgParser) { + val script by parser.adding("-s", "--script", help = "Script to run") + val repl by parser.flagging("-r", "--repl", help = "Start the REPL") + + val verbosity by parser.mapping( + "--vvv" to Logger.Level.DEBUG, + "--debug" to Logger.Level.DEBUG, + "--vv" to Logger.Level.INFO, + "--verbose" to Logger.Level.INFO, + "--info" to Logger.Level.INFO, + "-v" to Logger.Level.WARN, + "--warn" to Logger.Level.WARN, + "--error" to Logger.Level.ERROR, + help = "Set the verbosity level (default: WARN)", + ).default(Logger.defaultLevel) +} \ No newline at end of file diff --git a/src/prolog/builtins/arithmeticOperators.kt b/src/prolog/builtins/arithmeticOperators.kt index ca86a2e..306337e 100644 --- a/src/prolog/builtins/arithmeticOperators.kt +++ b/src/prolog/builtins/arithmeticOperators.kt @@ -144,7 +144,7 @@ class Divide(private val expr1: Expression, private val expr2: Expression) : // TODO Expr rem Expr class Between(private val expr1: Expression, private val expr2: Expression, private val expr3: Expression) : - Operator(Atom("between"), expr1, expr2) { + CompoundTerm(Atom("between"), listOf(expr1, expr2, expr3)), Satisfiable { override fun satisfy(subs: Substitutions): Answers { val e1 = expr1.simplify(subs) val e2 = expr2.simplify(subs) @@ -165,4 +165,6 @@ class Between(private val expr1: Expression, private val expr2: Expression, priv } } } + + override fun toString(): String = "$expr1..$expr3..$expr2" } From 0a32797df14687670337ad4e7545663decfd8212 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Tue, 29 Apr 2025 15:47:00 +0200 Subject: [PATCH 08/19] test: Rule met cut --- tests/prolog/builtins/ControlOperatorsTests.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt index 15ad926..2ffc38c 100644 --- a/tests/prolog/builtins/ControlOperatorsTests.kt +++ b/tests/prolog/builtins/ControlOperatorsTests.kt @@ -17,6 +17,24 @@ class ControlOperatorsTests { Program.clear() } + @Test + fun `rule with cut as body`() { + Program.load( + listOf( + Rule(Atom("foo"), Cut()), + Fact(Atom("foo")) + ) + ) + + val goal = Atom("foo") + + val result = Program.query(goal).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + assertTrue(result[0].getOrNull()!!.isEmpty(), "Expected empty substitutions") + } + // See also: https://stackoverflow.com/a/23292126 @Test From bfb509f41f349b463d142de0ceaf5f4d53302dc2 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 30 Apr 2025 09:18:46 +0200 Subject: [PATCH 09/19] Arithmetic ops parsing --- src/parser/grammars/TermsGrammar.kt | 41 ++++++++++++++++++++--------- src/parser/grammars/Tokens.kt | 4 ++- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 9a26f77..c57ab22 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -7,18 +7,12 @@ import com.github.h0tk3y.betterParse.combinators.unaryMinus import com.github.h0tk3y.betterParse.combinators.use import com.github.h0tk3y.betterParse.grammar.parser import com.github.h0tk3y.betterParse.parser.Parser +import prolog.ast.arithmetic.ArithmeticOperator +import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.logic.LogicOperand -import prolog.ast.logic.LogicOperator -import prolog.ast.terms.Atom -import prolog.ast.terms.Body -import prolog.ast.terms.Head -import prolog.ast.terms.Operator -import prolog.ast.terms.Structure -import prolog.ast.terms.Term -import prolog.ast.terms.Variable -import prolog.builtins.Conjunction +import prolog.ast.terms.* open class TermsGrammar : Tokens() { // Basic named terms @@ -37,19 +31,40 @@ open class TermsGrammar : Tokens() { protected val float: Parser by floatToken use { Float(text.toFloat()) } // Operators + protected val logOps: Parser by (dummy + or comma + or semicolon + ) use { this.text } protected val simpleLogicOperand: Parser by (dummy or compound or atom ) protected val logicOperand: Parser by (dummy - or parser(::operator) + or parser(::logicOperator) or simpleLogicOperand ) - protected val logicOperator: Parser by (simpleLogicOperand * -comma * logicOperand) use { - Conjunction(t1, t2) + protected val logicOperator: Parser by (simpleLogicOperand * logOps * logicOperand) use { + CompoundTerm(Atom(t2), listOf(t1, t3)) } - protected val operator: Parser by (dummy + + protected val arithmeticOps: Parser by (dummy + or plus + ) use { this.text } + protected val simpleArithmeticOperand: Parser by (dummy + or int + or float + ) + protected val arithmeticOperand: Parser by (dummy + or parser(::arithmeticOperator) + or simpleArithmeticOperand + ) use { this as Expression } + protected val arithmeticOperator: Parser by (simpleArithmeticOperand * arithmeticOps * arithmeticOperand) use { + CompoundTerm(Atom(t2), listOf(t1, t3)) + } + + protected val operator: Parser by (dummy or logicOperator + or arithmeticOperator ) // Parts diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index b2e8fd7..49afbee 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -18,9 +18,11 @@ abstract class Tokens : Grammar() { // 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 comma: Token by literalToken(",") + protected val semicolon: Token by literalToken(";") + protected val plus: Token by literalToken("+") protected val dot by literalToken(".") // Ignored tokens From 1e087c8339eaa03d7174276288572528f189a977 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 30 Apr 2025 10:52:58 +0200 Subject: [PATCH 10/19] Fix double printing --- src/Main.kt | 3 --- src/interpreter/Preprocessor.kt | 7 +++++++ src/repl/Repl.kt | 8 +++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Main.kt b/src/Main.kt index a9ea4f8..cb40600 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,11 +1,8 @@ -import com.sun.org.apache.bcel.internal.util.Args import com.xenomachina.argparser.ArgParser import com.xenomachina.argparser.mainBody import interpreter.FileLoader import io.GhentPrologArgParser import io.Logger -import prolog.Program -import prolog.ast.logic.Clause import repl.Repl fun main(args: Array) = mainBody { diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index a20535d..7ee95d0 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -2,6 +2,7 @@ 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 @@ -56,6 +57,8 @@ open class Preprocessor { Structure(Atom("fail"), emptyList()) -> Fail Atom("!") -> Cut() Structure(Atom("!"), emptyList()) -> Cut() + Atom("inf") -> Integer(Int.MAX_VALUE) + Atom("nl") -> Nl is Structure -> { // Preprocess the arguments first to recognize builtins val args = term.arguments.map { preprocess(it) } @@ -115,6 +118,10 @@ open class Preprocessor { Between(args[0] as Expression, args[1] as Expression, args[2] as Expression) } + // Other + term.functor == "write/1" -> Write(args[0]) + term.functor == "read/1" -> Read(args[0]) + else -> term } } diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 52cce29..5704cdc 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -24,19 +24,21 @@ class Repl { } fun query(): Answers { - val queryString = io.prompt("?-", { "" }) + val queryString = io.prompt("?-", { "| " }) val simpleQuery = parser.parse(queryString) val query = preprocessor.preprocess(simpleQuery) + Logger.debug("Satisfying query: $query") return query.satisfy(emptyMap()) } fun printAnswers(answers: Answers) { val knownCommands = setOf(";", "a", ".", "h") - if (answers.none()) { + val iterator = answers.iterator() + + if (!iterator.hasNext()) { io.say("false.") } else { - val iterator = answers.iterator() var previous = iterator.next() io.say(prettyPrint(previous)) From 43b364044e0ce561a820e98412d4634c863f82a8 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Wed, 30 Apr 2025 12:08:36 +0200 Subject: [PATCH 11/19] Quoted atoms --- src/parser/grammars/TermsGrammar.kt | 10 +++- src/parser/grammars/Tokens.kt | 5 ++ tests/parser/OperatorParserTests.kt | 5 +- tests/parser/grammars/LogicGrammarTests.kt | 57 +++++++++++----------- tests/parser/grammars/TermsGrammarTests.kt | 20 ++++++++ 5 files changed, 65 insertions(+), 32 deletions(-) diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index c57ab22..9bac47c 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -7,7 +7,6 @@ import com.github.h0tk3y.betterParse.combinators.unaryMinus import com.github.h0tk3y.betterParse.combinators.use import com.github.h0tk3y.betterParse.grammar.parser import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.arithmetic.ArithmeticOperator import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer @@ -15,9 +14,16 @@ import prolog.ast.logic.LogicOperand import prolog.ast.terms.* open class TermsGrammar : Tokens() { + // Basic named terms protected val variable: Parser by variableToken use { Variable(text) } - protected val atom: Parser by nameToken use { Atom(text) } + protected val simpleAtom: Parser by nameToken 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(::term), comma, diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index 49afbee..14c5be9 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -30,5 +30,10 @@ abstract class Tokens : Grammar() { 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") } } \ No newline at end of file diff --git a/tests/parser/OperatorParserTests.kt b/tests/parser/OperatorParserTests.kt index e15e89d..8cfdb95 100644 --- a/tests/parser/OperatorParserTests.kt +++ b/tests/parser/OperatorParserTests.kt @@ -7,15 +7,16 @@ 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 operator + override val rootParser: Parser by operator } - private var parser = OperatorParser() as Grammar + private var parser = OperatorParser() as Grammar @Test fun `parse conjunction`() { diff --git a/tests/parser/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index 5365e9f..9afb439 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -3,6 +3,7 @@ package parser.grammars import com.github.h0tk3y.betterParse.grammar.Grammar import com.github.h0tk3y.betterParse.grammar.parseToEnd import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest @@ -37,9 +38,9 @@ class LogicGrammarTests { fun `parse simple fact`(input: String) { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 fact") - Assertions.assertTrue(result[0] is Fact, "Expected a fact") - Assertions.assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'") + assertEquals(1, result.size, "Expected 1 fact") + assertTrue(result[0] is Fact, "Expected a fact") + assertEquals(input, "${result[0].toString()}.", "Expected fact to be '$input'") } @ParameterizedTest @@ -52,9 +53,9 @@ class LogicGrammarTests { fun `parse multiple facts`(input: String) { val result = parser.parseToEnd(input) - Assertions.assertEquals(2, result.size, "Expected 2 facts") - Assertions.assertTrue(result[0] is Fact, "Expected a fact") - Assertions.assertTrue(result[1] is Fact, "Expected a fact") + assertEquals(2, result.size, "Expected 2 facts") + assertTrue(result[0] is Fact, "Expected a fact") + assertTrue(result[1] is Fact, "Expected a fact") } @Test @@ -63,9 +64,9 @@ class LogicGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 rule") - Assertions.assertTrue(result[0] is Rule, "Expected a rule") - Assertions.assertEquals("a :- b", result[0].toString()) + assertEquals(1, result.size, "Expected 1 rule") + assertTrue(result[0] is Rule, "Expected a rule") + assertEquals("a :- b", result[0].toString()) } @ParameterizedTest @@ -76,8 +77,8 @@ class LogicGrammarTests { fun `parse simple rule`(input: String) { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 rule") - Assertions.assertTrue(result[0] is Rule, "Expected a rule") + assertEquals(1, result.size, "Expected 1 rule") + assertTrue(result[0] is Rule, "Expected a rule") } @Test @@ -86,22 +87,22 @@ class LogicGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 rule") - Assertions.assertTrue(result[0] is Rule, "Expected a rule") + assertEquals(1, result.size, "Expected 1 rule") + assertTrue(result[0] is Rule, "Expected a rule") val rule = result[0] as Rule - Assertions.assertTrue(rule.head is Structure, "Expected head to be a structure") + assertTrue(rule.head is Structure, "Expected head to be a structure") val head = rule.head as Structure - Assertions.assertEquals("parent/2", head.functor, "Expected functor 'parent/2'") - Assertions.assertEquals(Variable("X"), head.arguments[0], "Expected first argument 'X'") - Assertions.assertEquals(Variable("Y"), head.arguments[1], "Expected second argument 'Y'") + assertEquals("parent/2", head.functor, "Expected functor 'parent/2'") + assertEquals(Variable("X"), head.arguments[0], "Expected first argument 'X'") + assertEquals(Variable("Y"), head.arguments[1], "Expected second argument 'Y'") - Assertions.assertTrue(rule.body is Structure, "Expected body to be a structure") + assertTrue(rule.body is Structure, "Expected body to be a structure") val body = rule.body as Structure - Assertions.assertEquals("father/2", body.functor, "Expected functor 'father/2'") - Assertions.assertEquals(Variable("X"), body.arguments[0], "Expected first argument 'X'") - Assertions.assertEquals(Variable("Y"), body.arguments[1], "Expected second argument 'Y'") + assertEquals("father/2", body.functor, "Expected functor 'father/2'") + assertEquals(Variable("X"), body.arguments[0], "Expected first argument 'X'") + assertEquals(Variable("Y"), body.arguments[1], "Expected second argument 'Y'") } @Test @@ -110,10 +111,10 @@ class LogicGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 rule") - Assertions.assertInstanceOf(Rule::class.java, result[0], "Expected a rule") + assertEquals(1, result.size, "Expected 1 rule") + assertInstanceOf(Rule::class.java, result[0], "Expected a rule") val rule = result[0] as Rule - Assertions.assertInstanceOf(Conjunction::class.java, rule.body, "Expected body to be a conjunction") + assertInstanceOf(CompoundTerm::class.java, rule.body, "Expected body to be a compound term") } @Test @@ -122,10 +123,10 @@ class LogicGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertEquals(1, result.size, "Expected 1 rule") + assertEquals(1, result.size, "Expected 1 rule") val rule = result[0] as Rule - Assertions.assertTrue(rule.body is Conjunction, "Expected body to be a conjunction") - val conjunction = rule.body as Conjunction - Assertions.assertEquals("invited/2", (conjunction.left as CompoundTerm).functor, "Expected functor 'invited/2'") + assertInstanceOf(CompoundTerm::class.java, rule.body, "Expected body to be a conjunction") + val conjunction = rule.body as CompoundTerm + assertEquals("invited/2", (conjunction.arguments[0] as CompoundTerm).functor, "Expected functor 'invited/2'") } } \ No newline at end of file diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt index 339c7c2..203eac2 100644 --- a/tests/parser/grammars/TermsGrammarTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -31,6 +31,26 @@ class TermsGrammarTests { Assertions.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) + + Assertions.assertEquals(Atom(expected), result, "Expected atom") + } + @ParameterizedTest @ValueSource(strings = ["X", "X1", "X_1"]) fun `parse variable`(name: String) { From 9db1c66781147869229aa39ee8fa2fa3c99895b9 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 1 May 2025 17:13:35 +0200 Subject: [PATCH 12/19] Checkpoint --- build.gradle.kts | 36 ++-- examples/scratchpad.pl | 24 ++- src/Main.kt | 16 +- src/gpl | 2 +- src/interpreter/FileLoader.kt | 13 +- src/interpreter/Preprocessor.kt | 24 ++- src/io/Logger.kt | 15 +- src/io/Terminal.kt | 8 + src/parser/ScriptParser.kt | 8 +- src/parser/grammars/LogicGrammar.kt | 15 +- src/parser/grammars/TermsGrammar.kt | 49 ++---- src/parser/grammars/Tokens.kt | 2 + src/prolog/Program.kt | 82 +++------ src/prolog/Substitution.kt | 2 +- src/prolog/ast/Database.kt | 76 ++++++++ src/prolog/ast/arithmetic/Float.kt | 11 ++ src/prolog/ast/logic/Clause.kt | 47 +++-- src/prolog/ast/logic/Predicate.kt | 1 + src/prolog/ast/terms/Variable.kt | 13 +- src/prolog/builtins/io.kt | 4 + src/prolog/builtins/other.kt | 4 + src/prolog/builtins/unificationOperators.kt | 5 + src/prolog/logic/terms.kt | 53 ++++++ src/prolog/logic/unification.kt | 8 +- src/repl/Repl.kt | 22 ++- tests/compare.sh | 53 ++++++ tests/e2e/myClass.kt | 4 + tests/interpreter/PreprocessorTests.kt | 4 +- tests/interpreter/SourceFileReaderTests.kt | 4 - tests/parser/OperatorParserTests.kt | 13 ++ tests/parser/grammars/LogicGrammarTests.kt | 12 ++ tests/parser/grammars/TermsGrammarTests.kt | 15 +- tests/prolog/EvaluationTests.kt | 186 +++++++++++++++++++- tests/prolog/logic/TermsTests.kt | 109 ++++++++++++ 34 files changed, 746 insertions(+), 194 deletions(-) create mode 100644 src/prolog/ast/Database.kt create mode 100644 tests/compare.sh create mode 100644 tests/e2e/myClass.kt create mode 100644 tests/prolog/logic/TermsTests.kt diff --git a/build.gradle.kts b/build.gradle.kts index ea7f816..cd901a3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -28,20 +28,26 @@ sourceSets { } } -tasks { - withType { - manifest { - attributes["Main-Class"] = "MainKt" - } - from(configurations.runtimeClasspath.get().map { - if (it.isDirectory) it else zipTree(it) - }) - } - - test { - useJUnitPlatform() - testLogging { - events("passed", "skipped", "failed") - } +tasks.named("test") { + useJUnitPlatform() + testLogging { + events("passed", "skipped", "failed") + } +} + +tasks.register("fatJar") { + manifest { + attributes["Main-Class"] = "MainKt" + } + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(configurations.runtimeClasspath.get().map { + if (it.isDirectory) it else zipTree(it) + }) + with(tasks.jar.get() as CopySpec) +} + +tasks { + build { + dependsOn("fatJar") } } diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl index 357e09b..b122c4f 100644 --- a/examples/scratchpad.pl +++ b/examples/scratchpad.pl @@ -1 +1,23 @@ -choice(X) :- X = 1, !; X = 2. +% choice(X) :- X = 1, !; X = 2. +grade(alice, a). +grade(bob, b). +grade(carol, a). +grade(dave, c). + +got_an_a(Student) :- + grade(Student, Grade), + Grade = a. + +did_not_get_an_a(Student) :- + grade(Student, Grade), + Grade \= a. + +:- initialization(main). + +main :- + write("While "), + got_an_a(X), + write(X), write(" got an A, "), fail; + write("but "), + did_not_get_an_a(Y), + write(Y), write(" did not get an A, "), fail; write("unfortunately."), nl. diff --git a/src/Main.kt b/src/Main.kt index cb40600..55a12bf 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -5,7 +5,7 @@ import io.GhentPrologArgParser import io.Logger import repl.Repl -fun main(args: Array) = mainBody { +fun main(args: Array) { // Parse command line arguments val parsedArgs = ArgParser(args).parseInto(::GhentPrologArgParser) @@ -22,21 +22,9 @@ fun main(args: Array) = mainBody { // Check if REPL was requested if (repl) { - Repl().start() + Repl() } else { Logger.warn("REPL not started. Use -r or --repl to start the REPL.") } } } - -fun help() { - println(""" - Ghent Prolog: A Prolog interpreter in Kotlin - - Options: - -s, --source Specify the source file to load - -r, --repl Start the REPL (default) - -v, --verb - -h, --help Show this help message - """.trimIndent()) -} diff --git a/src/gpl b/src/gpl index c1eecb1..714a5a2 100755 --- a/src/gpl +++ b/src/gpl @@ -20,7 +20,7 @@ fi if [ ! -f "${JAR_PATH}" ]; then printf 'Info: JAR file not found at "%s"\n' "${JAR_PATH}" printf 'Info: Building the project...\n' - ./gradlew build + ./gradlew fatJar if [ "${?}" -ne 0 ]; then printf 'Error: Build failed\n' exit 1 diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt index 2cd7022..2121fd7 100644 --- a/src/interpreter/FileLoader.kt +++ b/src/interpreter/FileLoader.kt @@ -2,22 +2,27 @@ package interpreter import io.Logger import parser.ScriptParser +import prolog.ast.Database import prolog.Program import prolog.ast.logic.Clause class FileLoader { private val parser = ScriptParser() - fun load(filePath: String): () -> Unit { + 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) - Program.load(clauses) + val db = Database(filePath) + db.load(clauses) + Program.add(db) + db.initialize() - // TODO Pass next commands to execute - return {} + Logger.debug("Finished loading file: $filePath") } fun readFile(filePath: String): String { diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 7ee95d0..031cab9 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -47,7 +47,7 @@ open class Preprocessor { } } - protected open fun preprocess(term: Term): Term { + protected open fun preprocess(term: Term, nested: Boolean = false): Term { val prepped = when (term) { Atom("true") -> True Structure(Atom("true"), emptyList()) -> True @@ -61,7 +61,7 @@ open class Preprocessor { Atom("nl") -> Nl is Structure -> { // Preprocess the arguments first to recognize builtins - val args = term.arguments.map { preprocess(it) } + val args = term.arguments.map { preprocess(it, nested = true) } when { // TODO Remove hardcoding by storing the functors as constants in operators? @@ -77,7 +77,7 @@ open class Preprocessor { term.functor == "\\+/1" -> { Not(args[0] as Goal) } - // Arithmetic + term.functor == "=\\=/2" && args.all { it is Expression } -> { EvaluatesToDifferent(args[0] as Expression, args[1] as Expression) } @@ -90,6 +90,16 @@ open class Preprocessor { Is(args[0] as Expression, args[1] as Expression) } + // Arithmetic + + term.functor == "=/2" && args.all { it is Expression } -> { + Unify(args[0] as Expression, args[1] as Expression) + } + + term.functor == "\\=/2" && args.all { it is Expression } -> { + NotUnify(args[0] as Expression, args[1] as Expression) + } + term.functor == "-/1" && args.all { it is Expression } -> { Negate(args[0] as Expression) } @@ -121,6 +131,7 @@ open class Preprocessor { // 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 } @@ -129,9 +140,10 @@ open class Preprocessor { else -> term } - if (prepped != term || prepped::class != term::class) { - Logger.debug("Preprocessed term: $term -> $prepped (is ${prepped::class.simpleName})") - } + Logger.debug( + "Preprocessed term $term into $prepped (kind ${prepped::class.simpleName})", + !nested && (prepped != term || prepped::class != term::class) + ) return prepped } diff --git a/src/io/Logger.kt b/src/io/Logger.kt index a0db3cc..ac9de5a 100644 --- a/src/io/Logger.kt +++ b/src/io/Logger.kt @@ -8,17 +8,18 @@ object Logger { val defaultLevel: Level = Level.WARN var level: Level = defaultLevel - private val io: IoHandler = Terminal() + private val io = Terminal() - fun log(message: String, messageLevel: Level = defaultLevel) { - if (level <= messageLevel) { + 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) = 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) + 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 index ba12468..c75a3eb 100644 --- a/src/io/Terminal.kt +++ b/src/io/Terminal.kt @@ -1,5 +1,6 @@ package io +import prolog.Program import java.io.BufferedReader import java.io.BufferedWriter import java.io.InputStream @@ -60,4 +61,11 @@ class Terminal( error.close() System.exit(0) } + + fun checkNewLine() { + if (Program.storeNewLine) { + say("\n") + Program.storeNewLine = false + } + } } diff --git a/src/parser/ScriptParser.kt b/src/parser/ScriptParser.kt index a61a28c..f95731c 100644 --- a/src/parser/ScriptParser.kt +++ b/src/parser/ScriptParser.kt @@ -2,11 +2,17 @@ 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 = grammar.parseToEnd(input) + 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/grammars/LogicGrammar.kt b/src/parser/grammars/LogicGrammar.kt index 10c3b82..396aa50 100644 --- a/src/parser/grammars/LogicGrammar.kt +++ b/src/parser/grammars/LogicGrammar.kt @@ -1,22 +1,21 @@ package parser.grammars -import com.github.h0tk3y.betterParse.combinators.oneOrMore -import com.github.h0tk3y.betterParse.combinators.or -import com.github.h0tk3y.betterParse.combinators.separated -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.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 fact) * -dot) + protected val clause: Parser by ((rule or constraint or fact) * -dot) protected val clauses: Parser> by oneOrMore(clause) override val rootParser: Parser by clauses -} \ No newline at end of file +} diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 9bac47c..52d4ef0 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -1,16 +1,10 @@ package parser.grammars -import com.github.h0tk3y.betterParse.combinators.or -import com.github.h0tk3y.betterParse.combinators.separated -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.combinators.* import com.github.h0tk3y.betterParse.grammar.parser import com.github.h0tk3y.betterParse.parser.Parser -import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer -import prolog.ast.logic.LogicOperand import prolog.ast.terms.* open class TermsGrammar : Tokens() { @@ -37,42 +31,32 @@ open class TermsGrammar : Tokens() { protected val float: Parser by floatToken use { Float(text.toFloat()) } // Operators - protected val logOps: Parser by (dummy + protected val ops: Parser by (dummy + // Logic or comma or semicolon + // Arithmetic + or plus + or equals + or notEquals ) use { this.text } - protected val simpleLogicOperand: Parser by (dummy + protected val simpleOperand: Parser by (dummy + // Logic or compound or atom - ) - protected val logicOperand: Parser by (dummy - or parser(::logicOperator) - or simpleLogicOperand - ) - protected val logicOperator: Parser by (simpleLogicOperand * logOps * logicOperand) use { - CompoundTerm(Atom(t2), listOf(t1, t3)) - } - - protected val arithmeticOps: Parser by (dummy - or plus - ) use { this.text } - protected val simpleArithmeticOperand: Parser by (dummy + or variable + // Arithmetic or int or float ) - protected val arithmeticOperand: Parser by (dummy - or parser(::arithmeticOperator) - or simpleArithmeticOperand - ) use { this as Expression } - protected val arithmeticOperator: Parser by (simpleArithmeticOperand * arithmeticOps * arithmeticOperand) use { + protected val operand: Parser by (dummy + or parser(::operator) + or simpleOperand + ) + protected val operator: Parser by (simpleOperand * ops * operand) use { CompoundTerm(Atom(t2), listOf(t1, t3)) } - protected val operator: Parser by (dummy - or logicOperator - or arithmeticOperator - ) - // Parts protected val head: Parser by (dummy or compound @@ -81,6 +65,7 @@ open class TermsGrammar : Tokens() { protected val body: Parser by (dummy or operator or head + or variable ) use { this as Body } protected val term: Parser by (dummy diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index 14c5be9..cf55e09 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -22,6 +22,8 @@ abstract class Tokens : Grammar() { protected val rightParenthesis: Token by literalToken(")") protected val comma: Token by literalToken(",") protected val semicolon: Token by literalToken(";") + protected val equals: Token by literalToken("=") + protected val notEquals: Token by literalToken("\\=") protected val plus: Token by literalToken("+") protected val dot by literalToken(".") diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index 7bf9492..87e787e 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -1,32 +1,25 @@ package prolog import io.Logger +import prolog.ast.Database 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 to handle execution + * + * This object is a singleton that manages a list of databases. */ -object Program: Resolvent { - var predicates: Map = emptyMap() +object Program : Resolvent { + private val internalDb = Database("") + private val databases: MutableList = mutableListOf(internalDb) - init { - Logger.debug("Initializing ${this::class.java.simpleName}") - setup() - Logger.debug("Initialization of ${this::class.java.simpleName} complete") - } + var storeNewLine: Boolean = false + var variableRenamingStart: Int = 0 - private fun setup() { - Logger.debug("Setting up ${this::class.java.simpleName}") - - // Initialize the program with built-in predicates - load(listOf( - )) + fun add(database: Database) { + databases.add(database) } /** @@ -35,51 +28,24 @@ object Program: Resolvent { */ fun query(goal: Goal): Answers = solve(goal, emptyMap()) - override fun solve(goal: Goal, subs: Substitutions): Answers { + override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { Logger.debug("Solving goal $goal") - 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))) - } - - Logger.debug("Loaded clause $clause into predicate $functor") + for (database in databases) { + yieldAll(database.solve(goal, subs)) } } - 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 load(clauses: List) = internalDb.load(clauses) fun clear() { - Logger.debug("Clearing ${this::class.java.simpleName}") - predicates = emptyMap() - setup() + databases.forEach { it.clear() } + } + + fun clear(filePath: String) { + val correspondingDBs = databases.filter { it.sourceFile == filePath } + + require(correspondingDBs.isNotEmpty()) { "No database found for file: $filePath" } + + correspondingDBs.forEach { it.clear() } } } \ No newline at end of file diff --git a/src/prolog/Substitution.kt b/src/prolog/Substitution.kt index e9fb28b..062d63e 100644 --- a/src/prolog/Substitution.kt +++ b/src/prolog/Substitution.kt @@ -4,7 +4,7 @@ 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 diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt new file mode 100644 index 0000000..e295e39 --- /dev/null +++ b/src/prolog/ast/Database.kt @@ -0,0 +1,76 @@ +package prolog.ast + +import io.Logger +import prolog.Program +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 + */ +class Database(val sourceFile: String): Resolvent { + private var predicates: Map = emptyMap() + + fun initialize() { + 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() + } + } + } + + 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))) + } + + Logger.debug("Loaded clause $clause into predicate $functor") + } + } + + 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() { + Logger.debug("Clearing ${this::class.java.simpleName}") + predicates = emptyMap() + } +} \ No newline at end of file diff --git a/src/prolog/ast/arithmetic/Float.kt b/src/prolog/ast/arithmetic/Float.kt index 3bbf694..da49530 100644 --- a/src/prolog/ast/arithmetic/Float.kt +++ b/src/prolog/ast/arithmetic/Float.kt @@ -31,4 +31,15 @@ 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() + } } \ No newline at end of file diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index 58b2eb4..3bf4138 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -1,13 +1,13 @@ package prolog.ast.logic import prolog.Answers +import prolog.Program import prolog.Substitutions -import prolog.ast.terms.Body -import prolog.ast.terms.Functor -import prolog.ast.terms.Goal -import prolog.ast.terms.Head +import prolog.ast.terms.* import prolog.builtins.True import prolog.flags.AppliedCut +import prolog.logic.applySubstitution +import prolog.logic.numbervars import prolog.logic.unifyLazy /** @@ -21,23 +21,39 @@ import prolog.logic.unifyLazy 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. - unifyLazy(goal, head, subs).forEach { headAnswer -> - headAnswer.map { newHeadSubs -> + + // Since we are only interested in substitutions in the goal (as opposed to the head of this clause), + // we can use variable renaming and filter out the substitutions that are not in the goal. + val (end, renamed: Substitutions) = numbervars(head, Program.variableRenamingStart, subs) + + val reverse = renamed.entries.associate { (a, b) -> b to a } + Program.variableRenamingStart = end + + var newSubs: Substitutions = subs + renamed + unifyLazy(goal, head, newSubs).forEach { headAnswer -> + headAnswer.map { headSubs -> // If the body can be proven, yield the (combined) substitutions - body.satisfy(subs + newHeadSubs).forEach { bodyAnswer -> + newSubs = subs + renamed + headSubs + body.satisfy(newSubs).forEach { bodyAnswer -> bodyAnswer.fold( - onSuccess = { newBodySubs -> - yield(Result.success(newHeadSubs + newBodySubs)) + onSuccess = { bodySubs -> + var result = (headSubs + bodySubs) + .mapKeys { reverse[it.key] ?: it.key } + .mapValues { reverse[it.value] ?: it.value } + result = result.map { it.key to applySubstitution(it.value, result) } + .toMap() + .filterNot { it.key in renamed.keys } + yield(Result.success(result)) }, onFailure = { error -> if (error is AppliedCut) { // Find single solution and return immediately if (error.subs != null) { - yield(Result.failure(AppliedCut(newHeadSubs + error.subs))) + yield(Result.failure(AppliedCut(headSubs + error.subs))) } else { yield(Result.failure(AppliedCut())) } @@ -52,10 +68,5 @@ abstract class Clause(val head: Head, val body: Body) : Resolvent { } } - override fun toString(): String { - return when { - body is True -> head.toString() - else -> "$head :- $body" - } - } -} \ No newline at end of file + override fun toString(): String = if (body is True) head.toString() else "$head :- $body" +} diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 5bd9c17..236f286 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -51,6 +51,7 @@ class Predicate : Resolvent { 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 -> diff --git a/src/prolog/ast/terms/Variable.kt b/src/prolog/ast/terms/Variable.kt index 713c44d..0ef7ee6 100644 --- a/src/prolog/ast/terms/Variable.kt +++ b/src/prolog/ast/terms/Variable.kt @@ -1,10 +1,11 @@ package prolog.ast.terms +import prolog.Answers import prolog.Substitutions import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Simplification -data class Variable(val name: String) : Term, Expression { +data class Variable(val name: String) : Term, Body, 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 @@ -16,5 +17,15 @@ data class Variable(val name: String) : Term, Expression { return Simplification(this, result) } + override fun satisfy(subs: Substitutions): Answers { + // If the variable is bound, satisfy the bound term + if (this in subs) { + val boundTerm = subs[this]!! as Body + return boundTerm.satisfy(subs) + } + + return sequenceOf(Result.failure(IllegalArgumentException("Unbound variable: $this"))) + } + override fun toString(): String = name } \ No newline at end of file diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/io.kt index d04f74d..3f9aaae 100644 --- a/src/prolog/builtins/io.kt +++ b/src/prolog/builtins/io.kt @@ -4,6 +4,7 @@ import io.Logger import io.Terminal import parser.ReplParser import prolog.Answers +import prolog.Program import prolog.Substitutions import prolog.ast.logic.Satisfiable import prolog.ast.terms.Atom @@ -21,6 +22,8 @@ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satis Terminal().say(t.toString()) + Program.storeNewLine = true + return sequenceOf(Result.success(emptyMap())) } } @@ -31,6 +34,7 @@ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satis object Nl : Atom("nl"), Satisfiable { override fun satisfy(subs: Substitutions): Answers { Terminal().say("\n") + Program.storeNewLine = false return sequenceOf(Result.success(emptyMap())) } } diff --git a/src/prolog/builtins/other.kt b/src/prolog/builtins/other.kt index 9321880..8609473 100644 --- a/src/prolog/builtins/other.kt +++ b/src/prolog/builtins/other.kt @@ -6,6 +6,10 @@ 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) +} + class Query(val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) { override fun satisfy(subs: Substitutions): Answers = query.satisfy(subs) } diff --git a/src/prolog/builtins/unificationOperators.kt b/src/prolog/builtins/unificationOperators.kt index fc40df7..892c616 100644 --- a/src/prolog/builtins/unificationOperators.kt +++ b/src/prolog/builtins/unificationOperators.kt @@ -26,6 +26,11 @@ class Unify(private val term1: Term, private val term2: Term): Operator(Atom("=" } } +class NotUnify(term1: Term, term2: Term) : Operator(Atom("\\="), term1, term2) { + private val not = Not(Unify(term1, term2)) + override fun satisfy(subs: Substitutions): Answers = not.satisfy(subs) +} + class Equivalent(private val term1: Term, private val term2: Term) : Operator(Atom("=="), term1, term2) { override fun satisfy(subs: Substitutions): Answers = sequence { val t1 = applySubstitution(term1, subs) diff --git a/src/prolog/logic/terms.kt b/src/prolog/logic/terms.kt index 6bf2665..8d1eeb0 100644 --- a/src/prolog/logic/terms.kt +++ b/src/prolog/logic/terms.kt @@ -1,7 +1,10 @@ package prolog.logic +import prolog.Substitutions import prolog.ast.terms.Atom +import prolog.ast.terms.Structure import prolog.ast.terms.Term +import prolog.ast.terms.Variable /** * True when Term is a term with functor Name/Arity. If Term is a variable it is unified with a new term whose @@ -20,3 +23,53 @@ fun functor(term: Term, name: Atom, arity: Int): Boolean { // TODO Implement return true } + +/** + * Unify the free variables in Term with a term $VAR(N), where N is the number of the variable. + * Counting starts at Start. + * End is unified with the number that should be given to the next variable. + * + * Source: [SWI-Prolog Predicate numbervars/3](https://www.swi-prolog.org/pldoc/man?predicate=numbervars/3) + * + * @return Pair of the next number and only the new substitutions of variables to $VAR(N) + */ +fun numbervars( + term: Term, + start: Int = 0, + subs: Substitutions = emptyMap(), + sessionSubs: Substitutions = emptyMap() +): Pair { + when { + variable(term, subs) -> { + // All instances of the same variable are unified with the same term + if (term in sessionSubs) { + return Pair(start, emptyMap()) + } + + val from = term as Variable + var suggestedName = "${from.name}($start)" + // If the suggested name is already in use, find a new one + while ((subs + sessionSubs).filter { (it.key as Variable).name == suggestedName }.isNotEmpty()) { + 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 = term 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 08b4d1c..68242b1 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -36,7 +36,7 @@ fun applySubstitution(expr: Expression, subs: Substitutions): Expression = when } // Check if a variable occurs in a term -private fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean = when { +fun occurs(variable: Variable, term: Term, subs: Substitutions): Boolean = when { variable(term, subs) -> term == variable atomic(term, subs) -> false compound(term, subs) -> { @@ -53,18 +53,18 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence val t2 = applySubstitution(term2, subs) when { - equivalent(t1, t2, subs) -> yield(Result.success(subs)) + equivalent(t1, t2, subs) -> yield(Result.success(emptyMap())) variable(t1, subs) -> { val variable = t1 as Variable if (!occurs(variable, t2, subs)) { - yield(Result.success(subs + (variable to t2))) + yield(Result.success(mapOf(term1 to t2))) } } variable(t2, subs) -> { val variable = t2 as Variable if (!occurs(variable, t1, subs)) { - yield(Result.success(subs + (variable to t1))) + yield(Result.success(mapOf(term2 to t1))) } } diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index 5704cdc..a7c684a 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -6,14 +6,15 @@ import io.Terminal import parser.ReplParser import prolog.Answer import prolog.Answers +import prolog.Program class Repl { private val io = Terminal() private val parser = ReplParser() private val preprocessor = Preprocessor() - fun start() { - io.say("Prolog REPL. Type '^D' to quit.\n") + init { + welcome() while (true) { try { printAnswers(query()) @@ -23,15 +24,19 @@ class Repl { } } - fun query(): Answers { + 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) - Logger.debug("Satisfying query: $query") return query.satisfy(emptyMap()) } - fun printAnswers(answers: Answers) { + private fun printAnswers(answers: Answers) { val knownCommands = setOf(";", "a", ".", "h") val iterator = answers.iterator() @@ -68,7 +73,7 @@ class Repl { io.say("\n") } - fun help(): String { + private fun help(): String { io.say("Commands:\n") io.say(" ; find next solution\n") io.say(" a abort\n") @@ -77,12 +82,13 @@ class Repl { return "" } - fun prettyPrint(result: Answer): String { + private fun prettyPrint(result: Answer): String { result.fold( onSuccess = { val subs = result.getOrNull()!! if (subs.isEmpty()) { - return "true." + io.checkNewLine() + return "true.\n" } return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } }, diff --git a/tests/compare.sh b/tests/compare.sh new file mode 100644 index 0000000..9bdadab --- /dev/null +++ b/tests/compare.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# This script is expected to be run from the root of the project. + +# Paths to the two implementations +GPL="src/gpl" +SPL="swipl" + +GPL_FLAGS=("--debug") +SPL_FLAGS=("--quiet" "-t" "'true'") + +# Directory containing test files +TEST_DIR="examples" + +# Temporary files for storing outputs +GPL_OUT=$(mktemp) +SPL_OUT=$(mktemp) + +# 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") +for TESTFILE in "${files[@]}"; do + # Run both programs with the test file + "${SPL}" "${SPL_FLAGS[@]}" "$TESTFILE" > "${SPL_OUT}" 2>&1 + "${GPL}" "${GPL_FLAGS[@]}" -s "$TESTFILE" > "${GPL_OUT}" 2>&1 + + # Compare the outputs + if diff -q "$SPL_OUT" "$GPL_OUT" > /dev/null; then + PASSED=$((PASSED + 1)) + else + echo "Test failed! Outputs differ for $TESTFILE" + printf "\nTest:\n%s\n" "$(cat "$TESTFILE")" + printf "\nExpected:\n%s\n" "$(cat "$SPL_OUT")" + printf "\nGot:\n%s\n" "$(cat "$GPL_OUT")" + echo "-----------------------------------------" + FAILED=$((FAILED + 1)) + fi +done + +# Clean up temporary files +rm "$SPL_OUT" "$GPL_OUT" + +# 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/myClass.kt b/tests/e2e/myClass.kt new file mode 100644 index 0000000..f5ff7c3 --- /dev/null +++ b/tests/e2e/myClass.kt @@ -0,0 +1,4 @@ +package e2e + +class myClass { +} \ No newline at end of file diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index ed2862b..3f3ad3f 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -12,8 +12,8 @@ import prolog.builtins.* class PreprocessorTests { class OpenPreprocessor : Preprocessor() { - public override fun preprocess(input: Term): Term { - return super.preprocess(input) + public override fun preprocess(term: Term, nested: Boolean): Term { + return super.preprocess(term, nested) } } diff --git a/tests/interpreter/SourceFileReaderTests.kt b/tests/interpreter/SourceFileReaderTests.kt index 55fc73a..55cdbcc 100644 --- a/tests/interpreter/SourceFileReaderTests.kt +++ b/tests/interpreter/SourceFileReaderTests.kt @@ -16,8 +16,6 @@ class SourceFileReaderTests { val reader = FileLoader() reader.readFile(inputFile) - - println(Program.predicates) } @Test @@ -26,7 +24,5 @@ class SourceFileReaderTests { val reader = FileLoader() reader.readFile(inputFile) - - println(Program.predicates) } } \ No newline at end of file diff --git a/tests/parser/OperatorParserTests.kt b/tests/parser/OperatorParserTests.kt index 8cfdb95..a8bedb0 100644 --- a/tests/parser/OperatorParserTests.kt +++ b/tests/parser/OperatorParserTests.kt @@ -26,4 +26,17 @@ class OperatorParserTests { 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/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index 9afb439..d7e054e 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -129,4 +129,16 @@ class LogicGrammarTests { val conjunction = rule.body as CompoundTerm assertEquals("invited/2", (conjunction.arguments[0] as CompoundTerm).functor, "Expected functor 'invited/2'") } + + @Test + fun `parse constraints`() { + val input = ":- a." + + val result = parser.parseToEnd(input) + + assertEquals(1, result.size, "Expected 1 rule") + assertTrue(result[0] is Rule, "Expected a rule") + val rule = result[0] as Rule + assertEquals("/_", rule.head.functor, "Expected a constraint") + } } \ No newline at end of file diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt index 203eac2..9acbdfd 100644 --- a/tests/parser/grammars/TermsGrammarTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -2,9 +2,12 @@ package parser.grammars 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 +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach 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 @@ -14,6 +17,7 @@ import prolog.ast.terms.Structure import prolog.ast.terms.Term import prolog.ast.terms.Variable import prolog.logic.equivalent +import kotlin.test.assertEquals class TermsGrammarTests { private lateinit var parser: Grammar @@ -167,9 +171,12 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent(Float(-42.0f), result, emptyMap()), - "Expected float '-42.0'" - ) + assertEquals(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) } } } \ No newline at end of file diff --git a/tests/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt index 6fd5fe5..cab3d0a 100644 --- a/tests/prolog/EvaluationTests.kt +++ b/tests/prolog/EvaluationTests.kt @@ -2,6 +2,7 @@ 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.logic.Fact import prolog.ast.logic.Rule @@ -108,9 +109,9 @@ class EvaluationTests { val parent = Rule( Structure(Atom("parent"), listOf(variable1, variable2)), /* :- */ Disjunction( - Structure(Atom("father"), listOf(variable1, variable2)), - /* ; */ - Structure(Atom("mother"), listOf(variable1, variable2)) + Structure(Atom("father"), listOf(variable1, variable2)), + /* ; */ + Structure(Atom("mother"), listOf(variable1, variable2)) ) ) @@ -212,7 +213,182 @@ 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") } } -} \ No newline at end of file + + @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")))) + 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"), + Conjunction( + Atom("a"), + Atom("b") + ) + ) + + Program.load(listOf(fact1, fact2, rule1)) + + val result = Program.query(Atom("c")).toList() + + assertEquals(1, result.size, "Expected 1 result") + } + + @Test + fun `requires querying with variable`() { + val fact1 = Fact(Atom("a")) + val fact2 = Fact(Atom("b")) + val rule1 = Rule( + Structure(Atom("has fact"), listOf(Variable("X"))), + Variable("X") + ) + + Program.load(listOf(fact1, fact2, rule1)) + + val result = Program.query(Structure(Atom("has fact"), listOf(Atom("a")))).toList() + + assertEquals(1, result.size, "Expected 1 result") + assertTrue(result[0].isSuccess, "Expected success") + val subs = result[0].getOrNull()!! + assertEquals(0, subs.size, "Expected no substitutions") + } + + @Nested + class `requires querying with filled variable` { + @BeforeEach + fun setup() { + val fact1 = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza")))) + val fact2 = Fact(Structure(Atom("likes"), listOf(Atom("alice"), Atom("pasta")))) + val fact3 = Fact(Structure(Atom("likes"), listOf(Atom("bob"), Atom("pasta")))) + val rule1 = Rule( + Structure(Atom("likes_italian_food"), listOf(Variable("Person"))), + Disjunction( + Structure(Atom("likes"), listOf(Variable("Person"), Atom("pizza"))), + Structure(Atom("likes"), listOf(Variable("Person"), Atom("pasta"))) + ) + ) + + Program.clear() + Program.load(listOf(fact1, fact2, fact3, rule1)) + } + + @Test + fun `likes_italian_food(alice)`() { + val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Atom("alice")))).toList() + + assertEquals(2, result.size, "Expected 2 results") + + assertTrue(result[0].isSuccess, "Expected success") + val subs1 = result[0].getOrNull()!! + assertEquals(0, subs1.size, "Expected no substitutions") + + assertTrue(result[1].isSuccess, "Expected success") + val subs2 = result[1].getOrNull()!! + assertEquals(0, subs2.size, "Expected no substitutions") + } + + @Test + fun `likes_italian_food(X)`() { + val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Variable("X")))).toList() + + assertEquals(3, result.size, "Expected 3 results") + + assertTrue(result[0].isSuccess, "Expected success") + val subs3 = result[0].getOrNull()!! + assertEquals(1, subs3.size, "Expected 1 substitution, especially without 'Person'") + assertEquals(Atom("alice"), subs3[Variable("X")], "Expected alice") + + assertTrue(result[1].isSuccess, "Expected success") + val subs4 = result[1].getOrNull()!! + assertEquals(1, subs4.size, "Expected 1 substitution, especially without 'Person'") + assertEquals(Atom("alice"), subs4[Variable("X")], "Expected alice") + + assertTrue(result[2].isSuccess, "Expected success") + val subs5 = result[2].getOrNull()!! + assertEquals(1, subs5.size, "Expected 1 substitution, especially without 'Person'") + assertEquals(Atom("bob"), subs5[Variable("X")], "Expected bob") + } + } +} diff --git a/tests/prolog/logic/TermsTests.kt b/tests/prolog/logic/TermsTests.kt new file mode 100644 index 0000000..f67f46a --- /dev/null +++ b/tests/prolog/logic/TermsTests.kt @@ -0,0 +1,109 @@ +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 (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(variable), "Expected subs to contain the variable") + assertEquals(Variable("X($end1)"), subs2[variable], "Expected subs to contain the new term") + } +} \ No newline at end of file From 724e911a6fad6d9770dad8c5d615a077d0d2b89b Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 1 May 2025 21:16:48 +0200 Subject: [PATCH 13/19] Checkpoint --- examples/scratchpad.pl | 4 + src/interpreter/Preprocessor.kt | 16 +-- src/parser/grammars/TermsGrammar.kt | 110 +++++++++++++-------- src/parser/grammars/Tokens.kt | 3 + src/prolog/Program.kt | 6 ++ src/prolog/ast/arithmetic/Integer.kt | 6 +- src/prolog/ast/logic/Clause.kt | 3 +- src/prolog/ast/terms/Variable.kt | 3 +- src/prolog/builtins/io.kt | 7 +- tests/compare.sh | 46 ++++++--- tests/e2e/Examples.kt | 46 +++++++++ tests/e2e/myClass.kt | 4 - tests/parser/OperatorParserTests.kt | 3 +- tests/parser/grammars/LogicGrammarTests.kt | 11 ++- tests/parser/grammars/TermsGrammarTests.kt | 82 ++++++++++++--- tests/prolog/EvaluationTests.kt | 22 +++++ tests/prolog/builtins/IoOperatorsTests.kt | 11 +-- 17 files changed, 288 insertions(+), 95 deletions(-) create mode 100644 tests/e2e/Examples.kt delete mode 100644 tests/e2e/myClass.kt diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl index b122c4f..9367251 100644 --- a/examples/scratchpad.pl +++ b/examples/scratchpad.pl @@ -21,3 +21,7 @@ main :- write("but "), did_not_get_an_a(Y), write(Y), write(" did not get an A, "), fail; write("unfortunately."), nl. + +:- initialization(main). +main :- write('gpl zegt: '), groet(wereld), nl. +groet(X) :- write(dag(X)). diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 031cab9..686139f 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -66,6 +66,14 @@ open class Preprocessor { when { // TODO Remove hardcoding by storing the functors as constants in operators? // 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) } @@ -92,14 +100,6 @@ open class Preprocessor { // Arithmetic - term.functor == "=/2" && args.all { it is Expression } -> { - Unify(args[0] as Expression, args[1] as Expression) - } - - term.functor == "\\=/2" && args.all { it is Expression } -> { - NotUnify(args[0] as Expression, args[1] as Expression) - } - term.functor == "-/1" && args.all { it is Expression } -> { Negate(args[0] as Expression) } diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 52d4ef0..1d20fc1 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -7,6 +7,32 @@ import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.terms.* +/** + * Precedence is based on the following table: + * + * | Precedence | Type | Operators | + * |------------|------|-----------------------------------------------------------------------------------------------| + * | 1200 | xfx | --\>, :-, =\>, ==\> | + * | 1200 | fx | :-, ?- | + * | 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 | $ | + * + * @see [SWI-Prolog Predicate op/3](https://www.swi-prolog.org/pldoc/man?predicate=op/3) + */ open class TermsGrammar : Tokens() { // Basic named terms @@ -19,7 +45,7 @@ open class TermsGrammar : Tokens() { ) 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(::term), + parser(::termNoConjunction), comma, acceptZero = true ) * -rightParenthesis) use { @@ -30,51 +56,57 @@ open class TermsGrammar : Tokens() { protected val int: Parser by integerToken use { Integer(text.toInt()) } protected val float: Parser by floatToken use { Float(text.toFloat()) } - // Operators - protected val ops: Parser by (dummy - // Logic - or comma - or semicolon - // Arithmetic - or plus - or equals - or notEquals - ) use { this.text } - protected val simpleOperand: Parser by (dummy - // Logic + // Base terms (atoms, compounds, variables, numbers) + protected val baseTerm: Parser by (dummy + or (-leftParenthesis * parser(::term) * -rightParenthesis) or compound or atom or variable - // Arithmetic - or int or float + or int ) - protected val operand: Parser by (dummy - or parser(::operator) - or simpleOperand - ) - protected val operator: Parser by (simpleOperand * ops * operand) use { - CompoundTerm(Atom(t2), listOf(t1, t3)) + + // Level 200 - prefix operators (+, -, \) + 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) + + // Level 400 - multiplication, division + 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)) } } - // Parts - protected val head: Parser by (dummy - or compound - or atom - ) - protected val body: Parser by (dummy - or operator - or head - or variable - ) use { this as Body } + // Level 500 - addition, subtraction + 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 term: Parser by (dummy - or float - or int - or variable - or compound - or atom - ) + // Level 700 - comparison operators + protected val op700: Parser by (equals or notEquals) 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)) + } + + // Level 1000 - conjunction (,) + protected val term1000: Parser by (term700 * zeroOrMore(comma * term700)) use { + t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(","), listOf(acc, term)) } + } + + // Level 1100 - disjunction (;) + protected val term1100: Parser by (term1000 * zeroOrMore(semicolon * term1000)) use { + t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(";"), listOf(acc, term)) } + } + + // Term - highest level expression + protected val term: Parser by term1100 + 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 -} \ No newline at end of file +} diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index cf55e09..83e7e33 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -25,6 +25,9 @@ abstract class Tokens : Grammar() { protected val equals: Token by literalToken("=") protected val notEquals: Token by literalToken("\\=") protected val plus: Token by literalToken("+") + protected val minus: Token by literalToken("-") + protected val multiply: Token by literalToken("*") + protected val divide: Token by literalToken("/") protected val dot by literalToken(".") // Ignored tokens diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index 87e787e..d129f0d 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -48,4 +48,10 @@ object Program : Resolvent { correspondingDBs.forEach { it.clear() } } + + fun reset() { + clear() + variableRenamingStart = 0 + storeNewLine = false + } } \ No newline at end of file diff --git a/src/prolog/ast/arithmetic/Integer.kt b/src/prolog/ast/arithmetic/Integer.kt index 50028a9..dbf9c39 100644 --- a/src/prolog/ast/arithmetic/Integer.kt +++ b/src/prolog/ast/arithmetic/Integer.kt @@ -1,11 +1,15 @@ package prolog.ast.arithmetic +import prolog.Answers import prolog.Substitutions +import prolog.ast.logic.LogicOperand -data class Integer(override val value: Int) : Number { +data class Integer(override val value: Int) : Number, LogicOperand() { // Integers are already evaluated override fun simplify(subs: Substitutions): Simplification = Simplification(this, this) + override fun satisfy(subs: Substitutions): Answers = sequenceOf(Result.success(emptyMap())) + override fun toString(): String = value.toString() override operator fun plus(other: Number): Number = when (other) { diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index 3bf4138..f402951 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -8,6 +8,7 @@ import prolog.builtins.True import prolog.flags.AppliedCut import prolog.logic.applySubstitution import prolog.logic.numbervars +import prolog.logic.occurs import prolog.logic.unifyLazy /** @@ -46,7 +47,7 @@ abstract class Clause(val head: Head, val body: Body) : Resolvent { .mapValues { reverse[it.value] ?: it.value } result = result.map { it.key to applySubstitution(it.value, result) } .toMap() - .filterNot { it.key in renamed.keys } + .filterNot { it.key in renamed.keys && !occurs(it.key as Variable, goal, emptyMap())} yield(Result.success(result)) }, onFailure = { error -> diff --git a/src/prolog/ast/terms/Variable.kt b/src/prolog/ast/terms/Variable.kt index 0ef7ee6..f19ea86 100644 --- a/src/prolog/ast/terms/Variable.kt +++ b/src/prolog/ast/terms/Variable.kt @@ -4,8 +4,9 @@ import prolog.Answers import prolog.Substitutions import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Simplification +import prolog.ast.logic.LogicOperand -data class Variable(val name: String) : Term, Body, Expression { +data class Variable(val name: String) : Term, Body, Expression, LogicOperand() { override fun simplify(subs: Substitutions): Simplification { // If the variable is bound, return the value of the binding // If the variable is not bound, return the variable itself diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/io.kt index 3f9aaae..5806511 100644 --- a/src/prolog/builtins/io.kt +++ b/src/prolog/builtins/io.kt @@ -18,7 +18,12 @@ import prolog.logic.unifyLazy */ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satisfiable { override fun satisfy(subs: Substitutions): Answers { - val t = applySubstitution(term, subs) + var t = term + var temp = applySubstitution(t, subs) + while (t != temp) { + t = temp + temp = applySubstitution(t, subs) + } Terminal().say(t.toString()) diff --git a/tests/compare.sh b/tests/compare.sh index 9bdadab..d876e13 100644 --- a/tests/compare.sh +++ b/tests/compare.sh @@ -2,11 +2,13 @@ # 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=("--debug") +GPL_FLAGS=("--error") SPL_FLAGS=("--quiet" "-t" "'true'") # Directory containing test files @@ -14,7 +16,11 @@ 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 @@ -22,27 +28,43 @@ 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") +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>&1 - "${GPL}" "${GPL_FLAGS[@]}" -s "$TESTFILE" > "${GPL_OUT}" 2>&1 + "${SPL}" "${SPL_FLAGS[@]}" "$TESTFILE" > "${SPL_OUT}" 2> "${SPL_ERR}" + "${GPL}" "${GPL_FLAGS[@]}" -s "$TESTFILE" > "${GPL_OUT}" 2> "${GPL_ERR}" # Compare the outputs - if diff -q "$SPL_OUT" "$GPL_OUT" > /dev/null; then - PASSED=$((PASSED + 1)) - else - echo "Test failed! Outputs differ for $TESTFILE" - printf "\nTest:\n%s\n" "$(cat "$TESTFILE")" - printf "\nExpected:\n%s\n" "$(cat "$SPL_OUT")" - printf "\nGot:\n%s\n" "$(cat "$GPL_OUT")" + 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" +rm "$SPL_OUT" "$GPL_OUT" "$SPL_ERR" "$GPL_ERR" # Final result, summary if [ $FAILED -eq 0 ]; then diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt new file mode 100644 index 0000000..9febe53 --- /dev/null +++ b/tests/e2e/Examples.kt @@ -0,0 +1,46 @@ +package e2e + +import interpreter.FileLoader +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import prolog.Program +import java.io.ByteArrayOutputStream +import java.io.PrintStream + +class Examples { + private val loader = FileLoader() + + @BeforeEach + fun setup() { + Program.reset() + } + + @Test + fun fraternity() { + val inputFile = "examples/basics/fraternity.pl" + + loader.load(inputFile) + } + + @Test + fun unification() { + val inputFile = "examples/basics/unification.pl" + + loader.load(inputFile) + } + + @Test + fun write() { + val expected = "gpl zegt: dag(wereld)\n" + + val outStream = ByteArrayOutputStream() + System.setOut(PrintStream(outStream)) + + val inputFile = "examples/basics/write.pl" + + // Compare the stdio output with the expected output + loader.load(inputFile) + assertEquals(expected, outStream.toString()) + } +} \ No newline at end of file diff --git a/tests/e2e/myClass.kt b/tests/e2e/myClass.kt deleted file mode 100644 index f5ff7c3..0000000 --- a/tests/e2e/myClass.kt +++ /dev/null @@ -1,4 +0,0 @@ -package e2e - -class myClass { -} \ No newline at end of file diff --git a/tests/parser/OperatorParserTests.kt b/tests/parser/OperatorParserTests.kt index a8bedb0..9c19db2 100644 --- a/tests/parser/OperatorParserTests.kt +++ b/tests/parser/OperatorParserTests.kt @@ -1,5 +1,6 @@ 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 @@ -13,7 +14,7 @@ import prolog.ast.terms.Structure class OperatorParserTests { class OperatorParser: TermsGrammar() { - override val rootParser: Parser by operator + override val rootParser: Parser by body use { this as CompoundTerm } } private var parser = OperatorParser() as Grammar diff --git a/tests/parser/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index d7e054e..ba0143d 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -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].toString()}.", "Expected fact to be '$input'") + assertEquals(input, "${result[0]}.", "Expected fact to be '$input'") } @ParameterizedTest @@ -125,9 +125,12 @@ class LogicGrammarTests { assertEquals(1, result.size, "Expected 1 rule") val rule = result[0] as Rule - assertInstanceOf(CompoundTerm::class.java, rule.body, "Expected body to be a conjunction") - val conjunction = rule.body as CompoundTerm - assertEquals("invited/2", (conjunction.arguments[0] as CompoundTerm).functor, "Expected functor 'invited/2'") + assertEquals("guest/2", rule.head.functor, "Expected functor 'guest/2'") + assertEquals(",/2", (rule.body as CompoundTerm).functor, "Expected functor ',/2'") + val l1 = (rule.body as CompoundTerm).arguments[0] as CompoundTerm + assertEquals(",/2", l1.functor, "Expected functor ',/2'") + val l2 = l1.arguments[0] as CompoundTerm + assertEquals("invited/2", l2.functor, "Expected functor 'invited/2'") } @Test diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt index 9acbdfd..5166856 100644 --- a/tests/parser/grammars/TermsGrammarTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -2,10 +2,11 @@ package parser.grammars 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 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 @@ -17,7 +18,6 @@ import prolog.ast.terms.Structure import prolog.ast.terms.Term import prolog.ast.terms.Variable import prolog.logic.equivalent -import kotlin.test.assertEquals class TermsGrammarTests { private lateinit var parser: Grammar @@ -32,7 +32,7 @@ class TermsGrammarTests { fun `parse atom`(name: String) { val result = parser.parseToEnd(name) - Assertions.assertEquals(Atom(name), result, "Expected atom '$name'") + assertEquals(Atom(name), result, "Expected atom '$name'") } @ParameterizedTest @@ -52,7 +52,7 @@ class TermsGrammarTests { val expected = input.substring(1, input.length - 1) - Assertions.assertEquals(Atom(expected), result, "Expected atom") + assertEquals(Atom(expected), result, "Expected atom") } @ParameterizedTest @@ -60,7 +60,7 @@ class TermsGrammarTests { fun `parse variable`(name: String) { val result = parser.parseToEnd(name) - Assertions.assertEquals(Variable(name), result, "Expected atom '$name'") + assertEquals(Variable(name), result, "Expected atom '$name'") } @Test @@ -69,10 +69,7 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent(Structure(Atom("f"), emptyList()), result, emptyMap()), - "Expected atom 'f'" - ) + assertEquals(Structure(Atom("f"), emptyList()), result, "Expected atom 'f'") } @Test @@ -81,10 +78,7 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent(Structure(Atom("f"), listOf(Atom("a"))), result, emptyMap()), - "Expected atom 'f(a)'" - ) + assertEquals(Structure(Atom("f"), listOf(Atom("a"))), result, "Expected atom 'f(a)'") } @Test @@ -93,8 +87,9 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), result, emptyMap()), + assertEquals( + Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), + result, "Expected atom 'f(a, b)'" ) } @@ -105,7 +100,7 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( + assertTrue( equivalent(Structure(Atom("f"), listOf(Atom("a"), Variable("X"))), result, emptyMap()), "Expected atom 'f(a, X)'" ) @@ -179,4 +174,59 @@ class TermsGrammarTests { fun `parse unification`(input: String) { assertDoesNotThrow { parser.parseToEnd(input) } } + + @Nested + class `Operator precedence` { + private lateinit var parser: Grammar + + @BeforeEach + fun setup() { + parser = TermsGrammar() as Grammar + } + + @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 cab3d0a..3d185bb 100644 --- a/tests/prolog/EvaluationTests.kt +++ b/tests/prolog/EvaluationTests.kt @@ -390,5 +390,27 @@ class EvaluationTests { assertEquals(1, subs5.size, "Expected 1 substitution, especially without 'Person'") assertEquals(Atom("bob"), subs5[Variable("X")], "Expected bob") } + + @Test + fun `likes_italian_food(Person)`() { + val result = Program.query(Structure(Atom("likes_italian_food"), listOf(Variable("Person")))).toList() + + assertEquals(3, result.size, "Expected 3 results") + + assertTrue(result[0].isSuccess, "Expected success") + val subs3 = result[0].getOrNull()!! + assertEquals(1, subs3.size, "Expected 1 substitution") + assertEquals(Atom("alice"), subs3[Variable("Person")], "Expected alice") + + assertTrue(result[1].isSuccess, "Expected success") + val subs4 = result[1].getOrNull()!! + assertEquals(1, subs4.size, "Expected 1 substitution") + assertEquals(Atom("alice"), subs4[Variable("Person")], "Expected alice") + + assertTrue(result[2].isSuccess, "Expected success") + val subs5 = result[2].getOrNull()!! + assertEquals(1, subs5.size, "Expected 1 substitution") + assertEquals(Atom("bob"), subs5[Variable("Person")], "Expected bob") + } } } diff --git a/tests/prolog/builtins/IoOperatorsTests.kt b/tests/prolog/builtins/IoOperatorsTests.kt index 8f7d5fe..f36a41a 100644 --- a/tests/prolog/builtins/IoOperatorsTests.kt +++ b/tests/prolog/builtins/IoOperatorsTests.kt @@ -1,21 +1,18 @@ 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.Assertions.* 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 import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer import prolog.ast.terms.Atom import prolog.ast.terms.CompoundTerm -import java.io.ByteArrayOutputStream -import java.io.PrintStream -import prolog.ast.arithmetic.Integer import prolog.ast.terms.Variable import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.PrintStream class IoOperatorsTests { private var outStream = ByteArrayOutputStream() From 23b2ce936222e47bf0e3ffa69304a84e78d2c9ab Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 2 May 2025 09:09:23 +0200 Subject: [PATCH 14/19] test: Fix failing --- tests/prolog/builtins/IoOperatorsTests.kt | 2 +- tests/prolog/logic/UnifyTests.kt | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/prolog/builtins/IoOperatorsTests.kt b/tests/prolog/builtins/IoOperatorsTests.kt index f36a41a..05607ec 100644 --- a/tests/prolog/builtins/IoOperatorsTests.kt +++ b/tests/prolog/builtins/IoOperatorsTests.kt @@ -69,7 +69,7 @@ class IoOperatorsTests { val sub = Subtract(b, mul) val expr = EvaluatesTo(a, sub) - val expected1 = "1 =:= B - (2.0 * D)" + val expected1 = "(1 =:= (B - (2.0 * D)))" val expected2 = "=:=(1, -(B, *(2.0, D)))" val write = Write(expr) diff --git a/tests/prolog/logic/UnifyTests.kt b/tests/prolog/logic/UnifyTests.kt index f2b3c57..0f0199f 100644 --- a/tests/prolog/logic/UnifyTests.kt +++ b/tests/prolog/logic/UnifyTests.kt @@ -1,7 +1,6 @@ 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 @@ -9,6 +8,8 @@ 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.fail /* * Based on: https://en.wikipedia.org/wiki/Unification_%28computer_science%29#Examples_of_syntactic_unification_of_first-order_terms @@ -241,6 +242,7 @@ class UnifyTests { * 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"))) @@ -261,7 +263,6 @@ class UnifyTests { * ?- X = bar, Y = bar, X = Y. * X = Y, Y = bar. */ - @Disabled @Test fun multiple_unification() { val variable1 = Variable("X") From f9017da734495f1f7475c5f47601298fd05aca2d Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 2 May 2025 13:28:00 +0200 Subject: [PATCH 15/19] Checkpoint --- src/interpreter/Preprocessor.kt | 10 +- src/parser/grammars/TermsGrammar.kt | 6 +- src/parser/grammars/Tokens.kt | 22 +- src/prolog/ast/terms/AnonymousVariable.kt | 17 + src/prolog/ast/terms/Variable.kt | 12 +- src/prolog/builtins/controlOperators.kt | 2 +- src/prolog/builtins/io.kt | 7 +- src/prolog/logic/arithmetic.kt | 2 +- src/prolog/logic/unification.kt | 12 +- tests/e2e/Examples.kt | 46 +-- tests/interpreter/OpenPreprocessor.kt | 9 + .../ParserPreprocessorIntegrationTests.kt | 93 +++++ tests/interpreter/PreprocessorTests.kt | 87 +++- tests/parser/grammars/LogicGrammarTests.kt | 15 + tests/parser/grammars/TermsGrammarTests.kt | 171 ++++++-- tests/prolog/logic/ArithmeticTests.kt | 17 +- tests/prolog/logic/UnificationTests.kt | 370 ++++++++++++++++++ tests/prolog/logic/UnifyTests.kt | 328 ---------------- 18 files changed, 814 insertions(+), 412 deletions(-) create mode 100644 src/prolog/ast/terms/AnonymousVariable.kt create mode 100644 tests/interpreter/OpenPreprocessor.kt create mode 100644 tests/interpreter/ParserPreprocessorIntegrationTests.kt create mode 100644 tests/prolog/logic/UnificationTests.kt delete mode 100644 tests/prolog/logic/UnifyTests.kt diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 686139f..559a7b2 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -59,6 +59,7 @@ open class Preprocessor { 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) } @@ -86,6 +87,10 @@ open class Preprocessor { 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) } @@ -133,7 +138,10 @@ open class Preprocessor { term.functor == "read/1" -> Read(args[0]) term.functor == "initialization/1" -> Initialization(args[0] as Goal) - else -> term + else -> { + term.arguments = args + term + } } } diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 1d20fc1..487f9f7 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -36,8 +36,8 @@ import prolog.ast.terms.* open class TermsGrammar : Tokens() { // Basic named terms - protected val variable: Parser by variableToken use { Variable(text) } - protected val simpleAtom: Parser by nameToken use { Atom(text) } + 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 @@ -85,7 +85,7 @@ open class TermsGrammar : Tokens() { } // Level 700 - comparison operators - protected val op700: Parser by (equals or notEquals) use { text } + 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)) } diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index 83e7e33..bffc308 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -8,28 +8,32 @@ import com.github.h0tk3y.betterParse.lexer.regexToken import com.github.h0tk3y.betterParse.lexer.token abstract class Tokens : 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 - protected val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+") - protected val integerToken: Token by regexToken("-?([1-9][0-9]*|0)") - // Special tokens protected val neck by literalToken(":-") protected val leftParenthesis: Token by literalToken("(") protected val rightParenthesis: Token by literalToken(")") protected val comma: Token by literalToken(",") protected val semicolon: Token by literalToken(";") + protected val equivalent: Token by literalToken("==") protected val equals: Token by literalToken("=") protected val notEquals: Token by literalToken("\\=") protected val plus: Token by literalToken("+") protected val minus: Token by literalToken("-") protected val multiply: Token by literalToken("*") protected val divide: Token by literalToken("/") + protected val exclamation: Token by literalToken("!") + protected val isOp: Token by literalToken("is") 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) @@ -41,4 +45,4 @@ abstract class Tokens : Grammar() { // Helper protected val dummy by token { _, _ -> -1 } use { throw IllegalStateException("This parser should not be used") } -} \ No newline at end of file +} diff --git a/src/prolog/ast/terms/AnonymousVariable.kt b/src/prolog/ast/terms/AnonymousVariable.kt new file mode 100644 index 0000000..1bc0633 --- /dev/null +++ b/src/prolog/ast/terms/AnonymousVariable.kt @@ -0,0 +1,17 @@ +package prolog.ast.terms + +import io.Logger + +class AnonymousVariable(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 = "_" +} \ No newline at end of file diff --git a/src/prolog/ast/terms/Variable.kt b/src/prolog/ast/terms/Variable.kt index f19ea86..2d23170 100644 --- a/src/prolog/ast/terms/Variable.kt +++ b/src/prolog/ast/terms/Variable.kt @@ -6,7 +6,7 @@ import prolog.ast.arithmetic.Expression import prolog.ast.arithmetic.Simplification import prolog.ast.logic.LogicOperand -data class Variable(val name: String) : Term, Body, Expression, LogicOperand() { +open class Variable(val name: String) : Term, Body, Expression, LogicOperand() { override fun simplify(subs: Substitutions): Simplification { // If the variable is bound, return the value of the binding // If the variable is not bound, return the variable itself @@ -28,5 +28,15 @@ data class Variable(val name: String) : Term, Body, Expression, LogicOperand() { 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 } \ No newline at end of file diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index c1ebe63..01cd333 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -3,10 +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.logic.LogicOperator import prolog.flags.AppliedCut /** diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/io.kt index 5806511..3f9aaae 100644 --- a/src/prolog/builtins/io.kt +++ b/src/prolog/builtins/io.kt @@ -18,12 +18,7 @@ import prolog.logic.unifyLazy */ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satisfiable { override fun satisfy(subs: Substitutions): Answers { - var t = term - var temp = applySubstitution(t, subs) - while (t != temp) { - t = temp - temp = applySubstitution(t, subs) - } + val t = applySubstitution(term, subs) Terminal().say(t.toString()) diff --git a/src/prolog/logic/arithmetic.kt b/src/prolog/logic/arithmetic.kt index 0d4d341..36b903c 100644 --- a/src/prolog/logic/arithmetic.kt +++ b/src/prolog/logic/arithmetic.kt @@ -58,7 +58,7 @@ fun succ(term1: Expression, term2: Expression, subs: Substitutions): Answers { it.fold( onSuccess = { result -> val t1 = applySubstitution(term1, result) - if (t1 in result) { + if (t1 in result || t1 in result.values) { val e1 = t1.simplify(result) if (e1.to is Integer && e1.to.value < 0) { return@sequence diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 68242b1..0cf1321 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -13,7 +13,15 @@ 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 + variable(term, emptyMap()) -> { + var result = subs[(term as Variable)] + + while (result != null && result is Variable && result in subs) { + result = subs[result] + } + + result ?: term + } atomic(term, subs) -> term compound(term, subs) -> { val structure = term as Structure @@ -25,7 +33,7 @@ fun applySubstitution(term: Term, subs: Substitutions): Term = when { //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 + variable(expr, emptyMap()) -> applySubstitution(expr as Term, subs) as Expression atomic(expr, subs) -> expr expr is LogicOperator -> { expr.arguments = expr.arguments.map { applySubstitution(it, subs) } diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt index 9febe53..2b1f3fa 100644 --- a/tests/e2e/Examples.kt +++ b/tests/e2e/Examples.kt @@ -4,43 +4,43 @@ import interpreter.FileLoader import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach +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 org.junit.jupiter.params.provider.ValueSource import prolog.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() - } - @Test - fun fraternity() { - val inputFile = "examples/basics/fraternity.pl" - - loader.load(inputFile) - } - - @Test - fun unification() { - val inputFile = "examples/basics/unification.pl" - - loader.load(inputFile) - } - - @Test - fun write() { - val expected = "gpl zegt: dag(wereld)\n" - - val outStream = ByteArrayOutputStream() + outStream = ByteArrayOutputStream() System.setOut(PrintStream(outStream)) + } - val inputFile = "examples/basics/write.pl" - - // Compare the stdio output with the expected output + @ParameterizedTest + @MethodSource("expectations") + fun test(inputFile: String, expected: String) { loader.load(inputFile) assertEquals(expected, outStream.toString()) } + + fun expectations() = listOf( + Arguments.of("examples/basics/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("examples/basics/backtracking.pl", "0\ns(0)\ns(s(0))\ns(s(s(0)))\n"), + Arguments.of("examples/basics/cut.pl", "0\n"), + Arguments.of("examples/basics/disjunction.pl", "Alice likes Italian food.\nBob likes Italian food.\n"), + Arguments.of("examples/basics/equality.pl", "X == Y failed\nX = Y succeeded\nX == Y succeeded\nX = Y succeeded\nX == Y succeeded\n"), + Arguments.of("examples/basics/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("examples/basics/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("examples/basics/write.pl", "gpl zegt: dag(wereld)\n"), + ) } \ No newline at end of file diff --git a/tests/interpreter/OpenPreprocessor.kt b/tests/interpreter/OpenPreprocessor.kt new file mode 100644 index 0000000..4e35b2a --- /dev/null +++ b/tests/interpreter/OpenPreprocessor.kt @@ -0,0 +1,9 @@ +package interpreter + +import prolog.ast.terms.Term + +class OpenPreprocessor : Preprocessor() { + public override fun preprocess(term: Term, nested: Boolean): Term { + return super.preprocess(term, nested) + } +} \ No newline at end of file diff --git a/tests/interpreter/ParserPreprocessorIntegrationTests.kt b/tests/interpreter/ParserPreprocessorIntegrationTests.kt new file mode 100644 index 0000000..a572686 --- /dev/null +++ b/tests/interpreter/ParserPreprocessorIntegrationTests.kt @@ -0,0 +1,93 @@ +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.Program +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 index 3f3ad3f..d7d6c19 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -1,32 +1,80 @@ package interpreter -import org.junit.jupiter.api.Assertions.assertEquals +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.terms.Atom -import prolog.ast.terms.CompoundTerm -import prolog.ast.terms.Term -import prolog.ast.terms.Variable +import prolog.ast.terms.* import prolog.builtins.* class PreprocessorTests { - class OpenPreprocessor : Preprocessor() { - public override fun preprocess(term: Term, nested: Boolean): Term { - return super.preprocess(term, nested) - } - } + val preprocessor = OpenPreprocessor() companion object { + val preprocessor = OpenPreprocessor() + fun test(tests: Map) { for ((input, expected) in tests) { - val result = OpenPreprocessor().preprocess(input) + 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 @@ -432,5 +480,22 @@ class PreprocessorTests { ) ) } + + @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)) + ), + ) + ) + } } } diff --git a/tests/parser/grammars/LogicGrammarTests.kt b/tests/parser/grammars/LogicGrammarTests.kt index ba0143d..57a1f99 100644 --- a/tests/parser/grammars/LogicGrammarTests.kt +++ b/tests/parser/grammars/LogicGrammarTests.kt @@ -133,6 +133,21 @@ class LogicGrammarTests { 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." diff --git a/tests/parser/grammars/TermsGrammarTests.kt b/tests/parser/grammars/TermsGrammarTests.kt index 5166856..d3b45e2 100644 --- a/tests/parser/grammars/TermsGrammarTests.kt +++ b/tests/parser/grammars/TermsGrammarTests.kt @@ -17,6 +17,7 @@ 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 { @@ -60,7 +61,16 @@ class TermsGrammarTests { fun `parse variable`(name: String) { val result = parser.parseToEnd(name) - assertEquals(Variable(name), result, "Expected atom '$name'") + assertEquals(Variable(name), result) + } + + @Test + fun `parse anonymous variable`() { + val input = "_" + + val result = parser.parseToEnd(input) + + assertEquals(Variable("_"), result, "Expected anonymous variable") } @Test @@ -100,24 +110,33 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - assertTrue( - equivalent(Structure(Atom("f"), listOf(Atom("a"), Variable("X"))), result, emptyMap()), + 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) - Assertions.assertTrue( - equivalent( - Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))), - result, - emptyMap() - ), + assertEquals( + Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Atom("b"))))), + result, "Expected atom 'f(a, g(b))'" ) } @@ -128,24 +147,63 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent( - Structure(Atom("f"), listOf(Atom("a"), Structure(Atom("g"), listOf(Variable("X"))))), - result, - emptyMap() - ), + 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 = [-987654321, -543, -21, -1, 0, 1, 5, 12, 345, 123456789]) - fun `parse integer`(number: Int) { + @ValueSource(ints = [0, 1, 5, 12, 345, 123456789]) + fun `parse positive integer`(number: Int) { val input = number.toString() val result = parser.parseToEnd(input) - Assertions.assertEquals(Integer(number), result, "Expected integer '$number'") + 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 @@ -154,10 +212,7 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - Assertions.assertTrue( - equivalent(Float(42.0f), result, emptyMap()), - "Expected float '42.0'" - ) + assertEquals(Float(42.0f), result, "Expected float '42.0'") } @Test @@ -166,7 +221,7 @@ class TermsGrammarTests { val result = parser.parseToEnd(input) - assertEquals(Float(-42.0f), result, "Expected float '-42.0'") + assertEquals(Structure(Atom("-"), listOf(Float(42.0f))), result, "Expected float '-42.0'") } @ParameterizedTest @@ -176,7 +231,7 @@ class TermsGrammarTests { } @Nested - class `Operator precedence` { + class `Operators and precedence` { private lateinit var parser: Grammar @BeforeEach @@ -184,6 +239,74 @@ class TermsGrammarTests { 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" diff --git a/tests/prolog/logic/ArithmeticTests.kt b/tests/prolog/logic/ArithmeticTests.kt index 83fa8c7..ac532f3 100644 --- a/tests/prolog/logic/ArithmeticTests.kt +++ b/tests/prolog/logic/ArithmeticTests.kt @@ -583,10 +583,23 @@ class ArithmeticTests { assertTrue(equivalent(result[0].getOrThrow()[t3]!!, Float(6.0f), result[0].getOrNull()!!), "X should be equal to 6.0") } + @Test + fun `addition with negative`() { + val t1 = Integer(1) + val t2 = Integer(-1) + val t3 = Integer(0) + + val result = plus(t1, t2, t3, emptyMap()).toList() + + assertEquals(1, result.size, "There should be one solution") + assertTrue(result[0].isSuccess, "Expected success") + assertTrue(result[0].getOrNull()!!.isEmpty(), "1 + -1 should already be equal to 0") + } + @RepeatedTest(100) fun `random test for mul`() { - val t1 = Integer((0..1000).random()) - val t2 = Integer((0..1000).random()) + val t1 = Integer((-1000..1000).random()) + val t2 = Integer((-1000..1000).random()) val t3 = Variable("X") val result = mul(t1, t2, t3, emptyMap()).toList() diff --git a/tests/prolog/logic/UnificationTests.kt b/tests/prolog/logic/UnificationTests.kt new file mode 100644 index 0000000..10a37e7 --- /dev/null +++ b/tests/prolog/logic/UnificationTests.kt @@ -0,0 +1,370 @@ +package prolog.logic + +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test +import prolog.Substitutions +import prolog.ast.arithmetic.Integer +import prolog.ast.terms.Atom +import prolog.ast.terms.Structure +import prolog.ast.terms.Variable +import prolog.builtins.Add +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Nested + +/* + * Based on: https://en.wikipedia.org/wiki/Unification_%28computer_science%29#Examples_of_syntactic_unification_of_first-order_terms + */ +class UnificationTests { + @Nested + class `unify` { + @Test + fun identical_atoms_unify() { + val atom1 = Atom("a") + val atom2 = Atom("a") + + val result = unify(atom1, atom2) + + assertTrue(result.isSuccess, "Identical atoms should unify") + assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made") + } + + @Test + fun different_atoms_do_not_unify() { + val atom1 = Atom("a") + val atom2 = Atom("b") + + val result = unify(atom1, atom2) + + assertFalse(result.isSuccess, "Different atoms should not unify") + } + + /** + * ?- X = X. + * true. + */ + @Test + fun identical_variables_unify() { + val variable1 = Variable("X") + val variable2 = Variable("X") + + val result = unify(variable1, variable2) + + assertTrue(result.isSuccess, "Identical variables should unify") + assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made") + } + + @Test + fun variable_unifies_with_atom() { + val variable = Variable("X") + val atom = Atom("a") + + val result = unify(atom, variable) + + assertTrue(result.isSuccess, "Variable should unify with atom") + assertEquals(1, result.getOrNull()!!.size, "There should be one substitution") + assertEquals(atom, result.getOrNull()!![variable], "Variable should be substituted with atom") + } + + @Test + fun variables_alias_when_unified() { + val variable1 = Variable("X") + val variable2 = Variable("Y") + + val result = unify(variable1, variable2) + + assertTrue(result.isSuccess) + assertEquals(1, result.getOrNull()!!.size) + assertEquals(variable2, result.getOrNull()!![variable1], "Variable 1 should alias to variable 2") + } + + @Test + fun identical_compound_terms_unify() { + val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + + val result = unify(structure1, structure2) + + assertTrue(result.isSuccess, "Identical compound terms should unify") + assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made") + } + + @Test + fun compound_terms_with_different_arguments_do_not_unify() { + val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("c"))) + + val result = unify(structure1, structure2) + + assertFalse(result.isSuccess, "Different compound terms should not unify") + } + + @Test + fun compound_terms_with_different_functors_do_not_unify() { + val structure1 = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + val structure2 = Structure(Atom("g"), listOf(Atom("a"), Atom("b"))) + + val result = unify(structure1, structure2) + + assertFalse(result.isSuccess, "Compound terms with different functors should not unify") + } + + /** + * ?- X = f(a, b). + * X = f(a, b). + */ + @Test + fun variable_unifies_with_compound_term() { + val variable = Variable("X") + val structure = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + + val result = unify(variable, structure) + + assertTrue(result.isSuccess, "Variable should unify with compound term") + + val subs = result.getOrNull()!! + + assertEquals(1, subs.size, "There should be one substitution") + assertTrue(subs.containsKey(variable), "Variable should be in the substitution map") + assertTrue( + equivalent(Structure(Atom("f"), listOf(Atom("a"), Atom("b"))), subs[variable]!!, subs), + "Variable should be substituted with compound term" + ) + } + + @Test + fun compound_term_with_variable_unifies_with_part() { + val variable = Variable("X") + val structure1 = Structure(Atom("f"), listOf(Atom("a"), variable)) + val structure2 = Structure(Atom("f"), listOf(Atom("a"), Atom("b"))) + + val result = unify(structure1, structure2) + + assertTrue(result.isSuccess, "Compound term with variable should unify with part") + + val subs = result.getOrNull()!! + + assertEquals(1, subs.size, "There should be one substitution") + assertTrue(subs.containsKey(variable), "Variable should be in the substitution map") + val equivalence = equivalent(Atom("b"), subs[variable]!!, subs) + assertTrue(equivalence, "Variable should be substituted with atom") + } + + @Test + fun compound_terms_with_variable_arguments_lists_alias_variables() { + val variable1 = Variable("X") + val variable2 = Variable("Y") + + val structure1 = Structure(Atom("f"), listOf(variable1)) + val structure2 = Structure(Atom("f"), listOf(variable2)) + + val result = unify(structure1, structure2) + + assertTrue(result.isSuccess, "Compound terms with variable arguments should unify") + + val subs = result.getOrNull()!! + + assertEquals(1, subs.size, "There should be one substitution") + assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map") + assertEquals(variable2, subs[variable1], "Variable 1 should alias to variable 2") + } + + /** + * f(X) = f(Y, Z) + */ + @Test + fun compound_terms_with_different_arity_do_not_unify() { + val structure1 = Structure(Atom("f"), listOf(Variable("X"))) + val structure2 = Structure(Atom("f"), listOf(Variable("Y"), Variable("Z"))) + + val result = unify(structure1, structure2) + + assertFalse(result.isSuccess, "Compound terms with different arity should not unify") + } + + /** + * ?- f(g(X)) = f(Y). + * Y = g(X). + */ + @Test + fun nested_compound_terms_with_variables_unify() { + val variable2 = Variable("Y") + + val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(Variable("X"))))) + val structure2 = Structure(Atom("f"), listOf(variable2)) + + val result = unify(structure1, structure2) + + assertTrue(result.isSuccess, "Nested compound terms with variables should unify") + + val subs = result.getOrNull()!! + + assertEquals(1, subs.size, "There should be one substitution") + assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map") + assertTrue( + equivalent(Structure(Atom("g"), listOf(Variable("X"))), subs[variable2]!!, subs), + "Variable should be substituted with compound term" + ) + } + + /** + * ?- f(g(X), X) = f(Y, a). + * X = a, + * Y = g(a). + */ + @Test + fun compound_terms_with_more_variables() { + val variable1 = Variable("X") + val variable2 = Variable("Y") + + val structure1 = Structure(Atom("f"), listOf(Structure(Atom("g"), listOf(variable1)), variable1)) + val structure2 = Structure(Atom("f"), listOf(variable2, Atom("a"))) + + val result = unify(structure1, structure2) + + assertTrue(result.isSuccess, "Compound terms with more variables should unify") + + val subs = result.getOrNull()!! + + assertEquals(2, subs.size, "There should be two substitutions") + assertTrue(subs.containsKey(variable1), "Variable 1 should be in the substitution map") + assertTrue( + equivalent(Atom("a"), subs[variable1]!!, subs), + "Variable 1 should be substituted with atom" + ) + assertTrue(subs.containsKey(variable2), "Variable 2 should be in the substitution map") + assertTrue( + equivalent(Structure(Atom("g"), listOf(Atom("a"))), subs[variable2]!!, subs), + "Variable 2 should be substituted with compound term" + + ) + } + + /** + * ?- X = f(X). + * X = f(f(X)). + */ + @Test + @Disabled("If the occurs check is applied, this should fail") + fun recursive_unification() { + val variable1 = Variable("X") + val structure2 = Structure(Atom("f"), listOf(Variable("X"))) + + val result = unifyLazy(variable1, structure2, emptyMap()).toList() + + assertEquals(1, result.size, "There should be one result") + assertTrue(result[0].isSuccess, "Recursive unification should succeed") + + val subs = result[0].getOrNull()!! + + assertEquals(1, subs.size, "There should be one substitution") + assertTrue(subs.containsKey(variable1), "Variable should be in the substitution map") + assertEquals(structure2, subs[variable1], "Variable should be substituted with compound term") + } + + /** + * ?- X = bar, Y = bar, X = Y. + * X = Y, Y = bar. + */ + @Test + fun multiple_unification() { + val variable1 = Variable("X") + val variable2 = Variable("Y") + val atom = Atom("bar") + + val map: Substitutions = mapOf( + variable1 to atom, + variable2 to atom + ) + val result = unifyLazy(variable1, variable2, map).toList() + + assertEquals(1, result.size, "There should be one substitution") + assertTrue(result[0].isSuccess, "Multiple unification should succeed") + assertEquals(0, result[0].getOrNull()!!.size, "No (additional) substitutions should be made") + } + + /** + * ?- a = a(). + * false. + */ + @Test + fun atom_with_different_arity() { + val atom1 = Atom("a") + val structure2 = Structure(Atom("a"), emptyList()) + + val result = unify(atom1, structure2) + + assertFalse(result.isSuccess, "Atom with different arity should not unify") + } + + @Test + fun identical_integers_unify() { + val int1 = Integer(1) + val int2 = Integer(1) + + val result = unify(int1, int2) + + assertTrue(result.isSuccess, "Identical integers should unify") + assertEquals(0, result.getOrNull()!!.size, "No substitutions should be made") + } + + @Test + fun different_integers_do_not_unify() { + val int1 = Integer(1) + val int2 = Integer(2) + + val result = unify(int1, int2) + + assertFalse(result.isSuccess, "Different integers should not unify") + } + + + @Test + fun `1 + 2 does not unify with 3`() { + val expr1 = Add(Integer(1), Integer(2)) + val expr2 = Integer(3) + + val result = unify(expr1, expr2) + + assertFalse(result.isSuccess, "1 + 2 should not unify with 3") + } + } + + @Nested + class `applySubstitution` { + @Test + fun `apply substitution without sub`() { + val term = Variable("X") + val subs: Substitutions = emptyMap() + + val result = applySubstitution(term, subs) + + assertEquals(term, result) + } + + @Test + fun `apply single substitution`() { + val sub = Variable("X") to Integer(5) + val subs: Substitutions = mapOf(sub) + + val term = Variable("X") + + val result = applySubstitution(term, subs) + + assertEquals(Integer(5), result) + } + + @Test + fun `apply chained substitution`() { + val sub1 = Variable("HP") to Variable("HP(19)") + val sub2 = Variable("HP(19)") to Integer(35) + + val subs: Substitutions = mapOf(sub1, sub2) + + val term = Variable("HP") + + val result = applySubstitution(term, subs) + + assertEquals(Integer(35), result) + } + } +} \ No newline at end of file diff --git a/tests/prolog/logic/UnifyTests.kt b/tests/prolog/logic/UnifyTests.kt deleted file mode 100644 index 0f0199f..0000000 --- a/tests/prolog/logic/UnifyTests.kt +++ /dev/null @@ -1,328 +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.fail - -/* - * 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 - @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") - } -} \ No newline at end of file From 80fb3d1e605d16a86c0cf4de5b92f141058db206 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 2 May 2025 21:51:34 +0200 Subject: [PATCH 16/19] Assert{a,z,} --- src/interpreter/Preprocessor.kt | 86 +++------ src/parser/grammars/TermsGrammar.kt | 27 +-- src/prolog/Program.kt | 4 +- src/prolog/ast/Database.kt | 6 +- src/prolog/ast/logic/Clause.kt | 16 +- src/prolog/ast/logic/Predicate.kt | 4 +- src/prolog/builtins/databaseOperators.kt | 38 ++++ src/prolog/builtins/{io.kt => ioOperators.kt} | 0 tests/interpreter/PreprocessorTests.kt | 32 ++++ .../builtins/DatabaseOperatorsParserTests.kt | 65 +++++++ .../prolog/builtins/DatabaseOperatorsTests.kt | 174 ++++++++++++++++++ 11 files changed, 373 insertions(+), 79 deletions(-) create mode 100644 src/prolog/builtins/databaseOperators.kt rename src/prolog/builtins/{io.kt => ioOperators.kt} (100%) create mode 100644 tests/parser/builtins/DatabaseOperatorsParserTests.kt create mode 100644 tests/prolog/builtins/DatabaseOperatorsTests.kt diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 559a7b2..17e5dc9 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -66,72 +66,40 @@ open class Preprocessor { 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" -> 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" -> { - 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) - } + 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 == "-/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) - 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 == "assert/1" -> { + if (args[0] is Rule) { + Assert(args[0] as Rule) + } else { + Assert(Fact(args[0] as Head)) + } } + term.functor == "asserta/1" -> AssertA(args[0] as Clause) // Other term.functor == "write/1" -> Write(args[0]) diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index 487f9f7..f62e54d 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -31,10 +31,12 @@ import prolog.ast.terms.* * | 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) } @@ -66,42 +68,43 @@ open class TermsGrammar : Tokens() { or int ) - // Level 200 - prefix operators (+, -, \) 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) - // Level 400 - multiplication, division 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)) } } - // Level 500 - addition, subtraction 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)) } } - // Level 700 - comparison operators 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)) } - // Level 1000 - conjunction (,) - protected val term1000: Parser by (term700 * zeroOrMore(comma * term700)) use { - t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(","), listOf(acc, term)) } + 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)) } } - // Level 1100 - disjunction (;) - protected val term1100: Parser by (term1000 * zeroOrMore(semicolon * term1000)) use { - t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(";"), 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 op1200: Parser by (neck) use { text } + protected val term1200: Parser by (term1100 * 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 term1100 + protected val term: Parser by term1200 protected val termNoConjunction: Parser by term700 // Parts for clauses diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index d129f0d..c7f679a 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -12,7 +12,7 @@ import prolog.ast.terms.Goal * This object is a singleton that manages a list of databases. */ object Program : Resolvent { - private val internalDb = Database("") + val internalDb = Database("") private val databases: MutableList = mutableListOf(internalDb) var storeNewLine: Boolean = false @@ -35,7 +35,7 @@ object Program : Resolvent { } } - fun load(clauses: List) = internalDb.load(clauses) + fun load(clauses: List, index: Int? = null) = internalDb.load(clauses, index) fun clear() { databases.forEach { it.clear() } diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt index e295e39..d10695d 100644 --- a/src/prolog/ast/Database.kt +++ b/src/prolog/ast/Database.kt @@ -14,7 +14,7 @@ import prolog.ast.terms.Goal * Prolog Program or Database */ class Database(val sourceFile: String): Resolvent { - private var predicates: Map = emptyMap() + var predicates: Map = emptyMap() fun initialize() { Logger.info("Initializing database from $sourceFile") @@ -39,14 +39,14 @@ class Database(val sourceFile: String): Resolvent { /** * Loads a list of clauses into the program. */ - fun load(clauses: List) { + fun load(clauses: List, index: Int? = null) { 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) + predicate.add(clause, index) } else { // If the predicate does not exist, create a new one predicates += Pair(functor, Predicate(listOf(clause))) diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index f402951..a48fb1f 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -19,7 +19,7 @@ import prolog.logic.unifyLazy * @see [prolog.ast.terms.Variable] * @see [Predicate] */ -abstract class Clause(val head: Head, val body: Body) : Resolvent { +abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { val functor: Functor = head.functor override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { @@ -70,4 +70,18 @@ abstract class Clause(val head: Head, val body: Body) : 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 hashCode(): Int { + return super.hashCode() + } } diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 236f286..5640f57 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -36,9 +36,9 @@ class Predicate : Resolvent { /** * Adds a clause to the predicate. */ - fun add(clause: Clause) { + fun add(clause: Clause, index: Int? = null) { require(clause.functor == functor) { "Clause functor does not match predicate functor" } - clauses.add(clause) + if (index != null) clauses.add(index, clause) else clauses.add(clause) } /** diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt new file mode 100644 index 0000000..ec13779 --- /dev/null +++ b/src/prolog/builtins/databaseOperators.kt @@ -0,0 +1,38 @@ +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.Program +import prolog.ast.terms.Functor + +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) : Structure(Atom("asserta"), listOf(clause)) { + override fun satisfy(subs: Substitutions): Answers { + // Add clause to the program + Program.load(listOf(clause), 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) : Structure(Atom("assertz"), listOf(clause)) { + override fun satisfy(subs: Substitutions): Answers { + // Add clause to the program + Program.load(listOf(clause)) + + return sequenceOf(Result.success(emptyMap())) + } +} diff --git a/src/prolog/builtins/io.kt b/src/prolog/builtins/ioOperators.kt similarity index 100% rename from src/prolog/builtins/io.kt rename to src/prolog/builtins/ioOperators.kt diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index d7d6c19..d0467c6 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -6,6 +6,8 @@ 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.* @@ -498,4 +500,34 @@ class PreprocessorTests { ) } } + + @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) + } + + } } diff --git a/tests/parser/builtins/DatabaseOperatorsParserTests.kt b/tests/parser/builtins/DatabaseOperatorsParserTests.kt new file mode 100644 index 0000000..9f13e57 --- /dev/null +++ b/tests/parser/builtins/DatabaseOperatorsParserTests.kt @@ -0,0 +1,65 @@ +package parser.builtins + +import com.github.h0tk3y.betterParse.grammar.Grammar +import com.github.h0tk3y.betterParse.grammar.parseToEnd +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 +import kotlin.test.assertEquals + +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) + } +} diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt new file mode 100644 index 0000000..f3d7c9a --- /dev/null +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -0,0 +1,174 @@ +package prolog.builtins + +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.params.ParameterizedTest +import org.junit.jupiter.params.provider.ValueSource +import prolog.Program +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.Variable +import kotlin.test.assertTrue + +class DatabaseOperatorsTests { + abstract class AssertTestsBase { + protected abstract fun createAssert(clause: Clause): Structure + + @BeforeEach + fun setup() { + Program.clear() + } + + @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.internalDb.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.internalDb.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.internalDb.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.internalDb.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.internalDb.predicates.size, "Expected 1 predicate") + assertEquals(rule, Program.internalDb.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.internalDb.predicates.size, "Expected 1 predicate") + assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(rule1, Program.internalDb.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.internalDb.predicates.size, "Expected 1 predicate") + assertEquals(rule1, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[1]) + } + } + + @Test + fun `custom 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 From 5bfa1691dd9e4c1ca9be3d4fd9223cda24d657c6 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Fri, 2 May 2025 23:50:29 +0200 Subject: [PATCH 17/19] Retract --- .idea/2025LogProg-PrologInterpreter.iml | 8 ++ .idea/modules.xml | 8 ++ src/Main.kt | 1 - src/interpreter/Preprocessor.kt | 16 ++- src/prolog/Program.kt | 2 +- src/prolog/Substitution.kt | 2 +- src/prolog/builtins/databaseOperators.kt | 49 ++++++- src/repl/Repl.kt | 10 +- tests/interpreter/PreprocessorTests.kt | 75 +++++++++++ .../prolog/builtins/DatabaseOperatorsTests.kt | 122 +++++++++++++++++- 10 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 .idea/2025LogProg-PrologInterpreter.iml create mode 100644 .idea/modules.xml diff --git a/.idea/2025LogProg-PrologInterpreter.iml b/.idea/2025LogProg-PrologInterpreter.iml new file mode 100644 index 0000000..42d53f5 --- /dev/null +++ b/.idea/2025LogProg-PrologInterpreter.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5b434ac --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Main.kt b/src/Main.kt index 55a12bf..2416fdb 100644 --- a/src/Main.kt +++ b/src/Main.kt @@ -1,5 +1,4 @@ import com.xenomachina.argparser.ArgParser -import com.xenomachina.argparser.mainBody import interpreter.FileLoader import io.GhentPrologArgParser import io.Logger diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 17e5dc9..7477702 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -92,6 +92,7 @@ open class Preprocessor { 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 == "retract/1" -> Retract(args[0]) term.functor == "assert/1" -> { if (args[0] is Rule) { Assert(args[0] as Rule) @@ -99,7 +100,20 @@ open class Preprocessor { Assert(Fact(args[0] as Head)) } } - term.functor == "asserta/1" -> AssertA(args[0] as Clause) + 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]) diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt index c7f679a..e82b0bf 100644 --- a/src/prolog/Program.kt +++ b/src/prolog/Program.kt @@ -13,7 +13,7 @@ import prolog.ast.terms.Goal */ object Program : Resolvent { val internalDb = Database("") - private val databases: MutableList = mutableListOf(internalDb) + val databases: MutableList = mutableListOf(internalDb) var storeNewLine: Boolean = false var variableRenamingStart: Int = 0 diff --git a/src/prolog/Substitution.kt b/src/prolog/Substitution.kt index 062d63e..9058f8c 100644 --- a/src/prolog/Substitution.kt +++ b/src/prolog/Substitution.kt @@ -8,4 +8,4 @@ abstract class Substitution(val from: Term, val to: Term) { } typealias Substitutions = Map typealias Answer = Result -typealias Answers = Sequence \ No newline at end of file +typealias Answers = Sequence diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index ec13779..a002bfa 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -8,6 +8,11 @@ import prolog.ast.terms.Structure import prolog.ast.logic.Predicate import prolog.Program import prolog.ast.terms.Functor +import prolog.ast.terms.Term +import prolog.ast.logic.Fact +import prolog.ast.Database +import prolog.ast.terms.Operator +import prolog.logic.unifyLazy class Assert(clause: Clause) : AssertZ(clause) { override val functor: Functor = "assert/1" @@ -16,7 +21,7 @@ class Assert(clause: Clause) : AssertZ(clause) { /** * Assert a [Clause] as a first clause of the [Predicate] into the [Program]. */ -class AssertA(val clause: Clause) : Structure(Atom("asserta"), listOf(clause)) { +class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program Program.load(listOf(clause), 0) @@ -28,7 +33,7 @@ class AssertA(val clause: Clause) : Structure(Atom("asserta"), listOf(clause)) { /** * Assert a [Clause] as a last clause of the [Predicate] into the [Program]. */ -open class AssertZ(val clause: Clause) : Structure(Atom("assertz"), listOf(clause)) { +open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program Program.load(listOf(clause)) @@ -36,3 +41,43 @@ open class AssertZ(val clause: Clause) : Structure(Atom("assertz"), listOf(claus 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 + + Program.databases + .filter { it.predicates.containsKey(functorName) } + .mapNotNull { it.predicates[functorName] } + .map { predicate -> + val clausesIterator = predicate.clauses.iterator() + while (clausesIterator.hasNext()) { + val clause = clausesIterator.next() + unifyLazy(term, clause.head, subs).forEach { unifyResult -> + unifyResult.fold( + onSuccess = { substitutions -> + // If unification is successful, remove the clause + yield(Result.success(substitutions)) + clausesIterator.remove() + }, + onFailure = { + // If unification fails, do nothing + } + ) + } + } + } + } +} diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index a7c684a..aae1390 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -42,10 +42,9 @@ class Repl { val iterator = answers.iterator() if (!iterator.hasNext()) { - io.say("false.") + io.say("false.\n") } else { - var previous = iterator.next() - io.say(prettyPrint(previous)) + io.say(prettyPrint(iterator.next())) while (iterator.hasNext()) { var command = io.prompt("") @@ -57,8 +56,7 @@ class Repl { when (command) { ";" -> { - previous = iterator.next() - io.say(prettyPrint(previous)) + io.say(prettyPrint(iterator.next())) } "a" -> return "." -> return @@ -88,7 +86,7 @@ class Repl { val subs = result.getOrNull()!! if (subs.isEmpty()) { io.checkNewLine() - return "true.\n" + return "true." } return subs.entries.joinToString(",\n") { "${it.key} = ${it.value}" } }, diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index d0467c6..0b98ff4 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -529,5 +529,80 @@ class PreprocessorTests { 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) + } } } diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index f3d7c9a..a14453d 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -9,6 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource import prolog.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 @@ -16,6 +17,11 @@ import prolog.ast.terms.Variable import kotlin.test.assertTrue class DatabaseOperatorsTests { + @BeforeEach + fun setup() { + Program.clear() + } + abstract class AssertTestsBase { protected abstract fun createAssert(clause: Clause): Structure @@ -114,7 +120,111 @@ class DatabaseOperatorsTests { } @Test - fun `custom example`() { + 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.internalDb.load(predicate) + + assertEquals(1, Program.query(Atom("a")).count()) + + val retract = Retract(Atom("a")) + + assertTrue(retract.satisfy(emptyMap()).any(), "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.internalDb.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()) + + assertEquals(3, predicate.clauses.size, "Expected 3 clauses") + + var answer = result.first() + + assertTrue(answer.isSuccess, "Expected success") + var subs = answer.getOrNull()!! + assertTrue(subs.isEmpty(), "Expected no substitutions") + assertEquals(2, predicate.clauses.size, "Expected 2 clauses") + + assertTrue(result.first().isSuccess) + assertTrue(result.first().isSuccess) + + 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.internalDb.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()) + + assertEquals(3, predicate.clauses.size, "Expected 3 clauses") + + var answer = result.first() + + 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") + assertEquals(2, predicate.clauses.size, "Expected 2 clauses") + + answer = result.first() + + 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") + assertEquals(1, predicate.clauses.size, "Expected 1 clause") + + answer = result.first() + + 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") + assertEquals(0, predicate.clauses.size, "Expected no clauses") + + assertEquals(0, result.count(), "Expected no remaining results") + } + + @Test + fun `custom assert example`() { var query = Structure(Atom("likes"), listOf(Atom("alice"), Atom("pizza"))) var result = Program.query(query).toList() @@ -150,10 +260,12 @@ class DatabaseOperatorsTests { 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 = 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() From a85169dced2e2b57af5faf5c94395c9aabe7b32a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 4 May 2025 21:50:58 +0200 Subject: [PATCH 18/19] Checkpoint --- examples/scratchpad.pl | 43 +++++---- src/interpreter/FileLoader.kt | 21 +++-- src/interpreter/Preprocessor.kt | 1 + src/io/Terminal.kt | 2 +- src/parser/grammars/TermsGrammar.kt | 47 ++++++---- src/parser/grammars/Tokens.kt | 20 +++-- src/prolog/Program.kt | 57 ------------ src/prolog/ast/Database.kt | 90 +++++++++++++++---- src/prolog/ast/logic/Clause.kt | 12 +-- src/prolog/ast/logic/Predicate.kt | 21 +++-- src/prolog/ast/terms/Goal.kt | 2 +- src/prolog/builtins/controlOperators.kt | 3 +- src/prolog/builtins/databaseOperators.kt | 74 ++++++++++----- src/prolog/builtins/ioOperators.kt | 4 +- src/prolog/builtins/other.kt | 1 + src/prolog/logic/terms.kt | 8 +- src/prolog/logic/unification.kt | 31 +++---- src/repl/Repl.kt | 3 +- tests/e2e/Examples.kt | 49 ++++++---- .../ParserPreprocessorIntegrationTests.kt | 1 - tests/interpreter/PreprocessorTests.kt | 14 +++ tests/interpreter/SourceFileReaderTests.kt | 4 +- .../builtins/DatabaseOperatorsParserTests.kt | 12 ++- tests/prolog/EvaluationTests.kt | 5 +- .../prolog/builtins/ControlOperatorsTests.kt | 10 +-- .../prolog/builtins/DatabaseOperatorsTests.kt | 73 ++++++++------- tests/prolog/logic/TermsTests.kt | 19 ++-- 27 files changed, 377 insertions(+), 250 deletions(-) delete mode 100644 src/prolog/Program.kt diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl index 9367251..0ed5d04 100644 --- a/examples/scratchpad.pl +++ b/examples/scratchpad.pl @@ -1,27 +1,32 @@ % choice(X) :- X = 1, !; X = 2. -grade(alice, a). -grade(bob, b). -grade(carol, a). -grade(dave, c). +:- dynamic declaration/1. -got_an_a(Student) :- - grade(Student, Grade), - Grade = a. +add_declaration_first(NewDecl) :- + asserta(declaration(NewDecl)). -did_not_get_an_a(Student) :- - grade(Student, Grade), - Grade \= a. +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 :- - write("While "), - got_an_a(X), - write(X), write(" got an A, "), fail; - write("but "), - did_not_get_an_a(Y), - write(Y), write(" did not get an A, "), fail; write("unfortunately."), nl. + database, + show_declarations, + retractall(declaration(_)), + show_declarations. -:- initialization(main). -main :- write('gpl zegt: '), groet(wereld), nl. -groet(X) :- write(dag(X)). diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt index 2121fd7..7af15ab 100644 --- a/src/interpreter/FileLoader.kt +++ b/src/interpreter/FileLoader.kt @@ -3,8 +3,9 @@ package interpreter import io.Logger import parser.ScriptParser import prolog.ast.Database -import prolog.Program +import prolog.ast.Database.Program import prolog.ast.logic.Clause +import prolog.ast.logic.Predicate class FileLoader { private val parser = ScriptParser() @@ -17,10 +18,8 @@ class FileLoader { Logger.debug("Parsing content of $filePath") val clauses: List = parser.parse(input) - val db = Database(filePath) - db.load(clauses) - Program.add(db) - db.initialize() + Logger.debug("Adding clauses to program") + addToProgram(clauses, filePath) Logger.debug("Finished loading file: $filePath") } @@ -41,4 +40,16 @@ class FileLoader { 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 index 7477702..6c17705 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -92,6 +92,7 @@ open class Preprocessor { 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) { diff --git a/src/io/Terminal.kt b/src/io/Terminal.kt index c75a3eb..1b9df94 100644 --- a/src/io/Terminal.kt +++ b/src/io/Terminal.kt @@ -1,6 +1,6 @@ package io -import prolog.Program +import prolog.ast.Database.Program import java.io.BufferedReader import java.io.BufferedWriter import java.io.InputStream diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index f62e54d..3fb9e48 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -6,30 +6,32 @@ 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 | :-, ?- | - * | 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 | $ | + * | 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. @@ -58,6 +60,8 @@ open class TermsGrammar : Tokens() { 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) @@ -98,8 +102,13 @@ open class TermsGrammar : Tokens() { 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 (term1100 * zeroOrMore(op1200 * term1100)) use { + protected val term1200: Parser by (term1150 * zeroOrMore(op1200 * term1100)) use { t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } } diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index bffc308..ac8c36f 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -8,21 +8,29 @@ import com.github.h0tk3y.betterParse.lexer.regexToken import com.github.h0tk3y.betterParse.lexer.token abstract class Tokens : Grammar() { - // Special tokens - protected val neck by literalToken(":-") protected val leftParenthesis: Token by literalToken("(") protected val rightParenthesis: Token by literalToken(")") - protected val comma: 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 notEquals: 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("/") - protected val exclamation: Token by literalToken("!") - protected val isOp: Token by literalToken("is") + // 100 protected val dot by literalToken(".") // Prolog tokens diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt deleted file mode 100644 index e82b0bf..0000000 --- a/src/prolog/Program.kt +++ /dev/null @@ -1,57 +0,0 @@ -package prolog - -import io.Logger -import prolog.ast.Database -import prolog.ast.logic.Clause -import prolog.ast.logic.Resolvent -import prolog.ast.terms.Goal - -/** - * Object to handle execution - * - * This object is a singleton that manages a list of databases. - */ -object Program : Resolvent { - val internalDb = Database("") - val databases: MutableList = mutableListOf(internalDb) - - var storeNewLine: Boolean = false - var variableRenamingStart: Int = 0 - - fun add(database: Database) { - databases.add(database) - } - - /** - * 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 = sequence { - Logger.debug("Solving goal $goal") - for (database in databases) { - yieldAll(database.solve(goal, subs)) - } - } - - fun load(clauses: List, index: Int? = null) = internalDb.load(clauses, index) - - fun clear() { - databases.forEach { it.clear() } - } - - fun clear(filePath: String) { - val correspondingDBs = databases.filter { it.sourceFile == filePath } - - require(correspondingDBs.isNotEmpty()) { "No database found for file: $filePath" } - - correspondingDBs.forEach { it.clear() } - } - - fun reset() { - clear() - variableRenamingStart = 0 - storeNewLine = false - } -} \ No newline at end of file diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt index d10695d..be2da7a 100644 --- a/src/prolog/ast/Database.kt +++ b/src/prolog/ast/Database.kt @@ -1,7 +1,6 @@ package prolog.ast import io.Logger -import prolog.Program import prolog.Answers import prolog.Substitutions import prolog.ast.logic.Clause @@ -13,10 +12,21 @@ import prolog.ast.terms.Goal /** * Prolog Program or Database */ -class Database(val sourceFile: String): Resolvent { +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") @@ -28,35 +38,31 @@ class Database(val sourceFile: String): Resolvent { } } - 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. + * + * @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) { + 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) + predicate.add(clause, index, force) } else { - // If the predicate does not exist, create a new one - predicates += Pair(functor, Predicate(listOf(clause))) + // 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) { + fun load(predicate: Predicate, force: Boolean = false) { val functor = predicate.functor val existingPredicate = predicates[functor] @@ -70,7 +76,55 @@ class Database(val sourceFile: String): Resolvent { } fun clear() { - Logger.debug("Clearing ${this::class.java.simpleName}") - predicates = emptyMap() + 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) } -} \ No newline at end of file + + /** + * 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/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index a48fb1f..2abfa77 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -1,7 +1,7 @@ package prolog.ast.logic import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.terms.* import prolog.builtins.True @@ -16,10 +16,10 @@ import prolog.logic.unifyLazy * * A clause consists of a [Head] and body separated by the neck operator, or it is a [Fact]. * - * @see [prolog.ast.terms.Variable] + * @see [Variable] * @see [Predicate] */ -abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { +abstract class Clause(var head: Head, var body: Body) : Term, Resolvent { val functor: Functor = head.functor override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { @@ -35,7 +35,7 @@ abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { Program.variableRenamingStart = end var newSubs: Substitutions = subs + renamed - unifyLazy(goal, head, newSubs).forEach { headAnswer -> + unifyLazy(applySubstitution(goal, subs), head, newSubs).forEach { headAnswer -> headAnswer.map { headSubs -> // If the body can be proven, yield the (combined) substitutions newSubs = subs + renamed + headSubs @@ -43,8 +43,8 @@ abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { bodyAnswer.fold( onSuccess = { bodySubs -> var result = (headSubs + bodySubs) - .mapKeys { reverse[it.key] ?: it.key } - .mapValues { reverse[it.value] ?: it.value } + .mapKeys { applySubstitution(it.key, reverse)} + .mapValues { applySubstitution(it.value, reverse) } result = result.map { it.key to applySubstitution(it.value, result) } .toMap() .filterNot { it.key in renamed.keys && !occurs(it.key as Variable, goal, emptyMap())} diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 5640f57..25fb737 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -15,37 +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) { + constructor(functor: Functor, dynamic: Boolean = false) { this.functor = functor this.clauses = mutableListOf() + this.dynamic = dynamic } /** * Creates a predicate with the given clauses. */ - constructor(clauses: List) { + constructor(clauses: List, dynamic: Boolean = false) { this.functor = clauses.first().functor require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" } this.clauses = clauses.toMutableList() + this.dynamic = dynamic } /** * Adds a clause to the predicate. + * + * @param clause The clause to add. + * @param index The index at which to insert the clause. If null, the clause is added to the end of the list. + * @param force If true, the clause is added even if the predicate is static. */ - fun add(clause: Clause, index: Int? = null) { + fun add(clause: Clause, index: Int? = null, force: Boolean = false) { require(clause.functor == functor) { "Clause functor does not match predicate functor" } + require(dynamic || force) { "No permission to modify static procedure '$functor'" } + if (index != null) clauses.add(index, clause) else clauses.add(clause) } /** * Adds a list of clauses to the predicate. */ - fun addAll(clauses: List) { + fun addAll(clauses: List, force: Boolean = false) { require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" } + require(dynamic || force) { "No permission to modify static procedure '$functor'" } + this.clauses.addAll(clauses) } @@ -75,4 +86,4 @@ class Predicate : Resolvent { } } } -} +} \ No newline at end of file diff --git a/src/prolog/ast/terms/Goal.kt b/src/prolog/ast/terms/Goal.kt index e0a5c49..95f9016 100644 --- a/src/prolog/ast/terms/Goal.kt +++ b/src/prolog/ast/terms/Goal.kt @@ -1,7 +1,7 @@ package prolog.ast.terms import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.logic.LogicOperand diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 01cd333..613c915 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -120,7 +120,8 @@ class Bar(leftOperand: LogicOperand, rightOperand: LogicOperand) : Disjunction(l class Not(private val goal: Goal) : LogicOperator(Atom("\\+"), rightOperand = goal) { override fun satisfy(subs: Substitutions): Answers { // If the goal can be proven, return an empty sequence - if (goal.satisfy(subs).toList().isNotEmpty()) { + val goalResults = goal.satisfy(subs).iterator() + if (goalResults.hasNext()) { return emptySequence() } // If the goal cannot be proven, return a sequence with an empty map diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index a002bfa..eaf67b8 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -6,14 +6,44 @@ import prolog.ast.logic.Clause import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.logic.Predicate -import prolog.Program +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() +} + class Assert(clause: Clause) : AssertZ(clause) { override val functor: Functor = "assert/1" } @@ -24,7 +54,8 @@ class Assert(clause: Clause) : AssertZ(clause) { class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program - Program.load(listOf(clause), 0) + val evaluatedClause = applySubstitution(clause, subs) as Clause + Program.load(listOf(evaluatedClause), 0) return sequenceOf(Result.success(emptyMap())) } @@ -36,7 +67,8 @@ class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program - Program.load(listOf(clause)) + val evaluatedClause = applySubstitution(clause, subs) as Clause + Program.load(listOf(evaluatedClause)) return sequenceOf(Result.success(emptyMap())) } @@ -58,26 +90,24 @@ class Retract(val term: Term) : Operator(Atom("retract"), null, term) { val functorName = term.functor - Program.databases - .filter { it.predicates.containsKey(functorName) } - .mapNotNull { it.predicates[functorName] } - .map { predicate -> - val clausesIterator = predicate.clauses.iterator() - while (clausesIterator.hasNext()) { - val clause = clausesIterator.next() - unifyLazy(term, clause.head, subs).forEach { unifyResult -> - unifyResult.fold( - onSuccess = { substitutions -> - // If unification is successful, remove the clause - yield(Result.success(substitutions)) - clausesIterator.remove() - }, - onFailure = { - // If unification fails, do nothing - } - ) + 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 index 3f9aaae..1271296 100644 --- a/src/prolog/builtins/ioOperators.kt +++ b/src/prolog/builtins/ioOperators.kt @@ -4,7 +4,7 @@ import io.Logger import io.Terminal import parser.ReplParser import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.logic.Satisfiable import prolog.ast.terms.Atom @@ -26,6 +26,8 @@ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satis return sequenceOf(Result.success(emptyMap())) } + + override fun toString(): String = "write($term)" } /** diff --git a/src/prolog/builtins/other.kt b/src/prolog/builtins/other.kt index 8609473..4cc582f 100644 --- a/src/prolog/builtins/other.kt +++ b/src/prolog/builtins/other.kt @@ -8,6 +8,7 @@ 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) { diff --git a/src/prolog/logic/terms.kt b/src/prolog/logic/terms.kt index 8d1eeb0..8c2b26f 100644 --- a/src/prolog/logic/terms.kt +++ b/src/prolog/logic/terms.kt @@ -47,17 +47,17 @@ fun numbervars( } val from = term as Variable - var suggestedName = "${from.name}($start)" + var suggestedName = "${from.name}@$start" // If the suggested name is already in use, find a new one - while ((subs + sessionSubs).filter { (it.key as Variable).name == suggestedName }.isNotEmpty()) { + while ((subs + sessionSubs).any { (it.key as Variable).name == suggestedName }) { val randomInfix = ((0..9) + ('a'..'z') + ('A'..'Z')).random() - suggestedName = "${from.name}_${randomInfix}_($start)" + suggestedName = "${from.name}@${randomInfix}_($start)" } return Pair(start + 1, mapOf(from to Variable(suggestedName))) } compound(term, subs) -> { - val from = term as Structure + val from = applySubstitution(term, subs) as Structure var n = start val s: MutableMap = sessionSubs.toMutableMap() from.arguments.forEach { arg -> diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 0cf1321..1e7b57f 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -4,23 +4,24 @@ import prolog.Answer import prolog.Answers import prolog.Substitutions import prolog.ast.arithmetic.Expression -import prolog.ast.logic.LogicOperator -import prolog.ast.terms.* -import kotlin.NoSuchElementException -import prolog.ast.arithmetic.Number -import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer +import prolog.ast.arithmetic.Number +import prolog.ast.logic.Clause +import prolog.ast.logic.Fact +import prolog.ast.logic.LogicOperator +import prolog.ast.logic.Rule +import prolog.ast.terms.* // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { + term is Fact -> { + Fact(applySubstitution(term.head, subs) as Head) + } + variable(term, emptyMap()) -> { - var result = subs[(term as Variable)] - - while (result != null && result is Variable && result in subs) { - result = subs[result] - } - - result ?: term + val variable = term as Variable + subs[variable]?.let { applySubstitution(term = it, subs = subs) } ?: term } atomic(term, subs) -> term compound(term, subs) -> { @@ -105,9 +106,9 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence } fun unify(term1: Term, term2: Term): Answer { - val substitutions = unifyLazy(term1, term2, emptyMap()).toList() - return if (substitutions.isNotEmpty()) { - substitutions.first() + val substitutions = unifyLazy(term1, term2, emptyMap()).iterator() + return if (substitutions.hasNext()) { + substitutions.next() } else { Result.failure(NoSuchElementException()) } diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index aae1390..5e8b0d4 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -6,7 +6,6 @@ import io.Terminal import parser.ReplParser import prolog.Answer import prolog.Answers -import prolog.Program class Repl { private val io = Terminal() @@ -97,4 +96,4 @@ class Repl { } ) } -} \ No newline at end of file +} diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt index 2b1f3fa..f93fc35 100644 --- a/tests/e2e/Examples.kt +++ b/tests/e2e/Examples.kt @@ -1,15 +1,14 @@ package e2e import interpreter.FileLoader -import org.junit.jupiter.api.Test 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 org.junit.jupiter.params.provider.ValueSource -import prolog.Program +import prolog.ast.Database.Program import java.io.ByteArrayOutputStream import java.io.PrintStream @@ -26,21 +25,39 @@ class Examples { System.setOut(PrintStream(outStream)) } + @Test + fun debugHelper() { + loader.load("examples/basics/backtracking.pl") + } + @ParameterizedTest - @MethodSource("expectations") - fun test(inputFile: String, expected: String) { - loader.load(inputFile) + @MethodSource("basics") + fun `Identical output for basics`(inputFile: String, expected: String) { + loader.load("examples/basics/$inputFile") assertEquals(expected, outStream.toString()) } - fun expectations() = listOf( - Arguments.of("examples/basics/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("examples/basics/backtracking.pl", "0\ns(0)\ns(s(0))\ns(s(s(0)))\n"), - Arguments.of("examples/basics/cut.pl", "0\n"), - Arguments.of("examples/basics/disjunction.pl", "Alice likes Italian food.\nBob likes Italian food.\n"), - Arguments.of("examples/basics/equality.pl", "X == Y failed\nX = Y succeeded\nX == Y succeeded\nX = Y succeeded\nX == Y succeeded\n"), - Arguments.of("examples/basics/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("examples/basics/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("examples/basics/write.pl", "gpl zegt: dag(wereld)\n"), + @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"), ) -} \ No newline at end of file + + fun other() = listOf( + Arguments.of("program.pl", "10\nhello(world)") + ) +} diff --git a/tests/interpreter/ParserPreprocessorIntegrationTests.kt b/tests/interpreter/ParserPreprocessorIntegrationTests.kt index a572686..05df756 100644 --- a/tests/interpreter/ParserPreprocessorIntegrationTests.kt +++ b/tests/interpreter/ParserPreprocessorIntegrationTests.kt @@ -6,7 +6,6 @@ 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.Program import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.terms.Atom diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index 0b98ff4..bf54869 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -604,5 +604,19 @@ class PreprocessorTests { 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 55cdbcc..654a6ca 100644 --- a/tests/interpreter/SourceFileReaderTests.kt +++ b/tests/interpreter/SourceFileReaderTests.kt @@ -2,12 +2,12 @@ package interpreter import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import prolog.Program +import prolog.ast.Database.Program class SourceFileReaderTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } @Test diff --git a/tests/parser/builtins/DatabaseOperatorsParserTests.kt b/tests/parser/builtins/DatabaseOperatorsParserTests.kt index 9f13e57..74aa53e 100644 --- a/tests/parser/builtins/DatabaseOperatorsParserTests.kt +++ b/tests/parser/builtins/DatabaseOperatorsParserTests.kt @@ -2,13 +2,13 @@ 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 -import kotlin.test.assertEquals class DatabaseOperatorsParserTests { private lateinit var parser: Grammar @@ -62,4 +62,14 @@ class DatabaseOperatorsParserTests { 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/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt index 3d185bb..ac3df3f 100644 --- a/tests/prolog/EvaluationTests.kt +++ b/tests/prolog/EvaluationTests.kt @@ -13,11 +13,12 @@ import prolog.logic.equivalent import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Variable +import prolog.ast.Database.Program class EvaluationTests { @BeforeEach fun setUp() { - Program.clear() + Program.reset() } @Test @@ -350,7 +351,7 @@ class EvaluationTests { ) ) - Program.clear() + Program.reset() Program.load(listOf(fact1, fact2, fact3, rule1)) } diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt index 2ffc38c..bb6098f 100644 --- a/tests/prolog/builtins/ControlOperatorsTests.kt +++ b/tests/prolog/builtins/ControlOperatorsTests.kt @@ -3,7 +3,7 @@ package prolog.builtins import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import prolog.Program +import prolog.ast.Database.Program import prolog.ast.logic.Fact import prolog.ast.logic.Rule import prolog.ast.terms.Atom @@ -14,7 +14,7 @@ import prolog.ast.terms.Variable class ControlOperatorsTests { @BeforeEach fun setUp() { - Program.clear() + Program.reset() } @Test @@ -55,7 +55,7 @@ class ControlOperatorsTests { // Now with cut - Program.clear() + Program.reset() Program.load( listOf( @@ -104,7 +104,7 @@ class ControlOperatorsTests { // Now with cut in the middle - Program.clear() + Program.reset() Program.load( listOf( @@ -138,7 +138,7 @@ class ControlOperatorsTests { // Now with cut at the end - Program.clear() + Program.reset() Program.load( listOf( diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index a14453d..8e96085 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -1,12 +1,15 @@ 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.Program +import prolog.ast.Database +import prolog.ast.Database.Program import prolog.ast.logic.Clause import prolog.ast.logic.Fact import prolog.ast.logic.Predicate @@ -14,12 +17,11 @@ import prolog.ast.logic.Rule import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Variable -import kotlin.test.assertTrue class DatabaseOperatorsTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } abstract class AssertTestsBase { @@ -27,7 +29,7 @@ class DatabaseOperatorsTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } @ParameterizedTest @@ -36,8 +38,8 @@ class DatabaseOperatorsTests { val fact = Fact(Atom("a")) createAssert(fact).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.internalDb.predicates["a/_"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.db.predicates["a/_"]!!.clauses[0]) } @Test @@ -45,8 +47,8 @@ class DatabaseOperatorsTests { val fact = Fact(Structure(Atom("a"), listOf(Atom("b")))) createAssert(fact).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.db.predicates["a/1"]!!.clauses[0]) } @Test @@ -57,8 +59,8 @@ class DatabaseOperatorsTests { ) createAssert(rule).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(rule, Program.db.predicates["a/1"]!!.clauses[0]) } } @@ -88,9 +90,9 @@ class DatabaseOperatorsTests { AssertA(rule1).satisfy(emptyMap()) AssertA(rule2).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[0]) - assertEquals(rule1, Program.internalDb.predicates["a/1"]!!.clauses[1]) + 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]) } } @@ -113,9 +115,9 @@ class DatabaseOperatorsTests { AssertZ(rule1).satisfy(emptyMap()) AssertZ(rule2).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule1, Program.internalDb.predicates["a/1"]!!.clauses[0]) - assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[1]) + 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]) } } @@ -130,13 +132,13 @@ class DatabaseOperatorsTests { @Test fun `simple retract`() { val predicate = Predicate(listOf(Fact(Atom("a")))) - Program.internalDb.load(predicate) + Program.db.load(predicate) assertEquals(1, Program.query(Atom("a")).count()) val retract = Retract(Atom("a")) - assertTrue(retract.satisfy(emptyMap()).any(), "Expected 1 result") + assertEquals(1, retract.satisfy(emptyMap()).toList().size, "Expected 1 result") assertEquals(0, predicate.clauses.size, "Expected 0 clauses") assertTrue(retract.satisfy(emptyMap()).none()) @@ -149,7 +151,7 @@ class DatabaseOperatorsTests { Fact(Atom("a")), Fact(Atom("a")) )) - Program.internalDb.load(predicate) + Program.db.load(predicate) val control = Program.query(Atom("a")).toList() @@ -157,20 +159,25 @@ class DatabaseOperatorsTests { val retract = Retract(Atom("a")) - val result = retract.satisfy(emptyMap()) + val result = retract.satisfy(emptyMap()).iterator() assertEquals(3, predicate.clauses.size, "Expected 3 clauses") - var answer = result.first() + assertTrue(result.hasNext(), "Expected more results") + + val answer = result.next() assertTrue(answer.isSuccess, "Expected success") - var subs = answer.getOrNull()!! - assertTrue(subs.isEmpty(), "Expected no substitutions") + assertTrue(answer.getOrNull()!!.isEmpty(), "Expected no substitutions") + + assertTrue(result.hasNext(), "Expected more results") assertEquals(2, predicate.clauses.size, "Expected 2 clauses") - assertTrue(result.first().isSuccess) - assertTrue(result.first().isSuccess) + 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") } @@ -181,7 +188,7 @@ class DatabaseOperatorsTests { Fact(Structure(Atom("a"), listOf(Atom("c")))), Fact(Structure(Atom("a"), listOf(Atom("d")))) )) - Program.internalDb.load(predicate) + Program.db.load(predicate) val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() @@ -189,38 +196,40 @@ class DatabaseOperatorsTests { val retract = Retract(Structure(Atom("a"), listOf(Variable("X")))) - val result = retract.satisfy(emptyMap()) + val result = retract.satisfy(emptyMap()).iterator() assertEquals(3, predicate.clauses.size, "Expected 3 clauses") - var answer = result.first() + 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.first() + 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.first() + 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") - - assertEquals(0, result.count(), "Expected no remaining results") } @Test diff --git a/tests/prolog/logic/TermsTests.kt b/tests/prolog/logic/TermsTests.kt index f67f46a..dde66e6 100644 --- a/tests/prolog/logic/TermsTests.kt +++ b/tests/prolog/logic/TermsTests.kt @@ -29,7 +29,7 @@ class TermsTests { 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") + assertEquals(Variable("X@$start"), subs[term], "Expected subs to contain the new term") } @Test @@ -53,9 +53,9 @@ class TermsTests { 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") + 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") + assertEquals(Variable("Y@${start + 1}"), subs[term.arguments[1]], "Expected subs to contain the new term") } @Test @@ -68,9 +68,9 @@ class TermsTests { 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") + 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") + assertEquals(Variable("X@$start"), subs[term.arguments[1]], "Expected subs to contain the new term") } @Test @@ -83,7 +83,7 @@ class TermsTests { 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") + assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term") } @Test @@ -97,13 +97,14 @@ class TermsTests { 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") + 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(variable), "Expected subs to contain the variable") - assertEquals(Variable("X($end1)"), subs2[variable], "Expected subs to contain the new term") + 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 From a937b1bc4479b3c793bacf1b924dcad58b280ef1 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Mon, 5 May 2025 00:18:38 +0200 Subject: [PATCH 19/19] Sync --- src/prolog/ast/arithmetic/Float.kt | 4 ++++ src/prolog/ast/arithmetic/Integer.kt | 4 ++++ src/prolog/ast/logic/Clause.kt | 24 +++++++++++------------ src/prolog/ast/logic/Fact.kt | 8 +++++++- src/prolog/ast/logic/Rule.kt | 11 ++++++++++- src/prolog/ast/terms/AnonymousVariable.kt | 6 +++++- src/prolog/ast/terms/Atom.kt | 3 +++ src/prolog/ast/terms/Body.kt | 2 +- src/prolog/ast/terms/Structure.kt | 7 +++++++ src/prolog/ast/terms/Term.kt | 11 ++++++++--- src/prolog/ast/terms/Variable.kt | 3 +++ src/prolog/builtins/controlOperators.kt | 14 +++++++++++++ src/prolog/builtins/databaseOperators.kt | 3 +++ src/prolog/logic/unification.kt | 7 ++----- tests/prolog/EvaluationTests.kt | 16 +++++++++------ 15 files changed, 93 insertions(+), 30 deletions(-) diff --git a/src/prolog/ast/arithmetic/Float.kt b/src/prolog/ast/arithmetic/Float.kt index da49530..80d0bde 100644 --- a/src/prolog/ast/arithmetic/Float.kt +++ b/src/prolog/ast/arithmetic/Float.kt @@ -1,6 +1,7 @@ package prolog.ast.arithmetic import prolog.Substitutions +import prolog.ast.terms.Term class Float(override val value: kotlin.Float): Number { // Floats are already evaluated @@ -42,4 +43,7 @@ class Float(override val value: kotlin.Float): Number { 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 dbf9c39..d0a48cb 100644 --- a/src/prolog/ast/arithmetic/Integer.kt +++ b/src/prolog/ast/arithmetic/Integer.kt @@ -3,6 +3,7 @@ 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() { // Integers are already evaluated @@ -41,4 +42,7 @@ 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 2abfa77..ed8690f 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -27,27 +27,27 @@ abstract class Clause(var head: Head, var body: Body) : Term, Resolvent { // Only if the body can be proven, the substitutions should be returned. // Do this in a lazy way. - // Since we are only interested in substitutions in the goal (as opposed to the head of this clause), - // we can use variable renaming and filter out the substitutions that are not in the goal. - val (end, renamed: Substitutions) = numbervars(head, Program.variableRenamingStart, subs) + val preHead = applySubstitution(head, subs) + val preGoal = applySubstitution(goal, subs) - val reverse = renamed.entries.associate { (a, b) -> b to a } - Program.variableRenamingStart = end + val (headEnd, headRenaming) = numbervars(preHead, Program.variableRenamingStart, subs) + val headReverse = headRenaming.entries.associate { (a, b) -> b to a } + Program.variableRenamingStart = headEnd - var newSubs: Substitutions = subs + renamed - unifyLazy(applySubstitution(goal, subs), head, newSubs).forEach { headAnswer -> + val renamedHead = applySubstitution(head, headRenaming) + unifyLazy(preGoal, renamedHead, subs).forEach { headAnswer -> headAnswer.map { headSubs -> // If the body can be proven, yield the (combined) substitutions - newSubs = subs + renamed + headSubs - body.satisfy(newSubs).forEach { bodyAnswer -> + val preBody = applySubstitution(body, headRenaming + headSubs) as Body + preBody.satisfy(subs).forEach { bodyAnswer -> bodyAnswer.fold( onSuccess = { bodySubs -> var result = (headSubs + bodySubs) - .mapKeys { applySubstitution(it.key, reverse)} - .mapValues { applySubstitution(it.value, reverse) } + .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 renamed.keys && !occurs(it.key as Variable, goal, emptyMap())} + .filterNot { it.key in headRenaming.keys && !occurs(it.key as Variable, goal, emptyMap())} yield(Result.success(result)) }, onFailure = { error -> diff --git a/src/prolog/ast/logic/Fact.kt b/src/prolog/ast/logic/Fact.kt index a52990e..f0cae19 100644 --- a/src/prolog/ast/logic/Fact.kt +++ b/src/prolog/ast/logic/Fact.kt @@ -1,6 +1,12 @@ package prolog.ast.logic +import prolog.Substitutions import prolog.ast.terms.Head +import prolog.ast.terms.Term import prolog.builtins.True +import prolog.logic.applySubstitution -class Fact(head: Head) : Clause(head, True) \ No newline at end of file +class Fact(head: Head) : Clause(head, True) { + override fun clone(): Fact = Fact(head) + override fun applySubstitution(subs: Substitutions): Fact = Fact(applySubstitution(head as Term, subs) as Head) +} \ No newline at end of file diff --git a/src/prolog/ast/logic/Rule.kt b/src/prolog/ast/logic/Rule.kt index 9eba36e..add106d 100644 --- a/src/prolog/ast/logic/Rule.kt +++ b/src/prolog/ast/logic/Rule.kt @@ -1,6 +1,15 @@ package prolog.ast.logic +import prolog.Substitutions import prolog.ast.terms.Body import prolog.ast.terms.Head +import prolog.ast.terms.Term +import prolog.logic.applySubstitution -class Rule(head: Head, body: Body) : Clause(head, body) \ No newline at end of file +class Rule(head: Head, body: Body) : Clause(head, body) { + override fun clone(): Rule = Rule(head, body) + override fun applySubstitution(subs: Substitutions): Rule = Rule( + head = applySubstitution(head as Term, subs) as Head, + body = applySubstitution(body, subs) as Body + ) +} diff --git a/src/prolog/ast/terms/AnonymousVariable.kt b/src/prolog/ast/terms/AnonymousVariable.kt index 1bc0633..8211292 100644 --- a/src/prolog/ast/terms/AnonymousVariable.kt +++ b/src/prolog/ast/terms/AnonymousVariable.kt @@ -1,8 +1,9 @@ package prolog.ast.terms import io.Logger +import prolog.Substitutions -class AnonymousVariable(id: Int) : Variable("_$id") { +class AnonymousVariable(private val id: Int) : Variable("_$id") { companion object { private var counter = 0 fun create(): AnonymousVariable { @@ -14,4 +15,7 @@ class AnonymousVariable(id: Int) : Variable("_$id") { } override fun toString(): String = "_" + + override fun clone(): AnonymousVariable = AnonymousVariable(id) + override fun applySubstitution(subs: Substitutions): AnonymousVariable = this } \ No newline at end of file diff --git a/src/prolog/ast/terms/Atom.kt b/src/prolog/ast/terms/Atom.kt index 3a6afad..dcd0a7d 100644 --- a/src/prolog/ast/terms/Atom.kt +++ b/src/prolog/ast/terms/Atom.kt @@ -21,4 +21,7 @@ open class Atom(val name: String) : Goal(), Head, Body, Resolvent { override fun hashCode(): Int { return javaClass.hashCode() } + + override fun clone(): Atom = Atom(name) + override fun applySubstitution(subs: Substitutions): Atom = Atom(name) } \ No newline at end of file diff --git a/src/prolog/ast/terms/Body.kt b/src/prolog/ast/terms/Body.kt index dc61c7d..4918d1e 100644 --- a/src/prolog/ast/terms/Body.kt +++ b/src/prolog/ast/terms/Body.kt @@ -2,4 +2,4 @@ package prolog.ast.terms import prolog.ast.logic.Satisfiable -interface Body : Satisfiable \ No newline at end of file +interface Body : Term, Satisfiable \ No newline at end of file diff --git a/src/prolog/ast/terms/Structure.kt b/src/prolog/ast/terms/Structure.kt index 385da2c..bcd5e71 100644 --- a/src/prolog/ast/terms/Structure.kt +++ b/src/prolog/ast/terms/Structure.kt @@ -3,6 +3,7 @@ package prolog.ast.terms import prolog.Answers import prolog.Substitutions import prolog.ast.logic.Resolvent +import prolog.logic.applySubstitution import prolog.logic.unifyLazy typealias Argument = Term @@ -33,4 +34,10 @@ open class Structure(val name: Atom, var arguments: List) : Goal(), He override fun hashCode(): Int { return javaClass.hashCode() } + + override fun clone(): Structure = Structure(name, arguments) + override fun applySubstitution(subs: Substitutions): Structure = Structure( + name, + arguments.map { applySubstitution(it, subs) } + ) } diff --git a/src/prolog/ast/terms/Term.kt b/src/prolog/ast/terms/Term.kt index 0fdad49..8174310 100644 --- a/src/prolog/ast/terms/Term.kt +++ b/src/prolog/ast/terms/Term.kt @@ -1,14 +1,19 @@ package prolog.ast.terms +import prolog.Substitutions import prolog.logic.compare +import prolog.ast.arithmetic.Integer +import prolog.ast.arithmetic.Float /** * Value in Prolog. * - * A [Term] is either a [Variable], [Atom], [Integer][prolog.ast.arithmetic.Integer], - * [Float][prolog.ast.arithmetic.Float] or [CompoundTerm]. + * A [Term] is either a [Variable], [Atom], [Integer], + * [Float] or [CompoundTerm]. * In addition, SWI-Prolog also defines the type TODO string. */ -interface Term : Comparable { +interface Term : Comparable, Cloneable { override fun compareTo(other: Term): Int = compare(this, other, emptyMap()) + fun applySubstitution(subs: Substitutions): Term + public override fun clone(): Term } diff --git a/src/prolog/ast/terms/Variable.kt b/src/prolog/ast/terms/Variable.kt index 2d23170..8068864 100644 --- a/src/prolog/ast/terms/Variable.kt +++ b/src/prolog/ast/terms/Variable.kt @@ -39,4 +39,7 @@ open class Variable(val name: String) : Term, Body, Expression, LogicOperand() { } 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/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 613c915..b9eeef4 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -7,6 +7,7 @@ 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.flags.AppliedCut /** @@ -34,6 +35,8 @@ class Cut() : Atom("!") { override fun satisfy(subs: Substitutions): Answers { return sequenceOf(Result.failure(AppliedCut(emptyMap()))) } + + override fun applySubstitution(subs: Substitutions): Cut = Cut() } /** @@ -94,6 +97,11 @@ class Conjunction(val left: LogicOperand, private val right: LogicOperand) : ) } } + + override fun applySubstitution(subs: Substitutions): Conjunction = Conjunction( + left.applySubstitution(subs) as LogicOperand, + right.applySubstitution(subs) as LogicOperand + ) } /** @@ -105,6 +113,12 @@ open class Disjunction(private val left: LogicOperand, private val right: LogicO yieldAll(left.satisfy(subs)) yieldAll(right.satisfy(subs)) } + + override fun clone(): Disjunction = Disjunction(left.clone() as LogicOperand, right.clone() as LogicOperand) + override fun applySubstitution(subs: Substitutions): Disjunction = Disjunction( + left.applySubstitution(subs) as LogicOperand, + right.applySubstitution(subs) as LogicOperand + ) } @Deprecated("Use Disjunction instead") diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index eaf67b8..54ebeb3 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -42,6 +42,9 @@ class Dynamic(private val dynamicFunctor: Functor): Goal(), Body { } 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) { diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 1e7b57f..5b5d21e 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -15,9 +15,7 @@ import prolog.ast.terms.* // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { - term is Fact -> { - Fact(applySubstitution(term.head, subs) as Head) - } + term is Fact -> term.applySubstitution(subs) variable(term, emptyMap()) -> { val variable = term as Variable @@ -25,8 +23,7 @@ fun applySubstitution(term: Term, subs: Substitutions): Term = when { } atomic(term, subs) -> term compound(term, subs) -> { - val structure = term as Structure - Structure(structure.name, structure.arguments.map { applySubstitution(it, subs) }) + term.applySubstitution(subs) } else -> term diff --git a/tests/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt index ac3df3f..a1cbbfc 100644 --- a/tests/prolog/EvaluationTests.kt +++ b/tests/prolog/EvaluationTests.kt @@ -108,8 +108,8 @@ class EvaluationTests { val variable2 = Variable("Y") val parent = Rule( - Structure(Atom("parent"), listOf(variable1, variable2)), - /* :- */ Disjunction( + Structure(Atom("parent"), listOf(variable1, variable2)), /* :- */ + Disjunction( Structure(Atom("father"), listOf(variable1, variable2)), /* ; */ Structure(Atom("mother"), listOf(variable1, variable2)) @@ -118,10 +118,14 @@ class EvaluationTests { Program.load(listOf(father, mother, parent)) - val result1 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jimmy")))) - assertTrue(result1.toList().isNotEmpty()) - val result2 = Program.query(Structure(Atom("parent"), listOf(Atom("jane"), Atom("jimmy")))) - assertTrue(result2.toList().isNotEmpty()) + val result1 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jimmy")))).toList() + assertEquals(1, result1.size, "Expected 1 result") + assertTrue(result1[0].isSuccess, "Expected success") + assertTrue(result1[0].getOrNull()!!.isEmpty(), "Expected no substitutions") + val result2 = Program.query(Structure(Atom("parent"), listOf(Atom("jane"), Atom("jimmy")))).toList() + assertEquals(1, result2.size, "Expected 1 result") + assertTrue(result2[0].isSuccess, "Expected success") + assertTrue(result2[0].getOrNull()!!.isEmpty(), "Expected no substitutions") val result3 = Program.query(Structure(Atom("parent"), listOf(Atom("john"), Atom("jane")))) assertFalse(result3.any())