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

@ -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,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<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}")
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)
}
}
/**
* 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)
}
@ -75,4 +86,4 @@ class Predicate : Resolvent {
}
}
}
}
}

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

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

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()
@ -97,4 +96,4 @@ class Repl {
}
)
}
}
}