Coverage Summary for Class: StateRuleSelection (it.unibo.tuprolog.argumentation.core.engine.fsm)

Class Method, % Branch, % Line, % Instruction, %
StateRuleSelection 81.2% (13/16) 70.4% (38/54) 73.3% (63/86) 76.7% (468/610)
StateRuleSelection$Companion
StateRuleSelection$Companion$CutLimit 100% (2/2) 100% (2/2) 100% (3/3)
StateRuleSelection$Companion$CutLimit$Actual 100% (1/1) 100% (3/3) 100% (13/13)
StateRuleSelection$Companion$CutLimit$None 0% (0/4) 0% (0/4) 0% (0/8)
Total 69.6% (16/23) 70.4% (38/54) 71.6% (68/95) 76.3% (484/634)


 // Forked from 2p-kt's solve-prolog 3.0.2 (Apache License 2.0): see README.md in this package
 package it.unibo.tuprolog.argumentation.core.engine.fsm
 
 import it.unibo.tuprolog.argumentation.core.engine.ChoicePointContext
 import it.unibo.tuprolog.argumentation.core.engine.PrologExecutionContext
 import it.unibo.tuprolog.argumentation.core.engine.stdlib.rule.Catch
 import it.unibo.tuprolog.core.Clause
 import it.unibo.tuprolog.core.Rule
 import it.unibo.tuprolog.core.Struct
 import it.unibo.tuprolog.core.Term
 import it.unibo.tuprolog.solve.Signature
 import it.unibo.tuprolog.solve.exception.ResolutionException
 import it.unibo.tuprolog.solve.exception.error.ExistenceError
 import it.unibo.tuprolog.solve.exception.warning.MissingPredicate
 import it.unibo.tuprolog.solve.extractSignature
 import it.unibo.tuprolog.solve.flags.LastCallOptimization
 import it.unibo.tuprolog.solve.flags.LastCallOptimization.ON
 import it.unibo.tuprolog.solve.flags.Unknown
 import it.unibo.tuprolog.solve.stdlib.magic.MagicCut
 import it.unibo.tuprolog.theory.Theory
 import it.unibo.tuprolog.utils.Cursor
 
 /**
  * "Rule Selection", the clause-resolution counterpart of `StatePrimitiveSelection`, and where several ISO
  * special cases live: `true`/`!` succeed without touching the knowledge base at all (`!` additionally triggers
  * cut, see below); `fail`/`false`, and goals that don't exist anywhere, move to `StateBacktracking` or
  * `StateException` depending on the `Unknown` flag (`error`/`fail`/`warning`); the general case queries the
  * static/dynamic knowledge bases and the libraries' own theories, resolves the goal with the matching clauses (see
  * `ClauseResolver`: renamed apart, or as stored with the unificator of arg2p), and moves to `StateRuleExecution`
  * with the resolutions as a new choice point.
  *
  * The formal paper describes cut abstractly as "prune the choice-point queue up to the parent goal's choice
  * point"; [computeCutLimit]/[performCut] implement something considerably more careful, because ISO cut
  * semantics are subtler than that:
  * - Cut is **transparent** through conjunction, disjunction and if-then (the [transparentToCut] signatures) --
  *   a cut inside a clause body's conjunction cuts choice points belonging to the *clause*, not to some
  *   imaginary choice point for the comma itself; [computeCutLimit] walks up through these control constructs to
  *   find the real enclosing procedure.
  * - Cut interacts with last-call optimization ([isTailRecursive]): a tail call to the same predicate reuses
  *   the current execution-context frame ([replaceWithChildAppendingRulesAndChoicePoints]) rather than growing
  *   the stack, an optimisation the formal model does not need to talk about since it treats the stack as
  *   unbounded.
  * - A `MagicCut` marker exists for cuts injected by other built-ins (e.g. `once/1`, if-then-else) that must cut
  *   back to the *caller's* choice point rather than the lexically enclosing clause, since those built-ins are
  *   themselves implemented as ordinary rules over this same state machine (see `it.unibo.tuprolog.argumentation.core.engine.stdlib.rule`).
  *
  * None of this changes the shape of the FSM: cut is still handled here, still ends up at `StateGoalSelection`,
  * still only manipulates the choice-point queue.
  */
 data class StateRuleSelection(
     override val context: PrologExecutionContext,
     // arg2p: the current goal (callable) with the substitution applied, as primitive selection computed it
     val goal: Struct,
 ) : AbstractState(context) {
     companion object {
         private val catchSignature = Catch.signature
 
         private val transparentToCut =
             setOf(
                 Signature(",", 2),
                 Signature(";", 2),
                 Signature("->", 2),
             )
 
         private sealed class CutLimit {
             abstract val depthToCut: Int
             abstract val procedure: Struct?
 
             open val isNone: Boolean
                 get() = false
 
             object None : CutLimit() {
                 override val depthToCut: Int = -1
                 override val procedure: Struct? = null
 
                 override val isNone: Boolean
                     get() = true
             }
 
             class Actual(
                 override val depthToCut: Int,
                 override val procedure: Struct?,
             ) : CutLimit()
         }
     }
 
     private val failureState: StateBacktracking
         get() = StateBacktracking(context.copy(step = nextStep()))
 
     private fun exceptionalState(exception: ResolutionException): StateException =
         StateException(exception, context.copy(step = nextStep()))
 
     private fun missingProcedure(
         ruleSources: () -> Sequence<Theory>,
         missing: Signature,
     ): State =
         when (val unknown = context.flags[Unknown]) {
             Unknown.FAIL -> failureState
             else -> {
                 if (ruleSources().none { missing.toIndicator() in it }) {
                     when (unknown) {
                         Unknown.ERROR ->
                             exceptionalState(
                                 ExistenceError.forProcedure(
                                     context = context,
                                     procedure = missing,
                                 ),
                             )
                         Unknown.WARNING ->
                             failureState.also {
                                 context.warnings.write(
                                     MissingPredicate(
                                         context = context,
                                         signature = missing,
                                     ),
                                 )
                             }
                         else -> failureState
                     }
                 } else {
                     failureState
                 }
             }
         }
 
     private val ignoreState: StateGoalSelection
         get() =
             StateGoalSelection(
                 context.copy(goals = context.goals.next, step = nextStep()),
             )
 
     private fun Term.isCut(): Boolean = this.isAtom && this.castToAtom().value == "!"
 
     private fun PrologExecutionContext.computeCutLimit(magicCut: Boolean = false): CutLimit {
         val cutLimit =
             if (magicCut) {
                 this.pathToRoot.firstOrNull()
             } else {
                 this.pathToRoot.firstOrNull { it.procedure?.extractSignature() !in transparentToCut }
             }
         return if (cutLimit == null) {
             CutLimit.None
         } else {
             CutLimit.Actual(cutLimit.depth, cutLimit.procedure)
         }
     }
 
     private fun cutCanBeSkipped(
         cutLimit: CutLimit,
         executionContext: PrologExecutionContext,
     ): Boolean =
         cutLimit.isNone ||
             cutLimit.depthToCut > executionContext.depth ||
             cutLimit.depthToCut == executionContext.depth &&
             cutLimit.procedure != executionContext.procedure
 
     private fun ChoicePointContext?.performCut(cutLimit: CutLimit): ChoicePointContext? =
         when {
             this === null -> {
                 null
             }
             cutCanBeSkipped(cutLimit, executionContext!!) -> {
                 this
             }
             else -> {
                 val cutCandidates =
                     pathToRoot.filter {
                         it.executionContext!!.procedure == cutLimit.procedure
                     }
                 if (cutCandidates.any()) {
                     cutCandidates
                         .firstOrNull {
                             it.executionContext!!.depth <= cutLimit.depthToCut
                         }?.parent
                 } else {
                     this
                 }
             }
         }
 
     private fun PrologExecutionContext.isTailRecursive(currentGoal: Struct): Boolean =
         goals.next.isOver &&
             flags[LastCallOptimization] == ON &&
             currentGoal.extractSignature().let { it == procedure?.extractSignature() && it != catchSignature }
 
     override fun computeNext(): State =
         with(context) {
             val construct = ControlConstructs.of(goal)
 
             when {
                 goal.isTruth -> {
                     if (goal.isTrue) {
                         ignoreState
                     } else {
                         failureState
                     }
                 }
 
                 goal.isCut() -> {
                     val cutLimit = computeCutLimit(goal is MagicCut)
                     ignoreState.let {
                         it.copy(
                             context =
                                 it.context.copy(
                                     choicePoints = it.context.choicePoints.performCut(cutLimit),
                                 ),
                         )
                     }
                 }
 
                 // arg2p: the predicates the engine solves natively
                 construct != null -> construct.select(context, goal, executionTime)
 
                 // The clauses are selected once: the procedure is missing when none is selected
                 // (arg2p: checking first whether any clause matches matched them all twice). A single fact is
                 // resolved in this frame (see FactResolution).
                 else -> {
                     val clauses = ProgramClauses.of(this).clausesFor(goal, unificator)
                     FactResolution.of(context, goal, clauses, nextStep()) ?: selectRules(clauses)
                 }
             }
         }
 
     private fun selectRules(clauses: List<Clause>): State {
         val rules: Cursor<out Rule> = RuleCursor.of(clauses)
         return when {
             rules.isOver -> missingProcedure({ context.ruleSources() }, goal.extractSignature())
             context.isTailRecursive(goal) ->
                 StateRuleExecution(context.replaceWithChildAppendingRulesAndChoicePoints(rules, goal))
             else -> StateRuleExecution(context.createChildAppendingRulesAndChoicePoints(rules, goal))
         }
     }
 
     // The theories of the program, only needed to report a missing procedure
     private fun PrologExecutionContext.ruleSources(): Sequence<Theory> = sequenceOf(libraries.asTheory(unificator), staticKb, dynamicKb)
 }