Checkpoint

This commit is contained in:
Tibo De Peuter 2025-05-01 21:16:48 +02:00
parent 9db1c66781
commit 724e911a6f
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
17 changed files with 288 additions and 95 deletions

View file

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

View file

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

View file

@ -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<Atom> by (quotedAtom or simpleAtom)
protected val compound: Parser<Structure> 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<Integer> by integerToken use { Integer(text.toInt()) }
protected val float: Parser<Float> by floatToken use { Float(text.toFloat()) }
// Operators
protected val ops: Parser<String> by (dummy
// Logic
or comma
or semicolon
// Arithmetic
or plus
or equals
or notEquals
) use { this.text }
protected val simpleOperand: Parser<Operand> by (dummy
// Logic
// Base terms (atoms, compounds, variables, numbers)
protected val baseTerm: Parser<Term> by (dummy
or (-leftParenthesis * parser(::term) * -rightParenthesis)
or compound
or atom
or variable
// Arithmetic
or int
or float
or int
)
protected val operand: Parser<Operand> by (dummy
or parser(::operator)
or simpleOperand
)
protected val operator: Parser<CompoundTerm> by (simpleOperand * ops * operand) use {
CompoundTerm(Atom(t2), listOf(t1, t3))
// Level 200 - prefix operators (+, -, \)
protected val op200: Parser<CompoundTerm> by ((plus or minus) * parser(::term200)) use {
CompoundTerm(Atom(t1.text), listOf(t2))
}
protected val term200: Parser<Term> by (op200 or baseTerm)
// Level 400 - multiplication, division
protected val op400: Parser<String> by (multiply or divide) use { text }
protected val term400: Parser<Term> by (term200 * zeroOrMore(op400 * term200)) use {
t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
}
// Parts
protected val head: Parser<Head> by (dummy
or compound
or atom
)
protected val body: Parser<Body> by (dummy
or operator
or head
or variable
) use { this as Body }
// Level 500 - addition, subtraction
protected val op500: Parser<String> by (plus or minus) use { text }
protected val term500: Parser<Term> by (term400 * zeroOrMore(op500 * term400)) use {
t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
}
protected val term: Parser<Term> by (dummy
or float
or int
or variable
or compound
or atom
)
// Level 700 - comparison operators
protected val op700: Parser<String> by (equals or notEquals) use { text }
protected val term700: Parser<Term> 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<Term> by (term700 * zeroOrMore(comma * term700)) use {
t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(","), listOf(acc, term)) }
}
// Level 1100 - disjunction (;)
protected val term1100: Parser<Term> by (term1000 * zeroOrMore(semicolon * term1000)) use {
t2.fold(t1) { acc, (_, term) -> CompoundTerm(Atom(";"), listOf(acc, term)) }
}
// Term - highest level expression
protected val term: Parser<Term> by term1100
protected val termNoConjunction: Parser<Term> by term700
// Parts for clauses
protected val head: Parser<Head> by (compound or atom)
protected val body: Parser<Body> by term use { this as Body }
override val rootParser: Parser<Any> by term
}

View file

@ -25,6 +25,9 @@ abstract class Tokens : Grammar<Any>() {
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

View file

@ -48,4 +48,10 @@ object Program : Resolvent {
correspondingDBs.forEach { it.clear() }
}
fun reset() {
clear()
variableRenamingStart = 0
storeNewLine = false
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

46
tests/e2e/Examples.kt Normal file
View file

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

View file

@ -1,4 +0,0 @@
package e2e
class myClass {
}

View file

@ -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<CompoundTerm> by operator
override val rootParser: Parser<CompoundTerm> by body use { this as CompoundTerm }
}
private var parser = OperatorParser() as Grammar<CompoundTerm>

View file

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

View file

@ -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<Term>
@ -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<Term>
@BeforeEach
fun setup() {
parser = TermsGrammar() as Grammar<Term>
}
@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"
)
}
}
}

View file

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

View file

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