REPL checkpoint

This commit is contained in:
Tibo De Peuter 2025-04-18 20:36:11 +02:00
parent 69c156024a
commit 1b3280a947
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
21 changed files with 503 additions and 34 deletions

3
src/Debug.kt Normal file
View file

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

View file

@ -1,9 +1,98 @@
import better_parser.PrologParser
import better_parser.SimpleReplParser
import interpreter.SourceFileReader
import prolog.Answer
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", ".")
fun main() { fun main() {
for (i in 1..10) { SourceFileReader().readFile("tests/better_parser/resources/parent.pl")
println("Hello, Kotlin Command Line Utility!")
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}")
} }
} }
fun testMe(): String {
return "Hello, Kotlin Command Line Utility!"
} }

View file

@ -0,0 +1,17 @@
package better_parser
import com.github.h0tk3y.betterParse.grammar.Grammar
import com.github.h0tk3y.betterParse.grammar.parseToEnd
import prolog.Program
import prolog.ast.logic.Clause
import prolog.ast.terms.Atom
class PrologParser {
private val parser: Grammar<List<Clause>> = SimpleSourceParser() as Grammar<List<Clause>>
public fun parse(input: String) {
val clauses: List<Clause> = parser.parseToEnd(input)
Program.load(clauses)
}
}

View file

@ -0,0 +1,70 @@
package better_parser
import com.github.h0tk3y.betterParse.combinators.*
import com.github.h0tk3y.betterParse.grammar.Grammar
import com.github.h0tk3y.betterParse.lexer.literalToken
import com.github.h0tk3y.betterParse.lexer.regexToken
import com.github.h0tk3y.betterParse.parser.Parser
import prolog.ast.logic.Fact
import prolog.ast.arithmetic.Integer
import prolog.ast.arithmetic.Float
import prolog.ast.logic.Clause
import prolog.ast.logic.LogicOperand
import prolog.ast.logic.Rule
import prolog.ast.terms.*
import prolog.builtins.Conjunction
import prolog.builtins.Disjunction
class PrologSourceParser : Grammar<List<Clause>>() {
// 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<Term> by (numberParser or variableParser or compoundTermParser or atomParser)
private val logicOperandParser: Parser<LogicOperand> by (termParser or compoundTermParser or atomParser) map {
it as LogicOperand
}
private val conjunctionParser: Parser<Conjunction> by (logicOperandParser and comma and logicOperandParser) use {
Conjunction(t1, t3)
}
private val disjunctionParser: Parser<Disjunction> by (logicOperandParser and semicolon and logicOperandParser) use {
Disjunction(t1, t3)
}
private val operatorParser: Parser<Operator> 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<Clause> by (factParser or ruleParser)
override val rootParser: Parser<List<Clause>> by zeroOrMore(clauseParser)
}

View file

