packages feed

tala-compiler-0.1.0: test/LoopFlowTests.hs

-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1

{- | Regression tests for loop-carried Core facts.

These tests observe the public optimizer result rather than importing the
private abstract-domain implementation. That keeps the contract behavioral:
the optimizer may change its widening strategy without changing which source
paths are provable or which potentially failing operations must remain.
-}
module LoopFlowTests (loopFlowTests) where

import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.Optimizer
import Progmasoft.Tala.Core.Optimizer.IntegerFacts

loopFlowTests :: [(String, Bool)]
loopFlowTests =
    [ ("while false-edge facts reach the following branch", whileCounterExit)
    , ("a pre-test loop that starts false preserves its exact input", falseFirstIteration)
    , ("do-while transfers the mandatory first body execution", doWhileExecutesOnce)
    , ("do-while continue reaches its condition", doWhileContinueReachesCondition)
    , ("for continue still executes the update sequence", forContinueExecutesUpdate)
    , ("break contributes an exit without becoming a back-edge", breakIsAnExit)
    , ("break exits join with the condition-false edge", breakAndConditionExitsJoin)
    , ("a zero-valued break path is not lost at the loop join", breakCanCarryZero)
    , ("a return inside a do-loop does not contaminate normal exit facts", returnDoesNotJoinExit)
    , ("nested-loop break is consumed by its nearest loop", nestedBreakIsLocal)
    , ("nested-loop continue is consumed by its nearest loop", nestedContinueIsLocal)
    , ("calls in loop bodies invalidate loop-carried facts", callInBodyInvalidates)
    , ("calls in loop conditions invalidate loop-carried facts", callInConditionInvalidates)
    , ("overflowing induction arithmetic drops the interval", overflowingInductionIsUnknown)
    , ("a loop assignment can replace nonzero with exact zero", assignmentCanEstablishZero)
    , ("a do-loop join keeps zero exclusion common to both arms", doLoopNonzeroJoin)
    , ("a false while condition prevents body assignments from leaking", falseWhileDoesNotRun)
    , ("a false for condition prevents body assignments from leaking", falseForDoesNotRun)
    , ("an unconditional loop break retains its exact exit value", unconditionalBreakRetainsValue)
    , ("a continue-only infinite path is not treated as a break", continueDoesNotExit)
    , ("loop headers retain stable facts that do not expand", stableHeaderFact)
    , ("condition calls are not folded from stale pre-loop constants", callConditionStaysUnknown)
    , ("widened false-edge summaries include every concrete counter exit", boundedCounterExitOracle)
    , ("the fixed-point solver completes ordinary counters within its cap", boundedCounterSolverBudget)
    , ("do-loop summaries keep first-entry and condition facts distinct", doWhileSummarySeparatesPhases)
    , ("for continue facts enter the update before an update break", forContinueSummary)
    , ("a body break skips the for update", forBreakSkipsUpdate)
    , ("loop return facts remain separate from the normal exit", loopReturnSummary)
    , ("a called body produces unconstrained loop exits", calledBodySummaryIsTop)
    , ("a called for-update drops stale exit facts", callInUpdateInvalidates)
    , ("an update break carries its post-update assignment", updateBreakCarriesUpdatedValue)
    ]
        ++ boundedCounterCases

-- A counted while loop exercises interval widening: the header must stop
-- growing even though the concrete loop may execute many more than eight
-- iterations. Its false condition then proves the complementary lower bound.
whileCounterExit :: Bool
whileCounterExit = postGuardIsRemoved (counterLoop 0 17 WhileCounter)

falseFirstIteration :: Bool
falseFirstIteration = postGuardIsRemoved (counterLoop 12 5 WhileCounter)

doWhileExecutesOnce :: Bool
doWhileExecutesOnce =
    let index = loopVariable
        loop = CoreDoWhile [assign indexName 1] falseLiteral
        program = localLoopProgram 0 loop (notEqual index 1) []
     in postGuardIsRemoved program

doWhileContinueReachesCondition :: Bool
doWhileContinueReachesCondition =
    let index = loopVariable
        loop =
            CoreDoWhile
                [ assign indexName 1
                , CoreContinue
                , assign indexName 0
                ]
                falseLiteral
        program = localLoopProgram 0 loop (notEqual index 1) []
     in postGuardIsRemoved program

