packages feed

tala-syntax-0.1.0: src/Progmasoft/Tala/Completion.hs

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

{- | Whether control can reach the end of a statement or an expression.

This is a fact about control flow and is kept apart from types. A block that
ends with @return@, an @if@ expression whose two blocks both leave, and a
call one of whose arguments is such an expression never produce a value, but
no type says so: the question is answered from the typed tree by the
functions of this module. The type checker uses them to decide what must
have a value, and the desugarer uses the same functions to lower what never
completes as statements alone, so that nothing is stored for a value that
does not exist.

The answers err on the side of completing: a call is assumed to return, and
a condition is assumed to be able to take either value unless it is the
literal @true@. Whether a @break@ or @continue@ has a loop to leave is a
separate rule of the type checker.
-}
module Progmasoft.Tala.Completion
    ( blockCannotComplete
    , statementCannotComplete
    , doesNotComplete
    , neverCompletingOperand
    , matchArmsAlwaysAccept
    , acceptsEveryValue
    , isUnguarded
    , blockTransfers
    , statementTransfers
    , expressionTransfers
    , isBreak
    , isContinue
    ) where

import Progmasoft.Tala.AST

{- | Whether control can never reach the end of a block.

A statement cannot complete normally when it is a @return@, a @break@ or a
@continue@; an @if@ with an @else@ whose two blocks both cannot; a nested
block that cannot; a loop whose condition is the constant true, or absent in
a @for@, and that no @break@ leaves; a statement @match@ some arm of which
always accepts and all of whose arms cannot; or any statement an expression
of which is always evaluated and never completes. A block cannot complete
when one of its statements cannot, because the statements after that one are
never reached.
-}
blockCannotComplete :: Block name Type -> Bool
blockCannotComplete (Block statements) = any statementCannotComplete statements

-- | Whether control can never reach the point after a statement.
statementCannotComplete :: Statement name Type -> Bool
statementCannotComplete statement = case statement of
    ReturnStatement {} -> True
    BreakStatement {} -> True
    ContinueStatement {} -> True
    BindingStatement _ _ _ _ _ value -> doesNotComplete value
    AssignmentStatement _ _ _ value -> doesNotComplete value
    CompoundAssignmentStatement _ _ _ _ value -> doesNotComplete value
    DiscardStatement _ value -> doesNotComplete value
    IfStatement _ condition whenTrue whenFalse ->
        doesNotComplete condition
            || maybe False (\block -> blockCannotComplete whenTrue && blockCannotComplete block) whenFalse
    GuardStatement _ condition _ -> doesNotComplete condition
    BlockStatement _ nested -> blockCannotComplete nested
    -- A loop ends normally when a break leaves it, from its body or from
    -- its condition. Without one it does not end when its condition is the
    -- constant true or never completes; a do/while also when its body
    -- cannot complete, because its body runs first.
    WhileStatement _ condition body ->
        not (leftByBreak (expressionTransfers condition ++ blockTransfers body))
            && (doesNotComplete condition || isConstantTrue condition)
    DoWhileStatement _ body condition ->
        not (leftByBreak (expressionTransfers condition ++ blockTransfers body))
            && (doesNotComplete condition || isConstantTrue condition)
    ForStatement _ initializer condition updates body ->
        maybe False statementCannotComplete initializer
            || ( not
                    ( leftByBreak
                        ( maybe [] expressionTransfers condition
                            ++ blockTransfers body
                            ++ concatMap statementTransfers updates
                        )
                    )
                    && maybe True (\test -> doesNotComplete test || isConstantTrue test) condition
               )
    ExpressionStatement _ value _ -> doesNotComplete value
    _ -> False
    where
        isConstantTrue expression = case expression of
            LiteralExpression _ (BooleanLiteral True) _ -> True
            _ -> False
        leftByBreak = any isBreak

