From ec6ce31cc2a1b27bfc92664c873e8140a80c73f3 Mon Sep 17 00:00:00 2001 From: Fedor Isakov Date: Mon, 7 Mar 2016 15:23:57 +0100 Subject: [PATCH] Propagation history feature, tests. Minor refactorings. --- .../mps/logic/reactor/core/Handler.kt | 14 +- .../mps/logic/reactor/core/Matcher.kt | 62 ++++++- .../reactor/core/MemEvaluationSession.kt | 6 +- .../mps/logic/reactor/core/OccurrenceStore.kt | 8 +- .../mps/logic/reactor/core/PartialMatch.kt | 8 +- reactor/Test/test/TestHandler.kt | 170 ++++++++++++++++-- reactor/Test/test/TestMatcher.kt | 4 +- reactor/Test/test/TestProgram.kt | 92 ---------- 8 files changed, 230 insertions(+), 134 deletions(-) diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/Handler.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/Handler.kt index fdb191d2..91e4fcd9 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/Handler.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/Handler.kt @@ -14,7 +14,7 @@ import java.util.* * @author Fedor Isakov */ -class Handler : Matcher.AuxOccurrences { +class Handler : Matcher.AuxOccurrencesLookup { val sessionSolver: SessionSolver @@ -52,7 +52,14 @@ class Handler : Matcher.AuxOccurrences { } } - fun occurrences(): Set = occurrenceStore.allOccurrences().toSet() + fun allOccurrences(): Set = + occurrenceStore.allOccurrences().toSet() + + fun constraintSymbols(): Set = + occurrenceStore.allOccurrences().map { co -> co.constraint().symbol() }.toSet() + + fun occurrences(symbol: ConstraintSymbol): Set = + occurrenceStore.allOccurrences().filter { co -> co.constraint().symbol() == symbol }.toSet() fun tell(constraint: Constraint) { try { @@ -70,7 +77,7 @@ class Handler : Matcher.AuxOccurrences { } } - override fun findOccurrences( + override fun lookupAuxOccurrences( symbol: ConstraintSymbol, logicals: Iterable>, values: Iterable, @@ -107,6 +114,7 @@ class Handler : Matcher.AuxOccurrences { activationStack.push(match) trace.trigger(match) + matcher.recordPropagation(match) for ((cst, occ) in match.discarded) { occurrenceStore.discard(occ) diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/Matcher.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/Matcher.kt index 3b91395b..27b63f2e 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/Matcher.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/Matcher.kt @@ -1,5 +1,8 @@ package jetbrains.mps.logic.reactor.core +import com.github.andrewoma.dexx.collection.ConsList +import com.github.andrewoma.dexx.collection.Maps +import com.github.andrewoma.dexx.collection.List as PersList import jetbrains.mps.logic.reactor.evaluation.ConstraintOccurrence import jetbrains.mps.logic.reactor.evaluation.MatchRule import jetbrains.mps.logic.reactor.logical.Logical @@ -18,9 +21,9 @@ import jetbrains.mps.unification.Unification class Matcher { - interface AuxOccurrences { + interface AuxOccurrencesLookup { - fun findOccurrences( + fun lookupAuxOccurrences( symbol: ConstraintSymbol, logicals: Iterable>, values: Iterable = emptyList(), @@ -29,12 +32,13 @@ class Matcher { } val rules: RuleIndex - val aux: AuxOccurrences - val profiler: Profiler? + private val auxLookup: AuxOccurrencesLookup + private val profiler: Profiler? + private val propHistory = PropagationHistory() - constructor(rules: Collection, aux: AuxOccurrences, profiler: Profiler? = null) { + constructor(rules: Collection, aux: AuxOccurrencesLookup, profiler: Profiler? = null) { this.rules = RuleIndex(rules) - this.aux = aux + this.auxLookup = aux this.profiler = profiler } @@ -49,16 +53,60 @@ class Matcher { matchedDiscarded.map { cst -> PartialMatch(r, profiler).discard(cst, occ) } } - partialMatches?.flatMap { pm -> pm.completeMatch(aux) }?.filter { pm -> pm.matches() } ?: emptySequence() + partialMatches?.flatMap { pm -> + pm.completeMatch(auxLookup) }?.filter { pm -> + pm.matches() && !propHistory.isRecorded(pm) } ?: emptySequence() }) } + fun recordPropagation(pm: PartialMatch) = propHistory.record(pm) } +private class PropagationHistory { + var recordedPropagation = Maps.of>>>() + fun isRecorded(pm: PartialMatch): Boolean { + if (!pm.isPropagation()) return false + val test = pm.kept.map { pair -> IdWrapper(pair.second) }.sortedBy { idOcc -> idOcc.idHash }.toList() + + return recordedPropagation.get(pm.rule)?.let { hist -> + hist.any { recorded -> + recorded == test + } // use the reference equality via IdWrapper + } ?: false + } + + fun record(pm: PartialMatch): PartialMatch { + if (pm.isPropagation()) { + val idOccs = pm.kept.map { pair -> IdWrapper(pair.second) }.sortedBy { id -> id.idHash }.toList() + + val hist = recordedPropagation.get(pm.rule) ?: ConsList.empty>>() + + recordedPropagation = recordedPropagation.put(pm.rule, hist.prepend(idOccs)) + } + return pm + } + +} + +private class IdWrapper(val wrapped: T) { + + val idHash = System.identityHashCode(wrapped) + + override fun hashCode(): Int = idHash + + override fun equals(other: Any?): Boolean { + if (other is IdWrapper<*>) + return this.wrapped === other.wrapped // referential equality! + return false + } + + override fun toString(): String = "${wrapped.toString()} #$idHash" + +} /** * True iff the constraint matches the occurrence. diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/MemEvaluationSession.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/MemEvaluationSession.kt index 39100bc0..2d264350 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/MemEvaluationSession.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/MemEvaluationSession.kt @@ -86,13 +86,13 @@ class MemEvaluationSession : EvaluationSession { override fun sessionSolver(): SessionSolver = program.sessionSolver() override fun constraintSymbols(): Iterable = - handler.occurrences().map { co -> co.constraint().symbol() }.toSet() + handler.constraintSymbols() override fun constraintOccurrences(): Iterable = - handler.occurrences() + handler.allOccurrences() override fun constraintOccurrences(symbol: ConstraintSymbol): Iterable = - handler.occurrences().filter { co -> co.constraint().symbol() == symbol } + handler.occurrences(symbol) private class Backend : EvaluationSession.Backend { diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/OccurrenceStore.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/OccurrenceStore.kt index 5522bd4f..4baeb84e 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/OccurrenceStore.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/OccurrenceStore.kt @@ -125,7 +125,7 @@ class OccurrenceStore : LogicalObserver { } -private data class MemConstraintOccurrence(val handler: Handler, val constraint: Constraint, val arguments: List<*>, val id: Int) : +private data class MemConstraintOccurrence(val handler: Handler, val constraint: Constraint, val arguments: List<*>) : ConstraintOccurrence, LogicalObserver, StoreItem @@ -135,12 +135,8 @@ private data class MemConstraintOccurrence(val handler: Handler, val constraint: override var stored = false - companion object { - val random = Random() - } - constructor(handler: Handler, constraint: Constraint, arguments: Collection<*>) : - this(handler, constraint, ArrayList(arguments), random.nextInt()) + this(handler, constraint, ArrayList(arguments)) { for (a in arguments) { if (a is Logical<*>) { diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/PartialMatch.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/PartialMatch.kt index eaa68a56..c4b3d45b 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/PartialMatch.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/PartialMatch.kt @@ -45,7 +45,7 @@ class PartialMatch(val rule: Rule, val profiler: Profiler? = null) : MatchRule { } } - fun completeMatch(aux: Matcher.AuxOccurrences) : Sequence { + fun completeMatch(aux: Matcher.AuxOccurrencesLookup) : Sequence { if (!isPartial()) return sequenceOf(this) return profiler.profile>("completeMatch", { @@ -67,7 +67,7 @@ class PartialMatch(val rule: Rule, val profiler: Profiler? = null) : MatchRule { }) } - fun findOccurrences(aux: Matcher.AuxOccurrences, cst: Constraint): Sequence { + fun findOccurrences(aux: Matcher.AuxOccurrencesLookup, cst: Constraint): Sequence { val logicals = HashSet>() val values = HashSet() @@ -77,7 +77,7 @@ class PartialMatch(val rule: Rule, val profiler: Profiler? = null) : MatchRule { else values.add(arg!!) } - return aux.findOccurrences(cst.symbol(), logicals, values, { occ -> !hasOccurrence(occ) }) + return aux.lookupAuxOccurrences(cst.symbol(), logicals, values, { occ -> !hasOccurrence(occ) }) } fun keep (constraint: Constraint, occ: ConstraintOccurrence) = PartialMatch(this, Pair(constraint, occ), null) @@ -153,6 +153,8 @@ class PartialMatch(val rule: Rule, val profiler: Profiler? = null) : MatchRule { }) } + fun isPropagation(): Boolean = !kept.isEmpty && discarded.isEmpty + fun logicalContext(): LogicalContext = logicalContext override fun rule(): Rule = rule diff --git a/reactor/Test/test/TestHandler.kt b/reactor/Test/test/TestHandler.kt index cc4d0657..a5a5d8dd 100644 --- a/reactor/Test/test/TestHandler.kt +++ b/reactor/Test/test/TestHandler.kt @@ -11,6 +11,8 @@ import org.junit.Assert.* import org.junit.Before import org.junit.BeforeClass import org.junit.Test +import solver.eq +import solver.is_eq /** * @author Fedor Isakov @@ -30,6 +32,10 @@ class TestHandler { sessionSolver.tell(PredicateSymbol("equals", 2), left, right) } + private fun Handler.is_eq(left: Logical, right: Logical): Boolean { + return sessionSolver.ask(PredicateSymbol("equals", 2), left, right) + } + companion object { @BeforeClass @JvmStatic fun setup() { @@ -54,7 +60,7 @@ class TestHandler { handler().apply { queue(occurrence("main")) }.let { rh -> assertEquals( setOf(ConstraintSymbol("main", 0), ConstraintSymbol("foo", 0)), - rh.occurrences().map { it.constraint().symbol() }.toSet()) + rh.allOccurrences().map { it.constraint().symbol() }.toSet()) } } } @@ -83,7 +89,7 @@ class TestHandler { handler().apply { queue(occurrence("main")) }.let { rh -> assertEquals( setOf(ConstraintSymbol("bar", 0), ConstraintSymbol("foo", 0)), - rh.occurrences().map { it.constraint().symbol() }.toSet()) + rh.allOccurrences().map { it.constraint().symbol() }.toSet()) } } } @@ -167,7 +173,7 @@ class TestHandler { handler().apply { queue(occurrence("main")) }.let { rh -> assertEquals( setOf(ConstraintSymbol("main", 0), ConstraintSymbol("next", 0)), - rh.occurrences().map { it.constraint().symbol() }.toSet()) + rh.allOccurrences().map { it.constraint().symbol() }.toSet()) assertEquals("expected", test) } } @@ -222,8 +228,8 @@ class TestHandler { val a = logical("a") a.set("value") queue(occurrence("foo", a)) - assertEquals(1, occurrences().size) - val co = occurrences().first() + assertEquals(1, allOccurrences().size) + val co = allOccurrences().first() assertEquals(ConstraintSymbol("bar",1), co.constraint().symbol()) assertEquals(1, co.arguments().size) val arg = co.arguments().first() @@ -253,7 +259,7 @@ class TestHandler { queue(occurrence("foo")) assertEquals( setOf(ConstraintSymbol("expected1", 0), ConstraintSymbol("expected2", 0)), - occurrences().map { co -> co.constraint().symbol() }.toSet()) + allOccurrences().map { co -> co.constraint().symbol() }.toSet()) } } @@ -278,7 +284,7 @@ class TestHandler { queue(occurrence("foo")) assertEquals( setOf(ConstraintSymbol("expected1", 0), ConstraintSymbol("expected2", 0)), - occurrences().map { co -> co.constraint().symbol() }.toSet()) + allOccurrences().map { co -> co.constraint().symbol() }.toSet()) } } @@ -315,11 +321,11 @@ class TestHandler { ) ).handler().run { queue(occurrence("foo")) - assertEquals(3, occurrences().count()) + assertEquals(3, allOccurrences().count()) assertEquals( setOf(ConstraintSymbol("expected1", 0), ConstraintSymbol("expected2", 0), ConstraintSymbol("expected3", 1)), - occurrences().map { co -> co.constraint().symbol() }.toSet()) - val ex3 = occurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() + allOccurrences().map { co -> co.constraint().symbol() }.toSet()) + val ex3 = allOccurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() assertEquals(999, (ex3.arguments().first() as Logical).value()) } } @@ -356,11 +362,11 @@ class TestHandler { ).handler().run { handler = this queue(occurrence("foo")) - assertEquals(3, occurrences().count()) + assertEquals(3, allOccurrences().count()) assertEquals( setOf(ConstraintSymbol("expected1", 0), ConstraintSymbol("expected2", 0), ConstraintSymbol("expected3", 1)), - occurrences().map { co -> co.constraint().symbol() }.toSet()) - val ex3 = occurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() + allOccurrences().map { co -> co.constraint().symbol() }.toSet()) + val ex3 = allOccurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() assertEquals(123, (ex3.arguments().first() as Logical).value()) } } @@ -397,16 +403,144 @@ class TestHandler { ).handler().run { handler = this queue(occurrence("foo")) - assertEquals(3, occurrences().count()) - assertEquals( - setOf(ConstraintSymbol("expected1", 0), ConstraintSymbol("expected2", 0), ConstraintSymbol("expected3", 1)), - occurrences().map { co -> co.constraint().symbol() }.toSet()) - val ex3 = occurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() + assertEquals(3, allOccurrences().count()) + assertEquals(setOf( ConstraintSymbol("expected1", 0), + ConstraintSymbol("expected2", 0), + ConstraintSymbol("expected3", 1)), + allOccurrences().map { co -> co.constraint().symbol() }.toSet()) + val ex3 = allOccurrences().filter { co -> co.constraint().symbol() == ConstraintSymbol("expected3", 1) }.first() assertEquals(123, (ex3.arguments().first() as Logical).value()) } } + @Test + fun correctRulesOrder() { + val X= metaLogical("X") + + program( + rule("main", + headReplaced( constraint("main") ), body( statement({ x -> x.set(1) }, X), + constraint("bar"), + constraint("foo", X) ) + ), + rule("foo_if_zero", + headReplaced( constraint("foo", X) ), guard( expression({ x -> x.get() == 0 }, X) ), + body( constraint("foo_zero") ) + ), + rule("foo_and_bar", + headReplaced( constraint("foo", X) ), + headKept( constraint("bar") ), + body( constraint("foo_and_bar") ) + ), + rule("foo_if_non_zero", + headReplaced( constraint("foo", X) ), + guard( expression({ x -> x.get() != 0 }, X) ), + body( constraint("foo_non_zero") ) + ) + ).handler().run { + queue(occurrence("main")) + assertEquals(setOf(ConstraintSymbol("bar", 0), ConstraintSymbol("foo_and_bar", 0)), constraintSymbols()) + assertEquals(1, occurrences(ConstraintSymbol("foo_and_bar", 0)).count()) + assertEquals(1, occurrences(ConstraintSymbol("bar", 0)).count()) + } + } + + @Test + fun reactivateOnUnion() { + val (X,Y) = metaLogical("X", "Y") + val (C,D) = metaLogical("C", "D") + var handler : Handler? = null + program( + rule("main", + headReplaced( constraint("main") ), body( statement({ c -> c.set(0) }, C), + constraint("foo", X, C), + constraint("foo", Y, C), + statement({ x, y -> handler!!.eq(x, y) }, X, Y) ) + ), + rule("capture_foo", + headKept( constraint("foo", X, C) ), + body( statement({ c, d -> d.set(c.get() + 1) }, C, D), + constraint("capture", D) ) + ), + rule("capture_foo_foo", + headKept( constraint("foo", X, C) ), + headReplaced( constraint("foo", Y, C) ), guard( expression({x, y -> handler!!.is_eq(x, y) }, X, Y)), + body( constraint("replaced") ) + ) + ).handler().run { + handler = this + queue(occurrence("main")) + assertEquals(setOf( ConstraintSymbol("foo", 2), + ConstraintSymbol("capture", 1), + ConstraintSymbol("replaced", 0)), + constraintSymbols()) + assertEquals(1, occurrences(ConstraintSymbol("foo", 2)).count()) + assertEquals(2, occurrences(ConstraintSymbol("capture", 1)).count()) + assertEquals(1, occurrences(ConstraintSymbol("replaced", 0)).count()) + } + } + + @Test + fun propagationHistory() { + val (X,Y,Z) = metaLogical("X", "Y", "Z") + var handler : Handler? = null + program( + rule("main", + headReplaced( constraint("main") ), body( statement({ x, y -> handler!!.eq(x, y) }, X, Y), // rank(X) = 1 + constraint("foo", Y), + constraint("bar", Z), + // update Z's parent + statement({ x, z -> handler!!.eq(x, z) }, X, Z) ) + ), + rule("foobar", + headKept( constraint("foo", X) ), + headKept( constraint("bar", Y) ), + body( constraint("foobar") ) + ) + ).handler().run { + handler = this + queue(occurrence("main")) + assertEquals(setOf( ConstraintSymbol("foo", 1), + ConstraintSymbol("bar", 1), + ConstraintSymbol("foobar", 0)), + constraintSymbols()) + assertEquals(1, occurrences(ConstraintSymbol("foo", 1)).count()) + assertEquals(1, occurrences(ConstraintSymbol("bar", 1)).count()) + assertEquals(1, occurrences(ConstraintSymbol("foobar", 0)).count()) + } + } + + @Test + fun reactivateOnUnionKeepValue() { + val (X,Y,Z) = metaLogical("X", "Y", "Z") + var handler : Handler? = null + program( + rule("main", + headReplaced( constraint("main") ), body( statement({ x, y -> handler!!.eq(x, y) }, X, Y), // rank(X) = 1 + statement({ z -> z.set(42) }, Z), + constraint("foo", Z), + statement({ x, z -> handler!!.eq(x, z) }, X, Z) ) + ), + rule("capture_foo_free", + headKept( constraint("foo", X) ), guard( expression({ x -> x.getNullable() == null }, X) ), + body( constraint("free") ) + ), + rule("capture_foo_assigned", + headKept( constraint("foo", X) ), guard( expression({ x -> x.getNullable() != null }, X) ), + body( constraint("assigned") ) + ) + ).handler().run { + handler = this + queue(occurrence("main")) + assertEquals(setOf( ConstraintSymbol("foo", 1), + ConstraintSymbol("assigned", 0)), + constraintSymbols()) + assertEquals(1, occurrences(ConstraintSymbol("foo", 1)).count()) + assertEquals(1, occurrences(ConstraintSymbol("assigned", 0)).count()) + } + } + } diff --git a/reactor/Test/test/TestMatcher.kt b/reactor/Test/test/TestMatcher.kt index f88bb875..e13a7d91 100644 --- a/reactor/Test/test/TestMatcher.kt +++ b/reactor/Test/test/TestMatcher.kt @@ -18,8 +18,8 @@ class TestMatcher { val stored = occurrence.toList() - val aux = object : Matcher.AuxOccurrences { - override fun findOccurrences( + val aux = object : Matcher.AuxOccurrencesLookup { + override fun lookupAuxOccurrences( symbol: ConstraintSymbol, logicals: Iterable>, values: Iterable, diff --git a/reactor/Test/test/TestProgram.kt b/reactor/Test/test/TestProgram.kt index aba79437..f2b651f1 100644 --- a/reactor/Test/test/TestProgram.kt +++ b/reactor/Test/test/TestProgram.kt @@ -112,98 +112,6 @@ class TestProgram { } } - @Test - fun correctRulesOrder() { - val X= metaLogical("X") - - program( - rule("main", - headReplaced( constraint("main") ), body( statement({ x -> x.set(1) }, X), - constraint("bar"), - constraint("foo", X) ) - ), - rule("foo_if_zero", - headReplaced( constraint("foo", X) ), guard( expression({ x -> x.get() == 0 }, X) ), - body( constraint("foo_zero") ) - ), - rule("foo_and_bar", - headReplaced( constraint("foo", X) ), - headKept( constraint("bar") ), - body( constraint("foo_and_bar") ) - ), - rule("foo_if_non_zero", - headReplaced( constraint("foo", X) ), - guard( expression({ x -> x.get() != 0 }, X) ), - body( constraint("foo_non_zero") ) - ) - ).session("correctRulesOrder").run { - assertEquals(setOf(ConstraintSymbol("bar", 0), ConstraintSymbol("foo_and_bar", 0)), constraintSymbols()) - assertEquals(1, constraintOccurrences(ConstraintSymbol("foo_and_bar", 0)).count()) - assertEquals(1, constraintOccurrences(ConstraintSymbol("bar", 0)).count()) - } - } - - @Test - fun reactivateOnUnion() { - val (X,Y) = metaLogical("X", "Y") - val S = metaLogical("S") - - program( - rule("main", - headReplaced( constraint("main") ), body( constraint("foo", X, S), - constraint("foo", Y, S), - statement({ x, y -> x eq y }, X, Y) ) - ), - rule("capture_foo", - headKept( constraint("foo", X, S) ), - body( constraint("capture", S) ) - ), - rule("capture_foo_foo", - headKept( constraint("foo", X, S) ), - headReplaced( constraint("foo", Y, S) ), guard( expression({x, y -> x is_eq y }, X, Y)), - body( constraint("replaced") ) - ) - ).session("reactivateOnUnion").run { - assertEquals(setOf( ConstraintSymbol("foo", 2), - ConstraintSymbol("capture", 1), - ConstraintSymbol("replaced", 0)), - constraintSymbols()) - assertEquals(1, constraintOccurrences(ConstraintSymbol("foo", 2)).count()) - // FIXME: this count should be 2 instead as per the "activation history" feature - assertEquals(3, constraintOccurrences(ConstraintSymbol("capture", 1)).count()) - assertEquals(1, constraintOccurrences(ConstraintSymbol("replaced", 0)).count()) - } - } - - @Test - fun reactivateOnUnionKeepValue() { - val (X,Y,Z) = metaLogical("X", "Y", "Z") - - program( - rule("main", - headReplaced( constraint("main") ), body( statement({ x, y -> x eq y }, X, Y), // rank(X) = 1 - statement({ z -> z.set(42) }, Z), - constraint("foo", Z), - statement({ x, z -> x eq z }, X, Z) ) - ), - rule("capture_foo_free", - headKept( constraint("foo", X) ), guard( expression({x -> x.getNullable() == null }, X) ), - body( constraint("free") ) - ), - rule("capture_foo_assigned", - headKept( constraint("foo", X) ), guard( expression({x -> x.getNullable() != null }, X) ), - body( constraint("assigned") ) - ) - ).session("reactivateOnUnionKeepValue").run { - assertEquals(setOf( ConstraintSymbol("foo", 1), - ConstraintSymbol("assigned", 0)), - constraintSymbols()) - assertEquals(1, constraintOccurrences(ConstraintSymbol("foo", 1)).count()) - // FIXME: this count should be 1 instead as per the "activation history" feature (??? not sure) - assertEquals(2, constraintOccurrences(ConstraintSymbol("assigned", 0)).count()) - } - } - @Test fun gcd() { val (M, N, TMP) = metaLogical("M", "N", "TMP")