forContinueExecutesUpdate :: Bool
forContinueExecutesUpdate =
    let index = loopVariable
        increment = add index 1
        body =
            [ CoreIf
                (equal index 2)
                [CoreContinue]
                [CoreAssign indexName increment]
            ]
        update = [CoreAssign indexName increment]
        loop = CoreFor (lessThan index 4) body update
        program = localLoopProgram 0 loop (lessThan index 4) []
     in postGuardIsRemoved program

breakIsAnExit :: Bool
breakIsAnExit =
    let index = loopVariable
        loop = CoreWhile trueLiteral [CoreIf (equal index 0) [CoreBreak] []]
        program = localLoopProgram 0 loop (equal index 0) []
     in optimizedBody program `containsReturnedDivision` indexName

breakAndConditionExitsJoin :: Bool
breakAndConditionExitsJoin =
    let index = loopVariable
        loop =
            CoreWhile
                (lessThan index 12)
                [ CoreIf
                    (equal index 3)
                    [CoreBreak]
                    [CoreAssign indexName (add index 1)]
                ]
        program = localLoopProgram 0 loop (equal index 3) []
     in topLevelGuardRemains program

breakCanCarryZero :: Bool
breakCanCarryZero =
    let index = loopVariable
        loop = CoreWhile trueLiteral [CoreIf (equal index 0) [CoreBreak] [assign indexName 1]]
        program = localLoopProgram 0 loop (equal index 0) []
     in optimizedBody program `containsReturnedDivision` indexName

returnDoesNotJoinExit :: Bool
returnDoesNotJoinExit =
    let index = loopVariable
        flag = CoreVariable flagName boolType
        loop = CoreDoWhile [CoreIf flag [CoreReturn (literal 99)] [], assign indexName 1] falseLiteral
        program = localLoopProgram 0 loop (notEqual index 1) [(flagName, boolType)]
     in postGuardIsRemoved program

nestedBreakIsLocal :: Bool
nestedBreakIsLocal =
    let index = loopVariable
        inner = CoreWhile trueLiteral [CoreBreak]
        outer =
            CoreWhile
                (lessThan index 1)
                [inner, assign indexName 1]
        program = localLoopProgram 0 outer (lessThan index 1) []
     in postGuardIsRemoved program

nestedContinueIsLocal :: Bool
nestedContinueIsLocal =
    let index = loopVariable
        inner = CoreDoWhile [CoreContinue] falseLiteral
        outer = CoreWhile (lessThan index 1) [inner, assign indexName 1]
        program = localLoopProgram 0 outer (lessThan index 1) []
     in postGuardIsRemoved program

callInBodyInvalidates :: Bool
callInBodyInvalidates =
    let index = loopVariable
        callback = CoreVariable callbackName (FunctionType [] unitType)
        invoke = CoreEvaluate (CoreApply callback [] unitType)
        loop = CoreWhile trueLiteral [invoke, CoreBreak]
        program = localLoopProgram 2 loop (notEqual index 0) [(callbackName, FunctionType [] unitType)]
     in topLevelGuardRemains program

callInConditionInvalidates :: Bool
callInConditionInvalidates =
    let index = loopVariable
        predicate = CoreVariable predicateName (FunctionType [] boolType)
        loop = CoreWhile (CoreApply predicate [] boolType) [CoreBreak]
        program = localLoopProgram 2 loop (notEqual index 0) [(predicateName, FunctionType [] boolType)]
     in topLevelGuardRemains program

overflowingInductionIsUnknown :: Bool
overflowingInductionIsUnknown =
    let index = loopVariable
        flag = CoreVariable flagName boolType
        loop = CoreWhile flag [CoreAssign indexName (add index 1)]
        program = localLoopProgram 0 loop (equal index 0) [(flagName, boolType)]
     in topLevelGuardRemains program

assignmentCanEstablishZero :: Bool
assignmentCanEstablishZero =
    let index = loopVariable
        loop = CoreDoWhile [assign indexName 0] (notEqual index 0)
        program = localLoopProgram 7 loop (notEqual index 0) []
     in postGuardIsRemoved program

doLoopNonzeroJoin :: Bool
doLoopNonzeroJoin =
    let index = loopVariable
        flag = CoreVariable flagName boolType
        loop =
            CoreDoWhile
                [CoreIf flag [assign indexName 2] [assign indexName 3]]
                falseLiteral
        program = localLoopProgram 0 loop (equal index 0) [(flagName, boolType)]
     in postGuardIsRemoved program

