diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ComparatorExt.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ComparatorExt.kt new file mode 100644 index 00000000..dfd7fc6e --- /dev/null +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ComparatorExt.kt @@ -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: Comparator { + 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 +} \ No newline at end of file diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ConstraintsProcessing.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ConstraintsProcessing.kt index 7e4eda36..d9df4193 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ConstraintsProcessing.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ConstraintsProcessing.kt @@ -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 = mutableSetOf() - 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.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) { - // Track only root invalidated entities for invalidation, - // relying on transitivity of Justified.justifiedBy relation. - val justificationRoots = mutableListOf() + 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): Iterable { - // '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 - 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) { - - val activationCandidates = mutableListOf() - 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().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(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) = 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) = + 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): Boolean = utags.contains(this.ruleUniqueTag) || utags.any { utag -> dependsOnRule(utag) } diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ContinuedActivationQueue.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ContinuedActivationQueue.kt index b7c9ef82..b2502554 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ContinuedActivationQueue.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ContinuedActivationQueue.kt @@ -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) + + 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) = + override fun offerAll(continueFromPos: MatchJournal.Pos, occs: Iterable) = 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 diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ControllerImpl.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ControllerImpl.kt index 4d9d10f6..b5d63c11 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ControllerImpl.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/ControllerImpl.kt @@ -56,23 +56,10 @@ internal class ControllerImpl ( fun incrLaunch(constraint: Constraint, rulesDiff: RulesDiff): Pair { - if (rulesDiff.removed.isNotEmpty()) { - profiler.profile("invalidation") { - processing.invalidateRuleMatches(rulesDiff.removed) - } + val status = profiler.profile("journalTraverse") { + processing.invalidateAndAddRules(this, rulesDiff) } - if (rulesDiff.added.count() > 0) { - profiler.profile("adding_matches") { - processing.addRuleMatches(rulesDiff.added) - } - } - - val status = - profiler.profile("reexecution") { - processing.launchQueue(this) - } - return status to processing.invalidatedFeedback() } diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/IncrementalStage.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/IncrementalStage.kt new file mode 100644 index 00000000..9d025511 --- /dev/null +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/IncrementalStage.kt @@ -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 +//typealias ContinuedActivations = Collection + + +/** + * 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, + private val activationSink: ContinuedActivationSink, + private val stateCleaner: ConstraintsProcessing.ProgramStateCleaner, + private val trace: EvaluationTrace +): IncrementalStage { + + private val invalidJustifications = mutableListOf() + + private val invalidFeedbackKeys: MutableSet = mutableSetOf() + + fun invalidatedFeedback(): FeedbackKeySet = HashSet(invalidFeedbackKeys) + + + fun receive(invalid: Iterable) { 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): Iterable { +// fun invalidateChunk(chunk: MatchJournal.Chunk): Iterable { + // 'Undo' all activated in this chunk occurrences: clear Dispatcher & LogicalState + chunk.activatedLog().forEach(stateCleaner::erase) + + val validOccs: Sequence + 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): Boolean = + utags.contains(this.ruleUniqueTag) || utags.any { utag -> dependsOnRule(utag) } + +} + + +internal class AdditionStage( + override val ispec: IncrementalSpec, + private val addedRules: Iterable, + 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() + + 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> = hashMapOf() + + + fun onNext(reader: MatchJournal.ChunkReader) = + updateBasisPos(reader.current) + + fun process(active: Occurrence, matches: List): List = + 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.withPostponedMatches(active: Occurrence): List = + 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): List { + val currentMatches = mutableListOf() + 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 = ArrayList(1 + journalIndex.size / 8) // rough estimate + + private val seen: MutableSet = HashSet() + + + fun onNext(reader: MatchJournal.ChunkReader): Collection { + val continued = mutableListOf() + 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) = + 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 MutableList.top(): E = this.last() + private fun MutableList.pop(): E = this.removeAt(this.size - 1) + private fun MutableList.push(element: E): Boolean = this.add(element) +} \ No newline at end of file diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournal.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournal.kt index 993f1ef2..d62e6ae8 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournal.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/MatchJournal.kt @@ -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, EvidenceSource { @@ -118,11 +119,22 @@ interface MatchJournal : MutableIterable, 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 { + interface Index : ComparatorExt { + + val chunkComparator: Comparator 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, 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 } /** 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 4eb72599..6ebfbd45 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 @@ -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 get() = compareBy(this::orderOfThrow) + override fun compare(lhs: Pos, rhs: Pos): Int = - compareBy{ orderOf(it.chunk) } + compareBy{ 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 + } } diff --git a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/RuleOrdering.kt b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/RuleOrdering.kt index 8515f6dd..5ee2cf60 100644 --- a/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/RuleOrdering.kt +++ b/reactor/Core/src/jetbrains/mps/logic/reactor/core/internal/RuleOrdering.kt @@ -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): Comparator { +internal class RuleOrdering(order: Iterable): ComparatorExt { private val ruleOrder: Map = HashMap().apply { put(MatchJournalImpl.InitRuleMatch.rule().uniqueTag(), -1) // less than anything @@ -28,20 +31,37 @@ internal class RuleOrdering(order: Iterable): Comparator { val ruleTags: Set = 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(this::orderOfThrow).compare(lhs, rhs) - override fun compare(lhs: Rule, rhs: Rule): Int { - val lhsRuleOrder = orderOf(lhs) - val rhsRuleOrder = orderOf(rhs) + val matchComparator: Comparator get() = compareBy{ 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)) -} \ No newline at end of file + 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) +}