{- | Whether evaluating an expression never yields a value.

That is so when a part of it that is always evaluated never does, or when it
is a choice every branch of which leaves: an @if@ expression whose two
blocks do not complete, a block used as a value that cannot complete or
whose final expression does not, or a @match@ one arm of which always
accepts and no arm of which completes. An operand that is evaluated only
sometimes, such as the right operand of @&&@, decides nothing.
-}
doesNotComplete :: Expression name Type -> Bool
doesNotComplete expression = case expression of
    -- The final expression of a block is its last statement, so one walk
    -- of the statements covers it; a second look at it here would double
    -- the work at every level of nested blocks.
    BlockExpression _ block _ -> blockCannotComplete block
    ConditionalExpression _ condition first second _ ->
        doesNotComplete condition || (doesNotComplete first && doesNotComplete second)
    MatchExpression _ subjects arms _ ->
        any doesNotComplete subjects
            || ( not (null arms)
                    && matchArmsAlwaysAccept (subjectTypesOf arms) arms
                    && all (doesNotComplete . matchArmBody) arms
               )
    -- A loop used as an expression yields the value of the break that
    -- leaves it; without such a break it never yields one.
    LoopExpression _ loop _ -> statementCannotComplete loop
    _ -> case neverCompletingOperand expression of
        Just _ -> True
        Nothing -> False
    where
        -- Every typed pattern carries the type of the value it accepts.
        subjectTypesOf arms = case arms of
            first : _ -> map matchPatternAnnotation (matchArmPatterns first)
            [] -> []

{- | The operands of an expression that are evaluated before its first
operand that never completes, and that operand; nothing when every operand
that is always evaluated completes. Choices between branches are not
operands in this sense and are answered by 'doesNotComplete' itself.
-}
neverCompletingOperand :: Expression name Type -> Maybe ([Expression name Type], Expression name Type)
neverCompletingOperand expression = case break doesNotComplete (alwaysEvaluated expression) of
    (before, operand : _) -> Just (before, operand)
    (_, []) -> Nothing
    where
        alwaysEvaluated value = case value of
            MemberAccessExpression _ receiver _ _ -> [receiver]
            MethodReferenceExpression _ receiver _ _ -> [receiver]
            CallExpression _ callee arguments _ -> callee : arguments
            UnaryExpression _ _ operand _ -> [operand]
            BinaryExpression _ operator left right _
                | operator `elem` [LogicalAnd, LogicalOr] -> [left]
                | otherwise -> [left, right]
            IsPatternExpression _ subject _ _ -> [subject]
            CoalesceExpression _ left _ _ -> [left]
            AssignmentExpression _ _ _ operand _ -> [operand]
            MatchExpression _ subjects _ _ -> subjects
            _ -> []

-- | Whether a pattern accepts every value of its subject.
acceptsEveryValue :: MatchPattern name annotation -> Bool
acceptsEveryValue patternValue = case patternValue of
    MatchWildcardPattern {} -> True
    MatchTypePattern {} -> True
    _ -> False

{- | Whether some arm is certain to accept, whatever the subjects are.

That is the case when an arm without a guard has only patterns that accept
every value. It is also the case when every subject has a closed set of
values, a @bool@ or an enum, and the arms without guards accept each
combination of those values between them; the combinations are enumerated,
up to 'maximumEnumeratedCombinations' of them. Nothing else is recognized: a
guard may be false, and the literals of a wider type are never listed in
full.
-}
matchArmsAlwaysAccept :: [Type] -> [MatchArm name annotation] -> Bool
matchArmsAlwaysAccept subjectTypes arms = any catchAll unguarded || coversClosedValues
    where
        unguarded = filter isUnguarded arms
        catchAll arm = all acceptsEveryValue (matchArmPatterns arm)
        coversClosedValues = case traverse closedValues subjectTypes of
            Just domains
                | not (null domains)
                , product (map length domains) <= maximumEnumeratedCombinations ->
                    all (\values -> any (`acceptsLiterals` values) unguarded) (sequence domains)
            _ -> False

-- | The values of a type that has a closed set of them, as the literals a pattern names.
closedValues :: Type -> Maybe [Literal]
closedValues valueType
    | valueType == boolType = Just [BooleanLiteral True, BooleanLiteral False]
    | otherwise = map IntegerLiteral <$> enumMemberValues valueType

{- | The most combinations of subject values that are enumerated to decide
whether a match accepts every value. A match with more is complete only
through a catch-all arm; the bound keeps the check linear in practice.
-}
maximumEnumeratedCombinations :: Int
maximumEnumeratedCombinations = 256

