diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournalImpl.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournalImpl.kt index 28575954..515facf0 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournalImpl.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournalImpl.kt @@ -91,17 +91,25 @@ internal open class MatchJournalImpl( override fun isFront(): Boolean = current == hist.last override fun resetPos() { - posPtr = hist.listIterator() + // walk backwards and reset all occurrences + posPtr = hist.listIterator(hist.size) + while (posPtr.hasPrevious()) { + current = posPtr.previous() + resetOccurrences(current.entries) + } } override fun reset(pastPos: MatchJournal.Pos) { + // walk backwards, reset occurrences and remove history from posPtr down to pastPos while (posPtr.hasPrevious()) { current = posPtr.previous() if (current === pastPos.chunk) { + resetOccurrences(current.entries.drop(pastPos.entriesCount)) current.entries = current.entries.subList(0, pastPos.entriesCount) posPtr.next() // make 'posPtr' to always point right after 'current' return } + resetOccurrences(current.entries) posPtr.remove() } if (currentPos() != pastPos) throw IllegalStateException() @@ -119,6 +127,17 @@ internal open class MatchJournalImpl( if (currentPos() != futurePos) throw IllegalStateException() } + private fun resetOccurrences(occSpecs: Iterable) = + occSpecs.forEach { + if (it.discarded) { + it.occ.alive = true + it.occ.stored = true + } else { + it.occ.alive = false + it.occ.stored = false + } + } + private fun replayOccurrences(observable: LogicalStateObservable, occSpecs: Iterable) = occSpecs.forEach { if (it.discarded) { diff --git a/reactor/Test/test/TestIncrementalProgram.kt b/reactor/Test/test/TestIncrementalProgram.kt index e381d66b..ebdf4117 100644 --- a/reactor/Test/test/TestIncrementalProgram.kt +++ b/reactor/Test/test/TestIncrementalProgram.kt @@ -1158,4 +1158,71 @@ class TestIncrementalProgram { } } } + + + @Test + fun observerReactivatesFutureOccurrence() { + val progSpec = MockIncrProgSpec( + setOf("main", "bindVar", "produceBound", "eliminateBound"), + setOf(sym1("foo")) + ) + val X = metaLogical("X") + programWithRules( + rule("main", + headReplaced( + constraint("main") + ), + body( + princConstraint("foo", X) + )), + rule("produceBound", + headKept( + princConstraint("foo", X) + ), + body( + princConstraint("hasBound", X) + )), + rule("eliminateBound", + headReplaced( + princConstraint("hasBound", X) + ), + guard( + expression({ x -> x.isBound }, X) + ), + body( + constraint("unexpected") + ) + ) + ).launch("initial run", progSpec) { result -> + + result.storeView().constraintSymbols() shouldBe setOf(sym1("foo"), sym1("hasBound")) + + }.also { (builder, evalRes) -> + // Insert rule before produceBound: test whether + // occurrence reactivation due to var bind can occur + // before this occurrence is actually activated, + // which leads, of course, to inconsistent state. + builder.insertRulesAt(1, + rule("bindVar", + headKept( + princConstraint("foo", X) + ), + body( + statement({ x -> x.set(42) }, X) + )) + ).relaunch("test reactivation", progSpec, evalRes.token()) { result -> + + // Currently incremental processing can't handle such rule dependencies + // stemming from incremental changes to logicals, + // so "eliminateBound" rule won't be reexecuted. + // But reactivation of hasBound shouldn't pass either, + // because 'hasBound' isn't in store at the point of tried reactivation. + result.storeView().constraintSymbols() shouldBe setOf(sym1("foo"), sym1("hasBound")) + + // NB: this is correct if incremental processing + // takes into account rule dependencies due to logicals. + // result.storeView().constraintSymbols() shouldBe setOf(sym1("foo"), sym0("unexpected")) + } + } + } }