From a85169dced2e2b57af5faf5c94395c9aabe7b32a Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sun, 4 May 2025 21:50:58 +0200 Subject: [PATCH] Checkpoint --- examples/scratchpad.pl | 43 +++++---- src/interpreter/FileLoader.kt | 21 +++-- src/interpreter/Preprocessor.kt | 1 + src/io/Terminal.kt | 2 +- src/parser/grammars/TermsGrammar.kt | 47 ++++++---- src/parser/grammars/Tokens.kt | 20 +++-- src/prolog/Program.kt | 57 ------------ src/prolog/ast/Database.kt | 90 +++++++++++++++---- src/prolog/ast/logic/Clause.kt | 12 +-- src/prolog/ast/logic/Predicate.kt | 21 +++-- src/prolog/ast/terms/Goal.kt | 2 +- src/prolog/builtins/controlOperators.kt | 3 +- src/prolog/builtins/databaseOperators.kt | 74 ++++++++++----- src/prolog/builtins/ioOperators.kt | 4 +- src/prolog/builtins/other.kt | 1 + src/prolog/logic/terms.kt | 8 +- src/prolog/logic/unification.kt | 31 +++---- src/repl/Repl.kt | 3 +- tests/e2e/Examples.kt | 49 ++++++---- .../ParserPreprocessorIntegrationTests.kt | 1 - tests/interpreter/PreprocessorTests.kt | 14 +++ tests/interpreter/SourceFileReaderTests.kt | 4 +- .../builtins/DatabaseOperatorsParserTests.kt | 12 ++- tests/prolog/EvaluationTests.kt | 5 +- .../prolog/builtins/ControlOperatorsTests.kt | 10 +-- .../prolog/builtins/DatabaseOperatorsTests.kt | 73 ++++++++------- tests/prolog/logic/TermsTests.kt | 19 ++-- 27 files changed, 377 insertions(+), 250 deletions(-) delete mode 100644 src/prolog/Program.kt diff --git a/examples/scratchpad.pl b/examples/scratchpad.pl index 9367251..0ed5d04 100644 --- a/examples/scratchpad.pl +++ b/examples/scratchpad.pl @@ -1,27 +1,32 @@ % choice(X) :- X = 1, !; X = 2. -grade(alice, a). -grade(bob, b). -grade(carol, a). -grade(dave, c). +:- dynamic declaration/1. -got_an_a(Student) :- - grade(Student, Grade), - Grade = a. +add_declaration_first(NewDecl) :- + asserta(declaration(NewDecl)). -did_not_get_an_a(Student) :- - grade(Student, Grade), - Grade \= a. +add_declaration_last(NewDecl) :- + assertz(declaration(NewDecl)). + +database :- + add_declaration_first('Man is born free, and everywhere he is in chains.'), + retract(declaration(_)), + add_declaration_last('The revolution devours its own children.'), + add_declaration_first('I disapprove of what you say, but I will defend to the death your right to say it.'), + add_declaration_first('Give me Liberty, or give me Death!'), + add_declaration_last('So this is how liberty dies, with thunderous applause.'). + +show_declarations :- + declaration(Decl), + write(Decl), nl, + fail. + +show_declarations. :- initialization(main). main :- - write("While "), - got_an_a(X), - write(X), write(" got an A, "), fail; - write("but "), - did_not_get_an_a(Y), - write(Y), write(" did not get an A, "), fail; write("unfortunately."), nl. + database, + show_declarations, + retractall(declaration(_)), + show_declarations. -:- initialization(main). -main :- write('gpl zegt: '), groet(wereld), nl. -groet(X) :- write(dag(X)). diff --git a/src/interpreter/FileLoader.kt b/src/interpreter/FileLoader.kt index 2121fd7..7af15ab 100644 --- a/src/interpreter/FileLoader.kt +++ b/src/interpreter/FileLoader.kt @@ -3,8 +3,9 @@ package interpreter import io.Logger import parser.ScriptParser import prolog.ast.Database -import prolog.Program +import prolog.ast.Database.Program import prolog.ast.logic.Clause +import prolog.ast.logic.Predicate class FileLoader { private val parser = ScriptParser() @@ -17,10 +18,8 @@ class FileLoader { Logger.debug("Parsing content of $filePath") val clauses: List = parser.parse(input) - val db = Database(filePath) - db.load(clauses) - Program.add(db) - db.initialize() + Logger.debug("Adding clauses to program") + addToProgram(clauses, filePath) Logger.debug("Finished loading file: $filePath") } @@ -41,4 +40,16 @@ class FileLoader { throw RuntimeException("Error reading file: $filePath", e) } } + + fun addToProgram(clauses: List, filePath: String) { + Logger.debug("Grouping clauses by functor") + val groupedClauses = clauses.groupBy { it.functor } + val predicates: List = groupedClauses.map { (_, clauses) -> + Predicate(clauses) + } + + val database = Database(filePath) + predicates.forEach { database.load(it) } + Program.consult(database) + } } \ No newline at end of file diff --git a/src/interpreter/Preprocessor.kt b/src/interpreter/Preprocessor.kt index 7477702..6c17705 100644 --- a/src/interpreter/Preprocessor.kt +++ b/src/interpreter/Preprocessor.kt @@ -92,6 +92,7 @@ open class Preprocessor { term.functor == "between/3" && args.all { it is Expression } -> Between(args[0] as Expression, args[1] as Expression, args[2] as Expression) // Database + term.functor == "dynamic/1" -> Dynamic((args[0] as Atom).name) term.functor == "retract/1" -> Retract(args[0]) term.functor == "assert/1" -> { if (args[0] is Rule) { diff --git a/src/io/Terminal.kt b/src/io/Terminal.kt index c75a3eb..1b9df94 100644 --- a/src/io/Terminal.kt +++ b/src/io/Terminal.kt @@ -1,6 +1,6 @@ package io -import prolog.Program +import prolog.ast.Database.Program import java.io.BufferedReader import java.io.BufferedWriter import java.io.InputStream diff --git a/src/parser/grammars/TermsGrammar.kt b/src/parser/grammars/TermsGrammar.kt index f62e54d..3fb9e48 100644 --- a/src/parser/grammars/TermsGrammar.kt +++ b/src/parser/grammars/TermsGrammar.kt @@ -6,30 +6,32 @@ import com.github.h0tk3y.betterParse.parser.Parser import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.terms.* +import prolog.builtins.Dynamic /** * Precedence is based on the following table: * * | Precedence | Type | Operators | * |------------|------|-----------------------------------------------------------------------------------------------| - * | 1200 | xfx | --\>, :-, =\>, ==\> | - * | 1200 | fx | :-, ?- | - * | 1105 | xfy | \| | - * | 1100 | xfy | ; | - * | 1050 | xfy | -\>, \*-\> | - * | 1000 | xfy | , | - * | 990 | xfx | := | - * | 900 | fy | \\+ | - * | 700 | xfx | \<, =, =.., =:=, =\<, ==, =\\=, \>, \>=, \\=, \\==, as, is, \>:\<, :\< | - * | 600 | xfy | : | - * | 500 | yfx | +, -, /\\, \\/, xor | - * | 500 | fx | ? | - * | 400 | yfx | \*, /, //, div, rdiv, \<\<, \>\>, mod, rem | - * | 200 | xfx | \*\* | - * | 200 | xfy | ^ | - * | 200 | fy | +, -, \\ | - * | 100 | yfx | . | - * | 1 | fx | $ | + * | 1200 | xfx | --\>, :-, =\>, ==\> | + * | 1200 | fx | :-, ?- | + * | 1150 | fx | dynamic | + * | 1105 | xfy | | | + * | 1100 | xfy | ; | + * | 1050 | xfy | ->, *-> | + * | 1000 | xfy | , | + * | 990 | xfx | := | + * | 900 | fy | \+ | + * | 700 | xfx | <, =, =.., =:=, =<, ==, =\=, >, >=, \=, \==, as, is, >:<, :< | + * | 600 | xfy | : | + * | 500 | yfx | +, -, /\, \/, xor | + * | 500 | fx | ? | + * | 400 | yfx | *, /, //, div, rdiv, <<, >>, mod, rem | + * | 200 | xfx | ** | + * | 200 | xfy | ^ | + * | 200 | fy | +, -, \ | + * | 100 | yfx | . | + * | 1 | fx | $ | * * It is very easy to extend this grammar to support more operators. Just add them at the appropriate rule or create a * new rule and chain it to the existing ones. @@ -58,6 +60,8 @@ open class TermsGrammar : Tokens() { protected val int: Parser by integerToken use { Integer(text.toInt()) } protected val float: Parser by floatToken use { Float(text.toFloat()) } + protected val functor: Parser by (nameToken * divide * int) use { "${t1.text}${t2.text}$t3" } + // Base terms (atoms, compounds, variables, numbers) protected val baseTerm: Parser by (dummy or (-leftParenthesis * parser(::term) * -rightParenthesis) @@ -98,8 +102,13 @@ open class TermsGrammar : Tokens() { t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } } + protected val dynamic: Parser by (dynamicOp * functor) use { + CompoundTerm( Atom(t1.text), listOf(Atom(t2)) ) + } + protected val term1150: Parser by (dynamic or term1100) use { this } + protected val op1200: Parser by (neck) use { text } - protected val term1200: Parser by (term1100 * zeroOrMore(op1200 * term1100)) use { + protected val term1200: Parser by (term1150 * zeroOrMore(op1200 * term1100)) use { t2.fold(t1) { acc, (op, term) -> CompoundTerm(Atom(op), listOf(acc, term)) } } diff --git a/src/parser/grammars/Tokens.kt b/src/parser/grammars/Tokens.kt index bffc308..ac8c36f 100644 --- a/src/parser/grammars/Tokens.kt +++ b/src/parser/grammars/Tokens.kt @@ -8,21 +8,29 @@ import com.github.h0tk3y.betterParse.lexer.regexToken import com.github.h0tk3y.betterParse.lexer.token abstract class Tokens : Grammar() { - // Special tokens - protected val neck by literalToken(":-") protected val leftParenthesis: Token by literalToken("(") protected val rightParenthesis: Token by literalToken(")") - protected val comma: Token by literalToken(",") + protected val exclamation: Token by literalToken("!") + // 1200 + protected val neck by literalToken(":-") + // 1150 + protected val dynamicOp by literalToken("dynamic") + // 1100 protected val semicolon: Token by literalToken(";") + // 1000 + protected val comma: Token by literalToken(",") + // 700 protected val equivalent: Token by literalToken("==") protected val equals: Token by literalToken("=") - protected val notEquals: Token by literalToken("\\=") + protected val isOp: Token by literalToken("is") + // 500 protected val plus: Token by literalToken("+") protected val minus: Token by literalToken("-") + protected val notEquals: Token by literalToken("\\=") + // 400 protected val multiply: Token by literalToken("*") protected val divide: Token by literalToken("/") - protected val exclamation: Token by literalToken("!") - protected val isOp: Token by literalToken("is") + // 100 protected val dot by literalToken(".") // Prolog tokens diff --git a/src/prolog/Program.kt b/src/prolog/Program.kt deleted file mode 100644 index e82b0bf..0000000 --- a/src/prolog/Program.kt +++ /dev/null @@ -1,57 +0,0 @@ -package prolog - -import io.Logger -import prolog.ast.Database -import prolog.ast.logic.Clause -import prolog.ast.logic.Resolvent -import prolog.ast.terms.Goal - -/** - * Object to handle execution - * - * This object is a singleton that manages a list of databases. - */ -object Program : Resolvent { - val internalDb = Database("") - val databases: MutableList = mutableListOf(internalDb) - - var storeNewLine: Boolean = false - var variableRenamingStart: Int = 0 - - fun add(database: Database) { - databases.add(database) - } - - /** - * Queries the program with a goal. - * @return true if the goal can be proven, false otherwise. - */ - fun query(goal: Goal): Answers = solve(goal, emptyMap()) - - override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { - Logger.debug("Solving goal $goal") - for (database in databases) { - yieldAll(database.solve(goal, subs)) - } - } - - fun load(clauses: List, index: Int? = null) = internalDb.load(clauses, index) - - fun clear() { - databases.forEach { it.clear() } - } - - fun clear(filePath: String) { - val correspondingDBs = databases.filter { it.sourceFile == filePath } - - require(correspondingDBs.isNotEmpty()) { "No database found for file: $filePath" } - - correspondingDBs.forEach { it.clear() } - } - - fun reset() { - clear() - variableRenamingStart = 0 - storeNewLine = false - } -} \ No newline at end of file diff --git a/src/prolog/ast/Database.kt b/src/prolog/ast/Database.kt index d10695d..be2da7a 100644 --- a/src/prolog/ast/Database.kt +++ b/src/prolog/ast/Database.kt @@ -1,7 +1,6 @@ package prolog.ast import io.Logger -import prolog.Program import prolog.Answers import prolog.Substitutions import prolog.ast.logic.Clause @@ -13,10 +12,21 @@ import prolog.ast.terms.Goal /** * Prolog Program or Database */ -class Database(val sourceFile: String): Resolvent { +open class Database(val sourceFile: String) { var predicates: Map = emptyMap() + /** + * Initializes the database by running the initialization clauses of that database. + */ fun initialize() { + databases.add(this) + + if (sourceFile !== "") { + Logger.debug("Moving clauses from $sourceFile to main database") + predicates.filter { it.key != "/_" } + .forEach { (_, predicate) -> db.load(predicate, force = true) } + } + Logger.info("Initializing database from $sourceFile") if (predicates.contains("/_")) { Logger.debug("Loading clauses from /_ predicate") @@ -28,35 +38,31 @@ class Database(val sourceFile: String): Resolvent { } } - override fun solve(goal: Goal, subs: Substitutions): Answers { - val functor = goal.functor - // If the predicate does not exist, return false - val predicate = predicates[functor] ?: return emptySequence() - // If the predicate exists, evaluate the goal against it - return predicate.solve(goal, subs) - } - /** * Loads a list of clauses into the program. + * + * @param clauses The list of clauses to load. + * @param index The index at which to insert the clause. If null, the clause is added to the end of the list. + * @param force If true, the clause is added even if the predicate is static. */ - fun load(clauses: List, index: Int? = null) { + fun load(clauses: List, index: Int? = null, force: Boolean = false) { for (clause in clauses) { val functor = clause.functor val predicate = predicates[functor] if (predicate != null) { // If the predicate already exists, add the clause to it - predicate.add(clause, index) + predicate.add(clause, index, force) } else { - // If the predicate does not exist, create a new one - predicates += Pair(functor, Predicate(listOf(clause))) + // If the predicate does not exist, create a new one, usually during Program execution, so dynamic. + predicates += Pair(functor, Predicate(listOf(clause), dynamic = true)) } Logger.debug("Loaded clause $clause into predicate $functor") } } - fun load(predicate: Predicate) { + fun load(predicate: Predicate, force: Boolean = false) { val functor = predicate.functor val existingPredicate = predicates[functor] @@ -70,7 +76,55 @@ class Database(val sourceFile: String): Resolvent { } fun clear() { - Logger.debug("Clearing ${this::class.java.simpleName}") - predicates = emptyMap() + if (sourceFile == "") { + Logger.debug("Clearing main database") + predicates = emptyMap() + return + } + + Logger.debug("Clearing database $sourceFile") + // Remove our clauses from the database + predicates.forEach { (_, predicate) -> + val dbPredicate = db.predicates[predicate.functor] + predicate.clauses.forEach { clause -> dbPredicate?.clauses?.remove(clause) } + } + databases.remove(this) } -} \ No newline at end of file + + /** + * Object to handle execution + * + * This object is a singleton that manages a list of databases. + */ + companion object Program : Resolvent { + var db = Database("") + var databases: MutableList = mutableListOf(db) + var storeNewLine: Boolean = false + var variableRenamingStart: Int = 0 + + /** + * Queries the program with a goal. + * @return true if the goal can be proven, false otherwise. + */ + fun query(goal: Goal): Answers = solve(goal, emptyMap()) + + override fun solve(goal: Goal, subs: Substitutions): Answers { + val functor = goal.functor + // If the predicate does not exist, return false + val predicate = db.predicates[functor] ?: return emptySequence() + // If the predicate exists, evaluate the goal against it + return predicate.solve(goal, subs) + } + + fun consult(database: Database) = database.initialize() + fun disregard(database: Database) = database.clear() + + fun load(clauses: List, index: Int? = null, force: Boolean = false) = db.load(clauses, index, force) + + fun reset() { + databases.toList().map { it.clear() } + variableRenamingStart = 0 + storeNewLine = false + } + } +} diff --git a/src/prolog/ast/logic/Clause.kt b/src/prolog/ast/logic/Clause.kt index a48fb1f..2abfa77 100644 --- a/src/prolog/ast/logic/Clause.kt +++ b/src/prolog/ast/logic/Clause.kt @@ -1,7 +1,7 @@ package prolog.ast.logic import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.terms.* import prolog.builtins.True @@ -16,10 +16,10 @@ import prolog.logic.unifyLazy * * A clause consists of a [Head] and body separated by the neck operator, or it is a [Fact]. * - * @see [prolog.ast.terms.Variable] + * @see [Variable] * @see [Predicate] */ -abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { +abstract class Clause(var head: Head, var body: Body) : Term, Resolvent { val functor: Functor = head.functor override fun solve(goal: Goal, subs: Substitutions): Answers = sequence { @@ -35,7 +35,7 @@ abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { Program.variableRenamingStart = end var newSubs: Substitutions = subs + renamed - unifyLazy(goal, head, newSubs).forEach { headAnswer -> + unifyLazy(applySubstitution(goal, subs), head, newSubs).forEach { headAnswer -> headAnswer.map { headSubs -> // If the body can be proven, yield the (combined) substitutions newSubs = subs + renamed + headSubs @@ -43,8 +43,8 @@ abstract class Clause(val head: Head, val body: Body) : Term, Resolvent { bodyAnswer.fold( onSuccess = { bodySubs -> var result = (headSubs + bodySubs) - .mapKeys { reverse[it.key] ?: it.key } - .mapValues { reverse[it.value] ?: it.value } + .mapKeys { applySubstitution(it.key, reverse)} + .mapValues { applySubstitution(it.value, reverse) } result = result.map { it.key to applySubstitution(it.value, result) } .toMap() .filterNot { it.key in renamed.keys && !occurs(it.key as Variable, goal, emptyMap())} diff --git a/src/prolog/ast/logic/Predicate.kt b/src/prolog/ast/logic/Predicate.kt index 5640f57..25fb737 100644 --- a/src/prolog/ast/logic/Predicate.kt +++ b/src/prolog/ast/logic/Predicate.kt @@ -15,37 +15,48 @@ import prolog.flags.AppliedCut class Predicate : Resolvent { val functor: Functor val clauses: MutableList + var dynamic = false /** * Creates a predicate with the given functor and an empty list of clauses. */ - constructor(functor: Functor) { + constructor(functor: Functor, dynamic: Boolean = false) { this.functor = functor this.clauses = mutableListOf() + this.dynamic = dynamic } /** * Creates a predicate with the given clauses. */ - constructor(clauses: List) { + constructor(clauses: List, dynamic: Boolean = false) { this.functor = clauses.first().functor require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" } this.clauses = clauses.toMutableList() + this.dynamic = dynamic } /** * Adds a clause to the predicate. + * + * @param clause The clause to add. + * @param index The index at which to insert the clause. If null, the clause is added to the end of the list. + * @param force If true, the clause is added even if the predicate is static. */ - fun add(clause: Clause, index: Int? = null) { + fun add(clause: Clause, index: Int? = null, force: Boolean = false) { require(clause.functor == functor) { "Clause functor does not match predicate functor" } + require(dynamic || force) { "No permission to modify static procedure '$functor'" } + if (index != null) clauses.add(index, clause) else clauses.add(clause) } /** * Adds a list of clauses to the predicate. */ - fun addAll(clauses: List) { + fun addAll(clauses: List, force: Boolean = false) { require(clauses.all { it.functor == functor }) { "All clauses must have the same functor" } + require(dynamic || force) { "No permission to modify static procedure '$functor'" } + this.clauses.addAll(clauses) } @@ -75,4 +86,4 @@ class Predicate : Resolvent { } } } -} +} \ No newline at end of file diff --git a/src/prolog/ast/terms/Goal.kt b/src/prolog/ast/terms/Goal.kt index e0a5c49..95f9016 100644 --- a/src/prolog/ast/terms/Goal.kt +++ b/src/prolog/ast/terms/Goal.kt @@ -1,7 +1,7 @@ package prolog.ast.terms import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.logic.LogicOperand diff --git a/src/prolog/builtins/controlOperators.kt b/src/prolog/builtins/controlOperators.kt index 01cd333..613c915 100644 --- a/src/prolog/builtins/controlOperators.kt +++ b/src/prolog/builtins/controlOperators.kt @@ -120,7 +120,8 @@ class Bar(leftOperand: LogicOperand, rightOperand: LogicOperand) : Disjunction(l class Not(private val goal: Goal) : LogicOperator(Atom("\\+"), rightOperand = goal) { override fun satisfy(subs: Substitutions): Answers { // If the goal can be proven, return an empty sequence - if (goal.satisfy(subs).toList().isNotEmpty()) { + val goalResults = goal.satisfy(subs).iterator() + if (goalResults.hasNext()) { return emptySequence() } // If the goal cannot be proven, return a sequence with an empty map diff --git a/src/prolog/builtins/databaseOperators.kt b/src/prolog/builtins/databaseOperators.kt index a002bfa..eaf67b8 100644 --- a/src/prolog/builtins/databaseOperators.kt +++ b/src/prolog/builtins/databaseOperators.kt @@ -6,14 +6,44 @@ import prolog.ast.logic.Clause import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.logic.Predicate -import prolog.Program +import prolog.ast.Database.Program import prolog.ast.terms.Functor import prolog.ast.terms.Term import prolog.ast.logic.Fact import prolog.ast.Database +import prolog.ast.terms.Body +import prolog.ast.terms.Goal import prolog.ast.terms.Operator +import prolog.logic.applySubstitution import prolog.logic.unifyLazy +/** + * (Make) the [Predicate] with the corresponding [Functor] dynamic. + */ +class Dynamic(private val dynamicFunctor: Functor): Goal(), Body { + override val functor: Functor = "dynamic/1" + + override fun satisfy(subs: Substitutions): Answers { + val predicate = Program.db.predicates[dynamicFunctor] + if (predicate == null) { + return sequenceOf(Result.failure(Exception("Predicate $dynamicFunctor not found"))) + } + + predicate.dynamic = true + return sequenceOf(Result.success(emptyMap())) + } + + override fun toString(): String = "dynamic $dynamicFunctor" + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Dynamic) return false + return dynamicFunctor == other.dynamicFunctor + } + + override fun hashCode(): Int = super.hashCode() +} + class Assert(clause: Clause) : AssertZ(clause) { override val functor: Functor = "assert/1" } @@ -24,7 +54,8 @@ class Assert(clause: Clause) : AssertZ(clause) { class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program - Program.load(listOf(clause), 0) + val evaluatedClause = applySubstitution(clause, subs) as Clause + Program.load(listOf(evaluatedClause), 0) return sequenceOf(Result.success(emptyMap())) } @@ -36,7 +67,8 @@ class AssertA(val clause: Clause) : Operator(Atom("asserta"), null, clause) { open class AssertZ(val clause: Clause) : Operator(Atom("assertz"), null, clause) { override fun satisfy(subs: Substitutions): Answers { // Add clause to the program - Program.load(listOf(clause)) + val evaluatedClause = applySubstitution(clause, subs) as Clause + Program.load(listOf(evaluatedClause)) return sequenceOf(Result.success(emptyMap())) } @@ -58,26 +90,24 @@ class Retract(val term: Term) : Operator(Atom("retract"), null, term) { val functorName = term.functor - Program.databases - .filter { it.predicates.containsKey(functorName) } - .mapNotNull { it.predicates[functorName] } - .map { predicate -> - val clausesIterator = predicate.clauses.iterator() - while (clausesIterator.hasNext()) { - val clause = clausesIterator.next() - unifyLazy(term, clause.head, subs).forEach { unifyResult -> - unifyResult.fold( - onSuccess = { substitutions -> - // If unification is successful, remove the clause - yield(Result.success(substitutions)) - clausesIterator.remove() - }, - onFailure = { - // If unification fails, do nothing - } - ) + val predicate = Program.db.predicates[functorName] + if (predicate == null) { + return@sequence + } + + predicate.clauses.toList().forEach { clause -> + unifyLazy(term, clause.head, subs).forEach { unifyResult -> + unifyResult.fold( + onSuccess = { substitutions -> + // If unification is successful, remove the clause + predicate.clauses.remove(clause) + yield(Result.success(substitutions)) + }, + onFailure = { + // If unification fails, do nothing } - } + ) } + } } } diff --git a/src/prolog/builtins/ioOperators.kt b/src/prolog/builtins/ioOperators.kt index 3f9aaae..1271296 100644 --- a/src/prolog/builtins/ioOperators.kt +++ b/src/prolog/builtins/ioOperators.kt @@ -4,7 +4,7 @@ import io.Logger import io.Terminal import parser.ReplParser import prolog.Answers -import prolog.Program +import prolog.ast.Database.Program import prolog.Substitutions import prolog.ast.logic.Satisfiable import prolog.ast.terms.Atom @@ -26,6 +26,8 @@ class Write(private val term: Term) : Operator(Atom("write"), null, term), Satis return sequenceOf(Result.success(emptyMap())) } + + override fun toString(): String = "write($term)" } /** diff --git a/src/prolog/builtins/other.kt b/src/prolog/builtins/other.kt index 8609473..4cc582f 100644 --- a/src/prolog/builtins/other.kt +++ b/src/prolog/builtins/other.kt @@ -8,6 +8,7 @@ import prolog.ast.logic.LogicOperator class Initialization(val goal: LogicOperand) : LogicOperator(Atom(":-"), null, goal) { override fun satisfy(subs: Substitutions): Answers = goal.satisfy(subs).take(1) + override fun toString(): String = goal.toString() } class Query(val query: LogicOperand) : LogicOperator(Atom("?-"), null, query) { diff --git a/src/prolog/logic/terms.kt b/src/prolog/logic/terms.kt index 8d1eeb0..8c2b26f 100644 --- a/src/prolog/logic/terms.kt +++ b/src/prolog/logic/terms.kt @@ -47,17 +47,17 @@ fun numbervars( } val from = term as Variable - var suggestedName = "${from.name}($start)" + var suggestedName = "${from.name}@$start" // If the suggested name is already in use, find a new one - while ((subs + sessionSubs).filter { (it.key as Variable).name == suggestedName }.isNotEmpty()) { + while ((subs + sessionSubs).any { (it.key as Variable).name == suggestedName }) { val randomInfix = ((0..9) + ('a'..'z') + ('A'..'Z')).random() - suggestedName = "${from.name}_${randomInfix}_($start)" + suggestedName = "${from.name}@${randomInfix}_($start)" } return Pair(start + 1, mapOf(from to Variable(suggestedName))) } compound(term, subs) -> { - val from = term as Structure + val from = applySubstitution(term, subs) as Structure var n = start val s: MutableMap = sessionSubs.toMutableMap() from.arguments.forEach { arg -> diff --git a/src/prolog/logic/unification.kt b/src/prolog/logic/unification.kt index 0cf1321..1e7b57f 100644 --- a/src/prolog/logic/unification.kt +++ b/src/prolog/logic/unification.kt @@ -4,23 +4,24 @@ import prolog.Answer import prolog.Answers import prolog.Substitutions import prolog.ast.arithmetic.Expression -import prolog.ast.logic.LogicOperator -import prolog.ast.terms.* -import kotlin.NoSuchElementException -import prolog.ast.arithmetic.Number -import prolog.ast.arithmetic.Integer import prolog.ast.arithmetic.Float +import prolog.ast.arithmetic.Integer +import prolog.ast.arithmetic.Number +import prolog.ast.logic.Clause +import prolog.ast.logic.Fact +import prolog.ast.logic.LogicOperator +import prolog.ast.logic.Rule +import prolog.ast.terms.* // Apply substitutions to a term fun applySubstitution(term: Term, subs: Substitutions): Term = when { + term is Fact -> { + Fact(applySubstitution(term.head, subs) as Head) + } + variable(term, emptyMap()) -> { - var result = subs[(term as Variable)] - - while (result != null && result is Variable && result in subs) { - result = subs[result] - } - - result ?: term + val variable = term as Variable + subs[variable]?.let { applySubstitution(term = it, subs = subs) } ?: term } atomic(term, subs) -> term compound(term, subs) -> { @@ -105,9 +106,9 @@ fun unifyLazy(term1: Term, term2: Term, subs: Substitutions): Answers = sequence } fun unify(term1: Term, term2: Term): Answer { - val substitutions = unifyLazy(term1, term2, emptyMap()).toList() - return if (substitutions.isNotEmpty()) { - substitutions.first() + val substitutions = unifyLazy(term1, term2, emptyMap()).iterator() + return if (substitutions.hasNext()) { + substitutions.next() } else { Result.failure(NoSuchElementException()) } diff --git a/src/repl/Repl.kt b/src/repl/Repl.kt index aae1390..5e8b0d4 100644 --- a/src/repl/Repl.kt +++ b/src/repl/Repl.kt @@ -6,7 +6,6 @@ import io.Terminal import parser.ReplParser import prolog.Answer import prolog.Answers -import prolog.Program class Repl { private val io = Terminal() @@ -97,4 +96,4 @@ class Repl { } ) } -} \ No newline at end of file +} diff --git a/tests/e2e/Examples.kt b/tests/e2e/Examples.kt index 2b1f3fa..f93fc35 100644 --- a/tests/e2e/Examples.kt +++ b/tests/e2e/Examples.kt @@ -1,15 +1,14 @@ package e2e import interpreter.FileLoader -import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource -import org.junit.jupiter.params.provider.ValueSource -import prolog.Program +import prolog.ast.Database.Program import java.io.ByteArrayOutputStream import java.io.PrintStream @@ -26,21 +25,39 @@ class Examples { System.setOut(PrintStream(outStream)) } + @Test + fun debugHelper() { + loader.load("examples/basics/backtracking.pl") + } + @ParameterizedTest - @MethodSource("expectations") - fun test(inputFile: String, expected: String) { - loader.load(inputFile) + @MethodSource("basics") + fun `Identical output for basics`(inputFile: String, expected: String) { + loader.load("examples/basics/$inputFile") assertEquals(expected, outStream.toString()) } - fun expectations() = listOf( - Arguments.of("examples/basics/arithmetics.pl", "gimli is a level 4 fighter with 35 hitpoints.\nlegolas is a level 5 ranger with 30 hitpoints.\ngandalf is a level 10 wizard with 25 hitpoints.\nfrodo is a level 2 rogue with 20 hitpoints.\nlegolas threw gimli, and gimli took 5 damage.\ngimli is a level 4 fighter with 30 hitpoints.\ngandalf casts aid.\ngimli is a level 4 fighter with 35 hitpoints.\nlegolas leveled up.\nlegolas is a level 6 ranger with 30 hitpoints"), - Arguments.of("examples/basics/backtracking.pl", "0\ns(0)\ns(s(0))\ns(s(s(0)))\n"), - Arguments.of("examples/basics/cut.pl", "0\n"), - Arguments.of("examples/basics/disjunction.pl", "Alice likes Italian food.\nBob likes Italian food.\n"), - Arguments.of("examples/basics/equality.pl", "X == Y failed\nX = Y succeeded\nX == Y succeeded\nX = Y succeeded\nX == Y succeeded\n"), - Arguments.of("examples/basics/fraternity.pl", "Citizen robespierre is eligible for the event.\nCitizen danton is eligible for the event.\nCitizen camus is eligible for the event.\n"), - Arguments.of("examples/basics/unification.pl", "While alice got an A, carol got an A, but bob did not get an A, dave did not get an A, unfortunately.\n"), - Arguments.of("examples/basics/write.pl", "gpl zegt: dag(wereld)\n"), + @ParameterizedTest + @MethodSource("other") + fun `Identical output for other`(inputFile: String, expected: String) { + loader.load("examples/$inputFile") + assertEquals(expected, outStream.toString()) + } + + fun basics() = listOf( + Arguments.of("arithmetics.pl", "gimli is a level 4 fighter with 35 hitpoints.\nlegolas is a level 5 ranger with 30 hitpoints.\ngandalf is a level 10 wizard with 25 hitpoints.\nfrodo is a level 2 rogue with 20 hitpoints.\nlegolas threw gimli, and gimli took 5 damage.\ngimli is a level 4 fighter with 30 hitpoints.\ngandalf casts aid.\ngimli is a level 4 fighter with 35 hitpoints.\nlegolas leveled up.\nlegolas is a level 6 ranger with 30 hitpoints"), + Arguments.of("backtracking.pl", "0\ns(0)\ns(s(0))\ns(s(s(0)))\n"), + Arguments.of("cut.pl", "0\n"), + Arguments.of("disjunction.pl", "Alice likes Italian food.\nBob likes Italian food.\n"), + Arguments.of("equality.pl", "X == Y failed\nX = Y succeeded\nX == Y succeeded\nX = Y succeeded\nX == Y succeeded\n"), + Arguments.of("forall.pl", "Only alice likes pizza.\n"), + Arguments.of("fraternity.pl", "Citizen robespierre is eligible for the event.\nCitizen danton is eligible for the event.\nCitizen camus is eligible for the event.\n"), + Arguments.of("liberty.pl", "Give me Liberty, or give me Death!\nI disapprove of what you say, but I will defend to the death your right to say it.\nThe revolution devours its own children.\nSo this is how liberty dies, with thunderous applause.\n"), + Arguments.of("unification.pl", "While alice got an A, carol got an A, but bob did not get an A, dave did not get an A, unfortunately.\n"), + Arguments.of("write.pl", "gpl zegt: dag(wereld)\n"), ) -} \ No newline at end of file + + fun other() = listOf( + Arguments.of("program.pl", "10\nhello(world)") + ) +} diff --git a/tests/interpreter/ParserPreprocessorIntegrationTests.kt b/tests/interpreter/ParserPreprocessorIntegrationTests.kt index a572686..05df756 100644 --- a/tests/interpreter/ParserPreprocessorIntegrationTests.kt +++ b/tests/interpreter/ParserPreprocessorIntegrationTests.kt @@ -6,7 +6,6 @@ import org.junit.jupiter.api.Nested import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource import parser.grammars.TermsGrammar -import prolog.Program import prolog.ast.arithmetic.Float import prolog.ast.arithmetic.Integer import prolog.ast.terms.Atom diff --git a/tests/interpreter/PreprocessorTests.kt b/tests/interpreter/PreprocessorTests.kt index 0b98ff4..bf54869 100644 --- a/tests/interpreter/PreprocessorTests.kt +++ b/tests/interpreter/PreprocessorTests.kt @@ -604,5 +604,19 @@ class PreprocessorTests { assertEquals(expected, result) } + + @Test + fun `dynamic declaration`() { + val input = Structure( + Atom("dynamic"), listOf( + Atom("declaration/1") + ) + ) + val expected = Dynamic("declaration/1") + + val result = preprocessor.preprocess(input) + + assertEquals(expected, result) + } } } diff --git a/tests/interpreter/SourceFileReaderTests.kt b/tests/interpreter/SourceFileReaderTests.kt index 55cdbcc..654a6ca 100644 --- a/tests/interpreter/SourceFileReaderTests.kt +++ b/tests/interpreter/SourceFileReaderTests.kt @@ -2,12 +2,12 @@ package interpreter import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import prolog.Program +import prolog.ast.Database.Program class SourceFileReaderTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } @Test diff --git a/tests/parser/builtins/DatabaseOperatorsParserTests.kt b/tests/parser/builtins/DatabaseOperatorsParserTests.kt index 9f13e57..74aa53e 100644 --- a/tests/parser/builtins/DatabaseOperatorsParserTests.kt +++ b/tests/parser/builtins/DatabaseOperatorsParserTests.kt @@ -2,13 +2,13 @@ package parser.builtins import com.github.h0tk3y.betterParse.grammar.Grammar import com.github.h0tk3y.betterParse.grammar.parseToEnd +import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import parser.grammars.TermsGrammar import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Term -import kotlin.test.assertEquals class DatabaseOperatorsParserTests { private lateinit var parser: Grammar @@ -62,4 +62,14 @@ class DatabaseOperatorsParserTests { assertEquals(expected, result) } + + @Test + fun `parse dynamic declaration`() { + val input = "dynamic declaration/1" + val expected = Structure(Atom("dynamic"), listOf(Atom("declaration/1"))) + + val result = parser.parseToEnd(input) + + assertEquals(expected, result) + } } diff --git a/tests/prolog/EvaluationTests.kt b/tests/prolog/EvaluationTests.kt index 3d185bb..ac3df3f 100644 --- a/tests/prolog/EvaluationTests.kt +++ b/tests/prolog/EvaluationTests.kt @@ -13,11 +13,12 @@ import prolog.logic.equivalent import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Variable +import prolog.ast.Database.Program class EvaluationTests { @BeforeEach fun setUp() { - Program.clear() + Program.reset() } @Test @@ -350,7 +351,7 @@ class EvaluationTests { ) ) - Program.clear() + Program.reset() Program.load(listOf(fact1, fact2, fact3, rule1)) } diff --git a/tests/prolog/builtins/ControlOperatorsTests.kt b/tests/prolog/builtins/ControlOperatorsTests.kt index 2ffc38c..bb6098f 100644 --- a/tests/prolog/builtins/ControlOperatorsTests.kt +++ b/tests/prolog/builtins/ControlOperatorsTests.kt @@ -3,7 +3,7 @@ package prolog.builtins import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -import prolog.Program +import prolog.ast.Database.Program import prolog.ast.logic.Fact import prolog.ast.logic.Rule import prolog.ast.terms.Atom @@ -14,7 +14,7 @@ import prolog.ast.terms.Variable class ControlOperatorsTests { @BeforeEach fun setUp() { - Program.clear() + Program.reset() } @Test @@ -55,7 +55,7 @@ class ControlOperatorsTests { // Now with cut - Program.clear() + Program.reset() Program.load( listOf( @@ -104,7 +104,7 @@ class ControlOperatorsTests { // Now with cut in the middle - Program.clear() + Program.reset() Program.load( listOf( @@ -138,7 +138,7 @@ class ControlOperatorsTests { // Now with cut at the end - Program.clear() + Program.reset() Program.load( listOf( diff --git a/tests/prolog/builtins/DatabaseOperatorsTests.kt b/tests/prolog/builtins/DatabaseOperatorsTests.kt index a14453d..8e96085 100644 --- a/tests/prolog/builtins/DatabaseOperatorsTests.kt +++ b/tests/prolog/builtins/DatabaseOperatorsTests.kt @@ -1,12 +1,15 @@ package prolog.builtins import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.ValueSource -import prolog.Program +import prolog.ast.Database +import prolog.ast.Database.Program import prolog.ast.logic.Clause import prolog.ast.logic.Fact import prolog.ast.logic.Predicate @@ -14,12 +17,11 @@ import prolog.ast.logic.Rule import prolog.ast.terms.Atom import prolog.ast.terms.Structure import prolog.ast.terms.Variable -import kotlin.test.assertTrue class DatabaseOperatorsTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } abstract class AssertTestsBase { @@ -27,7 +29,7 @@ class DatabaseOperatorsTests { @BeforeEach fun setup() { - Program.clear() + Program.reset() } @ParameterizedTest @@ -36,8 +38,8 @@ class DatabaseOperatorsTests { val fact = Fact(Atom("a")) createAssert(fact).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.internalDb.predicates["a/_"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.db.predicates["a/_"]!!.clauses[0]) } @Test @@ -45,8 +47,8 @@ class DatabaseOperatorsTests { val fact = Fact(Structure(Atom("a"), listOf(Atom("b")))) createAssert(fact).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(fact, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(fact, Program.db.predicates["a/1"]!!.clauses[0]) } @Test @@ -57,8 +59,8 @@ class DatabaseOperatorsTests { ) createAssert(rule).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule, Program.internalDb.predicates["a/1"]!!.clauses[0]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(rule, Program.db.predicates["a/1"]!!.clauses[0]) } } @@ -88,9 +90,9 @@ class DatabaseOperatorsTests { AssertA(rule1).satisfy(emptyMap()) AssertA(rule2).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[0]) - assertEquals(rule1, Program.internalDb.predicates["a/1"]!!.clauses[1]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(rule2, Program.db.predicates["a/1"]!!.clauses[0]) + assertEquals(rule1, Program.db.predicates["a/1"]!!.clauses[1]) } } @@ -113,9 +115,9 @@ class DatabaseOperatorsTests { AssertZ(rule1).satisfy(emptyMap()) AssertZ(rule2).satisfy(emptyMap()) - assertEquals(1, Program.internalDb.predicates.size, "Expected 1 predicate") - assertEquals(rule1, Program.internalDb.predicates["a/1"]!!.clauses[0]) - assertEquals(rule2, Program.internalDb.predicates["a/1"]!!.clauses[1]) + assertEquals(1, Program.db.predicates.size, "Expected 1 predicate") + assertEquals(rule1, Program.db.predicates["a/1"]!!.clauses[0]) + assertEquals(rule2, Program.db.predicates["a/1"]!!.clauses[1]) } } @@ -130,13 +132,13 @@ class DatabaseOperatorsTests { @Test fun `simple retract`() { val predicate = Predicate(listOf(Fact(Atom("a")))) - Program.internalDb.load(predicate) + Program.db.load(predicate) assertEquals(1, Program.query(Atom("a")).count()) val retract = Retract(Atom("a")) - assertTrue(retract.satisfy(emptyMap()).any(), "Expected 1 result") + assertEquals(1, retract.satisfy(emptyMap()).toList().size, "Expected 1 result") assertEquals(0, predicate.clauses.size, "Expected 0 clauses") assertTrue(retract.satisfy(emptyMap()).none()) @@ -149,7 +151,7 @@ class DatabaseOperatorsTests { Fact(Atom("a")), Fact(Atom("a")) )) - Program.internalDb.load(predicate) + Program.db.load(predicate) val control = Program.query(Atom("a")).toList() @@ -157,20 +159,25 @@ class DatabaseOperatorsTests { val retract = Retract(Atom("a")) - val result = retract.satisfy(emptyMap()) + val result = retract.satisfy(emptyMap()).iterator() assertEquals(3, predicate.clauses.size, "Expected 3 clauses") - var answer = result.first() + assertTrue(result.hasNext(), "Expected more results") + + val answer = result.next() assertTrue(answer.isSuccess, "Expected success") - var subs = answer.getOrNull()!! - assertTrue(subs.isEmpty(), "Expected no substitutions") + assertTrue(answer.getOrNull()!!.isEmpty(), "Expected no substitutions") + + assertTrue(result.hasNext(), "Expected more results") assertEquals(2, predicate.clauses.size, "Expected 2 clauses") - assertTrue(result.first().isSuccess) - assertTrue(result.first().isSuccess) + assertTrue(result.next().isSuccess) + assertTrue(result.hasNext(), "Expected more results") + assertTrue(result.next().isSuccess) + assertFalse(result.hasNext(), "Expected more results") assertEquals(0, predicate.clauses.size, "Expected no remaining clauses") } @@ -181,7 +188,7 @@ class DatabaseOperatorsTests { Fact(Structure(Atom("a"), listOf(Atom("c")))), Fact(Structure(Atom("a"), listOf(Atom("d")))) )) - Program.internalDb.load(predicate) + Program.db.load(predicate) val control = Program.query(Structure(Atom("a"), listOf(Variable("X")))).toList() @@ -189,38 +196,40 @@ class DatabaseOperatorsTests { val retract = Retract(Structure(Atom("a"), listOf(Variable("X")))) - val result = retract.satisfy(emptyMap()) + val result = retract.satisfy(emptyMap()).iterator() assertEquals(3, predicate.clauses.size, "Expected 3 clauses") - var answer = result.first() + assertTrue(result.hasNext(), "Expected more results") + var answer = result.next() assertTrue(answer.isSuccess, "Expected success") var subs = answer.getOrNull()!! assertTrue(subs.isNotEmpty(), "Expected substitutions") assertTrue(Variable("X") in subs, "Expected variable X") assertEquals(Atom("b"), subs[Variable("X")], "Expected b") + assertTrue(result.hasNext(), "Expected more results") assertEquals(2, predicate.clauses.size, "Expected 2 clauses") - answer = result.first() + answer = result.next() assertTrue(answer.isSuccess, "Expected success") subs = answer.getOrNull()!! assertTrue(subs.isNotEmpty(), "Expected substitutions") assertTrue(Variable("X") in subs, "Expected variable X") assertEquals(Atom("c"), subs[Variable("X")], "Expected c") + assertTrue(result.hasNext(), "Expected more results") assertEquals(1, predicate.clauses.size, "Expected 1 clause") - answer = result.first() + answer = result.next() assertTrue(answer.isSuccess, "Expected success") subs = answer.getOrNull()!! assertTrue(subs.isNotEmpty(), "Expected substitutions") assertTrue(Variable("X") in subs, "Expected variable X") assertEquals(Atom("d"), subs[Variable("X")], "Expected d") + assertFalse(result.hasNext(), "Expected no more results") assertEquals(0, predicate.clauses.size, "Expected no clauses") - - assertEquals(0, result.count(), "Expected no remaining results") } @Test diff --git a/tests/prolog/logic/TermsTests.kt b/tests/prolog/logic/TermsTests.kt index f67f46a..dde66e6 100644 --- a/tests/prolog/logic/TermsTests.kt +++ b/tests/prolog/logic/TermsTests.kt @@ -29,7 +29,7 @@ class TermsTests { assertEquals(start + 1, end, "Expected end to be incremented by 1") assertEquals(1, subs.size, "Expected one substitution") assertTrue(subs.containsKey(term), "Expected subs to contain the original term") - assertEquals(Variable("X($start)"), subs[term], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs[term], "Expected subs to contain the new term") } @Test @@ -53,9 +53,9 @@ class TermsTests { assertEquals(start + 2, end, "Expected end to be incremented by 2") assertEquals(2, subs.size, "Expected two substitutions") assertTrue(subs.containsKey(term.arguments[0]), "Expected subs to contain the first original term") - assertEquals(Variable("X($start)"), subs[term.arguments[0]], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term") assertTrue(subs.containsKey(term.arguments[1]), "Expected subs to contain the second original term") - assertEquals(Variable("Y(${start + 1})"), subs[term.arguments[1]], "Expected subs to contain the new term") + assertEquals(Variable("Y@${start + 1}"), subs[term.arguments[1]], "Expected subs to contain the new term") } @Test @@ -68,9 +68,9 @@ class TermsTests { assertEquals(start + 1, end, "Expected end to be incremented by 1") assertEquals(1, subs.size, "Expected one substitution") assertTrue(subs.containsKey(term.arguments[0]), "Expected subs to contain the first original term") - assertEquals(Variable("X($start)"), subs[term.arguments[0]], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term") assertTrue(subs.containsKey(term.arguments[1]), "Expected subs to contain the second original term") - assertEquals(Variable("X($start)"), subs[term.arguments[1]], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs[term.arguments[1]], "Expected subs to contain the new term") } @Test @@ -83,7 +83,7 @@ class TermsTests { assertEquals(start + 1, end, "Expected end to be incremented by 1") assertEquals(1, subs.size, "Expected one substitution") assertTrue(subs.containsKey(Variable("X")), "Expected subs to contain the variable") - assertEquals(Variable("X($start)"), subs[term.arguments[0]], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs[term.arguments[0]], "Expected subs to contain the new term") } @Test @@ -97,13 +97,14 @@ class TermsTests { assertEquals(start + 1, end1, "Expected end to be incremented by 1") assertEquals(1, subs1.size, "Expected one substitution") assertTrue(subs1.containsKey(variable), "Expected subs to contain the variable") - assertEquals(Variable("X($start)"), subs1[variable], "Expected subs to contain the new term") + assertEquals(Variable("X@$start"), subs1[variable], "Expected subs to contain the new term") + val variable1 = subs1[variable] as Variable val (end2, subs2) = numbervars(term, end1, subs1) assertEquals(start + 2, end2, "Expected end to be incremented by 2") assertEquals(1, subs2.size, "Expected one substitution") - assertTrue(subs2.containsKey(variable), "Expected subs to contain the variable") - assertEquals(Variable("X($end1)"), subs2[variable], "Expected subs to contain the new term") + assertTrue(subs2.containsKey(variable1), "Expected subs to contain the variable") + assertEquals(Variable("X@$start@$end1"), subs2[variable1], "Expected subs to contain the new term") } } \ No newline at end of file