-- | Whether an arm's patterns accept the given values of its subjects.
acceptsLiterals :: MatchArm name annotation -> [Literal] -> Bool
acceptsLiterals arm values =
    length (matchArmPatterns arm) == length values && and (zipWith accepts (matchArmPatterns arm) values)
    where
        accepts patternValue value = case patternValue of
            MatchLiteralPattern _ literal _ -> literal == value
            _ -> acceptsEveryValue patternValue

-- | Whether an arm has no guard.
isUnguarded :: MatchArm name annotation -> Bool
isUnguarded arm = case matchArmGuard arm of
    Nothing -> True
    Just _ -> False

{- | The @break@ and @continue@ statements in a block that target the loop
the block belongs to.

Loops nested in the block keep their own transfers, in their bodies and in
their conditions and update clauses. A transfer in a block used as a value
inside an expression targets the same loop as one in a statement, so
expressions are searched as well; closures and loops used as expressions
are not, because a transfer in them cannot reach this loop.
-}
blockTransfers :: Block name annotation -> [Statement name annotation]
blockTransfers (Block statements) = concatMap statementTransfers statements

-- | The transfers of one statement that target the loop around it.
statementTransfers :: Statement name annotation -> [Statement name annotation]
statementTransfers statement = case statement of
    BreakStatement _ value -> statement : maybe [] expressionTransfers value
    ContinueStatement {} -> [statement]
    BindingStatement _ _ _ _ _ value -> expressionTransfers value
    AssignmentStatement _ _ _ value -> expressionTransfers value
    ReturnStatement _ value -> maybe [] expressionTransfers value
    IfStatement _ condition whenTrue whenFalse ->
        expressionTransfers condition ++ blockTransfers whenTrue ++ maybe [] blockTransfers whenFalse
    WhileStatement {} -> []
    DoWhileStatement {} -> []
    ForStatement _ initializer _ _ _ -> maybe [] statementTransfers initializer
    ForEachStatement _ _ _ _ _ source _ -> expressionTransfers source
    IncrementStatement {} -> []
    CompoundAssignmentStatement _ _ _ _ value -> expressionTransfers value
    DiscardStatement _ value -> expressionTransfers value
    GuardStatement _ condition block -> expressionTransfers condition ++ blockTransfers block
    BlockStatement _ block -> blockTransfers block
    ExpressionStatement _ value _ -> expressionTransfers value

-- | The transfers in an expression that target the loop around it.
expressionTransfers :: Expression name annotation -> [Statement name annotation]
expressionTransfers expression = case expression of
    NameExpression {} -> []
    LiteralExpression {} -> []
    MemberAccessExpression _ receiver _ _ -> expressionTransfers receiver
    MethodReferenceExpression _ receiver _ _ -> expressionTransfers receiver
    CallExpression _ callee arguments _ -> concatMap expressionTransfers (callee : arguments)
    UnaryExpression _ _ value _ -> expressionTransfers value
    BinaryExpression _ _ left right _ -> expressionTransfers left ++ expressionTransfers right
    IsPatternExpression _ subject _ _ -> expressionTransfers subject
    ConditionalExpression _ condition first second _ -> concatMap expressionTransfers [condition, first, second]
    CoalesceExpression _ left fallback _ -> expressionTransfers left ++ expressionTransfers fallback
    AssignmentExpression _ _ _ value _ -> expressionTransfers value
    IncrementExpression {} -> []
    LoopExpression {} -> []
    BlockExpression _ block _ -> blockTransfers block
    MatchExpression _ subjects arms _ ->
        concatMap expressionTransfers subjects
            ++ concatMap (\arm -> maybe [] expressionTransfers (matchArmGuard arm) ++ expressionTransfers (matchArmBody arm)) arms
    CallableExpression {} -> []

-- | Whether a statement is a @break@.
isBreak :: Statement name annotation -> Bool
isBreak statement = case statement of
    BreakStatement {} -> True
    _ -> False

-- | Whether a statement is a @continue@.
isContinue :: Statement name annotation -> Bool
isContinue statement = case statement of
    ContinueStatement {} -> True
    _ -> False