falseWhileDoesNotRun :: Bool
falseWhileDoesNotRun =
    let index = loopVariable
        loop = CoreWhile falseLiteral [assign indexName 4]
        program = localLoopProgram 0 loop (notEqual index 0) []
     in postGuardIsRemoved program

falseForDoesNotRun :: Bool
falseForDoesNotRun =
    let index = loopVariable
        loop = CoreFor falseLiteral [assign indexName 4] []
        program = localLoopProgram 0 loop (notEqual index 0) []
     in postGuardIsRemoved program

unconditionalBreakRetainsValue :: Bool
unconditionalBreakRetainsValue =
    let index = loopVariable
        loop = CoreWhile trueLiteral [CoreBreak]
        program = localLoopProgram 6 loop (notEqual index 6) []
     in postGuardIsRemoved program

continueDoesNotExit :: Bool
continueDoesNotExit =
    let index = loopVariable
        loop =
            CoreWhile
                trueLiteral
                [CoreIf (equal index 0) [CoreContinue] [CoreBreak]]
        program = localLoopProgram 0 loop (equal index 0) []
     in topLevelGuardRemains program

stableHeaderFact :: Bool
stableHeaderFact =
    let index = loopVariable
        loop = CoreWhile (lessThan index 4) [CoreEvaluate (equal index 3)]
        program = localLoopProgram 10 loop (notEqual index 10) []
     in postGuardIsRemoved program

callConditionStaysUnknown :: Bool
callConditionStaysUnknown =
    let index = loopVariable
        flag = CoreVariable flagName boolType
        predicate = CoreVariable predicateName (FunctionType [] boolType)
        shortCircuitCondition =
            CorePrimitive
                CoreLogicalAnd
                [flag, CoreApply predicate [] boolType]
                boolType
        loop = CoreWhile shortCircuitCondition [CoreBreak]
        program =
            localLoopProgram
                5
                loop
                (equal index 0)
                [(flagName, boolType), (predicateName, FunctionType [] boolType)]
     in topLevelGuardRemains program

{- | Small bounded counter programs act as an executable oracle for the
false-edge equation. Each loop either starts beyond its limit or advances by
one from a range where the increment is representable. In both cases the
post-loop @index < limit@ edge is infeasible, regardless of the concrete
number of runtime iterations.
-}
boundedCounterCases :: [(String, Bool)]
boundedCounterCases =
    [ ( "bounded counter " ++ show initial ++ " -> " ++ show limit ++ " prunes the false exit guard"
      , postGuardIsRemoved (counterLoop initial limit WhileCounter)
      )
    | initial <- [-4 .. 4]
    , limit <- [-4 .. 4]
    ]

boundedCounterExitOracle :: Bool
boundedCounterExitOracle = all counterSummaryContainsConcreteExit counterPairs

boundedCounterSolverBudget :: Bool
boundedCounterSolverBudget = all counterSummaryIsBounded counterPairs

counterPairs :: [(Integer, Integer)]
counterPairs = [(initial, limit) | initial <- [-4 .. 4], limit <- [-4 .. 4]]

counterSummaryContainsConcreteExit :: (Integer, Integer) -> Bool
counterSummaryContainsConcreteExit (initial, limit) =
    case counterSummary initial limit of
        Nothing -> False
        Just summary -> abstractStateContains (loopExitFacts summary) concreteExit
    where
        concreteExit = if initial < limit then limit else initial

counterSummaryIsBounded :: (Integer, Integer) -> Bool
counterSummaryIsBounded (initial, limit) =
    case counterSummary initial limit of
        Nothing -> False
        Just summary ->
            loopFixedPointIterations summary > 0
                && loopFixedPointIterations summary <= 8
                && not (loopAnalysisWidened summary)

counterSummary :: Integer -> Integer -> Maybe LoopFactSummary
counterSummary initial limit =
    let input = initialFacts initial
        index = loopVariable
        loop = CoreWhile (lessThan index limit) [CoreAssign indexName (add index 1)]
     in loopFactSummary input loop

doWhileSummarySeparatesPhases :: Bool
doWhileSummarySeparatesPhases =
    case loopFactSummary (initialFacts 0) (CoreDoWhile [assign indexName 1] falseLiteral) of
        Nothing -> False
        Just summary ->
            abstractStateIs (loopBodyFacts summary) 0
                && abstractStateIs (loopConditionFacts summary) 1
                && abstractStateIs (loopExitFacts summary) 1

