tala-compiler-0.1.0: test/FallThroughTests.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | How a function ends when its body ends.
A function without a result may end without a @return@: reaching the end of
its body returns. A function with a result returns on every path, so the end
of its body is never reached, and the block that stands there is marked as
unreachable rather than given a value nobody wrote.
The two must not be confused. A function without a result whose last block
was marked unreachable had no meaning once it was called, and a native
program whose @Main@ ended that way stopped instead of ending.
-}
module FallThroughTests (fallThroughTests) where
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.CorePrep
import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
fallThroughTests :: [(String, Bool)]
fallThroughTests =
[ ("an empty body without a result returns", terminators (prepared unitType []) == [returnsNothing])
, ("a body without a result that ends in a binding returns", terminators (prepared unitType [bind 2 1]) == [returnsNothing])
, ("a body without a result that ends in a call returns", terminators (prepared unitType [CoreEvaluate callee]) == [returnsNothing])
, ("a body without a result never ends in an unreachable block", all (noneUnreachable . prepared unitType) resultless)
, ("every body without a result passes verification", all (verified . prepared unitType) resultless)
, -- One return was written; the other is the end of the body.
("the block after a conditional return returns", returns (prepared unitType [returnWhen]) == 2)
, ("the block after a loop returns", returns (prepared unitType [CoreWhile condition [bind 3 1]]) == 1)
, ("the block after a loop that is left early returns", returns (prepared unitType [CoreWhile condition [CoreBreak]]) == 1)
, ("the block after nested conditionals returns", returns (prepared unitType [nested]) == 2)
, ("an explicit return keeps its own value", terminators (prepared unitType [CoreReturn nothing]) == [returnsNothing])
, ("a loop body still continues with its loop", loopBodyJumps)
, ("a body with a result is not given a value at its end", notElem returnsNothing (terminators (prepared intType [bothReturn])))
, ("the end of a body with a result stays unreachable", elem CorePrepUnreachable (terminators (prepared intType [bothReturn])))
, ("a body with a result that returns directly has one block", terminators (prepared intType [CoreReturn (integer 1)]) == [CorePrepReturn (CorePrepLiteral (CoreInteger 1) intType)])
, ("a closure without a result returns at the end of its body", closureReturns)
, ("a closure with a result is not given a value at its end", closureWithResultIsNotCompleted)
]
where
resultless =
[ []
, [bind 2 1]
, [CoreEvaluate callee]
, [returnWhen]
, [returnWhen, bind 2 1]
, [CoreWhile condition [bind 3 1]]
, [CoreWhile condition [CoreBreak]]
, [CoreWhile condition [CoreContinue]]
, [CoreDoWhile [bind 3 1] condition]
, [CoreFor condition [bind 3 1] [bind 4 1]]
, [nested]
, [nested, returnWhen, CoreWhile condition []]
, [CoreIf condition [CoreReturn nothing] [CoreReturn nothing]]
]
-- | What reaching the end of a body without a result does.
returnsNothing :: CorePrepTerminator
returnsNothing = CorePrepReturn (CorePrepLiteral CoreUnit unitType)
nothing :: CoreExpression
nothing = CoreLiteral CoreUnit unitType
integer :: Integer -> CoreExpression
integer value = CoreLiteral (CoreInteger value) intType
condition :: CoreExpression
condition = CoreVariable (name 20 "flag") boolType
callee :: CoreExpression
callee = CoreApply (CoreVariable (name 21 "Act") (FunctionType [] unitType)) [] unitType
bind :: Int -> Integer -> CoreStatement
bind symbol value = CoreBind (CoreBinding (name symbol "local") intType False (integer value))
-- | @if (flag) { return; }@
returnWhen :: CoreStatement
returnWhen = CoreIf condition [CoreReturn nothing] []
-- | @if (flag) { if (flag) { return; } } else { local = 1; }@
nested :: CoreStatement
nested = CoreIf condition [returnWhen] [bind 5 1]
-- | @if (flag) { return 1; } else { return 2; }@
bothReturn :: CoreStatement
bothReturn = CoreIf condition [CoreReturn (integer 1)] [CoreReturn (integer 2)]
prepared :: Type -> [CoreStatement] -> CorePrepModule
prepared returnType statements = case prepareCore moduleValue of
Right moduleResult -> moduleResult
Left _ -> error "prepareCore cannot fail on these bodies"
where
moduleValue = CoreModuleWithSources (QualifiedName [Identifier "FallThrough"]) [function] [] []
function =
CoreFunction
(name 1 "Evaluate")
[(name 20 "flag", boolType), (name 21 "Act", FunctionType [] unitType)]
returnType
statements
terminators :: CorePrepModule -> [CorePrepTerminator]
terminators moduleValue =
[ corePrepBlockTerminator block
| function <- corePrepModuleFunctions moduleValue
, block <- corePrepFunctionBlocks function
]
-- | How many blocks return without a value.
returns :: CorePrepModule -> Int
returns = length . filter (== returnsNothing) . terminators
noneUnreachable :: CorePrepModule -> Bool
noneUnreachable = notElem CorePrepUnreachable . terminators
verified :: CorePrepModule -> Bool
verified = either (const False) (const True) . verifyCorePrep
{- | The end of a loop body goes back to the loop, whatever the function
returns: only the end of the function's own body returns.
-}
loopBodyJumps :: Bool
loopBodyJumps =
length [() | CorePrepJump _ <- found] >= 2
&& length (filter (== returnsNothing) found) == 1
where
found = terminators (prepared unitType [CoreWhile condition [bind 3 1]])
closureReturns :: Bool
closureReturns = case corePrepModuleFunctions moduleValue of
[_, lifted] -> map corePrepBlockTerminator (corePrepFunctionBlocks lifted) == [returnsNothing]
_ -> False
where
moduleValue = prepared unitType [CoreBind (CoreBinding (name 6 "act") callable False closure)]
callable = FunctionType [] unitType
closure = CoreClosure [] [] unitType [bind 7 1] callable
closureWithResultIsNotCompleted :: Bool
closureWithResultIsNotCompleted = case corePrepModuleFunctions moduleValue of
[_, lifted] ->
let found = map corePrepBlockTerminator (corePrepFunctionBlocks lifted)
in notElem returnsNothing found && elem CorePrepUnreachable found
_ -> False
where
moduleValue = prepared unitType [CoreBind (CoreBinding (name 6 "pick") callable False closure)]
callable = FunctionType [] intType
closure = CoreClosure [] [] intType [bothReturn] callable
name :: Int -> String -> ResolvedName
name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)