@ -6,6 +6,7 @@ import com.github.h0tk3y.betterParse.grammar.parser
import com.github.h0tk3y.betterParse.lexer.Token import com.github.h0tk3y.betterParse.lexer.Token
import com.github.h0tk3y.betterParse.lexer.literalToken import com.github.h0tk3y.betterParse.lexer.literalToken
import com.github.h0tk3y.betterParse.lexer.regexToken import com.github.h0tk3y.betterParse.lexer.regexToken
import com.github.h0tk3y.betterParse.lexer.token
import com.github.h0tk3y.betterParse.parser.Parser import com.github.h0tk3y.betterParse.parser.Parser
import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Float
import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Integer
@ -16,28 +17,32 @@ import prolog.ast.terms.Variable
open class SimplePrologParser : Grammar<Any>() { open class SimplePrologParser : Grammar<Any>() {
// Prolog tokens // Prolog tokens
private val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*") protected val nameToken: Token by regexToken("[a-z][a-zA-Z0-9_]*")
private val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*") protected val variableToken: Token by regexToken("[A-Z][a-zA-Z0-9_]*")
// Arithmetic tokens // Arithmetic tokens
private val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+") private val floatToken: Token by regexToken("-?[1-9][0-9]*\\.[0-9]+")
private val integerToken: Token by regexToken("-?([1-9][0-9]*|0)") private val integerToken: Token by regexToken("-?([1-9][0-9]*|0)")
// Special tokens // Special tokens
private val comma: Token by literalToken(",") protected val neck by literalToken(":-")
private val leftParenthesis: Token by literalToken("(") protected val comma: Token by literalToken(",")
private val rightParenthesis: Token by literalToken(")") protected val leftParenthesis: Token by literalToken("(")
protected val rightParenthesis: Token by literalToken(")")
protected val dot by literalToken(".")
// Ignored tokens // Ignored tokens
private val whitespace: Token by regexToken("\\s+", ignore = true) protected val whitespace: Token by regexToken("\\s+", ignore = true)
private val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true) protected val singleLineComment: Token by regexToken("%[^\\n]*", ignore = true)
private val multiLineComment: Token by regexToken("/\\*.*?\\*/", 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 // Prolog parsers
private val atomParser: Parser<Atom> by nameToken use { Atom(text) } protected val variable: Parser<Variable> by variableToken use { Variable(text) }
private val variableParser: Parser<Variable> by variableToken use { Variable(text) } protected val atom: Parser<Atom> by nameToken use { Atom(text) }
private val structureParser: Parser<Structure> by (atomParser and skip(leftParenthesis) and separated( protected val compound: Parser<Structure> by (atom and skip(leftParenthesis) and separated(
parser(this::termParser), parser(::term),
comma, comma,
acceptZero = true acceptZero = true
) and skip(rightParenthesis)) use { ) and skip(rightParenthesis)) use {
@ -45,17 +50,18 @@ open class SimplePrologParser : Grammar<Any>() {
} }
// Arithmetic parsers // Arithmetic parsers
private val integerParser: Parser<Integer> by integerToken use { Integer(text.toInt()) } private val int: Parser<Integer> by integerToken use { Integer(text.toInt()) }
private val floatParser: Parser<Float> by floatToken use { private val float: Parser<Float> by floatToken use {
Float(text.toFloat()) Float(text.toFloat())
} }
private val termParser: Parser<Term> by (floatParser protected val term: Parser<Term> by (dummy
or integerParser or float
or variableParser or int
or structureParser or variable
or atomParser or compound
or atom
) map { it } ) map { it }
override val rootParser: Parser<Any> = termParser override val rootParser: Parser<Any> = term
} }

View file

@ -0,0 +1,27 @@
package better_parser
import com.github.h0tk3y.betterParse.combinators.times
import com.github.h0tk3y.betterParse.combinators.unaryMinus
import com.github.h0tk3y.betterParse.combinators.use
import com.github.h0tk3y.betterParse.grammar.parseToEnd
import com.github.h0tk3y.betterParse.parser.Parser
import prolog.ast.logic.LogicOperand
import prolog.builtins.Query
class SimpleReplParser(val debug: Boolean = false) : SimpleSourceParser() {
override val rootParser: Parser<Query> 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
}
}

View file

@ -0,0 +1,48 @@
package better_parser
import com.github.h0tk3y.betterParse.combinators.*
import com.github.h0tk3y.betterParse.grammar.parser
import com.github.h0tk3y.betterParse.lexer.literalToken
import com.github.h0tk3y.betterParse.parser.Parser
import prolog.ast.arithmetic.ArithmeticOperator
import prolog.ast.logic.*
import prolog.ast.terms.*
import prolog.builtins.Conjunction
import prolog.builtins.Disjunction
open class SimpleSourceParser : SimplePrologParser() {
protected val simpleLogicOperand: Parser<LogicOperand> by (dummy
or compound
or atom
)
protected val logicOperand: Parser<LogicOperand> by (dummy
or parser(::operator)
or simpleLogicOperand
)
protected val arithmeticOperator: Parser<ArithmeticOperator> by dummy
protected val logicOperator: Parser<LogicOperator> by (simpleLogicOperand * comma * logicOperand) use {
Conjunction(t1, t3)
}
protected val operator: Parser<Operator> by (arithmeticOperator or logicOperator) use { this as Operator }
protected val head: Parser<Head> by (dummy
or compound
or atom
)
protected val body: Parser<Body> by (dummy
or operator
or head
) use { this as Body }
// ----
private val rule: Parser<Rule> by (head * -neck * body) use { Rule(t1, t2) }
private val fact: Parser<Fact> by head use { Fact(this) }
private val clause: Parser<Clause> by ((rule or fact) * -dot)
private val clauses: Parser<List<Clause>> by zeroOrMore(clause)
override val rootParser: Parser<Any> by clauses
}