forContinueSummary :: Bool
forContinueSummary =
    let loop = CoreFor (lessThan loopVariable 4) [CoreContinue] [CoreBreak]
     in case loopFactSummary (initialFacts 2) loop of
            Nothing -> False
            Just summary ->
                abstractStateIs (loopUpdateFacts summary) 2
                    && abstractStateIs (loopExitFacts summary) 2

forBreakSkipsUpdate :: Bool
forBreakSkipsUpdate =
    let loop = CoreFor trueLiteral [CoreBreak] [assign indexName 9]
     in case loopFactSummary (initialFacts 6) loop of
            Nothing -> False
            Just summary ->
                isUnreachableFacts (loopUpdateFacts summary)
                    && abstractStateIs (loopExitFacts summary) 6

loopReturnSummary :: Bool
loopReturnSummary =
    let loop = CoreDoWhile [CoreReturn (literal 42)] falseLiteral
     in case loopFactSummary (initialFacts 6) loop of
            Nothing -> False
            Just summary ->
                abstractStateIs (loopReturnFacts summary) 6
                    && isUnreachableFacts (loopExitFacts summary)

calledBodySummaryIsTop :: Bool
calledBodySummaryIsTop =
    let callback = CoreVariable callbackName (FunctionType [] unitType)
        loop = CoreWhile trueLiteral [CoreEvaluate (CoreApply callback [] unitType), CoreBreak]
     in case loopFactSummary (initialFacts 6) loop of
            Nothing -> False
            Just summary ->
                lookupIntegerFact (loopExitFacts summary) (resolvedSymbol indexName) == Nothing

callInUpdateInvalidates :: Bool
callInUpdateInvalidates =
    let callback = CoreVariable callbackName (FunctionType [] unitType)
        loop = CoreFor trueLiteral [CoreContinue] [CoreEvaluate (CoreApply callback [] unitType), CoreBreak]
        program = localLoopProgram 2 loop (equal loopVariable 0) [(callbackName, FunctionType [] unitType)]
     in topLevelGuardRemains program

updateBreakCarriesUpdatedValue :: Bool
updateBreakCarriesUpdatedValue =
    let loop = CoreFor trueLiteral [CoreContinue] [assign indexName 7, CoreBreak]
     in case loopFactSummary (initialFacts 3) loop of
            Nothing -> False
            Just summary ->
                abstractStateIs (loopUpdateFacts summary) 3
                    && abstractStateIs (loopExitFacts summary) 7

initialFacts :: Integer -> IntegerFacts
initialFacts value =
    transferStatementFacts
        emptyIntegerFacts
        (CoreBind (CoreBinding indexName intType True (literal value)))

abstractStateContains :: IntegerFacts -> Integer -> Bool
abstractStateContains state value = case lookupIntegerFact state (resolvedSymbol indexName) of
    Nothing -> not (isUnreachableFacts state)
    Just fact ->
        maybe True (<= value) (integerMinimum fact)
            && maybe True (>= value) (integerMaximum fact)
            && not (value == 0 && integerExcludesZero fact)

abstractStateIs :: IntegerFacts -> Integer -> Bool
abstractStateIs state value =
    not (isUnreachableFacts state)
        && abstractStateContains state value
        && case lookupIntegerFact state (resolvedSymbol indexName) of
            Just fact -> integerMinimum fact == Just value && integerMaximum fact == Just value
            Nothing -> False

data CounterLoop = WhileCounter

counterLoop :: Integer -> Integer -> CounterLoop -> CoreFunction
counterLoop initial limit WhileCounter =
    let index = loopVariable
        loop = CoreWhile (lessThan index limit) [CoreAssign indexName (add index 1)]
     in localLoopProgram initial loop (lessThan index limit) []

localLoopProgram :: Integer -> CoreStatement -> CoreExpression -> [(ResolvedName, Type)] -> CoreFunction
localLoopProgram initial loop postCondition extraParameters =
    CoreFunction
        functionName
        extraParameters
        intType
        [ CoreBind (CoreBinding indexName intType True (literal initial))
        , loop
        , CoreIf
            postCondition
            [CoreReturn (divide (CoreVariable indexName intType))]
            [CoreReturn (literal 0)]
        ]

postGuardIsRemoved :: CoreFunction -> Bool
postGuardIsRemoved function = case optimizedBody function of
    Just statements -> case reverse statements of
        CoreReturn (CoreLiteral (CoreInteger 0) valueType) : _ -> valueType == intType
        _ -> False
    Nothing -> False

