Ensure MatchJournalImpl.replay is idempotent operation, add test for it

Add another test for replaying inside Chunk.
Doesn't pass for now for the lack of machinery for tracking precise Pos.
This commit is contained in:
Grigorii Kirgizov 2020-03-02 12:02:02 +03:00
parent 5c4b1f8263
commit 81721a0067
3 changed files with 207 additions and 48 deletions

View File

@ -88,13 +88,6 @@ interface MatchJournal : MutableIterable<MatchJournal.Chunk>, EvidenceSource {
*/
fun replay(observable: LogicalStateObservable, futurePos: Pos)
/**
* Same as [replay] but stops at the last descendant of [ancestor] if [until] isn't found before that.
* Suitable for "restricted" replay to [Pos] that can be already invalid (i.e. not in [MatchJournal]).
*/
fun replayDescendants(observable: LogicalStateObservable, ancestor: Chunk, until: Pos)
/**
* Returns snapshot of the journal.
*/
@ -191,7 +184,7 @@ interface MatchJournal : MutableIterable<MatchJournal.Chunk>, EvidenceSource {
override var entries: MutableList<Chunk.Entry> = mutableListOf()
override fun toString() = "(id=$evidence, ${justifications()}, activation of $occ)"
override fun toString() = "(id=$evidence, ${justifications()}, activation of $occ, $entries)"
}
open class Pos(val chunk: Chunk, val entriesCount: Int) {

View File

@ -120,6 +120,10 @@ internal open class MatchJournalImpl(
override fun isFront(): Boolean = current == hist.last
override fun reset(pastPos: MatchJournal.Pos) = resetPos(pastPos, true)
// override fun resetPos() = resetPos(Pos(initialChunk(), 0), false)
override fun resetPos() {
// walk backwards and reset all occurrences
posPtr = hist.listIterator(hist.size)
@ -129,56 +133,38 @@ internal open class MatchJournalImpl(
}
}
override fun reset(pastPos: MatchJournal.Pos) {
// walk backwards, reset occurrences and remove history from posPtr down to pastPos
private fun resetPos(pastPos: MatchJournal.Pos, drop: Boolean) {
while (posPtr.hasPrevious()) {
current = posPtr.previous()
if (current === pastPos.chunk) {
// todo: need reversed?
resetOccurrences(current.entries.drop(pastPos.entriesCount))
current.entries = current.entries.subList(0, pastPos.entriesCount)
if (drop) current.entries = current.entries.subList(0, pastPos.entriesCount)
posPtr.next() // make 'posPtr' to always point right after 'current'
return
}
// todo: need reversed?
resetOccurrences(current.entries)
posPtr.remove()
if (drop) posPtr.remove()
}
if (currentPos() != pastPos) throw IllegalStateException()
}
override fun replay(observable: LogicalStateObservable, futurePos: MatchJournal.Pos) {
while (posPtr.hasNext()) {
current = posPtr.next()
override fun replay(observable: LogicalStateObservable, futurePos: MatchJournal.Pos) = replayWith(futurePos, {})
private fun replayWith(futurePos: MatchJournal.Pos, action: (Chunk) -> Unit) {
do {
if (futurePos.chunk === current) {
replayOccurrences(observable, current.entries.take(futurePos.entriesCount))
replayOccurrences(current.entries.take(futurePos.entriesCount))
action(current)
return
}
replayOccurrences(observable, current.entries)
}
if (currentPos() != futurePos) throw IllegalStateException()
}
replayOccurrences(current.entries)
action(current)
override fun replayDescendants(observable: LogicalStateObservable, ancestor: Chunk, until: MatchJournal.Pos) {
while (posPtr.hasNext()) {
val previous = current
if (!posPtr.hasNext()) break
current = posPtr.next()
} while (true)
if (until.chunk === current) {
replayOccurrences(observable, current.entries.take(until.entriesCount))
return
}
if (previous.isDescendantOf(ancestor) && !current.isDescendantOf(ancestor)) {
// reset ptr so that it points after previous chunk
current = previous
posPtr.previous()
return
}
replayOccurrences(observable, current.entries)
}
if (currentPos() != until) throw IllegalStateException()
if (currentPos() != futurePos) throw IllegalStateException()
}
override fun dropDescendantsWhile(ancestor: Chunk, dropIf: (Chunk) -> Boolean) {
@ -211,7 +197,8 @@ internal open class MatchJournalImpl(
if (posPtr.hasNext()) posPtr.next()
}
private fun resetOccurrences(occSpecs: Iterable<MatchJournal.Chunk.Entry>) =
private fun resetOccurrences(occSpecs: Iterable<Chunk.Entry>) =
// todo: need iterating over reversed list?
occSpecs.forEach {
if (it.discarded) {
it.occ.alive = true
@ -222,7 +209,7 @@ internal open class MatchJournalImpl(
}
}
private fun replayOccurrences(observable: LogicalStateObservable, occSpecs: Iterable<MatchJournal.Chunk.Entry>) =
private fun replayOccurrences(occSpecs: Iterable<Chunk.Entry>) =
occSpecs.forEach {
if (it.discarded) {
// it.occ.terminate(observable)

View File

@ -44,6 +44,9 @@ class TestStoreAwareJournal {
d = d.expand(occ)
}
fun logExpand(id: String, vararg args: Any) =
logExpand(occurrence(id, * args))
fun logFirstMatch() = hist.logMatch(d.matches().first())
// log and expand occurrence while tracking its justifications
@ -189,6 +192,182 @@ class TestStoreAwareJournal {
}
}
@Test
fun testReplayToSamePosTwice() {
val mockController = MockController()
with(programWithRules(
rule("rule1",
headKept(
pconstraint("foo")
),
body(
pconstraint("bar")
)),
rule("rule2",
headReplaced(
pconstraint("bar"),
pconstraint("foo")
),
body(
pconstraint("qux")
)),
rule("rule3",
headKept(
pconstraint("qux")
),
body(
pconstraint("lax")
))
))
{
with(JournalDispatcherHelper(Dispatcher(RuleIndex(rulesLists)))) {
val initPos = hist.currentPos()
logExpand(justifiedOccurrenceInit("foo"))
val fooPos = hist.currentPos()
logFirstMatch()
logExpandJustified("bar")
logFirstMatch()
logExpandJustified("qux")
val quxPos = hist.currentPos()
logFirstMatch()
logExpandJustified("lax")
val lastPos = hist.currentPos()
// 'replay' to the saved pos after full 'resetStore' must restore the store
with(hist) {
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("lax"))
resetStore()
replay(mockController.logicalStateObservable(), fooPos)
storeView().constraintSymbols() shouldBe setOf(sym0("foo"))
// second replay to the same position, nothing should change
replay(mockController.logicalStateObservable(), fooPos)
storeView().constraintSymbols() shouldBe setOf(sym0("foo"))
replay(mockController.logicalStateObservable(), quxPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"))
// second replay to the same position, nothing should change
replay(mockController.logicalStateObservable(), quxPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"))
}
}
}
}
@Test
@Ignore("Journal in current state doesn't track precise position inside Chunk")
fun testReplayInsideChunk() {
val mockController = MockController()
with(programWithRules(
rule("rule1",
headKept(
pconstraint("foo")
),
body(
pconstraint("bar")
)),
rule("rule2",
headReplaced(
pconstraint("bar"),
pconstraint("foo")
),
body(
constraint("lax"),
constraint("qux"),
constraint("shwux"),
constraint("kex"),
pconstraint("buzz")
)),
rule("rule3",
headKept(
constraint("qux")
),
headReplaced(
constraint("lax"),
constraint("shwux")
),
body(
))
))
{
with(JournalDispatcherHelper(Dispatcher(RuleIndex(rulesLists)))) {
val initPos = hist.currentPos()
logExpand(justifiedOccurrenceInit("foo"))
val fooPos = hist.currentPos()
logFirstMatch()
logExpandJustified("bar")
logFirstMatch()
logExpand("lax")
logExpand("qux")
val quxPos = hist.currentPos()
logExpand("shwux")
val shwuxPos = hist.currentPos()
logFirstMatch()
logExpand("kex")
val kexPos = hist.currentPos()
logExpandJustified("buzz")
val lastPos = hist.currentPos()
// 'replay' to the saved pos after full 'resetStore' must restore the store
with(hist) {
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("kex"), sym0("buzz"))
resetStore()
replay(mockController.logicalStateObservable(), quxPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("lax"))
// second replay to the same position inside chunk, nothing should change
replay(mockController.logicalStateObservable(), quxPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("lax"))
replay(mockController.logicalStateObservable(), shwuxPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("lax"), sym0("shwux"))
// replay inside chunk when non principal-match happenned
replay(mockController.logicalStateObservable(), kexPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("kex"))
replay(mockController.logicalStateObservable(), lastPos)
storeView().constraintSymbols() shouldBe setOf(sym0("qux"), sym0("kex"), sym0("buzz"))
}
}
}
}
@Test
fun testResetStoreThenReplay() {
@ -272,7 +451,7 @@ class TestStoreAwareJournal {
// execute program
hist.testPush()
logExpand(occurrence("foo"))
logExpand("foo")
logFirstMatch()
// provide initial justification
@ -334,8 +513,8 @@ class TestStoreAwareJournal {
// rule2
logFirstMatch()
logExpand(occurrence("bar"))
logExpand(occurrence("bazz"))
logExpand("bar")
logExpand("bazz")
// use this production later, but create it now to get relevant justs
val quxOcc = justifiedOccurrence("qux", hist.nextEvidence(), hist.justifications())
@ -343,7 +522,7 @@ class TestStoreAwareJournal {
val curChunk = hist.currentPos().chunk
// rule3
logFirstMatch()
logExpand(occurrence("bazz"))
logExpand("bazz")
// matched on rule with heads without justifications, should remain in the same chunk
hist.currentPos().chunk shouldBeSame curChunk
@ -451,7 +630,7 @@ class TestStoreAwareJournal {
// rule3, this rule match will remain in history untouched
hist.logMatch(rule1matches.last())
logExpand(occurrence("qux"))
logExpand("qux")
// execution ended
@ -489,7 +668,7 @@ class TestStoreAwareJournal {
d.matches().count() shouldBe 1
// rule2b, this rule match is added at the place of rule2a match
logFirstMatch()
logExpand(occurrence("marker"))
logExpand("marker")
// reexecution ended