View file

@ -0,0 +1,23 @@
package interpreter
import better_parser.PrologParser
class SourceFileReader {
private val parser = PrologParser()
fun readFile(filePath: String) {
return try {
val file = java.io.File(filePath)
if (!file.exists()) {
throw IllegalArgumentException("File not found: $filePath")
}
val content = file.readText()
// Parse the content using SimpleSourceParser
parser.parse(content)
} catch (e: Exception) {
throw RuntimeException("Error reading file: $filePath", e)
}
}
}

View file

@ -1,5 +1,6 @@
package prolog package prolog
import Debug
import prolog.ast.logic.Clause import prolog.ast.logic.Clause
import prolog.ast.logic.Predicate import prolog.ast.logic.Predicate
import prolog.ast.logic.Resolvent import prolog.ast.logic.Resolvent
@ -12,13 +13,16 @@ typealias Database = Program
* Prolog Program or database. * Prolog Program or database.
*/ */
object Program: Resolvent { object Program: Resolvent {
private var predicates: Map<Functor, Predicate> = emptyMap() var predicates: Map<Functor, Predicate> = emptyMap()
init { init {
setup() setup()
} }
private fun setup() { private fun setup() {
if (Debug.on) {
println("Setting up Prolog program...")
}
// Initialize the program with built-in predicates // Initialize the program with built-in predicates
load(listOf( load(listOf(
)) ))

View file

@ -18,7 +18,7 @@ import prolog.logic.unifyLazy
* @see [prolog.ast.terms.Variable] * @see [prolog.ast.terms.Variable]
* @see [Predicate] * @see [Predicate]
*/ */
abstract class Clause(private val head: Head, private val body: Body) : Resolvent { abstract class Clause(val head: Head, val body: Body) : Resolvent {
val functor: Functor = head.functor val functor: Functor = head.functor
override fun solve (goal: Goal, subs: Substitutions): Answers = sequence { override fun solve (goal: Goal, subs: Substitutions): Answers = sequence {

View file

@ -39,6 +39,11 @@ class Predicate : Resolvent {
*/ */
fun add(clause: Clause) { fun add(clause: Clause) {
require(clause.functor == functor) { "Clause functor does not match predicate functor" } require(clause.functor == functor) { "Clause functor does not match predicate functor" }
if (Debug.on) {
println("Adding clause $clause to predicate $functor")
}
clauses.add(clause) clauses.add(clause)
} }

View file

@ -1,7 +1,5 @@
package prolog.ast.terms package prolog.ast.terms
import prolog.ast.arithmetic.Expression
typealias Operand = Term typealias Operand = Term
abstract class Operator( abstract class Operator(

View file

@ -39,7 +39,7 @@ class Cut() : Atom("!") {
/** /**
* Conjunction (and). True if both Goal1 and Goal2 are true. * Conjunction (and). True if both Goal1 and Goal2 are true.
*/ */
class Conjunction(private val left: LogicOperand, private val right: LogicOperand) : class Conjunction(val left: LogicOperand, private val right: LogicOperand) :
LogicOperator(Atom(","), left, right) { LogicOperator(Atom(","), left, right) {
override fun satisfy(subs: Substitutions): Answers = sequence { override fun satisfy(subs: Substitutions): Answers = sequence {
// Satisfy the left part first, which either succeeds or fails // Satisfy the left part first, which either succeeds or fails

4
src/repl/Repl.kt Normal file
View file

@ -0,0 +1,4 @@
package repl
class Repl {
}

View file

@ -16,8 +16,8 @@ import prolog.ast.terms.Term
import prolog.ast.terms.Variable import prolog.ast.terms.Variable
import prolog.logic.equivalent import prolog.logic.equivalent
class SimplePrologParserTests { class SimplePrologPrologParserTests {
lateinit var parser: Grammar<Term> private lateinit var parser: Grammar<Term>
@BeforeEach @BeforeEach
fun setup() { fun setup() {

View file

@ -0,0 +1,132 @@
package better_parser
import com.github.h0tk3y.betterParse.grammar.Grammar
import com.github.h0tk3y.betterParse.grammar.parseToEnd
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import prolog.ast.logic.Clause
import prolog.ast.logic.Fact
import prolog.ast.logic.Rule
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 {
private lateinit var parser: Grammar<List<Clause>>
@BeforeEach
fun setup() {
parser = SimpleSourceParser() as Grammar<List<Clause>>
}
@ParameterizedTest
@ValueSource(strings = [
"john.",
"mary.",
"jimmy.",
"male(john).",
"male(jimmy).",
"female(mary).",
"not(not(true)).",
"not(a, not(b, c), d, not(not(a)))."
])
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'")
}
@ParameterizedTest
@ValueSource(strings = [
"john. mary.",
"likes(john, mary). likes(mary, john).",
"belgium. capital(belgium, brussels).",
"plus(1, 2, 3). plus(3, 4, 7).",
])
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")
}
@Test
fun `simplest rule`() {
val input = "a :- b."
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())
}
@ParameterizedTest
@ValueSource(strings = [
"parent(X, Y) :- father(X, Y).",
"parent(X, Y) :- mother(X, Y)."
])
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")
}
@Test
fun `parse rule with very verbose checks`() {
val input = "parent(X, Y) :- father(X, Y)."
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
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'")
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'")
}
@Test
fun `parse rule with conjunction`() {
val input = "father(X, Y) :- parent(X, Y), male(X)."
val result = parser.parseToEnd(input)
assertEquals(1, result.size, "Expected 1 rule")
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")
}
@Test
fun `parse rule with nested conjunction`() {
val input = "guest(X, Y) :- invited(Y, X), has_time(X), not(sick(Y))."
val result = parser.parseToEnd(input)
assertEquals(1, result.size, "Expected 1 rule")
val rule = result[0] as Rule
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'")
}
}

View file

@ -0,0 +1 @@
a.

View file

@ -0,0 +1 @@
foo.

View file

@ -0,0 +1,9 @@
male(john).
male(jimmy).
female(mary).
parent(john, jimmy).
parent(mary, jimmy).
father(X, Y) :- parent(X, Y), male(X).
mother(X, Y) :- parent(X, Y), female(X).
kan_goed_koken(miriam).

View file

@ -0,0 +1,32 @@
package interpreter
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import prolog.Program
class SourceFileReaderTests {
@BeforeEach
fun setup() {
Program.clear()
}
@Test
fun a() {
val inputFile = "tests/better_parser/resources/a.pl"
val reader = SourceFileReader()
reader.readFile(inputFile)
println(Program.predicates)
}
@Test
fun foo() {
val inputFile = "tests/better_parser/resources/foo.pl"
val reader = SourceFileReader()
reader.readFile(inputFile)
println(Program.predicates)
}
}

View file

@ -10,7 +10,7 @@ import kotlin.test.assertEquals
* *
* These tests are based on the Prolog syntax. * These tests are based on the Prolog syntax.
*/ */
class ScanPrologTests { class ScanPrologParserTests {
@Test @Test
fun scan_simple_atom() { fun scan_simple_atom() {
val tokens = Lexer("atom.").scan() val tokens = Lexer("atom.").scan()