topLevelGuardRemains :: CoreFunction -> Bool
topLevelGuardRemains function = case optimizedBody function of
    Just statements -> case reverse statements of
        CoreIf {} : _ -> True
        _ -> False
    Nothing -> False

containsReturnedDivision :: Maybe [CoreStatement] -> ResolvedName -> Bool
containsReturnedDivision maybeStatements name =
    maybe False (any (statementContainsDivision name)) maybeStatements

statementContainsDivision :: ResolvedName -> CoreStatement -> Bool
statementContainsDivision name statement = case statement of
    CoreBind binding -> expressionContainsDivision name (coreBindingValue binding)
    CoreAssign _ expression -> expressionContainsDivision name expression
    CoreReturn expression -> expressionContainsDivision name expression
    CoreIf condition whenTrue whenFalse ->
        expressionContainsDivision name condition
            || any (statementContainsDivision name) whenTrue
            || any (statementContainsDivision name) whenFalse
    CoreEvaluate expression -> expressionContainsDivision name expression
    CoreWhile condition body ->
        expressionContainsDivision name condition || any (statementContainsDivision name) body
    CoreDoWhile body condition ->
        any (statementContainsDivision name) body || expressionContainsDivision name condition
    CoreFor condition body update ->
        expressionContainsDivision name condition
            || any (statementContainsDivision name) body
            || any (statementContainsDivision name) update
    CoreBreak -> False
    CoreContinue -> False

expressionContainsDivision :: ResolvedName -> CoreExpression -> Bool
expressionContainsDivision name expression = case expression of
    CorePrimitive CoreDivide [_numerator, CoreVariable divisor _] _ -> resolvedSymbol divisor == resolvedSymbol name
    CoreVariable {} -> False
    CoreLiteral {} -> False
    CoreApply callee arguments _ ->
        expressionContainsDivision name callee || any (expressionContainsDivision name) arguments
    CorePrimitive _ arguments _ -> any (expressionContainsDivision name) arguments
    CoreLet _ _ value body _ ->
        expressionContainsDivision name value || expressionContainsDivision name body
    CoreConditional condition whenTrue whenFalse _ -> any (expressionContainsDivision name) [condition, whenTrue, whenFalse]
    CoreClosure captures _ _ body _ ->
        any (expressionContainsDivision name . coreCaptureValue) captures
            || any (statementContainsDivision name) body

optimizedBody :: CoreFunction -> Maybe [CoreStatement]
optimizedBody function = do
    result <- either (const Nothing) Just (optimizeCoreWith loopTestOptions (CoreModule moduleName [function]))
    case coreModuleFunctions (optimizedCore result) of
        [optimized] -> Just (coreFunctionBody optimized)
        _ -> Nothing

loopTestOptions :: OptimizerOptions
loopTestOptions =
    defaultOptimizerOptions
        { optimizerInterproceduralEffects = False
        , optimizerInlining = False
        , optimizerDeadCodeElimination = False
        }

assign :: ResolvedName -> Integer -> CoreStatement
assign name value = CoreAssign name (literal value)

add :: CoreExpression -> Integer -> CoreExpression
add left right = CorePrimitive CoreAdd [left, literal right] intType

lessThan :: CoreExpression -> Integer -> CoreExpression
lessThan left right = CorePrimitive CoreLessThan [left, literal right] boolType

equal :: CoreExpression -> Integer -> CoreExpression
equal left right = CorePrimitive CoreEqual [left, literal right] boolType

notEqual :: CoreExpression -> Integer -> CoreExpression
notEqual left right = CorePrimitive CoreNotEqual [left, literal right] boolType

divide :: CoreExpression -> CoreExpression
divide value = CorePrimitive CoreDivide [literal 24, value] intType

literal :: Integer -> CoreExpression
literal value = CoreLiteral (CoreInteger value) intType

trueLiteral, falseLiteral :: CoreExpression
trueLiteral = CoreLiteral (CoreBoolean True) boolType
falseLiteral = CoreLiteral (CoreBoolean False) boolType

indexName, flagName, callbackName, predicateName, functionName :: ResolvedName
indexName = resolved 10 "index"
flagName = resolved 11 "flag"
callbackName = resolved 12 "callback"
predicateName = resolved 13 "predicate"
functionName = resolved 1 "LoopFacts"

loopVariable :: CoreExpression
loopVariable = CoreVariable indexName intType

resolved :: Int -> String -> ResolvedName
resolved symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)

moduleName :: QualifiedName
moduleName = QualifiedName [Identifier "Optimizer", Identifier "LoopFlow"]