Introduce IncrementalStage-s (MPSCR-71)

Incremental algorithm is stricter now in relation to
how it reads MatchJournal and operates on it.
Previously each incremental stage required full traversal.
Now all stages run on each chunk in a single traversal.

Separate journal traversal from incremental logic required for each Chunk.
This commit is contained in:
Grigorii Kirgizov 2020-08-11 15:26:14 +03:00
parent 19fa16c731
commit 1bcf80a9b2
8 changed files with 535 additions and 211 deletions

View File

@ -0,0 +1,25 @@
/*
* Copyright 2014-2020 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.logic.reactor.core.internal
interface ComparatorExt<T>: Comparator<T> {
infix fun T.before(other: T): Boolean = compare(this, other) < 0
infix fun T.beforeOrEq(other: T): Boolean = compare(this, other) <= 0
infix fun T.after(other: T): Boolean = compare(this, other) > 0
infix fun T.afterOrEq(other: T): Boolean = compare(this, other) >= 0
}

View File

@ -26,7 +26,6 @@ import jetbrains.mps.logic.reactor.program.Rule
import jetbrains.mps.logic.reactor.util.Profiler
import jetbrains.mps.logic.reactor.util.profile
import kotlin.collections.ArrayList
import kotlin.collections.HashSet
/**
@ -39,26 +38,73 @@ import kotlin.collections.HashSet
*
* @author Fedor Isakov
*/
internal class ConstraintsProcessing(
private var dispatchingFront: Dispatcher.DispatchingFront,
journal: MatchJournalImpl,
private val ruleIndex: RuleIndex,
val logicalState: LogicalState,
override val ispec: IncrementalSpec = IncrementalSpec.DefaultSpec,
val trace: EvaluationTrace = EvaluationTrace.NULL,
val profiler: Profiler? = null
) : StoreAwareJournalImpl(journal, logicalState), IncrSpecHolder {
internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.DispatchingFront,
journal: MatchJournalImpl,
private val ruleIndex: RuleIndex,
val logicalState: LogicalState,
override val ispec: IncrementalSpec = IncrementalSpec.DefaultSpec,
val trace: EvaluationTrace = EvaluationTrace.NULL,
val profiler: Profiler? = null)
: StoreAwareJournalImpl(journal, logicalState), IncrSpecHolder
{
private val journalIndex: MatchJournal.Index = journal.index()
private val activationQueue: ContinuedActivationQueue = ContinuedActivationQueue(journalIndex, RuleOrdering(ruleIndex))
private val occurrenceContractObserver: OccurrenceContractObserver? =
if (ispec.assertLevel().assertContracts()) OccurrenceContractObserver(logicalState, ispec) else null
private val invalidFeedbackKeys: MutableSet<Any> = mutableSetOf<Any>()
private data class MatchCandidate(val rule: Rule, val occChunk: MatchJournal.OccChunk)
inner class ProgramStateCleaner{
fun erase(occurrence: Occurrence) {
dispatchingFront = dispatchingFront.forget(occurrence)
occurrence.clearLogicalState(logicalState)
}
fun erase(match: RuleMatchEx) {
dispatchingFront = dispatchingFront.forget(match)
}
}
val stateCleaner = ProgramStateCleaner()
//TODO: move to MatchJournal
/**
* Preserves last valid [Chunk] while iterating on Chunks and removing them through [nextOrRemove].
*/
private class ChunkRemover(private val it: MutableIterator<MatchJournal.Chunk>): MatchJournal.ChunkReader {
private var _current_: MatchJournal.Chunk
private var _previous_: MatchJournal.Chunk
init {
assert(it.hasNext()) {"initial chunk must be always present"}
_current_ = it.next()
_previous_ = _current_
}
/**
* Removes next [Chunk] if [action] returns [true], else just advances.
* @return [true] if iterator advanced (i.e. next [Chunk] wasn't removed), [false] otherwise.
*/
inline fun nextOrRemove(action: (MatchJournal.ChunkReader) -> Boolean): Boolean {
val lastPrev = _previous_
_previous_ = _current_
_current_ = it.next()
val toRemove = action(this)
if (toRemove) {
it.remove()
// don't advance
_current_ = _previous_
_previous_ = lastPrev
}
return !toRemove
}
override val current: MatchJournal.Chunk get() = _current_
override val previous: MatchJournal.Chunk get() = _previous_
override val reachedEnd: Boolean get() = !it.hasNext()
}
fun activateContinue(controller: Controller, activeOcc: Occurrence, parent: MatchJournal.MatchChunk): FeedbackStatus {
@ -73,145 +119,73 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
return processActivated(controller, activeOcc, parent, FeedbackStatus.NORMAL())
}
/**
* Invalidation includes several activities:
* - removing chunks (i.e. principal matches) corresponding to
* removed rules and chunks depending on them from journal
* - reactivating occurrences that led to invalidated matches
* - pruning invalidated occurrences and matches from Dispatcher's state
*/
fun invalidateRuleMatches(ruleIds: Set<Any>) {
// Track only root invalidated entities for invalidation,
// relying on transitivity of Justified.justifiedBy relation.
val justificationRoots = mutableListOf<Justified>()
private lateinit var invalidator: InvalidationStage
private lateinit var adder: AdditionStage
private lateinit var postponer: PostponeMatchesStage
private lateinit var continuator: ContinueOccurrencesStage
val it = this.iterator()
var lastValidChunk = it.next() // skip initial chunk
fun invalidateAndAddRules(controller: Controller, rulesDiff: RulesDiff): FeedbackStatus {
val ruleOrdering = RuleOrdering(ruleIndex)
while (it.hasNext()) {
val chunk = it.next()
continuator = ContinueOccurrencesStage(ispec, journalIndex)
invalidator = InvalidationStage(ispec, rulesDiff.removed, continuator, stateCleaner, trace)
adder = AdditionStage(ispec, rulesDiff.added, continuator, ruleOrdering, ruleIndex, trace)
postponer = PostponeMatchesStage(ispec, this, journalIndex, ruleOrdering)
if (chunk is MatchJournal.MatchChunk && chunk.dependsOnAny(ruleIds)) {
justificationRoots.add(chunk)
}
val chunkReader = ChunkRemover(this.iterator())
// Invalidating dependent chunks
if (chunk.justifiedByAny(justificationRoots)) {
var status: FeedbackStatus = FeedbackStatus.NORMAL()
while (!chunkReader.reachedEnd) {
val advanced = chunkReader.nextOrRemove(invalidator::onNext)
if (!advanced) continue
// Remove chunk from the journal
it.remove()
adder.onNext(chunkReader)
val validOccs = invalidateChunk(chunk, justificationRoots)
postponer.onNext(chunkReader)
activationQueue.offerAll(lastValidChunk.toPos(), validOccs)
} else {
lastValidChunk = chunk
}
status = continuator.run(controller, chunkReader)
if (!status.operational) break
}
return status
}
private fun invalidateChunk(chunk: MatchJournal.Chunk, invalidJustifications: Collection<Justified>): Iterable<Occurrence> {
// 'Undo' all activated in this chunk occurrences: clear Dispatcher & LogicalState
chunk.activatedLog().forEach {
dispatchingFront = dispatchingFront.forget(it)
it.clearLogicalState(logicalState)
private fun ContinueOccurrencesStage.run(controller: Controller, chunkReader: MatchJournal.ChunkReader): FeedbackStatus {
var status: FeedbackStatus = FeedbackStatus.NORMAL()
val parentChunk = parentChunk()
for (continuedOcc in this.onNext(chunkReader)) {
if (continuedOcc.stored) {
status = activateContinue(controller, continuedOcc, parentChunk)
if (!status.operational) break
}
}
val validOccs: Sequence<Occurrence>
if (chunk is MatchJournal.MatchChunk) {
trace.invalidate(chunk.match)
invalidFeedbackKeys.add(chunk.match.feedbackKey)
dispatchingFront = dispatchingFront.forget(chunk.match as RuleMatchEx)
// Valid head occurrences could match more rules
// without this match, so need to reactivate them.
// E.g. occurrences discarded in this match on
// previous run but revived here can match more rules.
validOccs = chunk.match.allHeads().filter { occ ->
!occ.justifiedByAny(invalidJustifications)
}
// By definition of Chunk and principal rule,
// all occurrences from the head are principal.
assert(chunk.match.allHeads().all { it.isPrincipal })
} else validOccs = emptySequence()
return validOccs.asIterable()
return status
}
fun addRuleMatches(rules: Iterable<Rule>) {
val activationCandidates = mutableListOf<MatchCandidate>()
val it = this.iterator()
var chunk = it.next() // initial chunk
var prevChunk = chunk
while (true) {
if (chunk is MatchJournal.OccChunk) {
// filters out rules using occurrence's arguments
val allRuleCandidates = ruleIndex.forOccurrence(chunk.occ).map { it.uniqueTag() }.toHashSet()
for (rule in rules) {
if (allRuleCandidates.contains(rule.uniqueTag()) && rule.canMatch(chunk.occ.constraint)) {
// Can this rule be matched by principal occurrence?
// Then we will need to find the place among existing child chunks
// (i.e. among some number of following ones)
// to activate this occurrence, to (possibly) match this rule.
// Also remember the parent justification of this rule candidate
// to drop it from monitoring when child chunks end.
activationCandidates.add(MatchCandidate(rule, chunk))
// todo: also use the rule to help Dispatcher in future?
// i.e. try matching only on the candidate rule
}
}
}
val aIt = activationCandidates.listIterator()
while (aIt.hasNext()) {
val (candRule, occChunk) = aIt.next()
val pos =
if (activationQueue.canBeInserted(candRule, occChunk, chunk))
// We need the previous chunk as pos here (i.e. adding after it).
prevChunk.toPos()
else if (!it.hasNext())
// Case when adding at the very end
chunk.toPos()
else
continue
activationQueue.offer(pos, occChunk)
trace.potentialMatch(occChunk.occ, candRule)
// Drop the candidate if appropriate activation place is found.
aIt.remove()
}
if (!it.hasNext()) break
prevChunk = chunk
chunk = it.next()
}
}
fun launchQueue(controller: Controller): FeedbackStatus =
activationQueue.run(controller, this)
/**
* Clears state unneeded between incremental sessions
* and returns [SessionToken] with session results.
*/
fun endSession(): SessionToken {
invalidFeedbackKeys.clear()
val histView = view()
resetStore() // clear observers
// todo: need clearing occurrenceContractObservers? (MPSCR-66)
val rules = ArrayList<Rule>().apply { ruleIndex.forEach { add(it) } }
// preserve only relevant and non-empty RuleMatchers
val principalState = dispatchingFront.state().filterValues { ruleMatcher ->
ruleMatcher.rule().isPrincipal || ruleMatcher.probe().hasOccurrences()
}
val principalState = dispatchingFront.sessionState()
return SessionTokenImpl(histView, rules, principalState, logicalState.clear(), ruleIndex)
}
fun invalidatedFeedback(): FeedbackKeySet = HashSet<Any>(invalidFeedbackKeys)
/**
* Preserve data needed between sessions:
* preserve only relevant and non-empty RuleMatchers
*/
private fun Dispatcher.DispatchingFront.sessionState() =
this.state().filterValues { ruleMatcher ->
ruleMatcher.rule().isPrincipal || ruleMatcher.probe().hasOccurrences()
}
fun invalidatedFeedback(): FeedbackKeySet = invalidator.invalidatedFeedback()
/**
* Called to update the state with the currently active constraint occurrence.
@ -237,12 +211,7 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
}
val matches = dispatchingFront.matches().toList()
val currentMatches =
if (isFront() || !active.isPrincipal) {
matches
} else {
activationQueue.postponeFutureMatches(matches)
}
val currentMatches = postponeFutureMatches(active, matches)
val outStatus = currentMatches.fold(inStatus) { status, match ->
// TODO: paranoid check. should be isAlive() instead
@ -261,6 +230,7 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
return outStatus
}
@Deprecated("obsolete machinery, superseded by MPSCR-65")
private fun dropDiscardingMatchesFor(ancestor: MatchJournal.OccChunk, droppedRuleTags: MutableSet<Any>) =
this.dropDescendantsWhile(ancestor) { chunk ->
@ -296,21 +266,34 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
.also { trace.finish(match) }
private fun continueReplacedHeads(match: RuleMatchEx, parent: MatchJournal.MatchChunk) {
if (!isFront() && match.isPrincipal) {
profiler.profile("continueReplaced") {
val invalidJustifications = match.matchHeadReplaced().filter { it.isPrincipal }
if (invalidJustifications.isNotEmpty()) {
dropDescendants(invalidJustifications) { lastValidChunk, currentChunk ->
val validOccs = invalidateChunk(currentChunk, invalidJustifications)
activationQueue.offerAll(lastValidChunk.toPos(), validOccs)
}
}
}
if (requiresIncrementalProcessing(match)) {
val invalidJustifications = match.matchHeadReplaced().filter { it.isPrincipal }
invalidator.receive(invalidJustifications)
}
}
// private fun continueReplacedHeads(match: RuleMatchEx, parent: MatchJournal.MatchChunk) {
// if (!isFront() && match.isPrincipal) {
// profiler.profile("continueReplaced") {
//
// val invalidJustifications = match.matchHeadReplaced().filter { it.isPrincipal }
// if (invalidJustifications.isNotEmpty()) {
//
// dropDescendants(invalidJustifications) { lastValidChunk, currentChunk ->
// val validOccs = invalidator.invalidateChunk(currentChunk, invalidJustifications)
// activationQueue.offerAll(lastValidChunk.toPos(), validOccs)
// }
//
// }
// }
// }
// }
private fun postponeFutureMatches(active: Occurrence, matches: List<RuleMatchEx>) =
if (requiresIncrementalProcessing(active)) {
postponer.process(active, matches)
// activationQueue.postponeFutureMatches(matches)
} else matches
private fun accept(controller: Controller, match: RuleMatchEx) {
profiler.profile("logMatch") {
@ -348,7 +331,7 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
}
private fun Occurrence.clearLogicalState(observable: LogicalStateObservable) {
terminate(logicalState)
terminate(observable)
if (occurrenceContractObserver != null && this.isPrincipal) {
occurrenceContractObserver.onDiscarded(this)
}
@ -386,6 +369,10 @@ internal class ConstraintsProcessing(private var dispatchingFront: Dispatcher.Di
}
}
private fun requiresIncrementalProcessing(match: RuleMatchEx) = ispec.ability().allowed() && match.isPrincipal
private fun requiresIncrementalProcessing(occ: Occurrence) = ispec.ability().allowed() && occ.isPrincipal
private fun MatchJournal.MatchChunk.dependsOnAny(utags: Iterable<Any>): Boolean =
utags.contains(this.ruleUniqueTag) || utags.any { utag -> dependsOnRule(utag) }

View File

@ -17,13 +17,18 @@
package jetbrains.mps.logic.reactor.core.internal
import jetbrains.mps.logic.reactor.core.*
import jetbrains.mps.logic.reactor.program.Rule
import java.util.*
internal interface ContinuedActivationSink {
fun offerAll(continueFromPos: MatchJournal.Pos, occs: Iterable<Occurrence>)
fun offer(continueFromPos: MatchJournal.Pos, ancestor: MatchJournal.OccChunk): Boolean
}
internal class ContinuedActivationQueue(
private val journalIndex: MatchJournal.Index,
private val ruleOrdering: RuleOrdering
) {
private val journalIndex: MatchJournal.Index
): ContinuedActivationSink {
/**
* Specifies position in [MatchJournal] for continuing program evaluation.
@ -96,8 +101,7 @@ internal class ContinuedActivationQueue(
val occChunk = journalIndex.activationPos(m)
val pos = occChunk?.toPos()
// if it is a future match
if (pos != null && journalIndex.compare(lastIncrementalRootPos, pos) < 0) {
if (pos != null && isFuturePos(pos)) {
offer(pos, occChunk)
} else {
currentMatches.add(m)
@ -106,35 +110,17 @@ internal class ContinuedActivationQueue(
return currentMatches
}
/**
* Checks whether [candidateRule] can be inserted in journal as a child
* of [parentChunk] before one of its child chunks, [beforeChunk].
* It is assumed that [candidateRule] can be matched by [Occurrence] from [parentChunk].
*/
fun canBeInserted(candidateRule: Rule, parentChunk: MatchJournal.OccChunk, beforeChunk: MatchJournal.Chunk): Boolean {
// Place to try activating candidate rule is:
// either according to the ordering between rules
// or as the last one, after all existing activations
assert(candidateRule.canMatch(parentChunk.occ.constraint))
private fun isFuturePos(pos: MatchJournal.Pos): Boolean =
with(journalIndex) { pos after lastIncrementalRootPos }
val placeToInsertFound = beforeChunk is MatchJournal.MatchChunk
&& ruleOrdering.isEarlierThan(candidateRule, beforeChunk.match.rule())
val isDescendant = beforeChunk.justifiedBy(parentChunk)
val isSibling = beforeChunk.evidence == parentChunk.evidence && beforeChunk != parentChunk
val childChunksEnded = !isDescendant || isSibling
return (childChunksEnded || placeToInsertFound)
}
fun offerAll(continueFromPos: MatchJournal.Pos, occs: Iterable<Occurrence>) =
override fun offerAll(continueFromPos: MatchJournal.Pos, occs: Iterable<Occurrence>) =
occs.forEach {
journalIndex.activatingChunkOf(it)?.let { occChunk ->
offer(continueFromPos, occChunk)
}
}
fun offer(continueFromPos: MatchJournal.Pos, ancestor: MatchJournal.OccChunk): Boolean =
override fun offer(continueFromPos: MatchJournal.Pos, ancestor: MatchJournal.OccChunk) =
ExecPos(continueFromPos, ancestor).let {
it.assertValid()
if (seen.add(it)) execQueue.offer(it) else false

View File

@ -56,23 +56,10 @@ internal class ControllerImpl (
fun incrLaunch(constraint: Constraint, rulesDiff: RulesDiff): Pair<FeedbackStatus, FeedbackKeySet> {
if (rulesDiff.removed.isNotEmpty()) {
profiler.profile("invalidation") {
processing.invalidateRuleMatches(rulesDiff.removed)
}
val status = profiler.profile<FeedbackStatus>("journalTraverse") {
processing.invalidateAndAddRules(this, rulesDiff)
}
if (rulesDiff.added.count() > 0) {
profiler.profile("adding_matches") {
processing.addRuleMatches(rulesDiff.added)
}
}
val status =
profiler.profile<FeedbackStatus>("reexecution") {
processing.launchQueue(this)
}
return status to processing.invalidatedFeedback()
}

View File

@ -0,0 +1,303 @@
/*
* Copyright 2014-2020 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.logic.reactor.core.internal
import jetbrains.mps.logic.reactor.core.*
import jetbrains.mps.logic.reactor.core.feedbackKey
import jetbrains.mps.logic.reactor.evaluation.EvaluationTrace
import jetbrains.mps.logic.reactor.program.IncrementalSpec
import jetbrains.mps.logic.reactor.program.Rule
internal interface IncrementalStage: IncrSpecHolder {
// fun onNext(reader: MatchJournal.ChunkReader)
}
typealias RuleTagSet = Set<Any>
//typealias ContinuedActivations = Collection<Occurrence>
/**
* Invalidation includes several activities:
* - removing chunks (i.e. principal matches) corresponding to
* removed rules and chunks depending on them from journal
* - reactivating occurrences that led to invalidated matches
* - pruning invalidated occurrences and matches from Dispatcher's state
*/
internal class InvalidationStage(
override val ispec: IncrementalSpec,
// private val invalidRules: RuleTagSet,
private val invalidRuleIds: Set<Any>,
private val activationSink: ContinuedActivationSink,
private val stateCleaner: ConstraintsProcessing.ProgramStateCleaner,
private val trace: EvaluationTrace
): IncrementalStage {
private val invalidJustifications = mutableListOf<Justified>()
private val invalidFeedbackKeys: MutableSet<Any> = mutableSetOf<Any>()
fun invalidatedFeedback(): FeedbackKeySet = HashSet<Any>(invalidFeedbackKeys)
fun receive(invalid: Iterable<Justified>) { invalidJustifications.addAll(invalid) }
fun onNext(reader: MatchJournal.ChunkReader): Boolean {
val chunk = reader.current
if (chunk is MatchJournal.MatchChunk && chunk.dependsOnAny(invalidRuleIds)) {
invalidJustifications.add(chunk)
}
// Invalidating dependent chunks
if (chunk.justifiedByAny(invalidJustifications)) {
val validOccs = invalidateChunk(chunk, invalidJustifications)
activationSink.offerAll(reader.previous.toPos(), validOccs)
// Remove chunk from the journal
return true
}
return false
}
// todo: make private
fun invalidateChunk(chunk: MatchJournal.Chunk, invalidJustifications: Collection<Justified>): Iterable<Occurrence> {
// fun invalidateChunk(chunk: MatchJournal.Chunk): Iterable<Occurrence> {
// 'Undo' all activated in this chunk occurrences: clear Dispatcher & LogicalState
chunk.activatedLog().forEach(stateCleaner::erase)
val validOccs: Sequence<Occurrence>
if (chunk is MatchJournal.MatchChunk) {
trace.invalidate(chunk.match)
invalidFeedbackKeys.add(chunk.match.feedbackKey)
stateCleaner.erase(chunk.match as RuleMatchEx)
// Valid head occurrences could match more rules
// without this match, so need to reactivate them.
// E.g. occurrences discarded in this match on
// previous run but revived here can match more rules.
validOccs = chunk.match.allHeads().filter { occ ->
!occ.justifiedByAny(invalidJustifications)
}
// By definition of Chunk and principal rule,
// all occurrences from the head are principal.
assert(chunk.match.allHeads().all { it.isPrincipal })
} else validOccs = emptySequence()
return validOccs.asIterable()
}
private fun MatchJournal.MatchChunk.dependsOnAny(utags: Iterable<Any>): Boolean =
utags.contains(this.ruleUniqueTag) || utags.any { utag -> dependsOnRule(utag) }
}
internal class AdditionStage(
override val ispec: IncrementalSpec,
private val addedRules: Iterable<Rule>,
private val activationSink: ContinuedActivationSink,
private val ruleOrdering: RuleOrdering,
private val ruleIndex: RuleIndex,
private val trace: EvaluationTrace
): IncrementalStage {
private data class MatchCandidate(val rule: Rule, val occChunk: MatchJournal.OccChunk)
private val activationCandidates = mutableListOf<MatchCandidate>()
private val isEmpty = !addedRules.iterator().hasNext()
fun onNext(reader: MatchJournal.ChunkReader) {
if (isEmpty) return
val chunk = reader.current
if (chunk is MatchJournal.OccChunk) {
addRuleCandidates(chunk)
}
offerCandidates(reader)
}
private fun addRuleCandidates(chunk: MatchJournal.OccChunk) {
// filters out rules using occurrence's arguments
val allRuleCandidates = ruleIndex.forOccurrence(chunk.occ).map { it.uniqueTag() }.toHashSet()
for (rule in addedRules) {
if (allRuleCandidates.contains(rule.uniqueTag()) && rule.canMatch(chunk.occ.constraint)) {
// Can this rule be matched by principal occurrence?
// Then we will need to find the place among existing child chunks
// (i.e. among some number of following ones)
// to activate this occurrence, to (possibly) match this rule.
// Also remember the parent justification of this rule candidate
// to drop it from monitoring when child chunks end.
activationCandidates.add(MatchCandidate(rule, chunk))
// todo: also use the rule to help Dispatcher in future?
// i.e. try matching only on the candidate rule
}
}
}
private fun offerCandidates(reader: MatchJournal.ChunkReader) {
val aIt = activationCandidates.listIterator()
while (aIt.hasNext()) {
val (candRule, occChunk) = aIt.next()
val pos =
if (ruleOrdering.canBeInserted(candRule, occChunk, reader.current))
// We need the previous chunk as pos here (i.e. adding after it).
reader.previous.toPos()
else if (reader.reachedEnd)
// Case when adding at the very end
reader.current.toPos()
else
continue
activationSink.offer(pos, occChunk)
trace.potentialMatch(occChunk.occ, candRule)
// Drop the candidate if appropriate activation place is found.
aIt.remove()
}
}
}
internal class PostponeMatchesStage(
override val ispec: IncrementalSpec,
journal: MatchJournal, // todo: remove this dependency, needed only for initial chunk
private val journalIndex: MatchJournal.Index,
private val ruleOrdering: RuleOrdering
): IncrementalStage {
private var lastIncrementalRootPos: MatchJournal.Pos = journal.initialChunk().toPos()
private val postponedMatches: MutableMap<Int, MutableCollection<RuleMatchEx>> = hashMapOf()
fun onNext(reader: MatchJournal.ChunkReader) =
updateBasisPos(reader.current)
fun process(active: Occurrence, matches: List<RuleMatchEx>): List<RuleMatchEx> =
postponeFutureMatches(matches).withPostponedMatches(active)
private fun updateBasisPos(current: MatchJournal.Chunk) {
assert(journalIndex.isKnown(current)) {
"expected known position (from previous incremental session)"
}
lastIncrementalRootPos = current.toPos()
}
/**
* Adds previously postponed matches on [active] [Occurrence] if any, or returns original list.
*/
private fun List<RuleMatchEx>.withPostponedMatches(active: Occurrence): List<RuleMatchEx> =
postponedMatches.remove(active.identity)?.let { postponed ->
// Sort matches according to rule priorities
(this + postponed).sortedWith(ruleOrdering.matchComparator)
} ?: this
/**
* Determines, filters out, and postpones future matches. Returns only current matches.
*/
private fun postponeFutureMatches(matches: List<RuleMatchEx>): List<RuleMatchEx> {
val currentMatches = mutableListOf<RuleMatchEx>()
for (m in matches) {
// Returns null for matches with occurrences only from this session
// because journalIndex indexes only previous session.
val occChunk = journalIndex.activationPos(m)
val pos = occChunk?.toPos()
if (pos != null && isFuturePos(pos)) {
postponedMatches.putIfAbsent(occChunk.identity, mutableListOf())!!.add(m)
} else {
currentMatches.add(m)
}
}
return currentMatches
}
private fun isFuturePos(pos: MatchJournal.Pos): Boolean =
with(journalIndex) { pos after lastIncrementalRootPos }
private val MatchJournal.OccChunk.identity get() = this.occ.identity
}
internal class ContinueOccurrencesStage(
override val ispec: IncrementalSpec,
private val journalIndex: MatchJournal.Index
): IncrementalStage, ContinuedActivationSink {
/**
* Specifies position in [MatchJournal] for continuing program evaluation.
* Position of [reactivated] in journal must precede execution position [continueFrom].
* (see contract in [assertValid]).
*/
private data class ExecPos(val continueFrom: MatchJournal.Pos, val reactivated: MatchJournal.OccChunk) {
val reactivatedOcc: Occurrence get() = reactivated.occ
}
private infix fun MatchJournal.ChunkReader.isAt(execPos: ExecPos) =
this.current == execPos.continueFrom.chunk
private fun ExecPos.assertValid() {
val isAncestor = continueFrom.chunk.justifiedBy(reactivated)
val isPredecessor = journalIndex.compare(continueFrom, reactivated.toPos()) >= 0
// todo: ensure it must be so
val fromPreviousSession = journalIndex.isKnown(reactivated)
assert(fromPreviousSession && (isAncestor || isPredecessor))
}
private val queue: MutableList<ExecPos> = ArrayList<ExecPos>(1 + journalIndex.size / 8) // rough estimate
private val seen: MutableSet<ExecPos> = HashSet<ExecPos>()
fun onNext(reader: MatchJournal.ChunkReader): Collection<Occurrence> {
val continued = mutableListOf<Occurrence>()
if (queue.isEmpty()) return continued
while (reader isAt queue.top()) {
continued.add(queue.pop().reactivatedOcc)
}
return continued
}
// todo: check sorting is correct
override fun offerAll(continueFromPos: MatchJournal.Pos, occs: Iterable<Occurrence>) =
occs.mapNotNull(journalIndex::activatingChunkOf)
.sortedWith(journalIndex.chunkComparator)
.forEach { offer(continueFromPos, it) }
override fun offer(continueFromPos: MatchJournal.Pos, ancestor: MatchJournal.OccChunk): Boolean =
ExecPos(continueFromPos, ancestor).let {
it.assertValid()
if (seen.add(it)) queue.push(it) else false
}
private fun <E> MutableList<E>.top(): E = this.last()
private fun <E> MutableList<E>.pop(): E = this.removeAt(this.size - 1)
private fun <E> MutableList<E>.push(element: E): Boolean = this.add(element)
}

View File

@ -21,6 +21,7 @@ import jetbrains.mps.logic.reactor.evaluation.*
import jetbrains.mps.logic.reactor.program.*
import jetbrains.mps.logic.reactor.util.Id
import java.util.*
import kotlin.Comparator
interface MatchJournal : MutableIterable<MatchJournal.Chunk>, EvidenceSource {
@ -118,11 +119,22 @@ interface MatchJournal : MutableIterable<MatchJournal.Chunk>, EvidenceSource {
fun index(): Index
interface ChunkReader {
val current: Chunk get
val previous: Chunk get
val reachedEnd: Boolean get
}
/**
* Simplifies some search operations on [MatchJournal].
* Also serves as a comparator for positions: later in journal means greater.
*/
interface Index : Comparator<Pos> {
interface Index : ComparatorExt<Pos> {
val chunkComparator: Comparator<Chunk> get
fun isKnown(chunk: Chunk): Boolean
/**
* Returns [Chunk] where provided principal occurrence was activated.
* Returns null for non-principal occurrences.
@ -141,10 +153,6 @@ interface MatchJournal : MutableIterable<MatchJournal.Chunk>, EvidenceSource {
*/
val size: Int
// some context functions
infix fun Pos.before(other: Pos): Boolean = compare(this, other) <= 0
infix fun Pos.after(other: Pos): Boolean = compare(this, other) >= 0
}
/**

View File

@ -415,6 +415,8 @@ internal open class MatchJournalImpl(
override val size: Int = chunks.count()
override fun isKnown(chunk: Chunk): Boolean = chunkOrder.containsKey(Id(chunk))
override fun activatingChunkOf(occ: Occurrence) = occChunks[occ.identity]
override fun activationPos(match: RuleMatchEx): OccChunk? =
@ -424,13 +426,19 @@ internal open class MatchJournalImpl(
occSig?.let { activatingChunkOf(it.wrapped) }
}.maxBy { orderOf(it)!! } // compare positions: find latest
// todo: throw for invalid positions?
override val chunkComparator: Comparator<Chunk> get() = compareBy<Chunk>(this::orderOfThrow)
override fun compare(lhs: Pos, rhs: Pos): Int =
compareBy<Pos>{ orderOf(it.chunk) }
compareBy<Pos>{ orderOfThrow(it.chunk) }
.thenComparingInt { it.entriesCount }
.compare(lhs, rhs)
private fun orderOf(chunk: Chunk): Int? = chunkOrder[Id(chunk)]
private fun orderOfThrow(chunk: Chunk): Int = when (val ord = orderOf(chunk)) {
null -> throw IllegalStateException("Chunk (${chunk.evidence}) must be known by journal index!")
else -> ord
}
}

View File

@ -16,10 +16,13 @@
package jetbrains.mps.logic.reactor.core.internal
import jetbrains.mps.logic.reactor.core.Occurrence
import jetbrains.mps.logic.reactor.core.canMatch
import jetbrains.mps.logic.reactor.evaluation.RuleMatch
import jetbrains.mps.logic.reactor.program.Rule
internal class RuleOrdering(order: Iterable<Rule>): Comparator<Rule> {
internal class RuleOrdering(order: Iterable<Rule>): ComparatorExt<Rule> {
private val ruleOrder: Map<Any, Int> = HashMap<Any, Int>().apply {
put(MatchJournalImpl.InitRuleMatch.rule().uniqueTag(), -1) // less than anything
@ -28,20 +31,37 @@ internal class RuleOrdering(order: Iterable<Rule>): Comparator<Rule> {
val ruleTags: Set<Any> = order.map { it.uniqueTag() }.toHashSet() // NB: without initial rule
fun orderOf(rule: Rule): Int? = ruleOrder[rule.uniqueTag()]
override fun compare(lhs: Rule, rhs: Rule): Int = compareBy<Rule>(this::orderOfThrow).compare(lhs, rhs)
override fun compare(lhs: Rule, rhs: Rule): Int {
val lhsRuleOrder = orderOf(lhs)
val rhsRuleOrder = orderOf(rhs)
val matchComparator: Comparator<RuleMatch> get() = compareBy<RuleMatch>{ orderOfThrow(it.rule()) }
if (lhsRuleOrder == null || rhsRuleOrder == null) {
throw(IllegalStateException("Rules compared must be present in the rule index!"))
}
return lhsRuleOrder.compareTo(rhsRuleOrder)
private fun orderOf(rule: Rule): Int? = ruleOrder[rule.uniqueTag()]
private fun orderOfThrow(rule: Rule): Int = when (val res = orderOf(rule)) {
null -> throw IllegalStateException("Compared rule ($rule) must be present in rule index!")
else -> res
}
}
fun isEarlierThan(lhs: Rule, rhs: Rule): Boolean = compare(lhs, rhs) < 0
fun isLaterThan(lhs: Rule, rhs: Rule): Boolean = compare(lhs, rhs) > 0
/**
* Checks whether [candidateRule] can be inserted in journal as a child
* of [parentChunk] before one of its child chunks, [beforeChunk].
* It is assumed that [candidateRule] can be matched by [Occurrence] from [parentChunk].
*/
internal fun RuleOrdering.canBeInserted(candidateRule: Rule, parentChunk: MatchJournal.OccChunk, beforeChunk: MatchJournal.Chunk): Boolean {
// Place to try activating candidate rule is:
// either according to the ordering between rules
// or as the last one, after all existing activations
assert(candidateRule.canMatch(parentChunk.occ.constraint))
}
val placeToInsertFound = beforeChunk is MatchJournal.MatchChunk
&& candidateRule before beforeChunk.match.rule()
val isDescendant = beforeChunk.justifiedBy(parentChunk)
val isSibling = beforeChunk.evidence == parentChunk.evidence && beforeChunk != parentChunk
val childChunksEnded = !isDescendant || isSibling
return (childChunksEnded || placeToInsertFound)
}