Checkpoint

This commit is contained in:
Tibo De Peuter 2025-05-04 21:50:58 +02:00
parent 5bfa1691dd
commit a85169dced
Signed by: tdpeuter
GPG key ID: 38297DE43F75FFE2
27 changed files with 377 additions and 250 deletions

View file

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

View file

@ -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<Clause> = 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<Clause>, filePath: String) {
Logger.debug("Grouping clauses by functor")
val groupedClauses = clauses.groupBy { it.functor }
val predicates: List<Predicate> = groupedClauses.map { (_, clauses) ->
Predicate(clauses)
}
val database = Database(filePath)
predicates.forEach { database.load(it) }
Program.consult(database)
}
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ 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:
@ -14,20 +15,21 @@ import prolog.ast.terms.*
* |------------|------|-----------------------------------------------------------------------------------------------|
* | 1200 | xfx | --\>, :-, =\>, ==\> |
* | 1200 | fx | :-, ?- |
* | 1105 | xfy | \| |
* | 1150 | fx | dynamic |
* | 1105 | xfy | | |
* | 1100 | xfy | ; |
* | 1050 | xfy | -\>, \*-\> |
* | 1050 | xfy | ->, *-> |
* | 1000 | xfy | , |
* | 990 | xfx | := |
* | 900 | fy | \\+ |
* | 700 | xfx | \<, =, =.., =:=, =\<, ==, =\\=, \>, \>=, \\=, \\==, as, is, \>:\<, :\< |
* | 900 | fy | \+ |
* | 700 | xfx | <, =, =.., =:=, =<, ==, =\=, >, >=, \=, \==, as, is, >:<, :< |
* | 600 | xfy | : |
* | 500 | yfx | +, -, /\\, \\/, xor |
* | 500 | yfx | +, -, /\, \/, xor |
* | 500 | fx | ? |
* | 400 | yfx | \*, /, //, div, rdiv, \<\<, \>\>, mod, rem |
* | 200 | xfx | \*\* |
* | 400 | yfx | *, /, //, div, rdiv, <<, >>, mod, rem |
* | 200 | xfx | ** |
* | 200 | xfy | ^ |
* | 200 | fy | +, -, \\ |
* | 200 | fy | +, -, \ |
* | 100 | yfx | . |
* | 1 | fx | $ |
*
@ -58,6 +60,8 @@ 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()) }
protected val functor: Parser<String> by (nameToken * divide * int) use { "${t1.text}${t2.text}$t3" }
// Base terms (atoms, compounds, variables, numbers)
protected val baseTerm: Parser<Term> 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<Term> by (dynamicOp * functor) use {
CompoundTerm( Atom(t1.text), listOf(Atom(t2)) )
}
protected val term1150: Parser<Term> by (dynamic or term1100) use { this }
protected val op1200: Parser<String> by (neck) use { text }
protected val term1200: Parser<Term> by (term1100 * zeroOrMore(op1200 * term1100)) use {
protected val term1200: Parser<Term> by (term1150 * zeroOrMore(op1200 * term1100)) use {
t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) }
}

View file

@ -8,21 +8,29 @@ import com.github.h0tk3y.betterParse.lexer.regexToken
import com.github.h0tk3y.betterParse.lexer.token
abstract class Tokens : Grammar<Any>() {
// 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

View file

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

View file

@ -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<Functor, Predicate> = 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<Clause>, index: Int? = null) {
fun load(clauses: List<Clause>, 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}")
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)
}
/**
* 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<Database> = 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<Clause>, index: Int? = null, force: Boolean = false) = db.load(clauses, index, force)
fun reset() {
databases.toList().map { it.clear() }
variableRenamingStart = 0
storeNewLine = false
}
}
}

View file

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

View file

@ -15,37 +15,48 @@ import prolog.flags.AppliedCut
class Predicate : Resolvent {
val functor: Functor
val clauses: MutableList<Clause>
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<Clause>) {
constructor(clauses: List<Clause>, 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<Clause>) {
fun addAll(clauses: List<Clause>, 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)
}

View file

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

View file

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

View file

@ -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,19 +90,18 @@ 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()
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))
clausesIterator.remove()
},
onFailure = {
// If unification fails, do nothing
@ -79,5 +110,4 @@ class Retract(val term: Term) : Operator(Atom("retract"), null, term) {
}
}
}
}
}

View file

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

View file

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

View file

@ -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<Term, Term> = sessionSubs.toMutableMap()
from.arguments.forEach { arg ->

View file

@ -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 {
variable(term, emptyMap()) -> {
var result = subs[(term as Variable)]
while (result != null && result is Variable && result in subs) {
result = subs[result]
term is Fact -> {
Fact(applySubstitution(term.head, subs) as Head)
}
result ?: term
variable(term, emptyMap()) -> {
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())
}

View file

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

View file

@ -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"),
)
fun other() = listOf(
Arguments.of("program.pl", "10\nhello(world)")
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<T : Structure> {
@ -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

View file

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